From a1cadecc54ce4f57a5f3cda8eae8dfcf5c9f3af9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 12:45:12 +0800 Subject: [PATCH 01/19] Refactor re2c fix-item --- src/StaticPHP/Doctor/Item/Re2cVersionCheck.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php index eb9b917f..fce3350b 100644 --- a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -7,6 +7,7 @@ namespace StaticPHP\Doctor\Item; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Package\PackageInstaller; class Re2cVersionCheck { @@ -29,7 +30,9 @@ class Re2cVersionCheck #[FixItem('build-re2c')] public function buildRe2c(): bool { - // TODO: implement re2c build process - return false; + $installer = new PackageInstaller(); + $installer->addInstallPackage('re2c'); + $installer->run(false); + return true; } } From 80d922ab3b04f7c44b730cb6b1e379d9b212fc72 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:58:04 +0800 Subject: [PATCH 02/19] Use patch for current package exclusively --- src/Package/Extension/readline.php | 6 ++++-- src/Package/Library/imap.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Package/Extension/readline.php b/src/Package/Extension/readline.php index 2ecc533a..6395057e 100644 --- a/src/Package/Extension/readline.php +++ b/src/Package/Extension/readline.php @@ -7,6 +7,7 @@ namespace Package\Extension; use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PackageInstaller; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Util\SourcePatcher; @@ -14,7 +15,8 @@ use StaticPHP\Util\SourcePatcher; #[Extension('readline')] class readline { - #[BeforeStage('php', 'unix-make-cli')] + #[BeforeStage('php', 'unix-make-cli', 'ext-readline')] + #[PatchDescription('Fix readline static build with musl')] public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void { if ($toolchain->isStatic()) { @@ -23,7 +25,7 @@ class readline } } - #[AfterStage('php', 'unix-make-cli')] + #[AfterStage('php', 'unix-make-cli', '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 58e9397f..a80ff015 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -13,7 +13,7 @@ use StaticPHP\Util\FileSystem; #[Library('imap')] class imap { - #[AfterStage('php', 'patch-embed-scripts')] + #[AfterStage('php', 'patch-embed-scripts', 'imap')] #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] public function afterPatchScripts(): void { From 20e0711747d6b2fd7ffe12230ec3bdb35146d976 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:58:42 +0800 Subject: [PATCH 03/19] Add libedit package build --- src/Package/Library/libedit.php | 37 +++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/Package/Library/libedit.php diff --git a/src/Package/Library/libedit.php b/src/Package/Library/libedit.php new file mode 100644 index 00000000..2dac2817 --- /dev/null +++ b/src/Package/Library/libedit.php @@ -0,0 +1,37 @@ +getSourceDir()}/src/sys.h", + '|//#define\s+strl|', + '#define strl' + ); + } + + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(): void + { + UnixAutoconfExecutor::create($this) + ->appendEnv(['CFLAGS' => '-D__STDC_ISO_10646__=201103L']) + ->configure() + ->make(); + $this->patchPkgconfPrefix(['libedit.pc']); + } +} From 11e7a590c89eeae49c1fbecfb64b235a7b17bb10 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:58:47 +0800 Subject: [PATCH 04/19] Add ncurses package build --- src/Package/Library/ncurses.php | 61 +++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/Package/Library/ncurses.php diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php new file mode 100644 index 00000000..f0a21714 --- /dev/null +++ b/src/Package/Library/ncurses.php @@ -0,0 +1,61 @@ +appendEnv([ + 'LDFLAGS' => $toolchain->isStatic() ? '-static' : '', + ]) + ->configure( + '--enable-overwrite', + '--with-curses-h', + '--enable-pc-files', + '--enable-echo', + '--disable-widec', + '--with-normal', + '--with-ticlib', + '--without-tests', + '--without-dlsym', + '--without-debug', + '-enable-symlinks', + "--bindir={$package->getBinDir()}", + "--includedir={$package->getIncludeDir()}", + "--libdir={$package->getLibDir()}", + "--prefix={$package->getBuildRootPath()}", + ) + ->make(); + $new_files = $dirdiff->getIncrementFiles(true); + foreach ($new_files as $file) { + @unlink(BUILD_BIN_PATH . '/' . $file); + } + + shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); + shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); + + $pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc']; + $package->patchPkgconfPrefix($pkgconf_list); + + foreach ($pkgconf_list as $pkgconf) { + FileSystem::replaceFileStr("{$package->getLibDir()}/pkgconfig/{$pkgconf}", "-L{$package->getLibDir()}", '-L${libdir}'); + } + } +} From 321f2e13e88e54e8eafca3afb5007e1841fb1171 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 16:59:38 +0800 Subject: [PATCH 05/19] Allow all types of package can be built --- src/StaticPHP/Package/LibraryPackage.php | 17 ----------------- src/StaticPHP/Package/Package.php | 17 +++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 2063ee89..81c54996 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -13,23 +13,6 @@ use StaticPHP\Util\FileSystem; */ class LibraryPackage extends Package { - /** @var array $build_functions Build functions for different OS binding */ - protected array $build_functions = []; - - /** - * Add a build function for a specific platform. - * - * @param string $platform PHP_OS_FAMILY - * @param callable $func Function to build for the platform - */ - public function addBuildFunction(string $platform, callable $func): void - { - $this->build_functions[$platform] = $func; - if ($platform === PHP_OS_FAMILY) { - $this->addStage('build', $func); - } - } - public function isInstalled(): bool { foreach (PackageConfig::get($this->getName(), 'static-libs', []) as $lib) { diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 263a8882..6f590b02 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -19,6 +19,9 @@ abstract class Package */ protected array $stages = []; + /** @var array $build_functions Build functions for different OS binding */ + protected array $build_functions = []; + /** * @param string $name Name of the package * @param string $type Type of the package @@ -55,6 +58,20 @@ abstract class Package return $ret; } + /** + * Add a build function for a specific platform. + * + * @param string $os_family PHP_OS_FAMILY + * @param callable $func Function to build for the platform + */ + public function addBuildFunction(string $os_family, callable $func): void + { + $this->build_functions[$os_family] = $func; + if ($os_family === PHP_OS_FAMILY) { + $this->addStage('build', $func); + } + } + public function isInstalled(): bool { // By default, assume package is not installed. From f4bb0263f68b30eef36cc8158302d4bee44fdb08 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:03 +0800 Subject: [PATCH 06/19] Fix ncurses static-libs --- config/pkg.lib.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/pkg.lib.json b/config/pkg.lib.json index 52531b21..79e1a853 100644 --- a/config/pkg.lib.json +++ b/config/pkg.lib.json @@ -711,6 +711,9 @@ "ncurses": { "type": "library", "artifact": "ncurses", + "static-libs@unix": [ + "libncurses.a" + ], "license": { "type": "file", "path": "COPYING" From b38434572326a367bff82f5962d7f58d0ce24cdf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:18 +0800 Subject: [PATCH 07/19] Add php-micro patch for embed mode --- src/Package/Target/micro.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Package/Target/micro.php diff --git a/src/Package/Target/micro.php b/src/Package/Target/micro.php new file mode 100644 index 00000000..a95d4b4d --- /dev/null +++ b/src/Package/Target/micro.php @@ -0,0 +1,22 @@ +getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); + } +} From 80128edd39ac2e45124e17e622507b19d32601a5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:34 +0800 Subject: [PATCH 08/19] Add patch description display --- src/StaticPHP/DI/ApplicationContext.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index c2720353..9b702ccb 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -7,8 +7,10 @@ namespace StaticPHP\DI; use DI\Container; use DI\ContainerBuilder; use Psr\Container\ContainerInterface; +use StaticPHP\Attribute\PatchDescription; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use ZM\Logger\ConsoleColor; use function DI\factory; @@ -138,6 +140,14 @@ class ApplicationContext public static function invoke(callable $callback, array $context = []): mixed { logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure'))); + + // get if callback has attribute PatchDescription + $ref = new \ReflectionFunction(\Closure::fromCallable($callback)); + $attributes = $ref->getAttributes(PatchDescription::class); + foreach ($attributes as $attribute) { + $attrInstance = $attribute->newInstance(); + logger()->info(ConsoleColor::magenta('[PATCH]') . ConsoleColor::green(" {$attrInstance->description}")); + } return self::getInvoker()->invoke($callback, $context); } From 78234ef14778c5c6284da3de8f791d9295319d77 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:00:56 +0800 Subject: [PATCH 09/19] Add missing patchPkgconfPrefix function --- src/StaticPHP/Package/LibraryPackage.php | 30 ++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/StaticPHP/Package/LibraryPackage.php b/src/StaticPHP/Package/LibraryPackage.php index 81c54996..97ec8007 100644 --- a/src/StaticPHP/Package/LibraryPackage.php +++ b/src/StaticPHP/Package/LibraryPackage.php @@ -129,6 +129,36 @@ class LibraryPackage extends Package return trim($env); } + /** + * Patch pkgconfig file prefix, exec_prefix, libdir, includedir for correct build. + * + * @param array $files File list to patch, if empty, will use pkg-configs from config (e.g. ['zlib.pc', 'openssl.pc']) + * @param int $patch_option Patch options + * @param null|array $custom_replace Custom replace rules, if provided, will be used to replace in the format [regex, replacement] + */ + public function patchPkgconfPrefix(array $files = [], int $patch_option = PKGCONF_PATCH_ALL, ?array $custom_replace = null): void + { + logger()->info("Patching library [{$this->getName()}] pkgconfig"); + if ($files === [] && ($conf_pc = PackageConfig::get($this->getName(), 'pkg-configs', [])) !== []) { + $files = array_map(fn ($x) => "{$x}.pc", $conf_pc); + } + foreach ($files as $name) { + $realpath = realpath("{$this->getLibDir()}/pkgconfig/{$name}"); + if ($realpath === false) { + throw new PatchException('pkg-config prefix patcher', "Cannot find library [{$this->getName()}] pkgconfig file [{$name}] in {$this->getLibDir()}/pkgconfig/ !"); + } + logger()->debug("Patching {$realpath}"); + // replace prefix + $file = FileSystem::readFile($realpath); + $file = ($patch_option & PKGCONF_PATCH_PREFIX) === PKGCONF_PATCH_PREFIX ? preg_replace('/^prefix\s*=.*$/m', 'prefix=' . BUILD_ROOT_PATH, $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_EXEC_PREFIX) === PKGCONF_PATCH_EXEC_PREFIX ? preg_replace('/^exec_prefix\s*=.*$/m', 'exec_prefix=${prefix}', $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_LIBDIR) === PKGCONF_PATCH_LIBDIR ? preg_replace('/^libdir\s*=.*$/m', 'libdir=${prefix}/lib', $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_INCLUDEDIR) === PKGCONF_PATCH_INCLUDEDIR ? preg_replace('/^includedir\s*=.*$/m', 'includedir=${prefix}/include', $file) : $file; + $file = ($patch_option & PKGCONF_PATCH_CUSTOM) === PKGCONF_PATCH_CUSTOM && $custom_replace !== null ? preg_replace($custom_replace[0], $custom_replace[1], $file) : $file; + FileSystem::writeFile($realpath, $file); + } + } + /** * Get extra LIBS for current package. * You need to define the environment variable in the format of {LIBRARY_NAME}_LIBS From 7b16f683fc76e466b5adbffadbc572183d445c66 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:01:29 +0800 Subject: [PATCH 10/19] Allow package implementation using parent class functions --- src/StaticPHP/Package/PackageLoader.php | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Package/PackageLoader.php index c89bf392..878b3b94 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Package/PackageLoader.php @@ -143,8 +143,6 @@ class PackageLoader } self::$loaded_classes[$class_name] = true; - $instance_class = $refClass->newInstance(); - $attributes = $refClass->getAttributes(); foreach ($attributes as $attribute) { $pkg = null; @@ -160,6 +158,19 @@ class PackageLoader if ($package_type === null) { throw new WrongUsageException("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 + if ($refClass->getParentClass() !== false) { + if (is_a($class_name, Package::class, true)) { + self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type); + $instance_class = self::$packages[$attribute_instance->name]; + } + } + + if (!isset($instance_class)) { + $instance_class = $refClass->newInstance(); + } + $pkg = self::$packages[$attribute_instance->name]; // validate package type matches @@ -272,9 +283,6 @@ class PackageLoader private static function addBuildFunction(Package $pkg, object $attr, callable $fn): void { - if (!$pkg instanceof LibraryPackage) { - throw new ValidationException("Class [{$pkg->getName()}] must implement LibraryPackage for BuildFor attribute."); - } $pkg->addBuildFunction($attr->os, $fn); } } From a4bd2a79a9341fee723ff544a2e4f8c2671e8272 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:01:58 +0800 Subject: [PATCH 11/19] Add shared extension build support --- src/Package/Target/php.php | 98 +++++++---- src/StaticPHP/Package/PhpExtensionPackage.php | 158 +++++++++++++++++- 2 files changed, 219 insertions(+), 37 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c2e01647..4d3683ce 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -14,6 +14,7 @@ use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\Package\Validate; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; use StaticPHP\Exception\WrongUsageException; @@ -111,7 +112,7 @@ class php } #[ResolveBuild] - public function resolveBuild(TargetPackage $package): array + public function resolveBuild(TargetPackage $package, PackageInstaller $installer): array { // Parse extensions and additional packages for all php-* targets $static_extensions = parse_extension_list($package->getBuildArgument('extensions')); @@ -128,6 +129,7 @@ class php // get instances foreach ($extensions_pkg as $extension) { $extname = substr($extension, 4); + $config = PackageConfig::get($extension, 'php-extension', []); if (!PackageLoader::hasPackage($extension)) { throw new WrongUsageException("Extension [{$extname}] does not exist. Please check your extension name."); } @@ -137,13 +139,25 @@ class php } // set build static/shared if (in_array($extname, $static_extensions)) { + if (($config['build-static'] ?? true) === false) { + throw new WrongUsageException("Extension [{$extname}] cannot be built as static extension."); + } $instance->setBuildStatic(); } if (in_array($extname, $shared_extensions)) { + if (($config['build-shared'] ?? true) === false) { + throw new WrongUsageException("Extension [{$extname}] cannot be built as shared extension, please remove it from --build-shared option."); + } $instance->setBuildShared(); + $instance->setBuildWithPhp($config['build-with-php'] ?? false); } } + // building shared extensions need embed SAPI + if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -178,14 +192,14 @@ class php return []; } $sapis = array_filter([ - $installer->getBuildPackage('php-cli') ? 'cli' : null, - $installer->getBuildPackage('php-fpm') ? 'fpm' : null, - $installer->getBuildPackage('php-micro') ? 'micro' : null, - $installer->getBuildPackage('php-cgi') ? 'cgi' : null, - $installer->getBuildPackage('php-embed') ? 'embed' : null, - $installer->getBuildPackage('frankenphp') ? 'frankenphp' : null, + $installer->isPackageResolved('php-cli') ? 'cli' : null, + $installer->isPackageResolved('php-fpm') ? 'fpm' : null, + $installer->isPackageResolved('php-micro') ? 'micro' : null, + $installer->isPackageResolved('php-cgi') ? 'cgi' : null, + $installer->isPackageResolved('php-embed') ? 'embed' : null, + $installer->isPackageResolved('frankenphp') ? 'frankenphp' : null, ]); - $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() === 'php-extension'); + $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic()); $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); $install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-')); return [ @@ -281,15 +295,15 @@ class php $args[] = "--with-config-file-scan-dir={$option}"; } // perform enable cli options - $args[] = $installer->isBuildPackage('php-cli') ? '--enable-cli' : '--disable-cli'; - $args[] = $installer->isBuildPackage('php-fpm') ? '--enable-fpm' : '--disable-fpm'; - $args[] = $installer->isBuildPackage('php-micro') ? match (SystemTarget::getTargetOS()) { + $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; + $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm'; + $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { 'Linux' => '--enable-micro=all-static', default => '--enable-micro', } : null; - $args[] = $installer->isBuildPackage('php-cgi') ? '--enable-cgi' : '--disable-cgi'; + $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - $args[] = $installer->isBuildPackage('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; $args = implode(' ', array_filter($args)); @@ -311,19 +325,19 @@ class php logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - if ($installer->isBuildPackage('php-cli')) { + if ($installer->isPackageResolved('php-cli')) { $package->runStage('unix-make-cli'); } - if ($installer->isBuildPackage('php-cgi')) { + if ($installer->isPackageResolved('php-cgi')) { $package->runStage('unix-make-cgi'); } - if ($installer->isBuildPackage('php-fpm')) { + if ($installer->isPackageResolved('php-fpm')) { $package->runStage('unix-make-fpm'); } - if ($installer->isBuildPackage('php-micro')) { + if ($installer->isPackageResolved('php-micro')) { $package->runStage('unix-make-micro'); } - if ($installer->isBuildPackage('php-embed')) { + if ($installer->isPackageResolved('php-embed')) { $package->runStage('unix-make-embed'); } } @@ -359,6 +373,7 @@ class php } #[Stage('unix-make-micro')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { $phar_patched = false; @@ -375,6 +390,8 @@ class php shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$builder->concurrency} micro"); + + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx'); } finally { if ($phar_patched) { SourcePatcher::unpatchMicroPhar(); @@ -385,6 +402,7 @@ class php #[Stage('unix-make-embed')] public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); $shared_exts = array_filter( $installer->getResolvedPackages(), static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp() @@ -395,9 +413,11 @@ class php $diff = new DirDiff(BUILD_MODULES_PATH, true); $root = BUILD_ROOT_PATH; + $sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i'; + shell()->cd($package->getSourceDir()) ->setEnv($this->makeVars($installer)) - ->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile') + ->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile') ->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared ------------- @@ -417,12 +437,15 @@ class php // process shared extensions that built-with-php $increment_files = $diff->getChangedFiles(); foreach ($increment_files as $increment_file) { - $builder->deployBinary($increment_file, $libphp_so, false); + $builder->deployBinary($increment_file, $increment_file, false); } // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- // process libphp.a for static embed + if (!file_exists("{$package->getLibDir()}/libphp.a")) { + return; + } $ar = getenv('AR') ?: 'ar'; $libphp_a = "{$package->getLibDir()}/libphp.a"; shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}"); @@ -432,19 +455,9 @@ class php $package->runStage('patch-embed-scripts'); } - #[BuildFor('Darwin')] - #[BuildFor('Linux')] - public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void + #[Stage('unix-build-shared-ext')] + public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void { - // virtual target, do nothing - if ($package->getName() !== 'php') { - return; - } - - $package->runStage('unix-buildconf'); - $package->runStage('unix-configure'); - $package->runStage('unix-make'); - // collect shared extensions /** @var PhpExtensionPackage[] $shared_extensions */ $shared_extensions = array_filter( @@ -470,9 +483,10 @@ class php } try { + logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - logger()->info('Building shared extensions...'); - $extension->buildSharedExtension(); + InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); + $extension->buildShared(); } } finally { // restore php-config @@ -483,6 +497,22 @@ class php } } + #[BuildFor('Darwin')] + #[BuildFor('Linux')] + public function build(TargetPackage $package): void + { + // virtual target, do nothing + if ($package->getName() !== 'php') { + return; + } + + $package->runStage('unix-buildconf'); + $package->runStage('unix-configure'); + $package->runStage('unix-make'); + + $package->runStage('unix-build-shared-ext'); + } + /** * Patch phpize and php-config if needed */ diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 562d8f8e..667d9688 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -6,8 +6,10 @@ namespace StaticPHP\Package; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Util\SPCConfigUtil; /** * Represents a PHP extension package. @@ -41,6 +43,23 @@ class PhpExtensionPackage extends Package parent::__construct($name, $type); } + public function getSourceDir(): string + { + if ($this->getArtifact() === null) { + $path = SOURCE_PATH . '/php-src/ext/' . $this->getExtensionName(); + if (!is_dir($path)) { + throw new ValidationException("Extension source directory not found: {$path}", validation_module: "Extension {$this->getExtensionName()} source"); + } + return $path; + } + return parent::getSourceDir(); + } + + public function getExtensionName(): string + { + return str_replace('ext-', '', $this->getName()); + } + public function addCustomPhpConfigureArgCallback(string $os, callable $fn): void { if ($os === '') { @@ -59,7 +78,7 @@ class PhpExtensionPackage extends Package return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', substr($this->getName(), 4)); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -81,6 +100,22 @@ 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 @@ -108,8 +143,125 @@ class PhpExtensionPackage extends Package return $this->build_with_php; } - public function buildSharedExtension(): void + public function buildShared(): void { - // TODO: build common shared extensions code here... + if ($this->hasStage('build')) { + $this->runStage('build'); + } else { + throw new WrongUsageException("Extension [{$this->getExtensionName()}] cannot build shared target yet."); + } + } + + /** + * Get shared extension build environment variables for Unix. + * + * @return array{ + * CFLAGS: string, + * CXXFLAGS: string, + * LDFLAGS: string, + * LIBS: string, + * LD_LIBRARY_PATH: string + * } + */ + public function getSharedExtensionEnv(): array + { + $config = (new SPCConfigUtil())->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; + $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + return [ + 'CFLAGS' => $config['cflags'], + 'CXXFLAGS' => $config['cflags'], + 'LDFLAGS' => $config['ldflags'], + 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), + 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, + ]; + } + + /** + * @internal + * #[Stage('phpize')] + */ + public function _phpize(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 + { + $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + shell()->cd($package->getSourceDir()) + ->setEnv($env) + ->exec( + './configure ' . $this->getPhpConfigureArg(SystemTarget::getCurrentPlatformString(), true) . + ' --with-php-config=' . BUILD_BIN_PATH . '/php-config ' . + "--enable-shared --disable-static {$phpvars}" + ); + } + + /** + * @internal + * #[Stage('make')] + */ + public function _make(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void + { + shell()->cd($package->getSourceDir()) + ->setEnv($env) + ->exec('make clean') + ->exec("make -j{$builder->concurrency}") + ->exec('make install'); + } + + /** + * Build shared extension on Unix-like systems. + * Only for internal calling. For external use, call buildShared() instead. + * @internal + * #[Stage('build')] + */ + public function _buildSharedUnix(PackageBuilder $builder): void + { + $env = $this->getSharedExtensionEnv(); + + $this->runStage('phpize', ['env' => $env]); + $this->runStage('configure', ['env' => $env]); + $this->runStage('make', ['env' => $env]); + + // process *.so file + $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; + if (!file_exists($soFile)) { + throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build"); + } + $builder->deployBinary($soFile, $soFile, false); + } + + /** + * Splits a given string of library flags into static and shared libraries. + * + * @param string $allLibs A space-separated string of library flags (e.g., -lxyz). + * @return array an array containing two elements: the first is a space-separated string + * of static library flags, and the second is a space-separated string + * of shared library flags + */ + protected function splitLibsIntoStaticAndShared(string $allLibs): array + { + $staticLibString = ''; + $sharedLibString = ''; + $libs = explode(' ', $allLibs); + foreach ($libs as $lib) { + $staticLib = BUILD_LIB_PATH . '/lib' . str_replace('-l', '', $lib) . '.a'; + if (str_starts_with($lib, BUILD_LIB_PATH . '/lib') && str_ends_with($lib, '.a')) { + $staticLib = $lib; + } + if ($lib === '-lphp' || !file_exists($staticLib)) { + $sharedLibString .= " {$lib}"; + } else { + $staticLibString .= " {$lib}"; + } + } + return [trim($staticLibString), trim($sharedLibString)]; } } From 0db26be826e392e7625a279161b9b20fd38abd24 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:02:12 +0800 Subject: [PATCH 12/19] Correct SAPI-packages to be installed --- src/StaticPHP/Package/PackageInstaller.php | 59 +++++++++++----------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index c6aa3421..4be98090 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -150,27 +150,11 @@ class PackageInstaller } $builder = ApplicationContext::get(PackageBuilder::class); foreach ($this->packages as $package) { - if ( - $this->isBuildPackage($package) || - $package instanceof LibraryPackage && $package->hasStage('build') && !$package->getArtifact()->shouldUseBinary() - ) { - if ($interactive) { - InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); - } - try { - /** @var LibraryPackage $package */ - $status = $builder->buildPackage($package, $this->isBuildPackage($package)); - } catch (\Throwable $e) { - if ($interactive) { - InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); - echo PHP_EOL; - } - throw $e; - } - if ($interactive) { - InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); - } - } elseif ($package instanceof LibraryPackage && $package->getArtifact()->shouldUseBinary()) { + $is_to_build = $this->isBuildPackage($package); + $has_build_stage = $package instanceof LibraryPackage && $package->hasStage('build'); + $should_use_binary = $package instanceof LibraryPackage && ($package->getArtifact()?->shouldUseBinary() ?? false); + $has_source = $package->hasSource(); + if (!$is_to_build && $should_use_binary) { // install binary if ($interactive) { InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); @@ -187,7 +171,24 @@ class PackageInstaller if ($interactive) { InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } - } elseif ($package instanceof LibraryPackage) { + } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { + if ($interactive) { + InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); + } + try { + /** @var LibraryPackage $package */ + $status = $builder->buildPackage($package, $this->isBuildPackage($package)); + } catch (\Throwable $e) { + if ($interactive) { + InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); + echo PHP_EOL; + } + throw $e; + } + if ($interactive) { + InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); + } + } elseif ($package->getType() === 'library') { throw new WrongUsageException("Package '{$package->getName()}' cannot be installed: no build stage defined and no binary artifact available for current OS."); } } @@ -442,32 +443,32 @@ class PackageInstaller if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cli')) { $cli = PackageLoader::getPackage('php-cli'); - $this->build_packages[$cli->getName()] = $cli; + $this->install_packages[$cli->getName()] = $cli; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-fpm')) { $fpm = PackageLoader::getPackage('php-fpm'); - $this->build_packages[$fpm->getName()] = $fpm; + $this->install_packages[$fpm->getName()] = $fpm; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-micro')) { $micro = PackageLoader::getPackage('php-micro'); - $this->build_packages[$micro->getName()] = $micro; + $this->install_packages[$micro->getName()] = $micro; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-cgi')) { $cgi = PackageLoader::getPackage('php-cgi'); - $this->build_packages[$cgi->getName()] = $cgi; + $this->install_packages[$cgi->getName()] = $cgi; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-embed')) { $embed = PackageLoader::getPackage('php-embed'); - $this->build_packages[$embed->getName()] = $embed; + $this->install_packages[$embed->getName()] = $embed; $added = true; } if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { $frankenphp = PackageLoader::getPackage('frankenphp'); - $this->build_packages[$frankenphp->getName()] = $frankenphp; + $this->install_packages[$frankenphp->getName()] = $frankenphp; $added = true; } $this->build_packages[$package->getName()] = $package; @@ -481,7 +482,7 @@ class PackageInstaller } else { // process specific php sapi targets $this->build_packages['php'] = PackageLoader::getPackage('php'); - $this->build_packages[$package->getName()] = $package; + $this->install_packages[$package->getName()] = $package; } } From e004d108611307c4a7cc55d7bb23605b56675efe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 8 Dec 2025 17:04:00 +0800 Subject: [PATCH 13/19] Fix phpstan --- src/StaticPHP/Package/PackageLoader.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/StaticPHP/Package/PackageLoader.php b/src/StaticPHP/Package/PackageLoader.php index 878b3b94..5093dd7b 100644 --- a/src/StaticPHP/Package/PackageLoader.php +++ b/src/StaticPHP/Package/PackageLoader.php @@ -221,6 +221,10 @@ class PackageLoader self::$packages[$pkg->getName()] = $pkg; } + if (!isset($instance_class)) { + $instance_class = $refClass->newInstance(); + } + // parse non-package available attributes foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { $method_attributes = $method->getAttributes(); From 808aed2a6620fd7312a626c50265a7cbed38f612 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 14:58:11 +0800 Subject: [PATCH 14/19] 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); } From ac01867e9c39e1101c6ea00df408d11c4a706442 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 15:01:41 +0800 Subject: [PATCH 15/19] Refactor stage execution to use method references for improved clarity --- src/StaticPHP/Package/PhpExtensionPackage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 7853be08..84aa3020 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -211,9 +211,9 @@ class PhpExtensionPackage extends Package { $env = $this->getSharedExtensionEnv(); - $this->runStage('phpizeForUnix', ['env' => $env]); - $this->runStage('configureForUnix', ['env' => $env]); - $this->runStage('makeForUnix', ['env' => $env]); + $this->runStage([$this, 'phpizeForUnix'], ['env' => $env]); + $this->runStage([$this, 'configureForUnix'], ['env' => $env]); + $this->runStage([$this, 'makeForUnix'], ['env' => $env]); // process *.so file $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; From b0f630f95f0a7fb29da4dfa6cc7695535bb4a7f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 16:34:43 +0800 Subject: [PATCH 16/19] Add package outputs, colorize motd --- src/Package/Target/php.php | 19 +++++++- .../Attribute/Package/PatchBeforeBuild.php | 11 ----- src/StaticPHP/Command/BaseCommand.php | 5 +- src/StaticPHP/Command/BuildTargetCommand.php | 2 + src/StaticPHP/DI/CallbackInvoker.php | 46 +++++++++++++++++++ src/StaticPHP/Package/Package.php | 14 ++++++ src/StaticPHP/Package/PackageInstaller.php | 10 ++++ src/StaticPHP/Registry/PackageLoader.php | 17 ++----- 8 files changed, 98 insertions(+), 26 deletions(-) delete mode 100644 src/StaticPHP/Attribute/Package/PatchBeforeBuild.php diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 90c68a24..1e1c5930 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -45,7 +45,7 @@ use ZM\Logger\ConsoleColor; #[Target('php-cgi')] #[Target('php-embed')] #[Target('frankenphp')] -class php +class php extends TargetPackage { public static function getPHPVersionID(): int { @@ -350,6 +350,9 @@ class php shell()->cd($package->getSourceDir()) ->setEnv($this->makeVars($installer)) ->exec("make -j{$concurrency} cli"); + + $builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php'); + $package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php'); } #[Stage] @@ -360,6 +363,9 @@ class php shell()->cd($package->getSourceDir()) ->setEnv($this->makeVars($installer)) ->exec("make -j{$concurrency} cgi"); + + $builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi'); + $package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi'); } #[Stage] @@ -370,6 +376,9 @@ class php shell()->cd($package->getSourceDir()) ->setEnv($this->makeVars($installer)) ->exec("make -j{$concurrency} fpm"); + + $builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm'); + $package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm'); } #[Stage] @@ -392,6 +401,7 @@ class php ->exec("make -j{$builder->concurrency} micro"); $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', BUILD_BIN_PATH . '/micro.sfx'); + $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); } finally { if ($phar_patched) { SourcePatcher::unpatchMicroPhar(); @@ -432,12 +442,17 @@ class php } // deploy $builder->deployBinary($libphp_so, $libphp_so, false); + $package->setOutput('Library path for embed SAPI', $libphp_so); } // process shared extensions that built-with-php $increment_files = $diff->getChangedFiles(); foreach ($increment_files as $increment_file) { $builder->deployBinary($increment_file, $increment_file, false); + $files[] = basename($increment_file); + } + if (!empty($files)) { + $package->setOutput('Built shared extensions', implode(', ', $files)); } // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- @@ -524,6 +539,7 @@ class php logger()->debug('Patching phpize prefix'); FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', "prefix=''", "prefix='" . BUILD_ROOT_PATH . "'"); FileSystem::replaceFileStr(BUILD_BIN_PATH . '/phpize', 's##', 's#/usr/local#'); + $this->setOutput('phpize script path for embed SAPI', BUILD_BIN_PATH . '/phpize'); } // patch php-config if (file_exists(BUILD_BIN_PATH . '/php-config')) { @@ -535,6 +551,7 @@ class php // move lstdc++ to the end of libs $php_config_str = preg_replace('/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/', '$1$2 $4 $3"', $php_config_str); FileSystem::writeFile(BUILD_BIN_PATH . '/php-config', $php_config_str); + $this->setOutput('php-config script path for embed SAPI', BUILD_BIN_PATH . '/php-config'); } } diff --git a/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php b/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php deleted file mode 100644 index 2343954b..00000000 --- a/src/StaticPHP/Attribute/Package/PatchBeforeBuild.php +++ /dev/null @@ -1,11 +0,0 @@ -getVersionWithCommit(); if (!$this->no_motd) { - echo str_replace('{version}', $version, self::$motd); + echo str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); } } diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index e66f514b..2756070b 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -51,6 +51,8 @@ class BuildTargetCommand extends BaseCommand $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + $installer->printBuildPackageOutputs(); + return static::SUCCESS; } } diff --git a/src/StaticPHP/DI/CallbackInvoker.php b/src/StaticPHP/DI/CallbackInvoker.php index f14f9468..fa11a7f1 100644 --- a/src/StaticPHP/DI/CallbackInvoker.php +++ b/src/StaticPHP/DI/CallbackInvoker.php @@ -26,6 +26,10 @@ class CallbackInvoker * 4. Default value * 5. Null (if nullable) * + * Note: For object values in context, the invoker automatically registers + * the object under all its parent classes and interfaces, allowing type hints + * to match any type in the inheritance hierarchy. + * * @param callable $callback The callback to invoke * @param array $context Context parameters (type => value or name => value) * @@ -35,6 +39,9 @@ class CallbackInvoker */ public function invoke(callable $callback, array $context = []): mixed { + // Expand context to include all parent classes and interfaces for objects + $context = $this->expandContextHierarchy($context); + $reflection = new \ReflectionFunction(\Closure::fromCallable($callback)); $args = []; @@ -95,4 +102,43 @@ class CallbackInvoker 'void', 'null', 'false', 'true', 'never', ], true); } + + /** + * Expand context to include all parent classes and interfaces for object values. + * This allows type hints to match any type in the object's inheritance hierarchy. + * + * @param array $context Original context array + * @return array Expanded context with all class hierarchy mappings + */ + private function expandContextHierarchy(array $context): array + { + $expanded = []; + + foreach ($context as $key => $value) { + // Keep the original key-value pair + $expanded[$key] = $value; + + // If value is an object, add mappings for all parent classes and interfaces + if (is_object($value)) { + $reflection = new \ReflectionClass($value); + + // Add concrete class + $expanded[$reflection->getName()] = $value; + + // Add all parent classes + while ($parent = $reflection->getParentClass()) { + $expanded[$parent->getName()] = $value; + $reflection = $parent; + } + + // Add all interfaces + $interfaces = (new \ReflectionClass($value))->getInterfaceNames(); + foreach ($interfaces as $interface) { + $expanded[$interface] = $value; + } + } + } + + return $expanded; + } } diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 0317e1bc..6cad1fab 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -23,6 +23,9 @@ abstract class Package /** @var array $build_functions Build functions for different OS binding */ protected array $build_functions = []; + /** @var array */ + protected array $outputs = []; + /** * @param string $name Name of the package * @param string $type Type of the package @@ -69,6 +72,17 @@ abstract class Package return $ret; } + public function setOutput(string $key, string $value): static + { + $this->outputs[$key] = $value; + return $this; + } + + public function getOutputs(): array + { + return $this->outputs; + } + /** * Add a build function for a specific platform. * diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 9770a737..96316887 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -104,6 +104,16 @@ class PackageInstaller return $this; } + public function printBuildPackageOutputs(): void + { + foreach ($this->build_packages as $package) { + if (($outputs = $package->getOutputs()) !== []) { + InteractiveTerm::notice('Package ' . ConsoleColor::green($package->getName()) . ' outputs'); + $this->printArrayInfo(info: $outputs); + } + } + } + /** * Run the package installation process. */ diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 15ff0f4a..0ef3fb8e 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -12,7 +12,6 @@ use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; use StaticPHP\Attribute\Package\Library; -use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\Package\ResolveBuild; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; @@ -166,16 +165,14 @@ class PackageLoader if ($refClass->getParentClass() !== false) { if (is_a($class_name, Package::class, true)) { self::$packages[$attribute_instance->name] = new $class_name($attribute_instance->name, $package_type); - $instance_class = self::$packages[$attribute_instance->name]; } } - if (!isset($instance_class)) { - $instance_class = $refClass->newInstance(); - } - $pkg = self::$packages[$attribute_instance->name]; + // Use the package instance if it's a Package subclass, otherwise create a new instance + $instance_class = is_a($class_name, Package::class, true) ? $pkg : $refClass->newInstance(); + // validate package type matches $pkg_type_attr = match ($attribute->getName()) { Target::class => ['target', 'virtual-target'], @@ -204,18 +201,13 @@ class PackageLoader // #[Stage('stage_name')] 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, - $pkg::class => $pkg, - ]), + InitPackage::class => ApplicationContext::invoke([$instance_class, $method->getName()], ['package' => $pkg]), // #[InitBuild] ResolveBuild::class => $pkg instanceof TargetPackage ? $pkg->setResolveBuildCallback([$instance_class, $method->getName()]) : null, // #[Info] Info::class => $pkg->setInfoCallback([$instance_class, $method->getName()]), // #[Validate] Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]), - // #[PatchBeforeBuild] - PatchBeforeBuild::class => $pkg->setPatchBeforeBuildCallback([$instance_class, $method->getName()]), default => null, }; } @@ -224,6 +216,7 @@ class PackageLoader self::$packages[$pkg->getName()] = $pkg; } + // For classes without package attributes, create a simple instance for non-package stage callbacks if (!isset($instance_class)) { $instance_class = $refClass->newInstance(); } From bcaef59a1551c40363317957af866b55c2f4ce91 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 9 Dec 2025 16:54:29 +0800 Subject: [PATCH 17/19] Support full --no-ansi options --- src/StaticPHP/Command/BaseCommand.php | 3 +- src/StaticPHP/Exception/ExceptionHandler.php | 3 +- src/StaticPHP/Util/InteractiveTerm.php | 32 +++++++++++++++----- src/globals/functions.php | 4 +-- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index b6c09f3f..da01723a 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -70,7 +70,8 @@ abstract class BaseCommand extends Command }); $version = $this->getVersionWithCommit(); if (!$this->no_motd) { - echo str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); + $str = str_replace('{version}', '' . ConsoleColor::none("v{$version}"), '' . ConsoleColor::magenta(self::$motd)); + echo $this->input->getOption('no-ansi') ? strip_ansi_colors($str) : $str; } } diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index db11b8fd..a7732763 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -10,6 +10,7 @@ use SPC\builder\linux\LinuxBuilder; use SPC\builder\macos\MacOSBuilder; use SPC\builder\windows\WindowsBuilder; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; class ExceptionHandler @@ -189,7 +190,7 @@ class ExceptionHandler $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { - echo ConsoleColor::red($line) . PHP_EOL; + InteractiveTerm::plain(ConsoleColor::red($line) . ''); } } } diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 01e4bdc9..47932763 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -6,6 +6,7 @@ namespace StaticPHP\Util; use StaticPHP\DI\ApplicationContext; use Symfony\Component\Console\Helper\ProgressIndicator; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use ZM\Logger\ConsoleColor; @@ -15,50 +16,55 @@ class InteractiveTerm public static function notice(string $message, bool $indent = false): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->notice(strip_ansi_colors($message)); } else { - $output->writeln(ConsoleColor::cyan(($indent ? ' ' : '') . 'â–ķ ') . $message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::cyan(($indent ? ' ' : '') . 'â–ķ ') . $message)); } } public static function success(string $message, bool $indent = false): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { - $output->writeln(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); } } public static function plain(string $message): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { - $output->writeln($message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } } public static function info(string $message): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if (!$output->isVerbose()) { - $output->writeln(ConsoleColor::green('â–ķ ') . $message); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green('â–ķ ') . $message)); } logger()->info(strip_ansi_colors($message)); } public static function error(string $message, bool $indent = true): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->error(strip_ansi_colors($message)); } else { - $output->writeln('' . ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message)); + $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(($indent ? ' ' : '') . '✘ ' . $message))); } } @@ -69,11 +75,14 @@ class InteractiveTerm public static function setMessage(string $message): void { - self::$indicator?->setMessage($message); + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; + self::$indicator?->setMessage(($no_ansi ? 'strip_ansi_colors' : 'strval')($message)); } public static function finish(string $message, bool $status = true): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; + $message = $no_ansi ? strip_ansi_colors($message) : $message; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { if ($status) { @@ -85,9 +94,9 @@ class InteractiveTerm } if (self::$indicator !== null) { if (!$status) { - self::$indicator->finish($message, '' . ConsoleColor::red(' ✘')); + self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::red(' ✘'))); } else { - self::$indicator->finish($message, '' . ConsoleColor::green(' ✔')); + self::$indicator->finish($message, ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(' ✔'))); } self::$indicator = null; } @@ -95,6 +104,7 @@ class InteractiveTerm public static function indicateProgress(string $message): void { + $no_ansi = ApplicationContext::get(InputInterface::class)->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class); if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); @@ -106,6 +116,12 @@ class InteractiveTerm self::$indicator->advance(); return; } + // if no ansi, use a dot instead of spinner + if ($no_ansi) { + self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' â€Ē', ' â€Ē']); + self::$indicator->start(strip_ansi_colors($message)); + return; + } self::$indicator = new ProgressIndicator(ApplicationContext::get(OutputInterface::class), 'verbose', 100, [' ⠏', ' ⠛', ' â đ', ' âĒļ', ' â̰', ' âĢĪ', ' â̆', ' ⡇']); self::$indicator->start($message); } diff --git a/src/globals/functions.php b/src/globals/functions.php index 8621e7ad..93cd1ae0 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -271,11 +271,11 @@ function keyboard_interrupt_unregister(): void /** * Strip ANSI color codes from a string. */ -function strip_ansi_colors(string $text): string +function strip_ansi_colors(string|Stringable $text): string { // Regular expression to match ANSI escape sequences // Including color codes, cursor control, clear screen and other control sequences - return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', $text); + return preg_replace('/\e\[[0-9;]*[a-zA-Z]/', '', strval($text)); } /** From 4a968757baadfa726f6a8362d40131b4ed0cff80 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 10 Dec 2025 09:49:20 +0800 Subject: [PATCH 18/19] Update src/Package/Library/ncurses.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Library/ncurses.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index f0a21714..c7c39dc1 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -36,7 +36,7 @@ class ncurses '--without-tests', '--without-dlsym', '--without-debug', - '-enable-symlinks', + '--enable-symlinks', "--bindir={$package->getBinDir()}", "--includedir={$package->getIncludeDir()}", "--libdir={$package->getLibDir()}", From f68adc3256d52e74e9455bb0006223ef44bfccaa Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 10 Dec 2025 09:52:59 +0800 Subject: [PATCH 19/19] Update src/Package/Target/php.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Package/Target/php.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 1e1c5930..03b50073 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -447,6 +447,7 @@ class php extends TargetPackage // process shared extensions that built-with-php $increment_files = $diff->getChangedFiles(); + $files = []; foreach ($increment_files as $increment_file) { $builder->deployBinary($increment_file, $increment_file, false); $files[] = basename($increment_file);