From 6b67cb90fc1fdc9c3788f8bfbe528b6bba41082a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Wed, 11 Feb 2026 16:24:13 +0100 Subject: [PATCH 01/33] fix: Postgres build with ancient libc --- src/SPC/builder/unix/library/postgresql.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/SPC/builder/unix/library/postgresql.php b/src/SPC/builder/unix/library/postgresql.php index a72f3a1a6..2ad4f51be 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -4,29 +4,14 @@ namespace SPC\builder\unix\library; -use SPC\exception\FileSystemException; use SPC\store\FileSystem; use SPC\util\PkgConfigUtil; use SPC\util\SPCConfigUtil; -use SPC\util\SPCTarget; trait postgresql { public function patchBeforeBuild(): bool { - // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) - if (SPCTarget::getLibcVersion() === '2.17' && GNU_ARCH === 'aarch64') { - try { - FileSystem::replaceFileStr("{$this->source_dir}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); - FileSystem::replaceFileStr( - "{$this->source_dir}/src/port/pg_crc32c_armv8_choose.c", - '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', - '#if defined(__linux__) && !defined(HWCAP_CRC32)' - ); - } catch (FileSystemException) { - // allow file not-existence to make it compatible with old and new version - } - } // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries FileSystem::replaceFileStr("{$this->source_dir}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); // disable shared libs build From 1e4780397b5871d344b36a2c35d10f312bf53047 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 11 Feb 2026 23:32:19 +0800 Subject: [PATCH 02/33] Update test-extensions.php for PHP versions and extensions Commented out older PHP versions and Windows 2025 in the test configuration. Updated the extensions to test for Linux and Darwin. --- src/globals/test-extensions.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 8b22658ca..ba02e672d 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,10 +13,10 @@ // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', - '8.2', - '8.3', - '8.4', + // '8.1', + // '8.2', + // '8.3', + // '8.4', '8.5', // 'git', ]; @@ -26,12 +26,12 @@ // 'macos-15-intel', // bin/spc for x86_64 // 'macos-15', // bin/spc for arm64 // 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 - // 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - // 'ubuntu-24.04', // bin/spc for x86_64 - // 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - // 'ubuntu-24.04-arm', // bin/spc for arm64 + 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 + 'ubuntu-24.04', // bin/spc for x86_64 + 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 + 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-2022', // .\bin\spc.ps1 - 'windows-2025', + // 'windows-2025', ]; // whether enable thread safe @@ -50,13 +50,13 @@ // If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`). $extensions = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'mysqli,gmp', + 'Linux', 'Darwin' => 'pgsql', 'Windows' => 'com_dotnet', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => 'grpc,mysqlnd_parsec,mysqlnd_ed25519', + 'Linux' => '', 'Darwin' => '', 'Windows' => '', }; @@ -66,7 +66,7 @@ // If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. $with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'libwebp', + 'Linux', 'Darwin' => '', 'Windows' => '', }; From 0fe1442f7e6e64c1113fc0cc4d1ef64e43124add Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Thu, 12 Feb 2026 00:02:38 +0800 Subject: [PATCH 03/33] Bump version from 2.8.0 to 2.8.2 --- src/SPC/ConsoleApplication.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 19fdd41dc..415af40d9 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.0'; + public const string VERSION = '2.8.2'; public function __construct() { From 9a53ef34983e9c81de83ff866056f8b15849d90e Mon Sep 17 00:00:00 2001 From: Yoram Date: Fri, 13 Feb 2026 14:25:14 +0100 Subject: [PATCH 04/33] add input with-suggested-libs for build command --- .github/workflows/build-unix.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 0166bfa00..6549a94ee 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -46,6 +46,10 @@ on: description: Prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Build with suggested libs + type: boolean + default: false debug: description: Show full build logs type: boolean @@ -86,6 +90,10 @@ on: description: Prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Include suggested libs + type: boolean + default: false debug: description: Show full build logs type: boolean @@ -157,6 +165,9 @@ jobs: if [ ${{ inputs.prefer-pre-built }} == true ]; then DOWN_CMD="$DOWN_CMD --prefer-pre-built" fi + if [ ${{ inputs.with-suggested-libs }} == true ]; then + BUILD_CMD="$BUILD_CMD --with-suggested-libs" + fi if [ ${{ inputs.build-cli }} == true ]; then BUILD_CMD="$BUILD_CMD --build-cli" fi From d9834d05c6149d9e3850153690dc8b31fdde16c4 Mon Sep 17 00:00:00 2001 From: Yoram Date: Mon, 16 Feb 2026 11:22:25 +0100 Subject: [PATCH 05/33] upload debug logs on 'build php' failures --- .github/workflows/build-unix.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 6549a94ee..0f2fa172f 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -213,6 +213,14 @@ jobs: # if: ${{ failure() }} # uses: mxschmitt/action-tmate@v3 + # Upload debug logs + - if: ${{ inputs.debug && failure() }} + name: "Upload build logs on failure" + uses: actions/upload-artifact@v4 + with: + name: php-cli-logs-${{ inputs.php-version }}-${{ inputs.os }} + path: log/*.log + # Upload cli executable - if: ${{ inputs.build-cli == true }} name: "Upload PHP cli SAPI" From 661723c99a2f0fa4c71c6c9231397fd7132800d5 Mon Sep 17 00:00:00 2001 From: tricker Date: Mon, 16 Feb 2026 12:26:49 +0100 Subject: [PATCH 06/33] change logs name Co-authored-by: Marc --- .github/workflows/build-unix.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index 0f2fa172f..bf6df9ac4 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -218,7 +218,7 @@ jobs: name: "Upload build logs on failure" uses: actions/upload-artifact@v4 with: - name: php-cli-logs-${{ inputs.php-version }}-${{ inputs.os }} + name: spc-logs-${{ inputs.php-version }}-${{ inputs.os }} path: log/*.log # Upload cli executable From c6802996547f4ed938b2eab2c08fcc79b8df71de Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 19:12:19 +0700 Subject: [PATCH 07/33] libavif needs at least one encoder to work --- config/lib.json | 7 +++++++ src/SPC/builder/unix/library/libavif.php | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/config/lib.json b/config/lib.json index 3be972484..087c38931 100644 --- a/config/lib.json +++ b/config/lib.json @@ -373,6 +373,13 @@ ], "static-libs-windows": [ "avif.lib" + ], + "lib-suggests": [ + "libaom", + "libwebp", + "libjpeg", + "libxml2", + "libpng" ] }, "libcares": { diff --git a/src/SPC/builder/unix/library/libavif.php b/src/SPC/builder/unix/library/libavif.php index fbd4fa18c..a5b57aef2 100644 --- a/src/SPC/builder/unix/library/libavif.php +++ b/src/SPC/builder/unix/library/libavif.php @@ -11,6 +11,11 @@ trait libavif protected function build(): void { UnixCMakeExecutor::create($this) + ->optionalLib('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF') + ->optionalLib('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF') + ->optionalLib('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF') + ->optionalLib('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF') + ->optionalLib('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF') ->addConfigureArgs('-DAVIF_LIBYUV=OFF') ->build(); // patch pkgconfig From 608c915e14bc74a6adaac36400cb60f8c99157f0 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 19:14:29 +0700 Subject: [PATCH 08/33] should depend on it instead --- config/lib.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/config/lib.json b/config/lib.json index 087c38931..ebbf4b87b 100644 --- a/config/lib.json +++ b/config/lib.json @@ -374,8 +374,10 @@ "static-libs-windows": [ "avif.lib" ], + "lib-depends": [ + "libaom" + ], "lib-suggests": [ - "libaom", "libwebp", "libjpeg", "libxml2", From 98117c3a04b0749368b3c2f24f09c86ddf026fa3 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 19:56:59 +0700 Subject: [PATCH 09/33] remove pre built --- config/source.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/source.json b/config/source.json index 036260155..114118bb4 100644 --- a/config/source.json +++ b/config/source.json @@ -526,7 +526,7 @@ "libavif": { "type": "ghtar", "repo": "AOMediaCodec/libavif", - "provide-pre-built": true, + "provide-pre-built": false, "license": { "type": "file", "path": "LICENSE" From 5623fed37fa03aaed7e8da8e686dc6d03156a174 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 21:05:18 +0700 Subject: [PATCH 10/33] fix redownloading go-xcaddy every time --- src/SPC/ConsoleApplication.php | 2 +- src/SPC/store/pkg/GoXcaddy.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SPC/ConsoleApplication.php b/src/SPC/ConsoleApplication.php index 415af40d9..750c49e4b 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ */ final class ConsoleApplication extends Application { - public const string VERSION = '2.8.2'; + public const string VERSION = '2.8.3'; public function __construct() { diff --git a/src/SPC/store/pkg/GoXcaddy.php b/src/SPC/store/pkg/GoXcaddy.php index 93821aaa9..462342dba 100644 --- a/src/SPC/store/pkg/GoXcaddy.php +++ b/src/SPC/store/pkg/GoXcaddy.php @@ -30,8 +30,8 @@ public function getSupportName(): array public function fetch(string $name, bool $force = false, ?array $config = null): void { $pkgroot = PKG_ROOT_PATH; - $go_exec = "{$pkgroot}/{$name}/bin/go"; - $xcaddy_exec = "{$pkgroot}/{$name}/bin/xcaddy"; + $go_exec = "{$pkgroot}/go-xcaddy/bin/go"; + $xcaddy_exec = "{$pkgroot}/go-xcaddy/bin/xcaddy"; if ($force) { FileSystem::removeDir("{$pkgroot}/{$name}"); } From d83a597689b79f435b3fa902defdd5825f79af70 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 17 Feb 2026 21:49:30 +0700 Subject: [PATCH 11/33] unquote the string in case a shell script passes it stupidly --- src/SPC/builder/unix/UnixBuilderBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 464c9b2fc..2f192a12f 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -365,6 +365,7 @@ protected function processFrankenphpApp(): void $frankenphpAppPath = $this->getOption('with-frankenphp-app'); if ($frankenphpAppPath) { + $frankenphpAppPath = trim($frankenphpAppPath, "\"'"); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } From 471df00ea3950ec21c3bed44bcfa9f78562f014d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 19 Feb 2026 23:07:17 +0800 Subject: [PATCH 12/33] Use StaticPHP instead of static-php-cli --- README-zh.md | 8 ++++---- README.md | 9 ++++----- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README-zh.md b/README-zh.md index d8d1b3964..8dc8d0a3e 100755 --- a/README-zh.md +++ b/README-zh.md @@ -1,4 +1,4 @@ -# static-php-cli +# StaticPHP [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) @@ -6,7 +6,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) -**static-php-cli** 是一个用于构建静态、独立 PHP 运行时的强大工具,支持众多流行扩展。 +**StaticPHP** 是一个用于构建静态编译可执行文件(包括 PHP、扩展等)的强大工具。 ## 特性 @@ -80,7 +80,7 @@ download-options: ### 3. 静态 PHP 使用 -现在您可以将 static-php-cli 构建的二进制文件复制到另一台机器上,无需依赖即可运行: +现在您可以将 StaticPHP 构建的二进制文件复制到另一台机器上,无需依赖即可运行: ``` # php-cli @@ -97,7 +97,7 @@ buildroot/bin/php-fpm -v ## 文档 -当前 README 包含基本用法。有关 static-php-cli 的所有功能, +当前 README 包含基本用法。有关 StaticPHP 的所有功能, 请访问 。 ## 直接下载 diff --git a/README.md b/README.md index 3f3bfbf1e..1d355c469 100755 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# static-php-cli +# StaticPHP [![Chinese readme](https://img.shields.io/badge/README-%E4%B8%AD%E6%96%87%20%F0%9F%87%A8%F0%9F%87%B3-moccasin?style=flat-square)](README-zh.md) [![English readme](https://img.shields.io/badge/README-English%20%F0%9F%87%AC%F0%9F%87%A7-moccasin?style=flat-square)](README.md) @@ -6,8 +6,7 @@ [![CI](https://img.shields.io/github/actions/workflow/status/crazywhalecc/static-php-cli/tests.yml?branch=main&label=Build%20Test&style=flat-square)](https://github.com/crazywhalecc/static-php-cli/actions/workflows/tests.yml) [![License](https://img.shields.io/badge/License-MIT-blue.svg?style=flat-square)](https://github.com/crazywhalecc/static-php-cli/blob/main/LICENSE) -**static-php-cli** is a powerful tool designed for building static, standalone PHP runtime -with popular extensions. +**StaticPHP** is a powerful tool designed for building portable executables including PHP, extensions, and more. ## Features @@ -81,7 +80,7 @@ Run command: ### 3. Static PHP usage -Now you can copy binaries built by static-php-cli to another machine and run with no dependencies: +Now you can copy binaries built by StaticPHP to another machine and run with no dependencies: ``` # php-cli @@ -98,7 +97,7 @@ buildroot/bin/php-fpm -v ## Documentation -The current README contains basic usage. For all the features of static-php-cli, +The current README contains basic usage. For all the features of StaticPHP, see . ## Direct Download From d49545590221725cdfbc217ea4c8f70a9c661369 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 23 Feb 2026 10:32:08 +0800 Subject: [PATCH 13/33] Remove motd for lint-config command --- src/StaticPHP/Command/Dev/LintConfigCommand.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/StaticPHP/Command/Dev/LintConfigCommand.php b/src/StaticPHP/Command/Dev/LintConfigCommand.php index 1efba4d5a..ad1efb517 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -13,6 +13,8 @@ #[AsCommand('dev:lint-config', 'Lint configuration file format', ['dev:sort-config'])] class LintConfigCommand extends BaseCommand { + protected bool $no_motd = true; + public function handle(): int { $checkOnly = $this->input->getOption('check'); @@ -37,6 +39,9 @@ public function handle(): int return static::VALIDATION_ERROR; } + if (!$hasChanges) { + $this->output->writeln('No changes.'); + } return static::SUCCESS; } From 2a8fa7d15547fab68b7b320353896b2348a31809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Dunglas?= Date: Mon, 23 Feb 2026 16:29:43 +0100 Subject: [PATCH 14/33] Update build flags for FrankenPHP in UnixBuilderBase --- src/SPC/builder/unix/UnixBuilderBase.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 2f192a12f..1b532bbcf 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -456,6 +456,7 @@ protected function buildFrankenphp(): void 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . + '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", From a35751010990ee3c9ef90582aac2f2083052c6ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 10:02:16 +0800 Subject: [PATCH 15/33] Add frankenphp building message for console output --- src/Package/Target/php/frankenphp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 066870902..889f90b39 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -29,6 +29,7 @@ public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $in } // process --with-frankenphp-app option + InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); $package->runStage([$this, 'processFrankenphpApp']); // modules @@ -114,7 +115,6 @@ public function processFrankenphpApp(TargetPackage $package): void $frankenphpAppPath = $package->getBuildOption('with-frankenphp-app'); if ($frankenphpAppPath) { - InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('processing --with-frankenphp-app option')); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } From 08595cca73792faf587ffcb4fa4cd3dd32844f63 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:45:06 +0800 Subject: [PATCH 16/33] Add PatchDescription attribute to libacl for Unix FPM_EXTRA_LIBS fix --- src/Package/Library/libacl.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Package/Library/libacl.php b/src/Package/Library/libacl.php index a74cb2d43..97c57d39f 100644 --- a/src/Package/Library/libacl.php +++ b/src/Package/Library/libacl.php @@ -8,6 +8,7 @@ use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Util\FileSystem; @@ -16,6 +17,7 @@ class libacl { #[BeforeStage('php', [php::class, 'makeForUnix'], 'libacl')] + #[PatchDescription('Fix FPM_EXTRA_LIBS to avoid linking with acl on Unix')] public function patchBeforeMakePhpUnix(LibraryPackage $lib): void { $file_path = SOURCE_PATH . '/php-src/Makefile'; From 0f012f267bd8d96642abc09df318dec7dfaf686f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:45:16 +0800 Subject: [PATCH 17/33] Rename tracker file from .spc-tracker.json to .build.json --- src/StaticPHP/Util/BuildRootTracker.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Util/BuildRootTracker.php b/src/StaticPHP/Util/BuildRootTracker.php index 306bf90ca..eae9a21dc 100644 --- a/src/StaticPHP/Util/BuildRootTracker.php +++ b/src/StaticPHP/Util/BuildRootTracker.php @@ -15,7 +15,7 @@ class BuildRootTracker /** @var array}> Tracking data */ protected array $tracking_data = []; - protected static string $tracker_file = BUILD_ROOT_PATH . '/.spc-tracker.json'; + protected static string $tracker_file = BUILD_ROOT_PATH . '/.build.json'; protected ?DirDiff $current_diff = null; From a57b48fda6ab3ccb9b3a9b2ef2b67d4115f629ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:45:30 +0800 Subject: [PATCH 18/33] Add macOS check to patchBeforePHPConfigure for explicit_bzero detection --- src/Package/Library/postgresql.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 84b4657e0..98392ced7 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -27,8 +27,11 @@ class postgresql extends LibraryPackage #[PatchDescription('Patch to avoid explicit_bzero detection issues on some systems')] public function patchBeforePHPConfigure(TargetPackage $package): void { - shell()->cd($package->getSourceDir()) - ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + if (SystemTarget::getTargetOS() === 'Darwin') { + // on macOS, explicit_bzero is available but causes build failure due to detection issues, so we fake it as unavailable + shell()->cd($package->getSourceDir()) + ->exec('sed -i.backup "s/ac_cv_func_explicit_bzero\" = xyes/ac_cv_func_explicit_bzero\" = x_fake_yes/" ./configure'); + } } #[PatchBeforeBuild] From 3238c447451c661b23b659fa410af9fe8363b217 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 15:46:18 +0800 Subject: [PATCH 19/33] Refactor FrankenPHP build and smoke test processes for Unix --- src/Package/Target/php.php | 16 +- src/Package/Target/php/frankenphp.php | 27 +- src/Package/Target/php/unix.php | 354 +++++++++++++----- src/StaticPHP/Package/PhpExtensionPackage.php | 91 ++++- 4 files changed, 393 insertions(+), 95 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c21ae5909..ff92ed6a5 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,6 +4,7 @@ namespace Package\Target; +use Package\Target\php\frankenphp; use Package\Target\php\unix; use Package\Target\php\windows; use StaticPHP\Attribute\Package\BeforeStage; @@ -42,6 +43,7 @@ class php extends TargetPackage { use unix; use windows; + use frankenphp; /** @var string[] Supported major PHP versions */ public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; @@ -119,6 +121,7 @@ public function init(TargetPackage $package): void $package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d'); $package->addBuildOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); $package->addBuildOption('enable-zts', null, null, 'Enable thread safe support'); + $package->addBuildOption('no-smoke-test', null, InputOption::VALUE_OPTIONAL, 'Disable smoke test for specific SAPIs, or all if no value provided', false); // phpmicro build options if ($package->getName() === 'php' || $package->getName() === 'php-micro') { @@ -198,6 +201,11 @@ public function resolveBuild(TargetPackage $package, PackageInstaller $installer $installer->addBuildPackage('php-embed'); } + // frankenphp depends on embed SAPI (libphp.a) + if ($package->getName() === 'frankenphp') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -209,7 +217,7 @@ public function validate(Package $package): void if (!$package->getBuildOption('enable-zts')) { throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); } - // frankenphp doesn't support windows, BSD is currently not supported by static-php-cli + // frankenphp doesn't support windows, BSD is currently not supported by StaticPHP if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); } @@ -272,10 +280,10 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void // Patch StaticPHP version // detect patch (remove this when 8.3 deprecated) $file = FileSystem::readFile("{$package->getSourceDir()}/main/main.c"); - if (!str_contains($file, 'static-php-cli.version')) { + if (!str_contains($file, 'StaticPHP.version')) { $version = SPC_VERSION; - logger()->debug('Inserting static-php-cli.version to php-src'); - $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); + logger()->debug('Inserting StaticPHP.version to php-src'); + $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"StaticPHP.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file); } diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 889f90b39..d8324574c 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -7,6 +7,7 @@ use Package\Target\php; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,7 +23,7 @@ trait frankenphp { #[Stage] - public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void + public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void { if (getenv('GOROOT') === false) { throw new SPCInternalException('go-xcaddy is not initialized properly. GOROOT is not set.'); @@ -89,6 +90,7 @@ public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $in 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . + '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", @@ -103,6 +105,29 @@ public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $in $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); } + #[Stage] + public function smokeTestFrankenphpForUnix(): void + { + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); + $frankenphp = BUILD_BIN_PATH . '/frankenphp'; + if (!file_exists($frankenphp)) { + throw new ValidationException( + "FrankenPHP binary not found: {$frankenphp}", + validation_module: 'FrankenPHP smoke test' + ); + } + $prefix = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_' : 'LD_'; + [$ret, $output] = shell() + ->setEnv(["{$prefix}LIBRARY_PATH" => BUILD_LIB_PATH]) + ->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new ValidationException( + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', + validation_module: 'FrankenPHP smoke test' + ); + } + } + /** * Process the --with-frankenphp-app option * Creates app.tar and app.checksum in source/frankenphp directory diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 13c897807..bb64271e9 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -12,6 +12,7 @@ use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\PatchException; use StaticPHP\Exception\SPCException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,6 +23,7 @@ use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; +use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\SPCConfigUtil; use StaticPHP\Util\System\UnixUtil; use StaticPHP\Util\V2CompatLayer; @@ -29,8 +31,6 @@ trait unix { - use frankenphp; - #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] @@ -49,47 +49,11 @@ public function patchBeforeBuildconf(TargetPackage $package): void FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } - #[BeforeStage('php', [php::class, 'makeForUnix'], 'php')] - #[PatchDescription('Patch TSRM for musl TLS symbol visibility issue')] - #[PatchDescription('Patch ext/standard/info.c for configure command info')] - public function patchTSRMBeforeUnixMake(ToolchainInterface $toolchain): void - { - if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { - // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } else { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } - - if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef CONFIGURE_COMMAND', - '#ifdef NO_CONFIGURE_COMMAND', - ); - } else { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef NO_CONFIGURE_COMMAND', - '#ifdef CONFIGURE_COMMAND', - ); - } - } - #[Stage] public function buildconfForUnix(TargetPackage $package): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); V2CompatLayer::emitPatchPoint('before-php-buildconf'); - // run ./buildconf shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } @@ -102,6 +66,13 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args = []; $version_id = self::getPHPVersionID(); + + // disable undefined behavior sanitizer when opcache JIT is enabled (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && !$package->getBuildOption('disable-opcache-jit', false)) { + if ($version_id >= 80500 || $installer->isPackageResolved('ext-opcache')) { + f_putenv('SPC_COMPILER_EXTRA=-fno-sanitize=undefined'); + } + } // PHP JSON extension is built-in since PHP 8.0 if ($version_id < 80000) { $args[] = '--enable-json'; @@ -122,7 +93,9 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta } // perform enable cli options $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; - $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm'; + $args[] = $installer->isPackageResolved('php-fpm') + ? '--enable-fpm' . ($installer->isPackageResolved('libacl') ? ' --with-fpm-acl' : '') + : '--disable-fpm'; $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { 'Linux' => '--enable-micro=all-static', default => '--enable-micro', @@ -151,23 +124,18 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - // cli if ($installer->isPackageResolved('php-cli')) { $package->runStage([self::class, 'makeCliForUnix']); } - // cgi if ($installer->isPackageResolved('php-cgi')) { $package->runStage([self::class, 'makeCgiForUnix']); } - // fpm if ($installer->isPackageResolved('php-fpm')) { $package->runStage([self::class, 'makeFpmForUnix']); } - // micro if ($installer->isPackageResolved('php-micro')) { $package->runStage([self::class, 'makeMicroForUnix']); } - // embed if ($installer->isPackageResolved('php-embed')) { $package->runStage([self::class, 'makeEmbedForUnix']); } @@ -180,6 +148,9 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cli"); @@ -195,6 +166,9 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cgi"); @@ -210,6 +184,9 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} fpm"); @@ -219,44 +196,49 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install } #[Stage] - #[PatchDescription('Patch micro.sfx after UPX compression')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); - // apply --with-micro-fake-cli option - $vars = $this->makeVars($installer); - $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - - $dst = BUILD_BIN_PATH . '/micro.sfx'; - $builder->deployBinary("{$package->getSourceDir()}/sapi/micro/micro.sfx", $dst); - - /* - * Patch micro.sfx after UPX compression. - * micro needs special section handling in LinuxBuilder. - * The micro.sfx does not support UPX directly, but we can remove UPX - * info segment to adapt. - * This will also make micro.sfx with upx-packed more like a malware fore antivirus - */ - if ($package->getBuildOption('with-upx-pack') && SystemTarget::getTargetOS() === 'Linux') { - // strip first - // cut binary with readelf - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + $phar_patched = false; + try { + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); + // apply --with-micro-fake-cli option + $vars = $this->makeVars($installer); + $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $makeArgs = $this->makeVarsToArgs($vars); + // build + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); + + $dst = BUILD_BIN_PATH . '/micro.sfx'; + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); + // patch after UPX-ed micro.sfx (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { + // cut binary with readelf to remove UPX extra segment + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); + } + $package->setOutput('Binary path for micro SAPI', $dst); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); } - - $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); } #[Stage] @@ -285,18 +267,13 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta // process libphp.so for shared embed $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; - $libphp_so_dst = $libphp_so; if (file_exists($libphp_so)) { // rename libphp.so if -release is set if (SystemTarget::getTargetOS() === 'Linux') { - // deploy libphp.so - preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); - if (!empty($matches[1])) { - $libphp_so_dst = str_replace('.so', '-' . $matches[1] . '.so', $libphp_so); - } + $this->processLibphpSoFile($libphp_so, $installer); } // deploy - $builder->deployBinary($libphp_so, $libphp_so_dst, false); + $builder->deployBinary($libphp_so, $libphp_so, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -368,16 +345,68 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa } } + #[Stage] + public function smokeTestForUnix(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + // validate option + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=cli,fpm, skip specified smoke tests + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed', 'frankenphp']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + /* + 1. if option is not in valid tests, throw WrongUsageException + 2. if all passed options are valid, remove them from $valid_tests, and run the remaining tests + */ + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + // run cli tests + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForUnix']); + } + // run cgi tests + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForUnix']); + } + // run micro tests + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForUnix'], ['skipExtTest' => $skipExtTest]); + } + // run embed tests + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForUnix']); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function build(TargetPackage $package): void { - // virtual target, do nothing - if (in_array($package->getName(), ['php-cli', 'php-fpm', 'php-cgi', 'php-micro', 'php-embed'], true)) { + // frankenphp is not a php sapi, it's a standalone Go binary that depends on libphp.a (embed) + if ($package->getName() === 'frankenphp') { + /* @var php $this */ + $package->runStage([$this, 'buildFrankenphpForUnix']); + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); return; } - if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'buildFrankenphpUnix']); + // virtual target, do nothing + if ($package->getName() !== 'php') { return; } @@ -386,6 +415,7 @@ public function build(TargetPackage $package): void $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); + $package->runStage([$this, 'smokeTestForUnix']); } /** @@ -415,6 +445,132 @@ public function patchUnixEmbedScripts(): void } } + #[Stage] + public function smokeTestCliForUnix(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliUnix(); + } + } + + #[Stage] + public function smokeTestCgiForUnix(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + [$ret, $output] = shell()->execWithResult("echo 'Hello, World!\";' | " . BUILD_BIN_PATH . '/php-cgi -n'); + $raw_output = implode('', $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!') || !str_contains($raw_output, 'text/html')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForUnix(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '/micro.sfx'; + + // micro_ext_test + InteractiveTerm::setMessage('Running php-micro ext smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '/micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + + // micro_zend_bug_test + InteractiveTerm::setMessage('Running php-micro zend bug smoke test'); + $content = file_get_contents(ROOT_DIR . '/src/globals/common-tests/micro_zend_mm_heap_corrupted.txt'); + $test_file = SOURCE_PATH . '/micro_zend_bug_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + if ($ret !== 0) { + $raw_out = trim(implode('', $out)); + throw new ValidationException( + "micro_zend_bug_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_zend_bug_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForUnix(PackageInstaller $installer, ToolchainInterface $toolchain): void + { + $sample_file_path = SOURCE_PATH . '/embed-test'; + FileSystem::createDir($sample_file_path); + // copy embed test files + copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); + copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); + + $config = new SPCConfigUtil()->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; + if ($toolchain->isStatic()) { + $lens .= ' -static'; + } + + $dynamic_exports = ''; + $envVars = []; + $embedType = 'static'; + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $embedType = 'shared'; + $libPathKey = SystemTarget::getTargetOS() === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; + $envVars[$libPathKey] = BUILD_LIB_PATH . (($existing = getenv($libPathKey)) ? ':' . $existing : ''); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a'); + } else { + $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; + foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) { + unlink($file); + } + // calling getDynamicExportedSymbols on non-Linux is okay + if ($dynamic_exports = UnixUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { + $dynamic_exports = ' ' . $dynamic_exports; + } + } + + $cc = getenv('CC'); + InteractiveTerm::setMessage('Running php-embed build smoke test'); + [$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens}{$dynamic_exports}"); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: $embedType . ' libphp embed build smoke test' + ); + } + + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = shell()->cd($sample_file_path)->setEnv($envVars)->execWithResult('./embed'); + if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . implode("\n", $output), + validation_module: $embedType . ' libphp embed run smoke test' + ); + } + } + /** * Seek php-src/config.log when building PHP, add it to exception. */ @@ -431,6 +587,26 @@ protected function seekPhpSrcLogFileOnException(callable $callback, string $sour } } + /** + * Generate micro extension test php code. + */ + private function generateMicroExtTests(PackageInstaller $installer): string + { + $php = "getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if (!$ext->isBuildStatic()) { + continue; + } + $ext_name = $ext->getDistName(); + if (!empty($ext_name)) { + $php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n"; + $php .= "assert(extension_loaded('{$ext_name}'));\n\n"; + } + } + $php .= "echo '[micro-test-end]';\n"; + return $php; + } + /** * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. */ diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 3f2f18cf3..29dd29428 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -79,7 +79,7 @@ public function getPhpConfigureArg(string $os, bool $shared): string 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('_', '-', $this->getName()); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -146,6 +146,54 @@ public function buildShared(): void } } + /** + * Get the dist name used for `--ri` check in smoke test. + * Reads from config `dist-name` field, defaults to extension name. + */ + public function getDistName(): string + { + return $this->extension_config['dist-name'] ?? $this->getExtensionName(); + } + + /** + * Run smoke test for the extension on Unix CLI. + * Override this method in a subclass。 + */ + public function runSmokeTestCliUnix(): void + { + if (($this->extension_config['smoke-test'] ?? true) === false) { + return; + } + + $distName = $this->getDistName(); + // empty dist-name → no --ri check (e.g. password_argon2) + if ($distName === '') { + return; + } + + $sharedExtensions = $this->getSharedExtensionLoadString(); + [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $distName . '"', false); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed compile check: php-cli returned {$ret}", + validation_module: 'Extension ' . $this->getName() . ' sanity check' + ); + } + + $test_file = ROOT_DIR . '/src/globals/ext-tests/' . $this->getExtensionName() . '.php'; + if (file_exists($test_file)) { + // Trim additional content & escape special characters to allow inline usage + $test = self::escapeInlineTest(file_get_contents($test_file)); + [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' -r "' . trim($test) . '"'); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out), + validation_module: 'Extension ' . $this->getName() . ' function check' + ); + } + } + } + /** * Get shared extension build environment variables for Unix. * @@ -284,4 +332,45 @@ protected function splitLibsIntoStaticAndShared(string $allLibs): array } return [trim($staticLibString), trim($sharedLibString)]; } + + /** + * Builds the `-d extension_dir=... -d extension=...` string for all resolved shared extensions. + * Used in CLI smoke test to load shared extension dependencies at runtime. + */ + private function getSharedExtensionLoadString(): string + { + $sharedExts = array_filter( + $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), + fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() + ); + + if (empty($sharedExts)) { + return ''; + } + + $ret = ' -d "extension_dir=' . BUILD_MODULES_PATH . '"'; + foreach ($sharedExts as $ext) { + $extConfig = PackageConfig::get($ext->getName(), 'php-extension', []); + if ($extConfig['zend-extension'] ?? false) { + $ret .= ' -d "zend_extension=' . $ext->getExtensionName() . '"'; + } else { + $ret .= ' -d "extension=' . $ext->getExtensionName() . '"'; + } + } + + return $ret; + } + + /** + * Escape PHP test file content for inline `-r` usage. + * Strips Date: Thu, 26 Feb 2026 15:46:33 +0800 Subject: [PATCH 20/33] Add extension apcu --- config/pkg/ext/ext-apcu.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 config/pkg/ext/ext-apcu.yml diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml new file mode 100644 index 000000000..289de301c --- /dev/null +++ b/config/pkg/ext/ext-apcu.yml @@ -0,0 +1,11 @@ +ext-apcu: + type: php-extension + artifact: + source: + type: url + url: 'https://pecl.php.net/get/APCu' + extract: php-src/ext/apcu + filename: apcu.tgz + metadata: + license-files: [LICENSE] + license: PHP-3.01 From e9279940d7af55195420f1fcfba4a54badbf75f8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 26 Feb 2026 16:09:18 +0800 Subject: [PATCH 21/33] Add DumpStagesCommand to dump package stages and their locations --- .../Command/Dev/DumpStagesCommand.php | 157 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Package/Package.php | 10 ++ src/StaticPHP/Registry/PackageLoader.php | 20 +++ 4 files changed, 189 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/DumpStagesCommand.php diff --git a/src/StaticPHP/Command/Dev/DumpStagesCommand.php b/src/StaticPHP/Command/Dev/DumpStagesCommand.php new file mode 100644 index 000000000..4b20fe21b --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -0,0 +1,157 @@ +addArgument('packages', InputArgument::OPTIONAL, 'Comma-separated list of packages to dump, e.g. "openssl,zlib,curl". Dumps all packages if omitted.'); + $this->addArgument('output', InputArgument::OPTIONAL, 'Output file path', ROOT_DIR . '/dump-stages.json'); + $this->addOption('relative', 'r', InputOption::VALUE_NONE, 'Output file paths relative to ROOT_DIR'); + } + + public function handle(): int + { + $outputFile = $this->getArgument('output'); + $useRelative = (bool) $this->getOption('relative'); + + $filterPackages = null; + if ($packagesArg = $this->getArgument('packages')) { + $filterPackages = array_flip(parse_comma_list($packagesArg)); + } + + $result = []; + + foreach (PackageLoader::getPackages() as $name => $pkg) { + if ($filterPackages !== null && !isset($filterPackages[$name])) { + continue; + } + $entry = [ + 'type' => $pkg->getType(), + 'stages' => [], + 'before_stages' => [], + 'after_stages' => [], + ]; + + // Resolve main stages + foreach ($pkg->getStages() as $stageName => $callable) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry['stages'][$stageName] = $location; + } + + $result[$name] = $entry; + } + + // Resolve before/after stage external callbacks + foreach (PackageLoader::getAllBeforeStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['before_stages'][$stageName][] = $entry_data; + } + } + } + + foreach (PackageLoader::getAllAfterStages() as $pkgName => $stages) { + if ($filterPackages !== null && !isset($filterPackages[$pkgName])) { + continue; + } + foreach ($stages as $stageName => $callbacks) { + foreach ($callbacks as [$callable, $onlyWhen]) { + $location = $this->resolveCallableLocation($callable); + if ($location !== null && $useRelative) { + $location['file'] = $this->toRelativePath($location['file']); + } + $entry_data = $location ?? []; + if ($onlyWhen !== null) { + $entry_data['only_when_package_resolved'] = $onlyWhen; + } + $result[$pkgName]['after_stages'][$stageName][] = $entry_data; + } + } + } + + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + + $this->output->writeln('Dumped stages for ' . count($result) . " package(s) to: {$outputFile}"); + return static::SUCCESS; + } + + /** + * Resolve the file, start line, class and method name of a callable using reflection. + * + * @return null|array{file: string, line: false|int, class: string, method: string} + */ + private function resolveCallableLocation(mixed $callable): ?array + { + try { + if (is_array($callable) && count($callable) === 2) { + $ref = new \ReflectionMethod($callable[0], $callable[1]); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if ($callable instanceof \Closure) { + $ref = new \ReflectionFunction($callable); + $scopeClass = $ref->getClosureScopeClass(); + return [ + 'class' => $scopeClass !== null ? $scopeClass->getName() : '{closure}', + 'method' => '{closure}', + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + if (is_string($callable) && str_contains($callable, '::')) { + [$class, $method] = explode('::', $callable, 2); + $ref = new \ReflectionMethod($class, $method); + return [ + 'class' => $ref->getDeclaringClass()->getName(), + 'method' => $ref->getName(), + 'file' => (string) $ref->getFileName(), + 'line' => $ref->getStartLine(), + ]; + } + } catch (\ReflectionException) { + // ignore + } + return null; + } + + private function toRelativePath(string $absolutePath): string + { + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($absolutePath, $root)) { + return substr($absolutePath, strlen($root)); + } + return $absolutePath; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 8608f7617..4afa221c1 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; @@ -67,6 +68,7 @@ public function __construct() new EnvCommand(), new LintConfigCommand(), new PackLibCommand(), + new DumpStagesCommand(), ]); // add additional commands from registries diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index 64b9f2e44..fd59e98bd 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -128,6 +128,16 @@ public function addStage(string $name, callable $stage): void $this->stages[$name] = $stage; } + /** + * Get all defined stages for this package. + * + * @return array + */ + public function getStages(): array + { + return $this->stages; + } + /** * Check if the package has a specific stage defined. * diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index ca195ff0e..573ad7da8 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -240,6 +240,26 @@ public static function loadFromClass(mixed $class): void } } + /** + * Get all registered before-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllBeforeStages(): array + { + return self::$before_stages; + } + + /** + * Get all registered after-stage callbacks (raw). + * + * @return array>> + */ + public static function getAllAfterStages(): array + { + return self::$after_stages; + } + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { // match condition From da1f348daa1daad6bc6fd7e1f996f62fce9e19d1 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 27 Feb 2026 09:18:28 +0800 Subject: [PATCH 22/33] Update src/StaticPHP/Package/PhpExtensionPackage.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Package/PhpExtensionPackage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 29dd29428..582216d7e 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -157,7 +157,7 @@ public function getDistName(): string /** * Run smoke test for the extension on Unix CLI. - * Override this method in a subclass。 + * Override this method in a subclass. */ public function runSmokeTestCliUnix(): void { From 0e80f29e61ac6a8ebf55eeb839740a311eea64d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:27:19 +0800 Subject: [PATCH 23/33] Add DumpCapabilitiesCommand to output installable and buildable capabilities of packages --- .../Command/Dev/DumpCapabilitiesCommand.php | 111 ++++++++++++++++++ .../Command/Dev/DumpStagesCommand.php | 7 +- src/StaticPHP/ConsoleApplication.php | 4 + 3 files changed, 119 insertions(+), 3 deletions(-) create mode 100644 src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php diff --git a/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php new file mode 100644 index 000000000..e2f3dba99 --- /dev/null +++ b/src/StaticPHP/Command/Dev/DumpCapabilitiesCommand.php @@ -0,0 +1,111 @@ +addArgument('output', InputArgument::OPTIONAL, 'Output file path (JSON). Defaults to /dump-capabilities.json', ROOT_DIR . '/dump-capabilities.json'); + $this->addOption('print', null, InputOption::VALUE_NONE, 'Print capabilities as a table to the terminal instead of writing to a file'); + } + + public function handle(): int + { + $result = $this->buildCapabilities(); + + if ($this->getOption('print')) { + $this->printTable($result); + } else { + $outputFile = $this->getArgument('output'); + $json = json_encode($result, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + file_put_contents($outputFile, $json . PHP_EOL); + $this->output->writeln('Dumped capabilities for ' . count($result) . " package(s) to: {$outputFile}"); + } + + return static::SUCCESS; + } + + /** + * Build the capabilities map for all relevant packages. + * + * For library/target/virtual-target: + * buildable: string[] - OS families with a registered #[BuildFor] function + * installable: string[] - arch-os platforms with a declared binary + * + * For php-extension: + * buildable: array - {OS: 'yes'|'wip'|'partial'|'no'} (v2 support semantics) + * installable: (not applicable, omitted) + */ + private function buildCapabilities(): array + { + $result = []; + + // library / target / virtual-target + foreach (PackageLoader::getPackages(['library', 'target', 'virtual-target']) as $name => $pkg) { + $installable = []; + $artifact = $pkg->getArtifact(); + if ($artifact !== null) { + $installable = $artifact->getBinaryPlatforms(); + } + + $result[$name] = [ + 'type' => $pkg->getType(), + 'buildable' => $pkg->getBuildForOSList(), + 'installable' => $installable, + ]; + } + + // php-extension: buildable uses v2 support-field semantics + foreach (PackageLoader::getPackages('php-extension') as $name => $pkg) { + /* @var PhpExtensionPackage $pkg */ + $result[$name] = [ + 'type' => $pkg->getType(), + 'buildable' => $pkg->getBuildSupportStatus(), + ]; + } + + return $result; + } + + private function printTable(array $result): void + { + $table = new Table($this->output); + $table->setHeaders(['Package', 'Type', 'Buildable (OS)', 'Installable (arch-os)']); + + foreach ($result as $name => $info) { + // For php-extension, buildable is a map {OS => status} + if (is_array($info['buildable']) && array_is_list($info['buildable']) === false) { + $buildableStr = implode("\n", array_map( + static fn (string $os, string $status) => $status === 'yes' ? $os : "{$os} ({$status})", + array_keys($info['buildable']), + array_values($info['buildable']) + )); + } else { + $buildableStr = implode("\n", $info['buildable']) ?: ''; + } + + $table->addRow([ + $name, + $info['type'], + $buildableStr, + implode("\n", $info['installable'] ?? []) ?: '', + ]); + } + + $table->render(); + } +} diff --git a/src/StaticPHP/Command/Dev/DumpStagesCommand.php b/src/StaticPHP/Command/Dev/DumpStagesCommand.php index 4b20fe21b..c757ab868 100644 --- a/src/StaticPHP/Command/Dev/DumpStagesCommand.php +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -148,10 +148,11 @@ private function resolveCallableLocation(mixed $callable): ?array private function toRelativePath(string $absolutePath): string { + $normalized = realpath($absolutePath) ?: $absolutePath; $root = rtrim(ROOT_DIR, '/') . '/'; - if (str_starts_with($absolutePath, $root)) { - return substr($absolutePath, strlen($root)); + if (str_starts_with($normalized, $root)) { + return substr($normalized, strlen($root)); } - return $absolutePath; + return $normalized; } } diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 4afa221c1..023ddf840 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,10 +6,12 @@ use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\Dev\DumpCapabilitiesCommand; use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; +use StaticPHP\Command\Dev\PackageInfoCommand; use StaticPHP\Command\Dev\PackLibCommand; use StaticPHP\Command\Dev\ShellCommand; use StaticPHP\Command\DoctorCommand; @@ -69,6 +71,8 @@ public function __construct() new LintConfigCommand(), new PackLibCommand(), new DumpStagesCommand(), + new DumpCapabilitiesCommand(), + new PackageInfoCommand(), ]); // add additional commands from registries From d6ec0b78095993a18b62a67d505487513637a4e5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:31:37 +0800 Subject: [PATCH 24/33] Remove aarch64 build fix for glibc 2.17 from patchBeforeBuild method in postgresql.php --- src/Package/Library/postgresql.php | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 98392ced7..18893d0ec 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -10,7 +10,6 @@ use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Exception\FileSystemException; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -38,20 +37,6 @@ public function patchBeforePHPConfigure(TargetPackage $package): void #[PatchDescription('Various patches before building PostgreSQL')] public function patchBeforeBuild(): bool { - // fix aarch64 build on glibc 2.17 (e.g. CentOS 7) - if (SystemTarget::getLibcVersion() === '2.17' && SystemTarget::getTargetArch() === 'aarch64') { - try { - FileSystem::replaceFileStr("{$this->getSourceDir()}/src/port/pg_popcount_aarch64.c", 'HWCAP_SVE', '0'); - FileSystem::replaceFileStr( - "{$this->getSourceDir()}/src/port/pg_crc32c_armv8_choose.c", - '#if defined(__linux__) && !defined(__aarch64__) && !defined(HWCAP2_CRC32)', - '#if defined(__linux__) && !defined(HWCAP_CRC32)' - ); - } catch (FileSystemException) { - // allow file not-existence to make it compatible with old and new version - } - } - // skip the test on platforms where libpq infrastructure may be provided by statically-linked libraries FileSystem::replaceFileStr("{$this->getSourceDir()}/src/interfaces/libpq/Makefile", 'invokes exit\'; exit 1;', 'invokes exit\';'); // disable shared libs build From cfce1770704e7b53be09ed7b1c1606330eccadc1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:42:28 +0800 Subject: [PATCH 25/33] Add beforeMakeUnix method to patch TSRM.h for musl TLS symbol visibility --- src/Package/Target/php/unix.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index bb64271e9..10884c317 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -116,6 +116,26 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch TSRM.h to fix musl TLS symbol visibility for non-static builds')] + public function beforeMakeUnix(ToolchainInterface $toolchain): void + { + if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { + // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } else { + FileSystem::replaceFileStr( + SOURCE_PATH . '/php-src/TSRM/TSRM.h', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', + ); + } + } + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { From 28c82b811b4ebfc4bc99e1fd7dda417d16f73005 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:50:21 +0800 Subject: [PATCH 26/33] Add PackageInfoCommand to display package configuration information and support status --- .gitignore | 3 + src/StaticPHP/Artifact/Artifact.php | 33 +++ .../Command/Dev/PackageInfoCommand.php | 193 ++++++++++++++++++ src/StaticPHP/Package/Package.php | 10 + src/StaticPHP/Package/PhpExtensionPackage.php | 21 ++ 5 files changed, 260 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/PackageInfoCommand.php diff --git a/.gitignore b/.gitignore index d33eae535..2a351fcd9 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ log/ # spc.phar spc.phar spc.exe + +# dumped files from StaticPHP v3 +/dump-*.json diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index dc602538b..6dc35ad58 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -237,6 +237,39 @@ public function hasPlatformBinary(): bool return isset($this->config['binary'][$target]) || isset($this->custom_binary_callbacks[$target]); } + /** + * Get all platform strings for which a binary is declared (config or custom callback). + * + * For platforms where the binary type is "custom", a registered custom_binary_callback + * is required to consider it truly installable. + * + * @return string[] e.g. ['linux-x86_64', 'linux-aarch64', 'macos-aarch64'] + */ + public function getBinaryPlatforms(): array + { + $platforms = []; + if (isset($this->config['binary']) && is_array($this->config['binary'])) { + foreach ($this->config['binary'] as $platform => $platformConfig) { + $type = is_array($platformConfig) ? ($platformConfig['type'] ?? '') : ''; + if ($type === 'custom') { + // Only installable if a custom callback has been registered + if (isset($this->custom_binary_callbacks[$platform])) { + $platforms[] = $platform; + } + } else { + $platforms[] = $platform; + } + } + } + // Include custom callbacks for platforms not listed in config at all + foreach (array_keys($this->custom_binary_callbacks) as $platform) { + if (!in_array($platform, $platforms, true)) { + $platforms[] = $platform; + } + } + return $platforms; + } + public function getDownloadConfig(string $type): mixed { return $this->config[$type] ?? null; diff --git a/src/StaticPHP/Command/Dev/PackageInfoCommand.php b/src/StaticPHP/Command/Dev/PackageInfoCommand.php new file mode 100644 index 000000000..7c8691993 --- /dev/null +++ b/src/StaticPHP/Command/Dev/PackageInfoCommand.php @@ -0,0 +1,193 @@ +addArgument('package', InputArgument::REQUIRED, 'Package name to inspect'); + $this->addOption('json', null, InputOption::VALUE_NONE, 'Output as JSON instead of colored terminal display'); + } + + public function handle(): int + { + $packageName = $this->getArgument('package'); + + if (!PackageConfig::isPackageExists($packageName)) { + $this->output->writeln("Package '{$packageName}' not found."); + return static::USER_ERROR; + } + + $pkgConfig = PackageConfig::get($packageName); + $artifactConfig = ArtifactConfig::get($packageName); + $pkgInfo = Registry::getPackageConfigInfo($packageName); + $artifactInfo = Registry::getArtifactConfigInfo($packageName); + + if ($this->getOption('json')) { + return $this->outputJson($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + } + + return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + } + + private function outputJson(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + { + $data = [ + 'name' => $name, + 'registry' => $pkgInfo['registry'] ?? null, + 'package_config_file' => $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : null, + 'package' => $pkgConfig, + ]; + + if ($artifactConfig !== null) { + $data['artifact_config_file'] = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : null; + $data['artifact'] = $this->splitArtifactConfig($artifactConfig); + } + + $this->output->writeln(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::SUCCESS; + } + + private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + { + $type = $pkgConfig['type'] ?? 'unknown'; + $registry = $pkgInfo['registry'] ?? 'unknown'; + $pkgFile = $pkgInfo ? $this->toRelativePath($pkgInfo['config']) : 'unknown'; + + // Header + $this->output->writeln(''); + $this->output->writeln("Package: {$name} Type: {$type} Registry: {$registry}"); + $this->output->writeln("Config file: {$pkgFile}"); + $this->output->writeln(''); + + // Package config fields (excluding type and artifact which are shown separately) + $pkgFields = array_diff_key($pkgConfig, array_flip(['type', 'artifact'])); + if (!empty($pkgFields)) { + $this->output->writeln('── Package Config ──'); + $this->printYamlBlock($pkgFields, 0); + $this->output->writeln(''); + } + + // Artifact config + if ($artifactConfig !== null) { + $artifactFile = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : 'unknown'; + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); + + // Check if artifact config is inline (embedded in pkg config) or separate + $inlineArtifact = $pkgConfig['artifact'] ?? null; + if (is_array($inlineArtifact)) { + $this->output->writeln(' (inline in package config)'); + } + + $split = $this->splitArtifactConfig($artifactConfig); + + foreach ($split as $section => $value) { + $this->output->writeln(''); + $this->output->writeln(" [{$section}]"); + $this->printYamlBlock($value, 4); + } + $this->output->writeln(''); + } else { + $this->output->writeln('── Artifact Config ── (none)'); + $this->output->writeln(''); + } + + return static::SUCCESS; + } + + /** + * Split artifact config into logical sections for cleaner display. + * + * @return array + */ + private function splitArtifactConfig(array $config): array + { + $sections = []; + $sectionOrder = ['source', 'source-mirror', 'binary', 'binary-mirror', 'metadata']; + foreach ($sectionOrder as $key) { + if (array_key_exists($key, $config)) { + $sections[$key] = $config[$key]; + } + } + // Any remaining unknown keys + foreach ($config as $k => $v) { + if (!array_key_exists($k, $sections)) { + $sections[$k] = $v; + } + } + return $sections; + } + + /** + * Print a value as indented YAML-style output with Symfony Console color tags. + */ + private function printYamlBlock(mixed $value, int $indent): void + { + $pad = str_repeat(' ', $indent); + if (!is_array($value)) { + $this->output->writeln($pad . $this->colorScalar($value)); + return; + } + $isList = array_is_list($value); + foreach ($value as $k => $v) { + if ($isList) { + if (is_array($v)) { + $this->output->writeln($pad . '- '); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . '- ' . $this->colorScalar($v)); + } + } else { + if (is_array($v)) { + $this->output->writeln($pad . "{$k}:"); + $this->printYamlBlock($v, $indent + 2); + } else { + $this->output->writeln($pad . "{$k}: " . $this->colorScalar($v)); + } + } + } + } + + private function colorScalar(mixed $v): string + { + if (is_bool($v)) { + return '' . ($v ? 'true' : 'false') . ''; + } + if (is_int($v) || is_float($v)) { + return '' . $v . ''; + } + if ($v === null) { + return 'null'; + } + // Strings that look like URLs + if (is_string($v) && (str_starts_with($v, 'http://') || str_starts_with($v, 'https://'))) { + return '' . $v . ''; + } + return '' . $v . ''; + } + + private function toRelativePath(string $absolutePath): string + { + $normalized = realpath($absolutePath) ?: $absolutePath; + $root = rtrim(ROOT_DIR, '/') . '/'; + if (str_starts_with($normalized, $root)) { + return substr($normalized, strlen($root)); + } + return $normalized; + } +} diff --git a/src/StaticPHP/Package/Package.php b/src/StaticPHP/Package/Package.php index fd59e98bd..1ec1a503e 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -138,6 +138,16 @@ public function getStages(): array return $this->stages; } + /** + * Get the list of OS families that have a registered build function (via #[BuildFor]). + * + * @return string[] e.g. ['Linux', 'Darwin'] + */ + public function getBuildForOSList(): array + { + return array_keys($this->build_functions); + } + /** * Check if the package has a specific stage defined. * diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 582216d7e..07bc6abd8 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -280,6 +280,27 @@ public function buildSharedForUnix(PackageBuilder $builder): void $builder->deployBinary($soFile, $soFile, false); } + /** + * Get per-OS build support status for this php-extension. + * + * Rules (same as v2): + * - OS not listed in 'support' config => 'yes' (fully supported) + * - OS listed with 'wip' => 'wip' + * - OS listed with 'partial' => 'partial' + * - OS listed with 'no' => 'no' + * + * @return array e.g. ['Linux' => 'yes', 'Darwin' => 'partial', 'Windows' => 'no'] + */ + public function getBuildSupportStatus(): array + { + $exceptions = $this->extension_config['support'] ?? []; + $result = []; + foreach (['Linux', 'Darwin', 'Windows'] as $os) { + $result[$os] = $exceptions[$os] ?? 'yes'; + } + return $result; + } + /** * Register default stages if not already defined by attributes. * This is called after all attributes have been loaded. From f9fe2adb1d61e9ab8d5d4391d6c3d894db476d48 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:52:02 +0800 Subject: [PATCH 27/33] Trim quotes from frankenphp app path to ensure valid directory check --- src/Package/Target/php/frankenphp.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index d8324574c..669d00c38 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -140,6 +140,7 @@ public function processFrankenphpApp(TargetPackage $package): void $frankenphpAppPath = $package->getBuildOption('with-frankenphp-app'); if ($frankenphpAppPath) { + $frankenphpAppPath = trim($frankenphpAppPath, "\"'"); if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } From b3d67b928a950723a952ab4f724de487feff7d83 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:54:40 +0800 Subject: [PATCH 28/33] Add tryPatchMakefileUnix method to fix //lib path in Makefile for Linux builds --- src/Package/Target/php/unix.php | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 10884c317..888ae8bda 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -136,6 +136,18 @@ public function beforeMakeUnix(ToolchainInterface $toolchain): void } } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch Makefile to fix //lib path for Linux builds')] + public function tryPatchMakefileUnix(): void + { + if (SystemTarget::getTargetOS() !== 'Linux') { + return; + } + + // replace //lib with /lib in Makefile + shell()->cd(SOURCE_PATH . '/php-src')->exec('sed -i "s|//lib|/lib|g" Makefile'); + } + #[Stage] public function makeForUnix(TargetPackage $package, PackageInstaller $installer): void { @@ -168,9 +180,6 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cli"); @@ -186,9 +195,6 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cgi"); @@ -204,9 +210,6 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} fpm"); @@ -231,9 +234,6 @@ public function makeMicroForUnix(TargetPackage $package, PackageInstaller $insta $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; $makeArgs = $this->makeVarsToArgs($vars); // build - if (SystemTarget::getTargetOS() === 'Linux') { - shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); From 8c7d113c2f802edb4ca3e0e88edd45f1ca04d8de Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 09:59:55 +0800 Subject: [PATCH 29/33] Apply smoke test control option for frankenphp --- src/Package/Target/php/frankenphp.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 669d00c38..8b2fb81d4 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -106,8 +106,19 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller } #[Stage] - public function smokeTestFrankenphpForUnix(): void + public function smokeTestFrankenphpForUnix(PackageBuilder $builder): void { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test', false); + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=frankenphp,... + }; + if ($option === 'all' || (is_array($option) && in_array('frankenphp', $option, true))) { + return; + } + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); $frankenphp = BUILD_BIN_PATH . '/frankenphp'; if (!file_exists($frankenphp)) { From fa175963f93fcf73da71d5295e023f3b70485534 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 27 Feb 2026 10:03:25 +0800 Subject: [PATCH 30/33] Enable suggested libs by default in build configurations for Unix and Windows --- .github/workflows/build-unix.yml | 2 +- .github/workflows/build-windows-x86_64.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-unix.yml b/.github/workflows/build-unix.yml index bf6df9ac4..9a40960e5 100644 --- a/.github/workflows/build-unix.yml +++ b/.github/workflows/build-unix.yml @@ -49,7 +49,7 @@ on: with-suggested-libs: description: Build with suggested libs type: boolean - default: false + default: true debug: description: Show full build logs type: boolean diff --git a/.github/workflows/build-windows-x86_64.yml b/.github/workflows/build-windows-x86_64.yml index 57a681848..a53d9e437 100644 --- a/.github/workflows/build-windows-x86_64.yml +++ b/.github/workflows/build-windows-x86_64.yml @@ -29,6 +29,10 @@ on: description: prefer pre-built binaries (reduce build time) type: boolean default: true + with-suggested-libs: + description: Build with suggested libs + type: boolean + default: true debug: description: enable debug logs type: boolean From 7623b9e673c398fa103d0d82f6794b3d3d3f3627 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 09:47:51 +0800 Subject: [PATCH 31/33] Deprecate '--debug' option and update logging level handling --- src/StaticPHP/Command/BaseCommand.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index ddcb3671d..5673e6ba3 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -87,6 +87,14 @@ protected function execute(InputInterface $input, OutputInterface $output): int OutputInterface::VERBOSITY_VERY_VERBOSE, OutputInterface::VERBOSITY_DEBUG => 'debug', default => 'warning', }; + $isDebug = false; + // if '--debug' is set, override log level to debug + if ($this->input->getOption('debug')) { + $level = 'debug'; + logger()->warning('The --debug option is deprecated and will be removed in future versions. Please use -vv or -vvv to enable debug mode.'); + $this->output->setVerbosity(OutputInterface::VERBOSITY_DEBUG); + $isDebug = true; + } logger()->setLevel($level); // ansi @@ -95,7 +103,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } // Set debug mode in ApplicationContext - $isDebug = $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; + $isDebug = $isDebug ?: $this->output->getVerbosity() >= OutputInterface::VERBOSITY_DEBUG; ApplicationContext::setDebug($isDebug); // show raw argv list for logger()->debug From c218aef9478be4a055b7accb892a7920bbc899d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 10:32:50 +0800 Subject: [PATCH 32/33] Add doctor cache check and version management to ensure environment validation --- .gitignore | 3 ++ src/Package/Target/php.php | 20 ++++++++ src/StaticPHP/Command/BaseCommand.php | 16 +++++++ src/StaticPHP/Command/BuildLibsCommand.php | 2 + src/StaticPHP/Command/BuildTargetCommand.php | 2 + src/StaticPHP/Command/DoctorCommand.php | 1 + src/StaticPHP/Command/DownloadCommand.php | 2 + src/StaticPHP/Doctor/Doctor.php | 48 ++++++++++++++++++++ 8 files changed, 94 insertions(+) diff --git a/.gitignore b/.gitignore index 2a351fcd9..21bae186b 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ packlib_files.txt .php-cs-fixer.cache .phpunit.result.cache +# doctor cache fallback (when ~/.cache/spc/ is not writable) +.spc-doctor.lock + # exclude self-runtime /bin/* !/bin/spc* diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index ff92ed6a5..54d41dc5c 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -7,6 +7,7 @@ use Package\Target\php\frankenphp; use Package\Target\php\unix; use Package\Target\php\windows; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Info; use StaticPHP\Attribute\Package\InitPackage; @@ -104,6 +105,24 @@ public static function getPHPVersion(?string $from_custom_source = null, bool $r throw new WrongUsageException('PHP version file format is malformed, please remove "./source/php-src" dir and download/extract again'); } + /** + * Get PHP version from source archive filename + * + * @return null|string PHP version (e.g., "8.4.0") + */ + public static function getPHPVersionFromArchive(bool $return_null_if_failed = false): ?string + { + $archives = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src'); + $filename = $archives['filename'] ?? ''; + if (!preg_match('/php-(\d+\.\d+\.\d+(?:RC\d+|alpha\d+|beta\d+)?)\.tar\.(?:gz|bz2|xz)/', $filename, $match)) { + if ($return_null_if_failed) { + return null; + } + throw new WrongUsageException('PHP source archive filename format is malformed (got: ' . $filename . ')'); + } + return $match[1]; + } + #[InitPackage] public function init(TargetPackage $package): void { @@ -255,6 +274,7 @@ public function info(Package $package, PackageInstaller $installer): array 'Build Target' => getenv('SPC_TARGET') ?: '', 'Build Toolchain' => ToolchainManager::getToolchainClass(), 'Build SAPI' => implode(', ', $sapis), + 'PHP Version' => self::getPHPVersion(return_null_if_failed: true) ?? self::getPHPVersionFromArchive(return_null_if_failed: true) ?? 'Unknown', 'Static Extensions (' . count($static_extensions) . ')' => implode(',', array_map(fn ($x) => substr($x->getName(), 4), $static_extensions)), 'Shared Extensions (' . count($shared_extensions) . ')' => implode(',', $shared_extensions), 'Install Packages (' . count($install_packages) . ')' => implode(',', array_map(fn ($x) => $x->getName(), $install_packages)), diff --git a/src/StaticPHP/Command/BaseCommand.php b/src/StaticPHP/Command/BaseCommand.php index 5673e6ba3..fcd39e5fc 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -5,6 +5,7 @@ namespace StaticPHP\Command; use StaticPHP\DI\ApplicationContext; +use StaticPHP\Doctor\Doctor; use StaticPHP\Exception\ExceptionHandler; use StaticPHP\Exception\SPCException; use Symfony\Component\Console\Command\Command; @@ -118,6 +119,21 @@ protected function execute(InputInterface $input, OutputInterface $output): int } } + /** + * Warn the user if doctor has not been run (or is outdated). + * Set SPC_SKIP_DOCTOR_CHECK=1 to suppress. + */ + protected function checkDoctorCache(): void + { + if (getenv('SPC_SKIP_DOCTOR_CHECK') || Doctor::isHealthy()) { + return; + } + $this->output->writeln(''); + $this->output->writeln('[WARNING] Please run `spc doctor` first to verify your build environment.'); + $this->output->writeln(''); + sleep(2); + } + protected function getOption(string $name): mixed { return $this->input->getOption($name); diff --git a/src/StaticPHP/Command/BuildLibsCommand.php b/src/StaticPHP/Command/BuildLibsCommand.php index 63a3ad0f9..c18acb0fc 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -44,6 +44,8 @@ public function configure(): void public function handle(): int { + $this->checkDoctorCache(); + $libs = parse_comma_list($this->input->getArgument('libraries')); $installer = new PackageInstaller($this->input->getOptions()); diff --git a/src/StaticPHP/Command/BuildTargetCommand.php b/src/StaticPHP/Command/BuildTargetCommand.php index 2756070bf..8e1ed6329 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -37,6 +37,8 @@ public function __construct(private readonly string $target, ?string $descriptio public function handle(): int { + $this->checkDoctorCache(); + // resolve legacy options to new options V2CompatLayer::convertOptions($this->input); diff --git a/src/StaticPHP/Command/DoctorCommand.php b/src/StaticPHP/Command/DoctorCommand.php index 6ae6d68a1..40303d141 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -26,6 +26,7 @@ public function handle(): int }; $doctor = new Doctor($this->output, $fix_policy); if ($doctor->checkAll()) { + Doctor::markPassed(); $this->output->writeln('Doctor check complete !'); return static::SUCCESS; } diff --git a/src/StaticPHP/Command/DownloadCommand.php b/src/StaticPHP/Command/DownloadCommand.php index 270f55385..e021e58b9 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -56,6 +56,8 @@ public function handle(): int return $this->handleClean(); } + $this->checkDoctorCache(); + $downloader = new ArtifactDownloader(DownloaderOptions::extractFromConsoleOptions($this->input->getOptions())); // arguments diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index 36db37ae8..fc69cc2a8 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -9,6 +9,7 @@ use StaticPHP\Exception\SPCException; use StaticPHP\Registry\DoctorLoader; use StaticPHP\Runtime\Shell\Shell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Output\OutputInterface; use ZM\Logger\ConsoleColor; @@ -25,6 +26,29 @@ public function __construct(private ?OutputInterface $output = null, private int logger()->debug("Loaded doctor check items:\n\t" . implode("\n\t", $names)); } + /** + * Returns true if doctor was previously passed with the current SPC version. + */ + public static function isHealthy(): bool + { + $lock = self::getLockPath(); + return file_exists($lock) && trim((string) @file_get_contents($lock)) === \StaticPHP\ConsoleApplication::VERSION; + } + + /** + * Write current SPC version to the lock file, marking doctor as passed. + */ + public static function markPassed(): void + { + $primary = self::getLockPath(); + if (!is_dir(dirname($primary))) { + @mkdir(dirname($primary), 0755, true); + } + if (@file_put_contents($primary, \StaticPHP\ConsoleApplication::VERSION) === false) { + @file_put_contents((getcwd() ?: '.') . DIRECTORY_SEPARATOR . '.spc-doctor.lock', \StaticPHP\ConsoleApplication::VERSION); + } + } + /** * Check all valid check items. * @return bool true if all checks passed, false otherwise @@ -119,6 +143,30 @@ public function checkItem(CheckItem|string $check, bool $interactive = true): bo return false; } + private static function getLockPath(): string + { + if (SystemTarget::getTargetOS() === 'Windows') { + $trial_ls = [ + getenv('LOCALAPPDATA') ?: ((getenv('USERPROFILE') ?: 'C:\Users\Default') . '\AppData\Local') . '\.spc-doctor.lock', + sys_get_temp_dir() . '\.spc-doctor.lock', + WORKING_DIR . '\.spc-doctor.lock', + ]; + } else { + $trial_ls = [ + getenv('XDG_CACHE_HOME') ?: ((getenv('HOME') ?: '/tmp') . '/.cache') . '/.spc-doctor.lock', + sys_get_temp_dir() . '/.spc-doctor.lock', + WORKING_DIR . '/.spc-doctor.lock', + ]; + } + foreach ($trial_ls as $path) { + if (is_writable(dirname($path))) { + return $path; + } + } + // fallback to current directory + return WORKING_DIR . DIRECTORY_SEPARATOR . '.spc-doctor.lock'; + } + private function emitFix(string $fix_item, array $fix_item_params = []): bool { keyboard_interrupt_register(function () { From d316684995d32dee5d16b2c2eca86fb21822df08 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 10:37:38 +0800 Subject: [PATCH 33/33] Add optional package support for libaom, libsharpyuv, libjpeg, libxml2, and libpng in Unix build --- config/pkg/lib/libavif.yml | 7 +++++++ src/Package/Library/libavif.php | 5 +++++ 2 files changed, 12 insertions(+) diff --git a/config/pkg/lib/libavif.yml b/config/pkg/lib/libavif.yml index 0d7ae151d..c75b05c45 100644 --- a/config/pkg/lib/libavif.yml +++ b/config/pkg/lib/libavif.yml @@ -7,5 +7,12 @@ libavif: metadata: license-files: [LICENSE] license: BSD-2-Clause + depends: + - libaom + suggests: + - libwebp + - libjpeg + - libxml2 + - libpng static-libs@unix: - libavif.a diff --git a/src/Package/Library/libavif.php b/src/Package/Library/libavif.php index 87e6c650f..6db235e19 100644 --- a/src/Package/Library/libavif.php +++ b/src/Package/Library/libavif.php @@ -17,6 +17,11 @@ class libavif public function buildUnix(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) + ->optionalPackage('libaom', '-DAVIF_CODEC_AOM=SYSTEM', '-DAVIF_CODEC_AOM=OFF') + ->optionalPackage('libsharpyuv', '-DAVIF_LIBSHARPYUV=SYSTEM', '-DAVIF_LIBSHARPYUV=OFF') + ->optionalPackage('libjpeg', '-DAVIF_JPEG=SYSTEM', '-DAVIF_JPEG=OFF') + ->optionalPackage('libxml2', '-DAVIF_LIBXML2=SYSTEM', '-DAVIF_LIBXML2=OFF') + ->optionalPackage('libpng', '-DAVIF_LIBPNG=SYSTEM', '-DAVIF_LIBPNG=OFF') ->addConfigureArgs('-DAVIF_LIBYUV=OFF') ->build(); // patch pkgconfig