From 808aed2a6620fd7312a626c50265a7cbed38f612 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 14:58:11 +0800 Subject: [PATCH] Refactor package stage handling and update class structures for improved flexibility --- composer.json | 1 + composer.lock | 163 +++++++++++++++++- .../Command/SwitchPhpVersionCommand.php | 2 +- src/Package/Extension/readline.php | 5 +- src/Package/Library/imap.php | 3 +- src/Package/Library/libedit.php | 2 +- src/Package/Library/postgresql.php | 3 +- src/Package/Target/micro.php | 2 +- src/Package/Target/php.php | 48 +++--- src/StaticPHP/Artifact/ArtifactDownloader.php | 1 + src/StaticPHP/Artifact/ArtifactExtractor.php | 1 + .../Attribute/Package/AfterStage.php | 2 +- .../Attribute/Package/BeforeStage.php | 9 +- src/StaticPHP/Attribute/Package/Stage.php | 2 +- src/StaticPHP/Command/BuildTargetCommand.php | 2 +- src/StaticPHP/Command/DownloadCommand.php | 2 +- src/StaticPHP/Command/ExtractCommand.php | 4 +- src/StaticPHP/ConsoleApplication.php | 6 +- src/StaticPHP/Doctor/Doctor.php | 1 + src/StaticPHP/Doctor/Item/ZigCheck.php | 7 - src/StaticPHP/Exception/ExceptionHandler.php | 3 + src/StaticPHP/Exception/RegistryException.php | 7 + src/StaticPHP/Package/Package.php | 38 ++-- src/StaticPHP/Package/PackageInstaller.php | 1 + src/StaticPHP/Package/PhpExtensionPackage.php | 63 ++++--- .../{Artifact => Registry}/ArtifactLoader.php | 3 +- .../{Doctor => Registry}/DoctorLoader.php | 2 +- .../{Package => Registry}/PackageLoader.php | 121 +++++++++++-- src/StaticPHP/Registry/Registry.php | 27 +-- 29 files changed, 416 insertions(+), 115 deletions(-) create mode 100644 src/StaticPHP/Exception/RegistryException.php rename src/StaticPHP/{Artifact => Registry}/ArtifactLoader.php (99%) rename src/StaticPHP/{Doctor => Registry}/DoctorLoader.php (99%) rename src/StaticPHP/{Package => Registry}/PackageLoader.php (64%) diff --git a/composer.json b/composer.json index 360cdbdb..eadd2732 100644 --- a/composer.json +++ b/composer.json @@ -13,6 +13,7 @@ "ext-mbstring": "*", "ext-zlib": "*", "laravel/prompts": "~0.1", + "nette/php-generator": "^4.2", "php-di/php-di": "^7.1", "symfony/console": "^5.4 || ^6 || ^7", "symfony/process": "^7.2", diff --git a/composer.lock b/composer.lock index 6afc39df..a0538ce8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "14b3ad42c138807fa9288e6b510ac69f", + "content-hash": "edb3243ddaa8b05d8f6545266a146e93", "packages": [ { "name": "laravel/prompts", @@ -126,6 +126,167 @@ }, "time": "2025-11-21T20:52:36+00:00" }, + { + "name": "nette/php-generator", + "version": "v4.2.0", + "source": { + "type": "git", + "url": "https://github.com/nette/php-generator.git", + "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/php-generator/zipball/4707546a1f11badd72f5d82af4f8a6bc64bd56ac", + "reference": "4707546a1f11badd72f5d82af4f8a6bc64bd56ac", + "shasum": "" + }, + "require": { + "nette/utils": "^4.0.6", + "php": "8.1 - 8.5" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.4", + "nikic/php-parser": "^5.0", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.8" + }, + "suggest": { + "nikic/php-parser": "to use ClassType::from(withBodies: true) & ClassType::fromCode()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.2-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🐘 Nette PHP Generator: generates neat PHP code for you. Supports new PHP 8.5 features.", + "homepage": "https://nette.org", + "keywords": [ + "code", + "nette", + "php", + "scaffolding" + ], + "support": { + "issues": "https://github.com/nette/php-generator/issues", + "source": "https://github.com/nette/php-generator/tree/v4.2.0" + }, + "time": "2025-08-06T18:24:31+00:00" + }, + { + "name": "nette/utils", + "version": "v4.1.0", + "source": { + "type": "git", + "url": "https://github.com/nette/utils.git", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nette/utils/zipball/fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "reference": "fa1f0b8261ed150447979eb22e373b7b7ad5a8e0", + "shasum": "" + }, + "require": { + "php": "8.2 - 8.5" + }, + "conflict": { + "nette/finder": "<3", + "nette/schema": "<1.2.2" + }, + "require-dev": { + "jetbrains/phpstorm-attributes": "^1.2", + "nette/tester": "^2.5", + "phpstan/phpstan-nette": "^2.0@stable", + "tracy/tracy": "^2.9" + }, + "suggest": { + "ext-gd": "to use Image", + "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()", + "ext-json": "to use Nette\\Utils\\Json", + "ext-mbstring": "to use Strings::lower() etc...", + "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.1-dev" + } + }, + "autoload": { + "psr-4": { + "Nette\\": "src" + }, + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause", + "GPL-2.0-only", + "GPL-3.0-only" + ], + "authors": [ + { + "name": "David Grudl", + "homepage": "https://davidgrudl.com" + }, + { + "name": "Nette Community", + "homepage": "https://nette.org/contributors" + } + ], + "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.", + "homepage": "https://nette.org", + "keywords": [ + "array", + "core", + "datetime", + "images", + "json", + "nette", + "paginator", + "password", + "slugify", + "string", + "unicode", + "utf-8", + "utility", + "validation" + ], + "support": { + "issues": "https://github.com/nette/utils/issues", + "source": "https://github.com/nette/utils/tree/v4.1.0" + }, + "time": "2025-12-01T17:49:23+00:00" + }, { "name": "php-di/invoker", "version": "2.3.7", diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php index 38649dd7..3782a645 100644 --- a/src/Package/Command/SwitchPhpVersionCommand.php +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Command\BaseCommand; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; diff --git a/src/Package/Extension/readline.php b/src/Package/Extension/readline.php index 6395057e..80f3fa33 100644 --- a/src/Package/Extension/readline.php +++ b/src/Package/Extension/readline.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Package\Extension; +use Package\Target\php; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; @@ -15,7 +16,7 @@ use StaticPHP\Util\SourcePatcher; #[Extension('readline')] class readline { - #[BeforeStage('php', 'unix-make-cli', 'ext-readline')] + #[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')] #[PatchDescription('Fix readline static build with musl')] public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { @@ -25,7 +26,7 @@ class readline } } - #[AfterStage('php', 'unix-make-cli', 'ext-readline')] + #[AfterStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')] public function afterMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { if ($toolchain->isStatic()) { diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index a80ff015..bacfbe2e 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Package\Library; +use Package\Target\php; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\PatchDescription; @@ -13,7 +14,7 @@ use StaticPHP\Util\FileSystem; #[Library('imap')] class imap { - #[AfterStage('php', 'patch-embed-scripts', 'imap')] + #[AfterStage('php', [php::class, 'patchEmbedScripts'], 'imap')] #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] public function afterPatchScripts(): void { diff --git a/src/Package/Library/libedit.php b/src/Package/Library/libedit.php index 2dac2817..08a435da 100644 --- a/src/Package/Library/libedit.php +++ b/src/Package/Library/libedit.php @@ -14,7 +14,7 @@ use StaticPHP\Util\FileSystem; #[Library('libedit')] class libedit extends LibraryPackage { - #[BeforeStage('libedit', 'build')] + #[BeforeStage(stage: 'build')] public function patchBeforeBuild(): void { FileSystem::replaceFileRegex( diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 6636ccad..bd96da2c 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Package\Library; +use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\PatchDescription; @@ -12,7 +13,7 @@ use StaticPHP\Package\TargetPackage; #[Library('postgresql')] class postgresql { - #[BeforeStage('php', 'unix-configure', 'postgresql')] + #[BeforeStage('php', [php::class, 'configureForUnix'], 'postgresql')] #[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')] public function patchBeforePHPConfigure(TargetPackage $package): void { diff --git a/src/Package/Target/micro.php b/src/Package/Target/micro.php index a95d4b4d..64772efc 100644 --- a/src/Package/Target/micro.php +++ b/src/Package/Target/micro.php @@ -13,7 +13,7 @@ use StaticPHP\Util\FileSystem; #[Target('php-micro')] class micro { - #[BeforeStage('php', 'unix-make-embed', 'php-micro')] + #[BeforeStage('php', [php::class, 'makeEmbedForUnix'], 'php-micro')] #[PatchDescription('Patch Makefile to build only libphp.la for embedding')] public function patchBeforeEmbed(TargetPackage $package): void { diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 4d3683ce..90c68a24 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Package\Target; -use StaticPHP\Artifact\ArtifactLoader; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Info; @@ -21,9 +20,10 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\Package; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; -use StaticPHP\Package\PackageLoader; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\TargetPackage; +use StaticPHP\Registry\ArtifactLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ToolchainManager; @@ -241,7 +241,7 @@ class php FileSystem::removeDir(BUILD_MODULES_PATH); } - #[BeforeStage('php', 'unix-buildconf')] + #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] public function patchBeforeBuildconf(TargetPackage $package): void @@ -259,7 +259,7 @@ class php FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } - #[Stage('unix-buildconf')] + #[Stage] public function buildconfForUnix(TargetPackage $package): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); @@ -267,7 +267,7 @@ class php shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } - #[Stage('unix-configure')] + #[Stage] public function configureForUnix(TargetPackage $package, PackageInstaller $installer): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./configure')); @@ -317,7 +317,7 @@ class php ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } - #[Stage('unix-make')] + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { V2CompatLayer::emitPatchPoint('before-php-make'); @@ -326,23 +326,23 @@ class php shell()->cd($package->getSourceDir())->exec('make clean'); if ($installer->isPackageResolved('php-cli')) { - $package->runStage('unix-make-cli'); + $package->runStage([self::class, 'makeCliForUnix']); } if ($installer->isPackageResolved('php-cgi')) { - $package->runStage('unix-make-cgi'); + $package->runStage([self::class, 'makeCgiForUnix']); } if ($installer->isPackageResolved('php-fpm')) { - $package->runStage('unix-make-fpm'); + $package->runStage([self::class, 'makeFpmForUnix']); } if ($installer->isPackageResolved('php-micro')) { - $package->runStage('unix-make-micro'); + $package->runStage([self::class, 'makeMicroForUnix']); } if ($installer->isPackageResolved('php-embed')) { - $package->runStage('unix-make-embed'); + $package->runStage([self::class, 'makeEmbedForUnix']); } } - #[Stage('unix-make-cli')] + #[Stage] public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); @@ -352,7 +352,7 @@ class php ->exec("make -j{$concurrency} cli"); } - #[Stage('unix-make-cgi')] + #[Stage] public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); @@ -362,7 +362,7 @@ class php ->exec("make -j{$concurrency} cgi"); } - #[Stage('unix-make-fpm')] + #[Stage] public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); @@ -372,7 +372,7 @@ class php ->exec("make -j{$concurrency} fpm"); } - #[Stage('unix-make-micro')] + #[Stage] #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { @@ -399,7 +399,7 @@ class php } } - #[Stage('unix-make-embed')] + #[Stage] public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); @@ -452,10 +452,10 @@ class php UnixUtil::exportDynamicSymbols($libphp_a); // deploy embed php scripts - $package->runStage('patch-embed-scripts'); + $package->runStage([$this, 'patchEmbedScripts']); } - #[Stage('unix-build-shared-ext')] + #[Stage] public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void { // collect shared extensions @@ -506,18 +506,18 @@ class php return; } - $package->runStage('unix-buildconf'); - $package->runStage('unix-configure'); - $package->runStage('unix-make'); + $package->runStage([$this, 'buildconfForUnix']); + $package->runStage([$this, 'configureForUnix']); + $package->runStage([$this, 'makeForUnix']); - $package->runStage('unix-build-shared-ext'); + $package->runStage([$this, 'unixBuildSharedExt']); } /** * Patch phpize and php-config if needed */ - #[Stage('patch-embed-scripts')] - public function patchPhpScripts(): void + #[Stage] + public function patchEmbedScripts(): void { // patch phpize if (file_exists(BUILD_BIN_PATH . '/phpize')) { diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 2b7ac0de..315cfb11 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -24,6 +24,7 @@ use StaticPHP\Exception\ExecutionException; use StaticPHP\Exception\SPCException; use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 11b738a7..f860ec0f 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -9,6 +9,7 @@ use StaticPHP\Exception\FileSystemException; use StaticPHP\Exception\SPCInternalException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\Package; +use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; diff --git a/src/StaticPHP/Attribute/Package/AfterStage.php b/src/StaticPHP/Attribute/Package/AfterStage.php index 3c611d13..466a2d3a 100644 --- a/src/StaticPHP/Attribute/Package/AfterStage.php +++ b/src/StaticPHP/Attribute/Package/AfterStage.php @@ -10,5 +10,5 @@ namespace StaticPHP\Attribute\Package; #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] readonly class AfterStage { - public function __construct(public string $package_name, public string $stage, public ?string $only_when_package_resolved = null) {} + public function __construct(public string $package_name, public array|string $stage, public ?string $only_when_package_resolved = null) {} } diff --git a/src/StaticPHP/Attribute/Package/BeforeStage.php b/src/StaticPHP/Attribute/Package/BeforeStage.php index c781a4e6..182f6b5b 100644 --- a/src/StaticPHP/Attribute/Package/BeforeStage.php +++ b/src/StaticPHP/Attribute/Package/BeforeStage.php @@ -8,7 +8,12 @@ namespace StaticPHP\Attribute\Package; * Indicates that the annotated method should be executed before a specific stage of the build process for a given package. */ #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] -readonly class BeforeStage +class BeforeStage { - public function __construct(public string $package_name, public string $stage, public ?string $only_when_package_resolved = null) {} + public readonly array|string $stage; + + public function __construct(public string $package_name = '', array|callable|string $stage = '', public ?string $only_when_package_resolved = null) + { + $this->stage = $stage; + } } diff --git a/src/StaticPHP/Attribute/Package/Stage.php b/src/StaticPHP/Attribute/Package/Stage.php index e801cddf..9f88bc94 100644 --- a/src/StaticPHP/Attribute/Package/Stage.php +++ b/src/StaticPHP/Attribute/Package/Stage.php @@ -10,5 +10,5 @@ namespace StaticPHP\Attribute\Package; #[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)] readonly class Stage { - public function __construct(public string $name) {} + public function __construct(public ?string $function = null) {} } diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index 5efb9f1a..e66f514b 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -6,7 +6,7 @@ namespace StaticPHP\Command; use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\Package\PackageInstaller; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\V2CompatLayer; use Symfony\Component\Console\Input\InputOption; diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 92b80be1..277585e5 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -6,7 +6,7 @@ namespace StaticPHP\Command; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\DownloaderOptions; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index d28d2bbe..14951a34 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -6,9 +6,9 @@ namespace StaticPHP\Command; use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactExtractor; -use StaticPHP\Artifact\ArtifactLoader; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Registry\ArtifactLoader; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 0484c111..a12227fc 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -13,8 +13,9 @@ use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\ExtractCommand; use StaticPHP\Command\InstallPackageCommand; use StaticPHP\Command\SPCConfigCommand; -use StaticPHP\Package\PackageLoader; use StaticPHP\Package\TargetPackage; +use StaticPHP\Registry\PackageLoader; +use StaticPHP\Registry\Registry; use Symfony\Component\Console\Application; class ConsoleApplication extends Application @@ -29,6 +30,9 @@ class ConsoleApplication extends Application require_once ROOT_DIR . '/src/bootstrap.php'; + // check registry + Registry::checkLoadedRegistries(); + /** * @var string $name * @var TargetPackage $package diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 692a5b8c..22ca10f2 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -7,6 +7,7 @@ namespace StaticPHP\Doctor; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; +use StaticPHP\Registry\DoctorLoader; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index c8d00574..4157e9d6 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -38,13 +38,6 @@ class ZigCheck #[FixItem('install-zig')] public function installZig(): bool { - $arch = arch2gnu(php_uname('m')); - $os = match (PHP_OS_FAMILY) { - 'Windows' => 'win', - 'Darwin' => 'macos', - 'BSD' => 'freebsd', - default => 'linux', - }; $installer = new PackageInstaller(); $installer->addInstallPackage('zig'); $installer->run(false); diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 36ed1a63..db11b8fd 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -25,11 +25,13 @@ class ExceptionHandler SPCInternalException::class, ValidationException::class, WrongUsageException::class, + RegistryException::class, ]; public const array MINOR_LOG_EXCEPTIONS = [ InterruptException::class, WrongUsageException::class, + RegistryException::class, ]; /** @var null|BuilderBase Builder binding */ @@ -52,6 +54,7 @@ class ExceptionHandler SPCInternalException::class => "✗ SPC internal error: {$e->getMessage()}", ValidationException::class => "⚠ Validation failed: {$e->getMessage()}", WrongUsageException::class => $e->getMessage(), + RegistryException::class => "✗ Registry parsing error: {$e->getMessage()}", default => "✗ Unknown SPC exception {$class}: {$e->getMessage()}", }; self::logError($head_msg); diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php new file mode 100644 index 00000000..347a132a --- /dev/null +++ b/src/StaticPHP/Exception/RegistryException.php @@ -0,0 +1,7 @@ +stages[$name])) { + if (!$this->hasStage($name)) { + $name = match (true) { + is_string($name) => $name, + is_array($name) && count($name) === 2 => $name[1], // use function name + default => '{' . gettype($name) . '}', + }; throw new SPCInternalException("Stage '{$name}' is not defined for package '{$this->name}'."); } + $name = match (true) { + is_string($name) => $name, + is_array($name) && count($name) === 2 => $name[1], // use function name + default => throw new SPCInternalException('Invalid stage name type: ' . gettype($name)), + }; // Merge package context with provided context /** @noinspection PhpDuplicateArrayKeysInspection */ @@ -80,9 +91,6 @@ abstract class Package /** * Add a stage to the package. - * - * @param string $name Stage name - * @param callable $stage Stage callable */ public function addStage(string $name, callable $stage): void { @@ -92,11 +100,17 @@ abstract class Package /** * Check if the package has a specific stage defined. * - * @param string $name Stage name + * @param mixed $name Stage name */ - public function hasStage(string $name): bool + public function hasStage(mixed $name): bool { - return isset($this->stages[$name]); + if (is_array($name) && count($name) === 2) { + return isset($this->stages[$name[1]]); // use function name + } + if (is_string($name)) { + return isset($this->stages[$name]); // use defined name + } + return false; } /** diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 4be98090..9770a737 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -11,6 +11,7 @@ use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Artifact\DownloaderOptions; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 667d9688..7853be08 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace StaticPHP\Package; +use StaticPHP\Attribute\Package\Stage; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\ValidationException; @@ -100,22 +101,6 @@ class PhpExtensionPackage extends Package public function setBuildShared(bool $build_shared = true): void { $this->build_shared = $build_shared; - // Add build stages for shared build on Unix-like systems - // TODO: Windows shared build support - if ($build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { - if (!$this->hasStage('build')) { - $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, '_buildSharedUnix']); - } - if (!$this->hasStage('phpize')) { - $this->addStage('phpize', [$this, '_phpize']); - } - if (!$this->hasStage('configure')) { - $this->addStage('configure', [$this, '_configure']); - } - if (!$this->hasStage('make')) { - $this->addStage('make', [$this, '_make']); - } - } } public function setBuildStatic(bool $build_static = true): void @@ -180,18 +165,18 @@ class PhpExtensionPackage extends Package /** * @internal - * #[Stage('phpize')] */ - public function _phpize(array $env, PhpExtensionPackage $package): void + #[Stage] + public function phpizeForUnix(array $env, PhpExtensionPackage $package): void { shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize'); } /** * @internal - * #[Stage('configure')] */ - public function _configure(array $env, PhpExtensionPackage $package): void + #[Stage] + public function configureForUnix(array $env, PhpExtensionPackage $package): void { $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; shell()->cd($package->getSourceDir()) @@ -205,9 +190,9 @@ class PhpExtensionPackage extends Package /** * @internal - * #[Stage('make')] */ - public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void + #[Stage] + public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void { shell()->cd($package->getSourceDir()) ->setEnv($env) @@ -222,13 +207,13 @@ class PhpExtensionPackage extends Package * @internal * #[Stage('build')] */ - public function _buildSharedUnix(PackageBuilder $builder): void + public function buildSharedForUnix(PackageBuilder $builder): void { $env = $this->getSharedExtensionEnv(); - $this->runStage('phpize', ['env' => $env]); - $this->runStage('configure', ['env' => $env]); - $this->runStage('make', ['env' => $env]); + $this->runStage('phpizeForUnix', ['env' => $env]); + $this->runStage('configureForUnix', ['env' => $env]); + $this->runStage('makeForUnix', ['env' => $env]); // process *.so file $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; @@ -238,6 +223,32 @@ class PhpExtensionPackage extends Package $builder->deployBinary($soFile, $soFile, false); } + /** + * Register default stages if not already defined by attributes. + * This is called after all attributes have been loaded. + * + * @internal Called by PackageLoader after loading attributes + */ + public function registerDefaultStages(): void + { + // Add build stages for shared build on Unix-like systems + // TODO: Windows shared build support + if ($this->build_shared && in_array(SystemTarget::getTargetOS(), ['Linux', 'Darwin'])) { + if (!$this->hasStage('build')) { + $this->addBuildFunction(SystemTarget::getTargetOS(), [$this, 'buildSharedForUnix']); + } + if (!$this->hasStage('phpizeForUnix')) { + $this->addStage('phpizeForUnix', [$this, 'phpizeForUnix']); + } + if (!$this->hasStage('configureForUnix')) { + $this->addStage('configureForUnix', [$this, 'configureForUnix']); + } + if (!$this->hasStage('makeForUnix')) { + $this->addStage('makeForUnix', [$this, 'makeForUnix']); + } + } + } + /** * Splits a given string of library flags into static and shared libraries. * diff --git a/src/StaticPHP/Artifact/ArtifactLoader.php b/src/StaticPHP/Registry/ArtifactLoader.php similarity index 99% rename from src/StaticPHP/Artifact/ArtifactLoader.php rename to src/StaticPHP/Registry/ArtifactLoader.php index 6a839cb4..22942452 100644 --- a/src/StaticPHP/Artifact/ArtifactLoader.php +++ b/src/StaticPHP/Registry/ArtifactLoader.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace StaticPHP\Artifact; +namespace StaticPHP\Registry; +use StaticPHP\Artifact\Artifact; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\Artifact\BinaryExtract; diff --git a/src/StaticPHP/Doctor/DoctorLoader.php b/src/StaticPHP/Registry/DoctorLoader.php similarity index 99% rename from src/StaticPHP/Doctor/DoctorLoader.php rename to src/StaticPHP/Registry/DoctorLoader.php index 2bbbbd62..e992d556 100644 --- a/src/StaticPHP/Doctor/DoctorLoader.php +++ b/src/StaticPHP/Registry/DoctorLoader.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace StaticPHP\Doctor; +namespace StaticPHP\Registry; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php similarity index 64% rename from src/StaticPHP/Package/PackageLoader.php rename to src/StaticPHP/Registry/PackageLoader.php index 5093dd7b..15ff0f4a 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace StaticPHP\Package; +namespace StaticPHP\Registry; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; @@ -19,8 +19,13 @@ use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\Package\Validate; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; -use StaticPHP\Exception\ValidationException; +use StaticPHP\Exception\RegistryException; use StaticPHP\Exception\WrongUsageException; +use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\Package; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Package\TargetPackage; use StaticPHP\Util\FileSystem; class PackageLoader @@ -30,9 +35,7 @@ class PackageLoader private static array $before_stages = []; - private static array $after_stage = []; - - private static array $patch_before_builds = []; + private static array $after_stages = []; /** @var array Track loaded classes to prevent duplicates */ private static array $loaded_classes = []; @@ -53,7 +56,7 @@ class PackageLoader if ($pkg !== null) { self::$packages[$name] = $pkg; } else { - throw new WrongUsageException("Package [{$name}] has unknown type [{$item['type']}]"); + throw new RegistryException("Package [{$name}] has unknown type [{$item['type']}]"); } } } @@ -156,7 +159,7 @@ class PackageLoader } $package_type = PackageConfig::get($attribute_instance->name, 'type'); if ($package_type === null) { - throw new WrongUsageException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); + throw new RegistryException("Package [{$attribute_instance->name}] not defined in config, please check your config files."); } // if class has parent class and matches the attribute instance, use custom class @@ -181,10 +184,10 @@ class PackageLoader default => null, }; if (!in_array($package_type, $pkg_type_attr, true)) { - throw new ValidationException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].'); + throw new RegistryException("Package [{$attribute_instance->name}] type mismatch: config type is [{$package_type}], but attribute type is [" . implode('|', $pkg_type_attr) . '].'); } if ($pkg !== null && !PackageConfig::isPackageExists($pkg->getName())) { - throw new ValidationException("Package [{$pkg->getName()}] config not found for class {$class}"); + throw new RegistryException("Package [{$pkg->getName()}] config not found for class {$class}"); } // init method attributes @@ -199,7 +202,7 @@ class PackageLoader // #[CustomPhpConfigureArg(PHP_OS_FAMILY)] CustomPhpConfigureArg::class => self::bindCustomPhpConfigureArg($pkg, $method_attribute->newInstance(), [$instance_class, $method->getName()]), // #[Stage('stage_name')] - Stage::class => $pkg->addStage($method_attribute->newInstance()->name, [$instance_class, $method->getName()]), + Stage::class => self::addStage($method, $pkg, $instance_class, $method_instance), // #[InitPackage] (run now with package context) InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], [ Package::class => $pkg, @@ -232,9 +235,9 @@ class PackageLoader $method_instance = $method_attribute->newInstance(); match ($method_attribute->getName()) { // #[BeforeStage('package_name', 'stage')] and #[AfterStage('package_name', 'stage')] - BeforeStage::class => self::$before_stages[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved], - AfterStage::class => self::$after_stage[$method_instance->package_name][$method_instance->stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved], - // #[PatchBeforeBuild() + BeforeStage::class => self::addBeforeStage($method, $pkg ?? null, $instance_class, $method_instance), + AfterStage::class => self::addAfterStage($method, $pkg ?? null, $instance_class, $method_instance), + default => null, }; } @@ -258,7 +261,7 @@ class PackageLoader { // match condition $installer = ApplicationContext::get(PackageInstaller::class); - $stages = self::$after_stage[$package_name][$stage] ?? []; + $stages = self::$after_stages[$package_name][$stage] ?? []; $result = []; foreach ($stages as [$callback, $only_when_package_resolved]) { if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { @@ -269,9 +272,53 @@ class PackageLoader return $result; } - public static function getPatchBeforeBuildCallbacks(string $package_name): array + /** + * Register default stages for all PhpExtensionPackage instances. + * Should be called after all registries have been loaded. + */ + public static function registerAllDefaultStages(): void { - return self::$patch_before_builds[$package_name] ?? []; + foreach (self::$packages as $pkg) { + if ($pkg instanceof PhpExtensionPackage) { + $pkg->registerDefaultStages(); + } + } + } + + /** + * Check loaded stage events for consistency. + */ + public static function checkLoadedStageEvents(): void + { + foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) { + foreach ($ev_all as $package_name => $stages) { + // check package exists + if (!self::hasPackage($package_name)) { + throw new RegistryException( + "{$event_name} event registered for unknown package [{$package_name}]." + ); + } + $pkg = self::getPackage($package_name); + foreach ($stages as $stage_name => $before_events) { + foreach ($before_events as [$event_callable, $only_when_package_resolved]) { + // check only_when_package_resolved package exists + if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) { + throw new RegistryException("{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}]."); + } + // check callable is valid + if (!is_callable($event_callable)) { + throw new RegistryException( + "{$event_name} event in package [{$package_name}] for stage [{$stage_name}] has invalid callable.", + ); + } + } + // check stage exists + if (!$pkg->hasStage($stage_name)) { + throw new RegistryException("Package stage [{$stage_name}] is not registered in package [{$package_name}]."); + } + } + } + } } /** @@ -280,7 +327,7 @@ class PackageLoader private static function bindCustomPhpConfigureArg(Package $pkg, object $attr, callable $fn): void { if (!$pkg instanceof PhpExtensionPackage) { - throw new ValidationException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute."); + throw new RegistryException("Class [{$pkg->getName()}] must implement PhpExtensionPackage for CustomPhpConfigureArg attribute."); } $pkg->addCustomPhpConfigureArgCallback($attr->os, $fn); } @@ -289,4 +336,44 @@ class PackageLoader { $pkg->addBuildFunction($attr->os, $fn); } + + private static function addStage(\ReflectionMethod $method, Package $pkg, object $instance_class, object $method_instance): void + { + $name = $method_instance->function; + if ($name === null) { + $name = $method->getName(); + } + $pkg->addStage($name, [$instance_class, $method->getName()]); + } + + private static function addBeforeStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void + { + /** @var BeforeStage $method_instance */ + $stage = $method_instance->stage; + $stage = match (true) { + is_string($stage) => $stage, + is_array($stage) && count($stage) === 2 => $stage[1], + default => throw new RegistryException('Invalid stage definition in BeforeStage attribute.'), + }; + if ($method_instance->package_name === '' && $pkg === null) { + throw new RegistryException('Package name must not be empty when no package context is available for BeforeStage attribute.'); + } + $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; + self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + } + + private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void + { + $stage = $method_instance->stage; + $stage = match (true) { + is_string($stage) => $stage, + is_array($stage) && count($stage) === 2 => $stage[1], + default => throw new RegistryException('Invalid stage definition in AfterStage attribute.'), + }; + if ($method_instance->package_name === '' && $pkg === null) { + throw new RegistryException('Package name must not be empty when no package context is available for AfterStage attribute.'); + } + $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; + self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + } } diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 1b579ef2..71d53f82 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -4,13 +4,10 @@ declare(strict_types=1); namespace StaticPHP\Registry; -use StaticPHP\Artifact\ArtifactLoader; use StaticPHP\Config\ArtifactConfig; use StaticPHP\Config\PackageConfig; use StaticPHP\ConsoleApplication; -use StaticPHP\Doctor\DoctorLoader; -use StaticPHP\Exception\EnvironmentException; -use StaticPHP\Package\PackageLoader; +use StaticPHP\Exception\RegistryException; use StaticPHP\Util\FileSystem; use Symfony\Component\Yaml\Yaml; @@ -30,19 +27,19 @@ class Registry { $yaml = file_get_contents($registry_file); if ($yaml === false) { - throw new EnvironmentException("Failed to read registry file: {$registry_file}"); + throw new RegistryException("Failed to read registry file: {$registry_file}"); } $data = match (pathinfo($registry_file, PATHINFO_EXTENSION)) { 'json' => json_decode($yaml, true), 'yaml', 'yml' => Yaml::parse($yaml), - default => throw new EnvironmentException("Unsupported registry file format: {$registry_file}"), + default => throw new RegistryException("Unsupported registry file format: {$registry_file}"), }; if (!is_array($data)) { - throw new EnvironmentException("Invalid registry format in file: {$registry_file}"); + throw new RegistryException("Invalid registry format in file: {$registry_file}"); } $registry_name = $data['name'] ?? null; if (!is_string($registry_name) || empty($registry_name)) { - throw new EnvironmentException("Registry 'name' is missing or invalid in file: {$registry_file}"); + throw new RegistryException("Registry 'name' is missing or invalid in file: {$registry_file}"); } // Prevent loading the same registry twice @@ -190,6 +187,16 @@ class Registry } } + public static function checkLoadedRegistries(): void + { + // Register default stages for all PhpExtensionPackage instances + // This must be done after all registries are loaded to ensure custom stages take precedence + PackageLoader::registerAllDefaultStages(); + + // check BeforeStage, AfterStage is valid + PackageLoader::checkLoadedStageEvents(); + } + /** * Get list of loaded registry names. * @@ -252,7 +259,7 @@ class Registry } // Class not found and no file path provided - throw new EnvironmentException( + throw new RegistryException( "Class '{$class}' not found. For external registries, either:\n" . " 1. Add an 'autoload' entry pointing to your composer autoload file\n" . " 2. Use 'psr-4' instead of 'classes' for auto-discovery\n" . @@ -272,7 +279,7 @@ class Registry $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; } if (!file_exists($path)) { - throw new EnvironmentException("Path does not exist: {$path}"); + throw new RegistryException("Path does not exist: {$path}"); } return FileSystem::convertPath($path); }