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/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"
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 2ecc533a..80f3fa33 100644
--- a/src/Package/Extension/readline.php
+++ b/src/Package/Extension/readline.php
@@ -4,9 +4,11 @@ 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;
+use StaticPHP\Attribute\PatchDescription;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Util\SourcePatcher;
@@ -14,7 +16,8 @@ use StaticPHP\Util\SourcePatcher;
#[Extension('readline')]
class readline
{
- #[BeforeStage('php', 'unix-make-cli')]
+ #[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-readline')]
+ #[PatchDescription('Fix readline static build with musl')]
public function beforeMakeLinuxCli(PackageInstaller $installer, ToolchainInterface $toolchain): void
{
if ($toolchain->isStatic()) {
@@ -23,7 +26,7 @@ class readline
}
}
- #[AfterStage('php', 'unix-make-cli')]
+ #[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 58e9397f..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')]
+ #[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
new file mode 100644
index 00000000..08a435da
--- /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']);
+ }
+}
diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php
new file mode 100644
index 00000000..c7c39dc1
--- /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}');
+ }
+ }
+}
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
new file mode 100644
index 00000000..64772efc
--- /dev/null
+++ b/src/Package/Target/micro.php
@@ -0,0 +1,22 @@
+getSourceDir()}/Makefile", 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la');
+ }
+}
diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php
index c2e01647..03b50073 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;
@@ -14,15 +13,17 @@ 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;
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;
@@ -44,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
{
@@ -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 [
@@ -227,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
@@ -245,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'));
@@ -253,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'));
@@ -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));
@@ -303,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');
@@ -311,24 +325,24 @@ class php
logger()->info('cleaning up php-src build files');
shell()->cd($package->getSourceDir())->exec('make clean');
- if ($installer->isBuildPackage('php-cli')) {
- $package->runStage('unix-make-cli');
+ if ($installer->isPackageResolved('php-cli')) {
+ $package->runStage([self::class, 'makeCliForUnix']);
}
- if ($installer->isBuildPackage('php-cgi')) {
- $package->runStage('unix-make-cgi');
+ if ($installer->isPackageResolved('php-cgi')) {
+ $package->runStage([self::class, 'makeCgiForUnix']);
}
- if ($installer->isBuildPackage('php-fpm')) {
- $package->runStage('unix-make-fpm');
+ if ($installer->isPackageResolved('php-fpm')) {
+ $package->runStage([self::class, 'makeFpmForUnix']);
}
- if ($installer->isBuildPackage('php-micro')) {
- $package->runStage('unix-make-micro');
+ if ($installer->isPackageResolved('php-micro')) {
+ $package->runStage([self::class, 'makeMicroForUnix']);
}
- if ($installer->isBuildPackage('php-embed')) {
- $package->runStage('unix-make-embed');
+ if ($installer->isPackageResolved('php-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'));
@@ -336,9 +350,12 @@ 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('unix-make-cgi')]
+ #[Stage]
public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi'));
@@ -346,9 +363,12 @@ 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('unix-make-fpm')]
+ #[Stage]
public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void
{
InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm'));
@@ -356,9 +376,13 @@ 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('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
{
$phar_patched = false;
@@ -375,6 +399,9 @@ 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');
+ $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx');
} finally {
if ($phar_patched) {
SourcePatcher::unpatchMicroPhar();
@@ -382,9 +409,10 @@ 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'));
$shared_exts = array_filter(
$installer->getResolvedPackages(),
static fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildShared() && $x->isBuildWithPhp()
@@ -395,9 +423,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 -------------
@@ -412,39 +442,38 @@ 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();
+ $files = [];
foreach ($increment_files as $increment_file) {
- $builder->deployBinary($increment_file, $libphp_so, false);
+ $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 -------------
// 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}");
UnixUtil::exportDynamicSymbols($libphp_a);
// deploy embed php scripts
- $package->runStage('patch-embed-scripts');
+ $package->runStage([$this, 'patchEmbedScripts']);
}
- #[BuildFor('Darwin')]
- #[BuildFor('Linux')]
- public function build(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain): void
+ #[Stage]
+ 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 +499,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,17 +513,34 @@ class php
}
}
+ #[BuildFor('Darwin')]
+ #[BuildFor('Linux')]
+ public function build(TargetPackage $package): void
+ {
+ // virtual target, do nothing
+ if ($package->getName() !== 'php') {
+ return;
+ }
+
+ $package->runStage([$this, 'buildconfForUnix']);
+ $package->runStage([$this, 'configureForUnix']);
+ $package->runStage([$this, 'makeForUnix']);
+
+ $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')) {
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')) {
@@ -505,6 +552,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/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/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);
+ $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/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php
index 5efb9f1a..2756070b 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;
@@ -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/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/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);
}
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/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/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;
}
}
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..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
@@ -25,11 +26,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 +55,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);
@@ -186,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/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 @@
+ $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) {
@@ -146,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
diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php
index 263a8882..6cad1fab 100644
--- a/src/StaticPHP/Package/Package.php
+++ b/src/StaticPHP/Package/Package.php
@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace StaticPHP\Package;
use StaticPHP\Artifact\Artifact;
-use StaticPHP\Artifact\ArtifactLoader;
use StaticPHP\Config\PackageConfig;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\SPCInternalException;
+use StaticPHP\Registry\ArtifactLoader;
+use StaticPHP\Registry\PackageLoader;
abstract class Package
{
@@ -19,6 +20,12 @@ abstract class Package
*/
protected array $stages = [];
+ /** @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
@@ -29,15 +36,25 @@ abstract class Package
* Run a defined stage of the package.
* If the stage is not defined, an exception should be thrown.
*
- * @param string $name Name of the stage to run
- * @param array $context Additional context to pass to the stage callback
- * @return mixed Based on the stage definition, return the result of the stage
+ * @param array|callable|string $name Name of the stage to run (can be callable)
+ * @param array $context Additional context to pass to the stage callback
+ * @return mixed Based on the stage definition, return the result of the stage
*/
- public function runStage(string $name, array $context = []): mixed
+ public function runStage(mixed $name, array $context = []): mixed
{
- if (!isset($this->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 */
@@ -55,6 +72,31 @@ 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.
+ *
+ * @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.
@@ -63,9 +105,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
{
@@ -75,11 +114,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 c6aa3421..96316887 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;
@@ -103,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.
*/
@@ -150,27 +161,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 +182,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 +454,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 +493,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;
}
}
diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php
index 562d8f8e..84aa3020 100644
--- a/src/StaticPHP/Package/PhpExtensionPackage.php
+++ b/src/StaticPHP/Package/PhpExtensionPackage.php
@@ -4,10 +4,13 @@ declare(strict_types=1);
namespace StaticPHP\Package;
+use StaticPHP\Attribute\Package\Stage;
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 +44,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 +79,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()) {
@@ -108,8 +128,151 @@ 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]
+ public function phpizeForUnix(array $env, PhpExtensionPackage $package): void
+ {
+ shell()->cd($package->getSourceDir())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize');
+ }
+
+ /**
+ * @internal
+ */
+ #[Stage]
+ public function configureForUnix(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]
+ public function makeForUnix(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 buildSharedForUnix(PackageBuilder $builder): void
+ {
+ $env = $this->getSharedExtensionEnv();
+
+ $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';
+ 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);
+ }
+
+ /**
+ * 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.
+ *
+ * @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)];
}
}
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 58%
rename from src/StaticPHP/Package/PackageLoader.php
rename to src/StaticPHP/Registry/PackageLoader.php
index c89bf392..0ef3fb8e 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;
@@ -12,15 +12,19 @@ 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;
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 +34,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 +55,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']}]");
}
}
}
@@ -143,8 +145,6 @@ class PackageLoader
}
self::$loaded_classes[$class_name] = true;
- $instance_class = $refClass->newInstance();
-
$attributes = $refClass->getAttributes();
foreach ($attributes as $attribute) {
$pkg = null;
@@ -158,10 +158,21 @@ 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
+ 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);
+ }
+ }
+
$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'],
@@ -170,10 +181,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
@@ -188,20 +199,15 @@ 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,
- $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,
};
}
@@ -210,6 +216,11 @@ 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();
+ }
+
// parse non-package available attributes
foreach ($refClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
$method_attributes = $method->getAttributes();
@@ -217,9 +228,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,
};
}
@@ -243,7 +254,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)) {
@@ -254,9 +265,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}].");
+ }
+ }
+ }
+ }
}
/**
@@ -265,16 +320,53 @@ 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);
}
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);
}
+
+ 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);
}
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));
}
/**