diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index d8236ebf..40961b5e 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -443,7 +443,27 @@ trait unix $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); - $package->runStage([$this, 'smokeTestForUnix']); + } + + #[Stage('postInstall')] + public function postInstall(TargetPackage $package, PackageInstaller $installer): void + { + if ($package->getName() === 'frankenphp') { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + return; + } + if ($package->getName() !== 'php') { + return; + } + if (SystemTarget::isUnix()) { + if ($installer->interactive) { + InteractiveTerm::indicateProgress('Running PHP smoke tests'); + } + $package->runStage([$this, 'smokeTestForUnix']); + if ($installer->interactive) { + InteractiveTerm::finish('PHP smoke tests passed'); + } + } } /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index a9a25915..6cc57439 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -106,7 +106,7 @@ class ArtifactDownloader * no-shallow-clone?: bool * } $options Downloader options */ - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { // Allow setting concurrency via options $this->parallel = max(1, (int) ($options['parallel'] ?? 1)); @@ -273,12 +273,10 @@ class ArtifactDownloader /** * Download all artifacts, with optional parallel processing. - * - * @param bool $interactive Enable interactive mode with Ctrl+C handling */ - public function download(bool $interactive = true): void + public function download(): void { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(function () { InteractiveTerm::advance(); }); @@ -311,7 +309,7 @@ class ArtifactDownloader $count = count($this->artifacts); $artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts)); // mute the first line if not interactive - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ..."); } try { @@ -329,19 +327,19 @@ class ArtifactDownloader $skipped = []; foreach ($this->artifacts as $artifact) { ++$current; - if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { + if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { $skipped[] = $artifact->getName(); continue; } $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; } - if ($interactive) { + if ($this->interactive) { $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true); } } } finally { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(null); keyboard_interrupt_unregister(); } @@ -537,7 +535,7 @@ class ArtifactDownloader return $dl->checkUpdate($artifact_name, $platform_config, null, $this); } - private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int { $queue = $this->generateQueue($artifact); // already downloaded @@ -558,7 +556,7 @@ class ArtifactDownloader }; $try_h = $try ? 'Try downloading' : 'Downloading'; logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); } // is valid download type @@ -597,13 +595,13 @@ class ArtifactDownloader } // process lock ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : ''; InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} ."); } return SPC_DOWNLOAD_STATUS_SUCCESS; } catch (DownloaderException|ExecutionException $e) { - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 23032261..b5fb8d2c 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -34,9 +34,9 @@ class InstallPackageCommand extends BaseCommand public function handle(): int { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], false); $installer->addInstallPackage($this->input->getArgument('package')); - $installer->run(true, true); + $installer->run(true); return static::SUCCESS; } } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index f011482c..4a4f75db 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -16,6 +16,7 @@ class ConfigValidator public const array PACKAGE_FIELD_TYPES = [ // package fields 'type' => ConfigType::STRING, + 'description' => ConfigType::STRING, 'depends' => ConfigType::LIST_ARRAY, // @ 'suggests' => ConfigType::LIST_ARRAY, // @ 'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT @@ -43,6 +44,7 @@ class ConfigValidator public const array PACKAGE_FIELDS = [ 'type' => true, + 'description' => false, 'depends' => false, // @ 'suggests' => false, // @ 'artifact' => false, diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index fc69cc2a..1239a30c 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -18,7 +18,7 @@ use function Laravel\Prompts\confirm; readonly class Doctor { - public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT) + public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT, public readonly bool $interactive = true) { // debug shows all loaded doctor items $items = DoctorLoader::getDoctorItems(); @@ -53,13 +53,13 @@ readonly class Doctor * Check all valid check items. * @return bool true if all checks passed, false otherwise */ - public function checkAll(bool $interactive = true): bool + public function checkAll(): bool { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { - if (!$this->checkItem($check, $interactive)) { + if (!$this->checkItem($check)) { return false; } } @@ -72,7 +72,7 @@ readonly class Doctor * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ - public function checkItem(CheckItem|string $check, bool $interactive = true): bool + public function checkItem(CheckItem|string $check): bool { if (is_string($check)) { $found = null; @@ -88,7 +88,7 @@ readonly class Doctor } $check = $found; } - $prepend = $interactive ? ' - ' : ''; + $prepend = $this->interactive ? ' - ' : ''; $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index b01b7b7b..df3b5241 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -59,8 +59,8 @@ class LinuxMuslCheck #[FixItem('fix-musl-wrapper')] public function fixMusl(): bool { - $downloader = new ArtifactDownloader(); - $downloader->add('musl-wrapper')->download(false); + $downloader = new ArtifactDownloader(interactive: false); + $downloader->add('musl-wrapper')->download(); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); $extractor->extract('musl-wrapper'); @@ -96,9 +96,9 @@ class LinuxMuslCheck Shell::passthruCallback(function () { InteractiveTerm::advance(); }); - $downloader = new ArtifactDownloader(); + $downloader = new ArtifactDownloader(interactive: false); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); - $downloader->add('musl-toolchain')->download(false); + $downloader->add('musl-toolchain')->download(); $extractor->extract('musl-toolchain'); $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; f_passthru("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); diff --git a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php index 4a0ba498..88865163 100644 --- a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php +++ b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php @@ -45,9 +45,9 @@ class PkgConfigCheck public function fix(): bool { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller(['dl-binary-only' => true]); + $installer = new PackageInstaller(['dl-binary-only' => true], interactive: false); $installer->addInstallPackage('pkg-config'); - $installer->run(false, true); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php index fce3350b..3316be3f 100644 --- a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -30,9 +30,9 @@ class Re2cVersionCheck #[FixItem('build-re2c')] public function buildRe2c(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('re2c'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php index e6a042d3..08e140f4 100644 --- a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -107,7 +107,7 @@ class WindowsToolCheck { $installer = new PackageInstaller(); $installer->addInstallPackage('strawberry-perl'); - $installer->run(false); + $installer->run(true); GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl'); return true; } @@ -116,27 +116,27 @@ class WindowsToolCheck public function installSDK(): bool { FileSystem::removeDir(getenv('PHP_SDK_PATH')); - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('php-sdk-binary-tools'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-nasm')] public function installNasm(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('nasm'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-vswhere')] public function installVSWhere(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('vswhere'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index c3a6aa9f..baa6d4cb 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -34,9 +34,9 @@ class ZigCheck #[FixItem('install-zig')] public function installZig(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('zig'); - $installer->run(false); + $installer->run(true); return $installer->isPackageInstalled('zig'); } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 628900fa..d8d745f6 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -46,7 +46,7 @@ class PackageInstaller /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */ protected ?BuildRootTracker $tracker = null; - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { ApplicationContext::set(PackageInstaller::class, $this); $builder = new PackageBuilder($options); @@ -143,7 +143,7 @@ class PackageInstaller /** * Run the package installation process. */ - public function run(bool $interactive = true, bool $disable_delay_msg = false): void + public function run(bool $disable_delay_msg = false): void { // apply build toolchain envs GlobalEnvManager::afterInit(); @@ -153,7 +153,7 @@ class PackageInstaller $this->resolvePackages(); } - if ($interactive && !$disable_delay_msg) { + if ($this->interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output $this->printInstallerInfo(); @@ -167,14 +167,17 @@ class PackageInstaller // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); - $downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]); - $downloader->addArtifacts($this->getArtifacts())->download($interactive); + $downloader = new ArtifactDownloader( + [...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))], + $this->interactive + ); + $downloader->addArtifacts($this->getArtifacts())->download(); } else { logger()->notice('Skipping download (--no-download option enabled)'); } // extract sources - $this->extractSourceArtifacts(interactive: $interactive); + $this->extractSourceArtifacts(); // validate packages foreach ($this->packages as $package) { @@ -183,7 +186,7 @@ class PackageInstaller } // build/install packages - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Building/Installing packages ...'); keyboard_interrupt_register(function () { InteractiveTerm::finish('Build/Install process interrupted by user!', false); @@ -198,7 +201,7 @@ class PackageInstaller $has_source = $package->hasSource(); if (!$is_to_build && $should_use_binary) { // install binary - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -210,17 +213,17 @@ class PackageInstaller } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -243,22 +246,20 @@ class PackageInstaller } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } } } - $this->dumpLicenseFiles($this->packages); - if ($interactive) { - InteractiveTerm::success('Exported package licenses', true); - } + // perform after-install actions and emit post-install events + $this->emitPostInstallEvents(); } public function isBuildPackage(Package|string $package): bool @@ -311,6 +312,17 @@ class PackageInstaller return false; } + public function emitPostInstallEvents(): void + { + foreach ($this->packages as $package) { + if ($package->hasStage('postInstall')) { + $package->runStage('postInstall'); + } + } + + $this->dumpLicenseFiles($this->packages); + } + /** * Returns the download status of all artifacts for the resolved packages. * @@ -368,7 +380,7 @@ class PackageInstaller /** * Extract all artifacts for resolved packages. */ - public function extractSourceArtifacts(bool $interactive = true): void + public function extractSourceArtifacts(): void { FileSystem::createDir(SOURCE_PATH); $packages = array_values($this->packages); @@ -403,7 +415,7 @@ class PackageInstaller } // Extract each artifact - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...'); InteractiveTerm::indicateProgress('Extracting artifacts'); } @@ -411,7 +423,7 @@ class PackageInstaller try { V2CompatLayer::beforeExtsExtractHook(); foreach ($artifacts as $artifact) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName())); } if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { @@ -423,12 +435,12 @@ class PackageInstaller } } V2CompatLayer::afterExtsExtractHook(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Extracted all sources successfully.'); echo PHP_EOL; } } catch (\Throwable $e) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Artifact extraction failed!', false); echo PHP_EOL; } @@ -525,6 +537,9 @@ class PackageInstaller } } $dumper->dump(BUILD_ROOT_PATH . '/license'); + if ($this->interactive) { + InteractiveTerm::success('Exported package licenses', true); + } } /**