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 001/178] 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 a72f3a1a..2ad4f51b 100644 --- a/src/SPC/builder/unix/library/postgresql.php +++ b/src/SPC/builder/unix/library/postgresql.php @@ -4,29 +4,14 @@ declare(strict_types=1); 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 002/178] 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 8b22658c..ba02e672 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,10 +13,10 @@ declare(strict_types=1); // 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 @@ $test_os = [ // '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 @@ $prefer_pre_built = false; // 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 @@ $with_suggested_libs = false; // 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 003/178] 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 19fdd41d..415af40d 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ use Symfony\Component\Console\Application; */ 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 004/178] 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 0166bfa0..6549a94e 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 005/178] 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 6549a94e..0f2fa172 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 006/178] 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 0f2fa172..bf6df9ac 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 007/178] 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 3be97248..087c3893 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 fbd4fa18..a5b57aef 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 008/178] 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 087c3893..ebbf4b87 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 009/178] 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 03626015..114118bb 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 010/178] 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 415af40d..750c49e4 100644 --- a/src/SPC/ConsoleApplication.php +++ b/src/SPC/ConsoleApplication.php @@ -34,7 +34,7 @@ use Symfony\Component\Console\Application; */ 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 93821aaa..462342db 100644 --- a/src/SPC/store/pkg/GoXcaddy.php +++ b/src/SPC/store/pkg/GoXcaddy.php @@ -30,8 +30,8 @@ class GoXcaddy extends CustomPackage 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 011/178] 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 464c9b2f..2f192a12 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -365,6 +365,7 @@ abstract class UnixBuilderBase extends BuilderBase $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 012/178] 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 d8d1b396..8dc8d0a3 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 3f3bfbf1..1d355c46 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 013/178] 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 1efba4d5..ad1efb51 100644 --- a/src/StaticPHP/Command/Dev/LintConfigCommand.php +++ b/src/StaticPHP/Command/Dev/LintConfigCommand.php @@ -13,6 +13,8 @@ use Symfony\Component\Yaml\Yaml; #[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 @@ class LintConfigCommand extends BaseCommand 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 014/178] 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 2f192a12..1b532bbc 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -456,6 +456,7 @@ abstract class UnixBuilderBase extends BuilderBase '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 015/178] 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 06687090..889f90b3 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -29,6 +29,7 @@ trait frankenphp } // 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 @@ trait frankenphp $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 016/178] 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 a74cb2d4..97c57d39 100644 --- a/src/Package/Library/libacl.php +++ b/src/Package/Library/libacl.php @@ -8,6 +8,7 @@ use Package\Target\php; 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 @@ use StaticPHP\Util\FileSystem; 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 017/178] 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 306bf90c..eae9a21d 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 018/178] 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 84b4657e..98392ced 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 019/178] 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 c21ae590..ff92ed6a 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,6 +4,7 @@ declare(strict_types=1); 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 @@ class php extends TargetPackage $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 @@ class php extends TargetPackage $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 @@ class php extends TargetPackage 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 @@ class php extends TargetPackage // 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 889f90b3..d8324574 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -7,6 +7,7 @@ namespace Package\Target\php; 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 @@ use ZM\Logger\ConsoleColor; 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 @@ trait frankenphp '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 @@ trait frankenphp $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 13c89780..bb64271e 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -12,6 +12,7 @@ use StaticPHP\Attribute\PatchDescription; 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\Toolchain\Interface\ToolchainInterface; 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 @@ use ZM\Logger\ConsoleColor; 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 @@ trait unix 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 @@ trait unix $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 @@ trait unix } // 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 @@ trait unix 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 @@ trait unix $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 @@ trait unix $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 @@ trait unix $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 @@ trait unix } #[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()); } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); - } + 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"); - $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); + $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(); + } + } } #[Stage] @@ -285,18 +267,13 @@ trait unix // 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 @@ trait unix } } + #[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 @@ trait unix $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); + $package->runStage([$this, 'smokeTestForUnix']); } /** @@ -415,6 +445,132 @@ trait unix } } + #[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 @@ trait unix } } + /** + * 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 3f2f18cf..29dd2942 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -79,7 +79,7 @@ class PhpExtensionPackage extends Package return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', $this->getName()); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -146,6 +146,54 @@ class PhpExtensionPackage extends Package } } + /** + * 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 @@ class PhpExtensionPackage extends Package } 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 020/178] 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 00000000..289de301 --- /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 021/178] 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 00000000..4b20fe21 --- /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 8608f761..4afa221c 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ namespace StaticPHP; 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 @@ class ConsoleApplication extends Application 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 64b9f2e4..fd59e98b 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -128,6 +128,16 @@ abstract class Package $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 ca195ff0..573ad7da 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -240,6 +240,26 @@ class PackageLoader } } + /** + * 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 022/178] 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 29dd2942..582216d7 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -157,7 +157,7 @@ class PhpExtensionPackage extends Package /** * 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 023/178] 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 00000000..e2f3dba9 --- /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 4b20fe21..c757ab86 100644 --- a/src/StaticPHP/Command/Dev/DumpStagesCommand.php +++ b/src/StaticPHP/Command/Dev/DumpStagesCommand.php @@ -148,10 +148,11 @@ class DumpStagesCommand extends BaseCommand 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 4afa221c..023ddf84 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,10 +6,12 @@ namespace StaticPHP; 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 @@ class ConsoleApplication extends Application 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 024/178] 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 98392ced..18893d0e 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -10,7 +10,6 @@ use StaticPHP\Attribute\Package\BuildFor; 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 @@ class postgresql extends LibraryPackage #[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 025/178] 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 bb64271e..10884c31 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -116,6 +116,26 @@ trait unix ])->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 026/178] 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 d33eae53..2a351fcd 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 dc602538..6dc35ad5 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -237,6 +237,39 @@ class Artifact 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 00000000..7c869199 --- /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 fd59e98b..1ec1a503 100644 --- a/src/StaticPHP/Package/Package.php +++ b/src/StaticPHP/Package/Package.php @@ -138,6 +138,16 @@ abstract class Package 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 582216d7..07bc6abd 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -280,6 +280,27 @@ class PhpExtensionPackage extends Package $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 027/178] 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 d8324574..669d00c3 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -140,6 +140,7 @@ trait frankenphp $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 028/178] 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 10884c31..888ae8bd 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -136,6 +136,18 @@ trait unix } } + #[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 @@ trait unix $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 @@ trait unix $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 @@ trait unix $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 @@ trait unix $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 029/178] 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 669d00c3..8b2fb81d 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -106,8 +106,19 @@ trait frankenphp } #[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 030/178] 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 bf6df9ac..9a40960e 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 57a68184..a53d9e43 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 031/178] 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 ddcb3671..5673e6ba 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -87,6 +87,14 @@ abstract class BaseCommand extends Command 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 @@ abstract class BaseCommand extends Command } // 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 032/178] 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 2a351fcd..21bae186 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 ff92ed6a..54d41dc5 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -7,6 +7,7 @@ namespace Package\Target; 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 @@ class php extends TargetPackage 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 @@ class php extends TargetPackage '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 5673e6ba..fcd39e5f 100644 --- a/src/StaticPHP/Command/BaseCommand.php +++ b/src/StaticPHP/Command/BaseCommand.php @@ -5,6 +5,7 @@ declare(strict_types=1); 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 @@ abstract class BaseCommand extends Command } } + /** + * 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 63a3ad0f..c18acb0f 100644 --- a/src/StaticPHP/Command/BuildLibsCommand.php +++ b/src/StaticPHP/Command/BuildLibsCommand.php @@ -44,6 +44,8 @@ class BuildLibsCommand extends BaseCommand 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 2756070b..8e1ed632 100644 --- a/src/StaticPHP/Command/BuildTargetCommand.php +++ b/src/StaticPHP/Command/BuildTargetCommand.php @@ -37,6 +37,8 @@ class BuildTargetCommand extends BaseCommand 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 6ae6d68a..40303d14 100644 --- a/src/StaticPHP/Command/DoctorCommand.php +++ b/src/StaticPHP/Command/DoctorCommand.php @@ -26,6 +26,7 @@ class DoctorCommand extends BaseCommand }; $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 270f5538..e021e58b 100644 --- a/src/StaticPHP/Command/DownloadCommand.php +++ b/src/StaticPHP/Command/DownloadCommand.php @@ -56,6 +56,8 @@ class DownloadCommand extends BaseCommand 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 36db37ae..fc69cc2a 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -9,6 +9,7 @@ use StaticPHP\DI\ApplicationContext; 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 @@ readonly class Doctor 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 @@ readonly class Doctor 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 033/178] 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 0d7ae151..c75b05c4 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 87e6c650..6db235e1 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 From 2d550a8db44f90bb063487f15111ba330727c2ad Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:43:28 +0800 Subject: [PATCH 034/178] Add simple output handling to exception classes --- src/StaticPHP/Exception/InterruptException.php | 5 ++++- src/StaticPHP/Exception/RegistryException.php | 5 ++++- src/StaticPHP/Exception/SPCException.php | 12 ++++++++++++ src/StaticPHP/Exception/WrongUsageException.php | 5 ++++- 4 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Exception/InterruptException.php b/src/StaticPHP/Exception/InterruptException.php index 77b5240a..3f55d7a8 100644 --- a/src/StaticPHP/Exception/InterruptException.php +++ b/src/StaticPHP/Exception/InterruptException.php @@ -7,4 +7,7 @@ namespace StaticPHP\Exception; /** * Exception caused by manual intervention. */ -class InterruptException extends SPCException {} +class InterruptException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/RegistryException.php b/src/StaticPHP/Exception/RegistryException.php index 347a132a..17d65cf2 100644 --- a/src/StaticPHP/Exception/RegistryException.php +++ b/src/StaticPHP/Exception/RegistryException.php @@ -4,4 +4,7 @@ declare(strict_types=1); namespace StaticPHP\Exception; -class RegistryException extends SPCException {} +class RegistryException extends SPCException +{ + protected bool $simple_output = true; +} diff --git a/src/StaticPHP/Exception/SPCException.php b/src/StaticPHP/Exception/SPCException.php index 307cf6cd..7ec27abe 100644 --- a/src/StaticPHP/Exception/SPCException.php +++ b/src/StaticPHP/Exception/SPCException.php @@ -20,6 +20,8 @@ use StaticPHP\Package\TargetPackage; */ abstract class SPCException extends \Exception { + protected bool $simple_output = false; + /** @var null|array Package information */ private ?array $package_info = null; @@ -155,6 +157,16 @@ abstract class SPCException extends \Exception return $this->extra_log_files; } + public function isSimpleOutput(): bool + { + return $this->simple_output; + } + + public function setSimpleOutput(bool $simple_output = true): void + { + $this->simple_output = $simple_output; + } + /** * Load stack trace information to detect Package, Builder, and Installer context. */ diff --git a/src/StaticPHP/Exception/WrongUsageException.php b/src/StaticPHP/Exception/WrongUsageException.php index 2044a82c..631a242a 100644 --- a/src/StaticPHP/Exception/WrongUsageException.php +++ b/src/StaticPHP/Exception/WrongUsageException.php @@ -10,4 +10,7 @@ namespace StaticPHP\Exception; * This exception is used to indicate that the SPC is being used incorrectly. * Such as when a command is not supported or an invalid argument is provided. */ -class WrongUsageException extends SPCException {} +class WrongUsageException extends SPCException +{ + protected bool $simple_output = true; +} From ed5a516004355e699f72470c39d824367baf76ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:44:23 +0800 Subject: [PATCH 035/178] Implement check-update functionality for artifacts and enhance download result handling --- src/StaticPHP/Artifact/ArtifactCache.php | 10 ++- src/StaticPHP/Artifact/ArtifactDownloader.php | 39 +++++++++++ .../Artifact/Downloader/DownloadResult.php | 26 +++++--- .../Artifact/Downloader/Type/BitBucketTag.php | 2 +- .../Downloader/Type/CheckUpdateInterface.php | 20 ++++++ .../Downloader/Type/CheckUpdateResult.php | 14 ++++ .../Artifact/Downloader/Type/FileList.php | 38 +++++++---- .../Artifact/Downloader/Type/Git.php | 64 +++++++++++++++++-- .../Downloader/Type/GitHubRelease.php | 19 +++++- .../Downloader/Type/GitHubTarball.php | 20 +++++- .../Downloader/Type/HostedPackageBin.php | 4 +- .../Artifact/Downloader/Type/LocalDir.php | 2 +- .../Artifact/Downloader/Type/PIE.php | 54 ++++++++++------ .../Artifact/Downloader/Type/PhpRelease.php | 52 +++++++++++---- .../Artifact/Downloader/Type/Url.php | 2 +- src/StaticPHP/Command/CheckUpdateCommand.php | 64 +++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + src/StaticPHP/Exception/ExceptionHandler.php | 13 +--- 18 files changed, 368 insertions(+), 77 deletions(-) create mode 100644 src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php create mode 100644 src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php create mode 100644 src/StaticPHP/Command/CheckUpdateCommand.php diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index 3302a37b..dcd75ef7 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -18,7 +18,8 @@ class ArtifactCache * filename?: string, * dirname?: string, * extract: null|'&custom'|string, - * hash: null|string + * hash: null|string, + * downloader: null|string * }, * binary: array{ * windows-x86_64?: null|array{ @@ -28,7 +29,8 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, - * version?: null|string + * version?: null|string, + * downloader: null|string * } * } * }> @@ -108,6 +110,7 @@ class ArtifactCache 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'file') { $obj = [ @@ -118,6 +121,7 @@ class ArtifactCache 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'git') { $obj = [ @@ -128,6 +132,7 @@ class ArtifactCache 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } elseif ($download_result->cache_type === 'local') { $obj = [ @@ -138,6 +143,7 @@ class ArtifactCache 'hash' => null, 'version' => $download_result->version, 'config' => $download_result->config, + 'downloader' => $download_result->downloader, ]; } if ($obj === null) { diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index fd3caeaf..b2773c80 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,6 +6,8 @@ namespace StaticPHP\Artifact; use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; use StaticPHP\Artifact\Downloader\Type\Git; use StaticPHP\Artifact\Downloader\Type\LocalDir; @@ -323,6 +325,43 @@ class ArtifactDownloader } } + public function checkUpdate(string $artifact_name, bool $prefer_source = false, bool $bare = false): CheckUpdateResult + { + $artifact = ArtifactLoader::getArtifactInstance($artifact_name); + if ($artifact === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); + } + if ($bare) { + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + throw new WrongUsageException("Artifact '{$artifact_name}' has no source config for bare update check."); + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); + } + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $config, null, $this); + } + $cache = ApplicationContext::get(ArtifactCache::class); + if ($prefer_source) { + $info = $cache->getSourceInfo($artifact_name) ?? $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()); + } else { + $info = $cache->getBinaryInfo($artifact_name, SystemTarget::getCurrentPlatformString()) ?? $cache->getSourceInfo($artifact_name); + } + if ($info === null) { + throw new WrongUsageException("Artifact '{$artifact_name}' is not downloaded yet, cannot check update."); + } + if (is_a($info['downloader'] ?? null, CheckUpdateInterface::class, true)) { + $cls = $info['downloader']; + /** @var CheckUpdateInterface $downloader */ + $downloader = new $cls(); + return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); + } + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } + public function getRetry(): int { return $this->retry; diff --git a/src/StaticPHP/Artifact/Downloader/DownloadResult.php b/src/StaticPHP/Artifact/Downloader/DownloadResult.php index 6fa40bed..2efe6945 100644 --- a/src/StaticPHP/Artifact/Downloader/DownloadResult.php +++ b/src/StaticPHP/Artifact/Downloader/DownloadResult.php @@ -17,6 +17,7 @@ class DownloadResult * @param bool $verified Whether the download has been verified (hash check) * @param null|string $version Version of the downloaded artifact (e.g., "1.2.3", "v2.0.0") * @param array $metadata Additional metadata (e.g., commit hash, release notes, etc.) + * @param null|string $downloader Class name of the downloader that performed this download */ private function __construct( public readonly string $cache_type, @@ -27,6 +28,7 @@ class DownloadResult public bool $verified = false, public readonly ?string $version = null, public readonly array $metadata = [], + public readonly ?string $downloader = null, ) { switch ($this->cache_type) { case 'archive': @@ -59,11 +61,12 @@ class DownloadResult mixed $extract = null, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { // judge if it is archive or just a pure file $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, extract: $extract, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } public static function file( @@ -71,10 +74,11 @@ class DownloadResult array $config, bool $verified = false, ?string $version = null, - array $metadata = [] + array $metadata = [], + ?string $downloader = null, ): DownloadResult { $cache_type = self::isArchiveFile($filename) ? 'archive' : 'file'; - return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata); + return new self($cache_type, config: $config, filename: $filename, verified: $verified, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -85,9 +89,9 @@ class DownloadResult * @param null|string $version Version string (tag, branch, or commit) * @param array $metadata Additional metadata (e.g., commit hash) */ - public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + public static function git(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('git', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -98,9 +102,9 @@ class DownloadResult * @param null|string $version Version string if known * @param array $metadata Additional metadata */ - public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = []): DownloadResult + public static function local(string $dirname, array $config, mixed $extract = null, ?string $version = null, array $metadata = [], ?string $downloader = null): DownloadResult { - return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata); + return new self('local', config: $config, dirname: $dirname, extract: $extract, version: $version, metadata: $metadata, downloader: $downloader); } /** @@ -136,7 +140,8 @@ class DownloadResult $this->extract, $this->verified, $version, - $this->metadata + $this->metadata, + $this->downloader, ); } @@ -154,7 +159,8 @@ class DownloadResult $this->extract, $this->verified, $this->version, - array_merge($this->metadata, [$key => $value]) + array_merge($this->metadata, [$key => $value]), + $this->downloader, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php index 30942fe1..2ecc48df 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php +++ b/src/StaticPHP/Artifact/Downloader/Type/BitBucketTag.php @@ -36,6 +36,6 @@ class BitBucketTag implements DownloadTypeInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} version {$ver} from BitBucket: {$download_url}"); default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php new file mode 100644 index 00000000..18445648 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -0,0 +1,20 @@ +fetchFileList($name, $config, $downloader); + if (isset($config['download-url'])) { + $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); + } else { + $url = $config['url'] . $filename; + } + $filename = end($versions); + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchFileList($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + + protected function fetchFileList(string $name, array $config, ArtifactDownloader $downloader): array { logger()->debug("Fetching file list from {$config['url']}"); $page = default_shell()->executeCurl($config['url'], retries: $downloader->getRetry()); @@ -33,15 +58,6 @@ class FileList implements DownloadTypeInterface uksort($versions, 'version_compare'); $filename = end($versions); $version = array_key_last($versions); - if (isset($config['download-url'])) { - $url = str_replace(['{file}', '{version}'], [$filename, $version], $config['download-url']); - } else { - $url = $config['url'] . $filename; - } - $filename = end($versions); - $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; - logger()->debug("Downloading {$name} from URL: {$url}"); - default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return [$filename, $version, $versions]; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 83c236eb..f518b396 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException; use StaticPHP\Util\FileSystem; /** git */ -class Git implements DownloadTypeInterface +class Git implements DownloadTypeInterface, CheckUpdateInterface { public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { @@ -21,8 +21,10 @@ class Git implements DownloadTypeInterface // direct branch clone if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $version = "dev-{$config['rev']}"; - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + $hash_result = shell(false)->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; + $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } if (!isset($config['regex'])) { throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); @@ -64,8 +66,62 @@ class Git implements DownloadTypeInterface $branch = $matched_version_branch[$version]; logger()->info("Matched version {$version} from branch {$branch} for {$name}"); default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null); - return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } throw new DownloaderException("No matching branch found for regex {$config['regex']} (checked {$matched_count} branches)."); } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (isset($config['rev'])) { + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url']) . ' ' . escapeshellarg('refs/heads/' . $config['rev'])); + if ($result[0] !== 0 || empty($result[1])) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $new_hash = substr($result[1][0], 0, 40); + $new_version = "dev-{$config['rev']}+{$new_hash}"; + // Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash + $old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_hash === null || $new_hash !== $old_hash, + ); + } + if (!isset($config['regex'])) { + throw new DownloaderException('Either "rev" or "regex" must be specified for git download type.'); + } + + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $result = $shell->execWithResult(SPC_GIT_EXEC . ' ls-remote ' . escapeshellarg($config['url'])); + if ($result[0] !== 0) { + throw new DownloaderException("Failed to ls-remote from {$config['url']}"); + } + $refs = $result[1]; + $matched_version_branch = []; + + $regex = '/^' . $config['regex'] . '$/'; + foreach ($refs as $ref) { + $matches = null; + if (preg_match('/^[0-9a-f]{40}\s+refs\/heads\/(.+)$/', $ref, $matches)) { + $branch = $matches[1]; + if (preg_match($regex, $branch, $vermatch) && isset($vermatch['version'])) { + $matched_version_branch[$vermatch['version']] = $vermatch[0]; + } + } + } + uksort($matched_version_branch, function ($a, $b) { + return version_compare($b, $a); + }); + if (!empty($matched_version_branch)) { + $version = array_key_first($matched_version_branch); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + throw new DownloaderException("No matching branch found for regex {$config['regex']}."); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 7b041288..15626089 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -9,7 +9,7 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; /** ghrel */ -class GitHubRelease implements DownloadTypeInterface, ValidatorInterface +class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -48,6 +48,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface */ public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array { + logger()->debug("Fetching {$name} GitHub release from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); @@ -95,7 +96,7 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; logger()->debug("Downloading {$name} asset from URL: {$asset_url}"); default_shell()->executeCurlDownload($asset_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $this->version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -117,4 +118,18 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("No sha256 digest found for GitHub release asset of {$name}, skipping hash validation"); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + if (!isset($config['match'])) { + throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); + } + $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 8aa1ac69..a9283722 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -10,7 +10,7 @@ use StaticPHP\Exception\DownloaderException; /** ghtar */ /** ghtagtar */ -class GitHubTarball implements DownloadTypeInterface +class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface { use GitHubTokenSetupTrait; @@ -77,6 +77,22 @@ class GitHubTarball implements DownloadTypeInterface [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $rel_type = match ($config['type']) { + 'ghtar' => 'releases', + 'ghtagtar' => 'tags', + default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), + }; + $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + $new_version = $this->version ?? $old_version ?? ''; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php index c5cbb3b5..11caa19d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php +++ b/src/StaticPHP/Artifact/Downloader/Type/HostedPackageBin.php @@ -26,7 +26,7 @@ class HostedPackageBin implements DownloadTypeInterface public static function getReleaseInfo(): array { if (empty(self::$release_info)) { - $rel = (new GitHubRelease())->getGitHubReleases('hosted', self::BASE_REPO); + $rel = new GitHubRelease()->getGitHubReleases('hosted', self::BASE_REPO); if (empty($rel)) { throw new DownloaderException('No releases found for hosted package-bin'); } @@ -55,7 +55,7 @@ class HostedPackageBin implements DownloadTypeInterface $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; $headers = $this->getGitHubTokenHeaders(); default_shell()->executeCurlDownload($download_url, $path, headers: $headers, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } throw new DownloaderException("No matching asset found for hosted package-bin {$name}: {$find_str}"); diff --git a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php index 93315ce3..77ac3d09 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php +++ b/src/StaticPHP/Artifact/Downloader/Type/LocalDir.php @@ -13,6 +13,6 @@ class LocalDir implements DownloadTypeInterface public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { logger()->debug("Using local source directory for {$name} from {$config['dirname']}"); - return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null); + return DownloadResult::local($config['dirname'], $config, extract: $config['extract'] ?? null, downloader: static::class); } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index e4f1a117..3a3ccc02 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -9,28 +9,13 @@ use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; /** pie */ -class PIE implements DownloadTypeInterface +class PIE implements DownloadTypeInterface, CheckUpdateInterface { public const string PACKAGIST_URL = 'https://repo.packagist.org/p2/'; public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult { - $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; - logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); - $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); - if ($data === false) { - throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); - } - $data = json_decode($data, true); - if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { - throw new DownloaderException("failed to find {$name} repo info from packagist"); - } - // get the first version - $first = $data['packages'][$config['repo']][0] ?? []; - // check 'type' => 'php-ext' or contains 'php-ext' key - if (!isset($first['php-ext'])) { - throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); - } + $first = $this->fetchPackagistInfo($name, $config, $downloader); // get download link from dist $dist_url = $first['dist']['url'] ?? null; $dist_type = $first['dist']['type'] ?? null; @@ -42,6 +27,39 @@ class PIE implements DownloadTypeInterface $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $first = $this->fetchPackagistInfo($name, $config, $downloader); + $new_version = $first['version'] ?? null; + if ($new_version === null) { + throw new DownloaderException("failed to find version info for {$name} from packagist"); + } + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || version_compare($new_version, $old_version, '>'), + ); + } + + protected function fetchPackagistInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $packagist_url = self::PACKAGIST_URL . "{$config['repo']}.json"; + logger()->debug("Fetching {$name} source from packagist index: {$packagist_url}"); + $data = default_shell()->executeCurl($packagist_url, retries: $downloader->getRetry()); + if ($data === false) { + throw new DownloaderException("Failed to fetch packagist index for {$name} from {$packagist_url}"); + } + $data = json_decode($data, true); + if (!isset($data['packages'][$config['repo']]) || !is_array($data['packages'][$config['repo']])) { + throw new DownloaderException("failed to find {$name} repo info from packagist"); + } + $first = $data['packages'][$config['repo']][0] ?? []; + if (!isset($first['php-ext'])) { + throw new DownloaderException("failed to find {$name} php-ext info from packagist, maybe not a php extension package"); + } + return $first; } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index ec6c33fa..372c7f50 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -8,7 +8,7 @@ use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; use StaticPHP\Exception\DownloaderException; -class PhpRelease implements DownloadTypeInterface, ValidatorInterface +class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; @@ -24,16 +24,7 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface $this->sha256 = null; return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); } - - // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); - if ($info === false) { - throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); - } - $info = json_decode($info, true); - if (!is_array($info) || !isset($info['version'])) { - throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); - } + $info = $this->fetchPhpReleaseInfo($name, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -49,7 +40,7 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } public function validate(string $name, array $config, ArtifactDownloader $downloader, DownloadResult $result): bool @@ -73,4 +64,41 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface logger()->debug("SHA256 checksum validated successfully for {$name}."); return true; } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $phpver = $downloader->getOption('with-php', '8.4'); + if ($phpver === 'git') { + // git version: delegate to Git checkUpdate with master branch + return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); + } + $info = $this->fetchPhpReleaseInfo($name, $downloader); + $new_version = $info['version']; + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } + + protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloader): array + { + $phpver = $downloader->getOption('with-php', '8.4'); + // Handle 'git' version to clone from php-src repository + if ($phpver === 'git') { + // cannot fetch release info for git version, return empty info to skip validation + throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); + } + + // Fetch PHP release info first + $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + if ($info === false) { + throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); + } + $info = json_decode($info, true); + if (!is_array($info) || !isset($info['version'])) { + throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); + } + return $info; + } } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Url.php b/src/StaticPHP/Artifact/Downloader/Type/Url.php index a56f4dc7..02425fe5 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Url.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Url.php @@ -18,6 +18,6 @@ class Url implements DownloadTypeInterface logger()->debug("Downloading {$name} from URL: {$url}"); $version = $config['version'] ?? null; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); } } diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php new file mode 100644 index 00000000..965fb201 --- /dev/null +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -0,0 +1,64 @@ +addArgument('artifact', InputArgument::REQUIRED, 'The name of the artifact(s) to check for updates, comma-separated'); + $this->addOption('json', null, null, 'Output result in JSON format'); + $this->addOption('bare', null, null, 'Check update without requiring the artifact to be downloaded first (old version will be null)'); + + // --with-php option for checking updates with a specific PHP version context + $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'); + } + + public function handle(): int + { + $artifacts = parse_comma_list($this->input->getArgument('artifact')); + + try { + $downloader = new ArtifactDownloader($this->input->getOptions()); + $bare = (bool) $this->getOption('bare'); + if ($this->getOption('json')) { + $outputs = []; + foreach ($artifacts as $artifact) { + $result = $downloader->checkUpdate($artifact, bare: $bare); + $outputs[$artifact] = [ + 'need-update' => $result->needUpdate, + 'old' => $result->old, + 'new' => $result->new, + ]; + } + $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::OK; + } + foreach ($artifacts as $artifact) { + $result = $downloader->checkUpdate($artifact, bare: $bare); + if (!$result->needUpdate) { + $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); + } else { + $this->output->writeln("Update available for artifact: {$artifact}"); + $this->output->writeln(" Old version: {$result->old}"); + $this->output->writeln(" New version: {$result->new}"); + } + } + return static::OK; + } catch (SPCException $e) { + $e->setSimpleOutput(); + throw $e; + } + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 023ddf84..a02b38c7 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -6,6 +6,7 @@ namespace StaticPHP; use StaticPHP\Command\BuildLibsCommand; use StaticPHP\Command\BuildTargetCommand; +use StaticPHP\Command\CheckUpdateCommand; use StaticPHP\Command\Dev\DumpCapabilitiesCommand; use StaticPHP\Command\Dev\DumpStagesCommand; use StaticPHP\Command\Dev\EnvCommand; @@ -63,6 +64,7 @@ class ConsoleApplication extends Application new SPCConfigCommand(), new DumpLicenseCommand(), new ResetCommand(), + new CheckUpdateCommand(), // dev commands new ShellCommand(), diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 20cf9395..9dddc910 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -29,12 +29,6 @@ class ExceptionHandler RegistryException::class, ]; - public const array MINOR_LOG_EXCEPTIONS = [ - InterruptException::class, - WrongUsageException::class, - RegistryException::class, - ]; - /** @var array Build PHP extra info binding */ private static array $build_php_extra_info = []; @@ -57,10 +51,7 @@ class ExceptionHandler }; self::logError($head_msg); - // ---------------------------------------- - $minor_logs = in_array($class, self::MINOR_LOG_EXCEPTIONS, true); - - if ($minor_logs) { + if ($e->isSimpleOutput()) { return self::getReturnCode($e); } @@ -283,6 +274,6 @@ class ExceptionHandler self::printArrayInfo($info); } - self::logError("---------------------------------------------------------\n", color: 'none'); + self::logError("-----------------------------------------------------------\n", color: 'none'); } } From 40e36982d342a7abcaba4277f8ebee5ee9a52c02 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 13:55:52 +0800 Subject: [PATCH 036/178] Add custom binary check-update support for artifacts --- src/Package/Artifact/go_xcaddy.php | 21 ++++++++++++++ src/Package/Artifact/zig.php | 29 +++++++++++++++++++ src/StaticPHP/Artifact/Artifact.php | 21 ++++++++++++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 9 ++++-- .../Artifact/CustomBinaryCheckUpdate.php | 11 +++++++ src/StaticPHP/Registry/ArtifactLoader.php | 22 ++++++++++++++ 6 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 5fa7327e..056a10e3 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -6,8 +6,10 @@ namespace Package\Artifact; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\GlobalEnvManager; @@ -65,6 +67,25 @@ class go_xcaddy return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); } + #[CustomBinaryCheckUpdate('go-xcaddy', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + if ($version === '') { + throw new \RuntimeException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + } + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || $version !== $old_version, + ); + } + #[AfterBinaryExtract('go-xcaddy', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 9a143063..0d334e5f 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -6,8 +6,10 @@ namespace Package\Artifact; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Attribute\Artifact\AfterBinaryExtract; use StaticPHP\Attribute\Artifact\CustomBinary; +use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; @@ -59,6 +61,33 @@ class zig return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); } + #[CustomBinaryCheckUpdate('zig', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); + $index_json = json_decode($index_json ?: '', true); + $latest_version = null; + foreach ($index_json as $version => $data) { + if ($version !== 'master') { + $latest_version = $version; + break; + } + } + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Zig version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || version_compare($latest_version, $old_version, '>'), + ); + } + #[AfterBinaryExtract('zig', [ 'linux-x86_64', 'linux-aarch64', diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 6dc35ad5..8bdd86a8 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -30,6 +30,9 @@ class Artifact /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; + /** @var array Bind custom binary check-update callbacks */ + protected array $custom_binary_check_update_callbacks = []; + /** @var null|callable Bind custom source extract callback (completely takes over extraction) */ protected mixed $source_extract_callback = null; @@ -433,6 +436,24 @@ class Artifact $this->custom_binary_callbacks[$target_os] = $callback; } + /** + * Set custom binary check-update callback for a specific target OS. + * + * @param string $target_os Target OS platform string (e.g. linux-x86_64) + * @param callable $callback Custom binary check-update callback + */ + public function setCustomBinaryCheckUpdateCallback(string $target_os, callable $callback): void + { + ConfigValidator::validatePlatformString($target_os); + $this->custom_binary_check_update_callbacks[$target_os] = $callback; + } + + public function getCustomBinaryCheckUpdateCallback(): ?callable + { + $current_platform = SystemTarget::getCurrentPlatformString(); + return $this->custom_binary_check_update_callbacks[$current_platform] ?? null; + } + // ==================== Extraction Callbacks ==================== /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b2773c80..7740d27e 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -358,8 +358,13 @@ class ArtifactDownloader /** @var CheckUpdateInterface $downloader */ $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); - } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } // custom binary: delegate to registered check-update callback + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); } public function getRetry(): int diff --git a/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php new file mode 100644 index 00000000..aa59af1a --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomBinaryCheckUpdate.php @@ -0,0 +1,11 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); + self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); self::processBinaryExtractAttribute($ref, $method, $class_instance); self::processAfterSourceExtractAttribute($ref, $method, $class_instance); @@ -118,6 +120,26 @@ class ArtifactLoader } } + /** + * Process #[CustomBinaryCheckUpdate] attribute. + */ + private static function processCustomBinaryCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomBinaryCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomBinaryCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + foreach ($instance->support_os as $os) { + self::$artifacts[$artifact_name]->setCustomBinaryCheckUpdateCallback($os, [$class_instance, $method->getName()]); + } + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomBinaryCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[SourceExtract] attribute. * This attribute allows completely taking over the source extraction process. From 550f6cad6048ec5dea7c7752d73d0cc1ac0a7fdb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:02:32 +0800 Subject: [PATCH 037/178] Replace RuntimeException with DownloaderException for Go version retrieval failure --- src/Package/Artifact/go_xcaddy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 056a10e3..ca61bd46 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -77,7 +77,7 @@ class go_xcaddy { [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); if ($version === '') { - throw new \RuntimeException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); + throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } return new CheckUpdateResult( old: $old_version, From 0a07f6b27cfc1454f9eb3e51cb656521d66b50f2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:07:05 +0800 Subject: [PATCH 038/178] cs fix --- src/StaticPHP/Artifact/ArtifactDownloader.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 7740d27e..219dfb8b 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -364,7 +364,8 @@ class ArtifactDownloader ArtifactDownloader::class => $this, 'old_version' => $info['version'], ]); - } throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + } + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); } public function getRetry(): int From a7b04d908144c2a17e6fc65d82cfe7f1815852ef Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:16:57 +0800 Subject: [PATCH 039/178] Update src/StaticPHP/Artifact/Downloader/Type/Git.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/Git.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index f518b396..4a712005 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -21,7 +21,8 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface // direct branch clone if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); - $hash_result = shell(false)->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); + $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); From 64b0e7290873ee1ec5df1452ec4d8049882b8c87 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:17:48 +0800 Subject: [PATCH 040/178] Update src/StaticPHP/Artifact/Downloader/Type/PIE.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/PIE.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index 3a3ccc02..a84cffe5 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -27,7 +27,7 @@ class PIE implements DownloadTypeInterface, CheckUpdateInterface $filename = "{$name}-{$version}." . ($dist_type === 'zip' ? 'zip' : 'tar.gz'); $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; default_shell()->executeCurlDownload($dist_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive($filename, $config, $config['extract'] ?? null, downloader: static::class); + return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $version, downloader: static::class); } public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult From 6ef5e9e067f9fb21169efbb230dc3a1b47668915 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 28 Feb 2026 14:18:32 +0800 Subject: [PATCH 041/178] Update src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php index 18445648..1adcdfea 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateInterface.php @@ -13,7 +13,7 @@ interface CheckUpdateInterface * * @param string $name the name of the artifact * @param array $config the configuration for the artifact - * @param string $old_version old version or identifier of the artifact to compare against + * @param null|string $old_version old version or identifier of the artifact to compare against * @param ArtifactDownloader $downloader the artifact downloader instance */ public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult; From 28f4a5c52387e40d02112fdc9fcc128ed03c56e2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:35:48 +0800 Subject: [PATCH 042/178] Add support for custom source check-update callbacks in artifacts --- src/StaticPHP/Artifact/Artifact.php | 16 +++++ src/StaticPHP/Artifact/ArtifactDownloader.php | 71 ++++++++++++++++--- .../Artifact/CustomSourceCheckUpdate.php | 11 +++ src/StaticPHP/Registry/ArtifactLoader.php | 20 ++++++ 4 files changed, 107 insertions(+), 11 deletions(-) create mode 100644 src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 8bdd86a8..841775e3 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -27,6 +27,9 @@ class Artifact /** @var null|callable Bind custom source fetcher callback */ protected mixed $custom_source_callback = null; + /** @var null|callable Bind custom source check-update callback */ + protected mixed $custom_source_check_update_callback = null; + /** @var array Bind custom binary fetcher callbacks */ protected mixed $custom_binary_callbacks = []; @@ -408,6 +411,19 @@ class Artifact return $this->custom_source_callback ?? null; } + /** + * Set custom source check-update callback. + */ + public function setCustomSourceCheckUpdateCallback(callable $callback): void + { + $this->custom_source_check_update_callback = $callback; + } + + public function getCustomSourceCheckUpdateCallback(): ?callable + { + return $this->custom_source_check_update_callback ?? null; + } + public function getCustomBinaryCallback(): ?callable { $current_platform = SystemTarget::getCurrentPlatformString(); diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 219dfb8b..86c06c19 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -332,17 +332,14 @@ class ArtifactDownloader throw new WrongUsageException("Artifact '{$artifact_name}' not found, please check the name."); } if ($bare) { - $config = $artifact->getDownloadConfig('source'); - if (!is_array($config)) { - throw new WrongUsageException("Artifact '{$artifact_name}' has no source config for bare update check."); + [$first, $second] = $prefer_source + ? [fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name), fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name)] + : [fn () => $this->probeBinaryCheckUpdate($artifact, $artifact_name), fn () => $this->probeSourceCheckUpdate($artifact, $artifact_name)]; + $result = $first() ?? $second(); + if ($result !== null) { + return $result; } - $cls = $this->downloaders[$config['type']] ?? null; - if (!is_a($cls, CheckUpdateInterface::class, true)) { - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); - } - /** @var CheckUpdateInterface $downloader */ - $downloader = new $cls(); - return $downloader->checkUpdate($artifact_name, $config, null, $this); + throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); } $cache = ApplicationContext::get(ArtifactCache::class); if ($prefer_source) { @@ -358,7 +355,15 @@ class ArtifactDownloader /** @var CheckUpdateInterface $downloader */ $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); - } // custom binary: delegate to registered check-update callback + } + // custom source: delegate to registered check-update callback + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => $info['version'], + ]); + } + // custom binary: delegate to registered check-update callback if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, @@ -383,6 +388,50 @@ class ArtifactDownloader return $this->options[$name] ?? $default; } + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $config = $artifact->getDownloadConfig('source'); + if (!is_array($config)) { + return null; + } + $cls = $this->downloaders[$config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $config, null, $this); + } + + private function probeBinaryCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult + { + // custom binary callback takes precedence over config-based binary + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { + return ApplicationContext::invoke($callback, [ + ArtifactDownloader::class => $this, + 'old_version' => null, + ]); + } + $binary_config = $artifact->getDownloadConfig('binary'); + $platform_config = is_array($binary_config) ? ($binary_config[SystemTarget::getCurrentPlatformString()] ?? null) : null; + if (!is_array($platform_config)) { + return null; + } + $cls = $this->downloaders[$platform_config['type']] ?? null; + if (!is_a($cls, CheckUpdateInterface::class, true)) { + return null; + } + /** @var CheckUpdateInterface $dl */ + $dl = new $cls(); + return $dl->checkUpdate($artifact_name, $platform_config, null, $this); + } + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int { $queue = $this->generateQueue($artifact); diff --git a/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php b/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php new file mode 100644 index 00000000..df6e07d6 --- /dev/null +++ b/src/StaticPHP/Attribute/Artifact/CustomSourceCheckUpdate.php @@ -0,0 +1,11 @@ +getMethods(\ReflectionMethod::IS_PUBLIC) as $method) { self::processCustomSourceAttribute($ref, $method, $class_instance); + self::processCustomSourceCheckUpdateAttribute($ref, $method, $class_instance); self::processCustomBinaryAttribute($ref, $method, $class_instance); self::processCustomBinaryCheckUpdateAttribute($ref, $method, $class_instance); self::processSourceExtractAttribute($ref, $method, $class_instance); @@ -100,6 +102,24 @@ class ArtifactLoader } } + /** + * Process #[CustomSourceCheckUpdate] attribute. + */ + private static function processCustomSourceCheckUpdateAttribute(\ReflectionClass $ref, \ReflectionMethod $method, object $class_instance): void + { + $attributes = $method->getAttributes(CustomSourceCheckUpdate::class); + foreach ($attributes as $attribute) { + /** @var CustomSourceCheckUpdate $instance */ + $instance = $attribute->newInstance(); + $artifact_name = $instance->artifact_name; + if (isset(self::$artifacts[$artifact_name])) { + self::$artifacts[$artifact_name]->setCustomSourceCheckUpdateCallback([$class_instance, $method->getName()]); + } else { + throw new ValidationException("Artifact '{$artifact_name}' not found for #[CustomSourceCheckUpdate] on '{$ref->getName()}::{$method->getName()}'"); + } + } + } + /** * Process #[CustomBinary] attribute. */ From 029f8efa120672547e40862b7c2d0fc9b5ce5739 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 14:55:18 +0800 Subject: [PATCH 043/178] Avoid empty output --- src/StaticPHP/Command/CheckUpdateCommand.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 965fb201..9a2d5f5c 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -51,8 +51,9 @@ class CheckUpdateCommand extends BaseCommand $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); } else { $this->output->writeln("Update available for artifact: {$artifact}"); - $this->output->writeln(" Old version: {$result->old}"); - $this->output->writeln(" New version: {$result->new}"); + [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; + $this->output->writeln(" Old version: {$old}"); + $this->output->writeln(" New version: {$new}"); } } return static::OK; From 4f2ca17bde89314b6fbadb41f059aa2321dfd520 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 15:16:31 +0800 Subject: [PATCH 044/178] cs fix --- src/StaticPHP/Artifact/ArtifactDownloader.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 86c06c19..b5e54078 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -356,14 +356,14 @@ class ArtifactDownloader $downloader = new $cls(); return $downloader->checkUpdate($artifact_name, $info['config'], $info['version'], $this); } - // custom source: delegate to registered check-update callback + if (($info['lock_type'] ?? null) === 'source' && ($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, 'old_version' => $info['version'], ]); } - // custom binary: delegate to registered check-update callback + if (($callback = $artifact->getCustomBinaryCheckUpdateCallback()) !== null) { return ApplicationContext::invoke($callback, [ ArtifactDownloader::class => $this, From 174ef3dba7dcae8f87adc02defc6a21e3f985f87 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 15:36:04 +0800 Subject: [PATCH 045/178] Refactor ReturnCode constants for clarity and consistency --- src/StaticPHP/Command/ReturnCode.php | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/StaticPHP/Command/ReturnCode.php b/src/StaticPHP/Command/ReturnCode.php index d152101e..5accebc3 100644 --- a/src/StaticPHP/Command/ReturnCode.php +++ b/src/StaticPHP/Command/ReturnCode.php @@ -17,26 +17,28 @@ trait ReturnCode { public const int OK = 0; - public const SUCCESS = 0; // alias of OK + public const SUCCESS = 0; // alias - public const int INTERNAL_ERROR = 1; // unsorted or internal error + public const FAILURE = 1; // generic failure - /** @deprecated Use specified error code instead */ - public const FAILURE = 1; + // 64-69: reserved for standard errors + public const int USER_ERROR = 64; // wrong usage, bad arguments - public const int USER_ERROR = 2; // wrong usage or user error + public const int VALIDATION_ERROR = 65; // invalid input or config values - public const int ENVIRONMENT_ERROR = 3; // environment not suitable for operation + public const int ENVIRONMENT_ERROR = 69; // required tools/env not available - public const int VALIDATION_ERROR = 4; // validation failed + // 70+: application-specific errors + public const int INTERNAL_ERROR = 70; // internal logic error or unexpected state - public const int FILE_SYSTEM_ERROR = 5; // file system related error + public const int BUILD_ERROR = 72; // build / compile process failed - public const int DOWNLOAD_ERROR = 6; // network related error + public const int PATCH_ERROR = 73; // patching or modifying files failed - public const int BUILD_ERROR = 7; // build process error + public const int FILE_SYSTEM_ERROR = 74; // filesystem / IO error - public const int PATCH_ERROR = 8; // patching process error + public const int DOWNLOAD_ERROR = 75; // network / remote resource error - public const int INTERRUPT_SIGNAL = 130; // process interrupted by user (e.g., Ctrl+C) + // 128+: reserved for standard signals and interrupts + public const int INTERRUPT_SIGNAL = 130; // SIGINT (Ctrl+C) } From dc0a80975f9d878a13c3d3428d363b16571287a7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 28 Feb 2026 17:07:24 +0800 Subject: [PATCH 046/178] Add PECL download type and support for PECL artifacts --- config/downloader.php | 2 + config/pkg/ext/builtin-extensions.yml | 29 +++++++ config/pkg/ext/ext-amqp.yml | 6 +- config/pkg/ext/ext-apcu.yml | 6 +- config/pkg/ext/ext-ast.yml | 6 ++ config/pkg/ext/ext-mbregex.yml | 10 --- config/pkg/ext/ext-mbstring.yml | 4 - config/pkg/ext/ext-phar.yml | 4 - config/pkg/ext/ext-readline.yml | 11 --- src/StaticPHP/Artifact/ArtifactCache.php | 4 +- .../Artifact/Downloader/Type/Git.php | 4 +- .../Artifact/Downloader/Type/PECL.php | 79 +++++++++++++++++++ src/StaticPHP/Config/ConfigValidator.php | 1 + src/StaticPHP/Runtime/Shell/DefaultShell.php | 9 ++- 14 files changed, 130 insertions(+), 45 deletions(-) create mode 100644 config/pkg/ext/ext-ast.yml delete mode 100644 config/pkg/ext/ext-mbregex.yml delete mode 100644 config/pkg/ext/ext-mbstring.yml delete mode 100644 config/pkg/ext/ext-phar.yml delete mode 100644 config/pkg/ext/ext-readline.yml create mode 100644 src/StaticPHP/Artifact/Downloader/Type/PECL.php diff --git a/config/downloader.php b/config/downloader.php index 48710a88..2d81a57e 100644 --- a/config/downloader.php +++ b/config/downloader.php @@ -10,6 +10,7 @@ use StaticPHP\Artifact\Downloader\Type\GitHubRelease; use StaticPHP\Artifact\Downloader\Type\GitHubTarball; use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; +use StaticPHP\Artifact\Downloader\Type\PECL; use StaticPHP\Artifact\Downloader\Type\PhpRelease; use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; @@ -24,6 +25,7 @@ return [ 'ghtagtar' => GitHubTarball::class, 'local' => LocalDir::class, 'pie' => PIE::class, + 'pecl' => PECL::class, 'url' => Url::class, 'php-release' => PhpRelease::class, 'hosted' => HostedPackageBin::class, diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index d12cd219..0149fe97 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -1,5 +1,19 @@ ext-bcmath: type: php-extension +ext-mbregex: + type: php-extension + depends: + - onig + - ext-mbstring + php-extension: + arg-type: custom + build-shared: false + build-static: true + display-name: mbstring +ext-mbstring: + type: php-extension + php-extension: + arg-type: custom ext-openssl: type: php-extension depends: @@ -10,6 +24,21 @@ ext-openssl: arg-type: custom arg-type@windows: with build-with-php: true +ext-phar: + type: php-extension + depends: + - zlib +ext-readline: + type: php-extension + depends: + - libedit + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path + build-shared: false + build-static: true ext-zlib: type: php-extension depends: diff --git a/config/pkg/ext/ext-amqp.yml b/config/pkg/ext/ext-amqp.yml index 93756914..1c802360 100644 --- a/config/pkg/ext/ext-amqp.yml +++ b/config/pkg/ext/ext-amqp.yml @@ -2,10 +2,8 @@ ext-amqp: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/amqp' - extract: php-src/ext/amqp - filename: amqp.tgz + type: pecl + name: amqp metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-apcu.yml b/config/pkg/ext/ext-apcu.yml index 289de301..331b04f7 100644 --- a/config/pkg/ext/ext-apcu.yml +++ b/config/pkg/ext/ext-apcu.yml @@ -2,10 +2,8 @@ ext-apcu: type: php-extension artifact: source: - type: url - url: 'https://pecl.php.net/get/APCu' - extract: php-src/ext/apcu - filename: apcu.tgz + type: pecl + name: APCu metadata: license-files: [LICENSE] license: PHP-3.01 diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml new file mode 100644 index 00000000..0684959d --- /dev/null +++ b/config/pkg/ext/ext-ast.yml @@ -0,0 +1,6 @@ +ext-ast: + type: php-extension + artifact: + source: + type: pecl + name: ast diff --git a/config/pkg/ext/ext-mbregex.yml b/config/pkg/ext/ext-mbregex.yml deleted file mode 100644 index ae59f023..00000000 --- a/config/pkg/ext/ext-mbregex.yml +++ /dev/null @@ -1,10 +0,0 @@ -ext-mbregex: - type: php-extension - depends: - - onig - - ext-mbstring - php-extension: - arg-type: custom - build-shared: false - build-static: true - display-name: mbstring diff --git a/config/pkg/ext/ext-mbstring.yml b/config/pkg/ext/ext-mbstring.yml deleted file mode 100644 index 6583ca61..00000000 --- a/config/pkg/ext/ext-mbstring.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-mbstring: - type: php-extension - php-extension: - arg-type: custom diff --git a/config/pkg/ext/ext-phar.yml b/config/pkg/ext/ext-phar.yml deleted file mode 100644 index 3625d2c0..00000000 --- a/config/pkg/ext/ext-phar.yml +++ /dev/null @@ -1,4 +0,0 @@ -ext-phar: - type: php-extension - depends: - - zlib diff --git a/config/pkg/ext/ext-readline.yml b/config/pkg/ext/ext-readline.yml deleted file mode 100644 index 19b1886c..00000000 --- a/config/pkg/ext/ext-readline.yml +++ /dev/null @@ -1,11 +0,0 @@ -ext-readline: - type: php-extension - depends: - - libedit - php-extension: - support: - Windows: wip - BSD: wip - arg-type: with-path - build-shared: false - build-static: true diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index dcd75ef7..ea3bde41 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -163,7 +163,7 @@ class ArtifactCache throw new SPCInternalException("Invalid lock type '{$lock_type}' for artifact {$artifact_name}"); } // save cache to file - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } /** @@ -281,7 +281,7 @@ class ArtifactCache */ public function save(): void { - file_put_contents($this->cache_file, json_encode($this->cache, JSON_PRETTY_PRINT)); + file_put_contents($this->cache_file, json_encode($this->cache, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); } private function isObjectDownloaded(?array $object, bool $compare_hash = false): bool diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 4a712005..1ee9da4d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -22,7 +22,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface if (isset($config['rev'])) { default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); - $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse HEAD'); + $hash_result = $shell->execWithResult(SPC_GIT_EXEC . ' -C ' . escapeshellarg($path) . ' rev-parse --short HEAD'); $hash = ($hash_result[0] === 0 && !empty($hash_result[1])) ? trim($hash_result[1][0]) : ''; $version = $hash !== '' ? "dev-{$config['rev']}+{$hash}" : "dev-{$config['rev']}"; return DownloadResult::git($name, $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); @@ -80,7 +80,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface if ($result[0] !== 0 || empty($result[1])) { throw new DownloaderException("Failed to ls-remote from {$config['url']}"); } - $new_hash = substr($result[1][0], 0, 40); + $new_hash = substr($result[1][0], 0, 7); $new_version = "dev-{$config['rev']}+{$new_hash}"; // Extract stored hash from "dev-{rev}+{hash}", null if bare mode or old format without hash $old_hash = ($old_version !== null && str_contains($old_version, '+')) ? substr(strrchr($old_version, '+'), 1) : null; diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php new file mode 100644 index 00000000..0b14b05d --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -0,0 +1,79 @@ +VERSIONSTATE per release */ + private const string PECL_REST_URL = 'https://pecl.php.net/rest/r/%s/allreleases.xml'; + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + [, $version] = $this->fetchPECLInfo($name, $config, $downloader); + return new CheckUpdateResult( + old: $old_version, + new: $version, + needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + ); + } + + public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult + { + [$filename, $version] = $this->fetchPECLInfo($name, $config, $downloader); + $url = self::PECL_BASE_URL . '/get/' . $filename; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . $filename; + logger()->debug("Downloading {$name} from URL: {$url}"); + default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); + $extract = $config['extract'] ?? ('php-src/ext/' . $this->getExtractName($name)); + return DownloadResult::archive($filename, $config, $extract, version: $version, downloader: static::class); + } + + protected function fetchPECLInfo(string $name, array $config, ArtifactDownloader $downloader): array + { + $peclName = strtolower($config['name'] ?? $this->getExtractName($name)); + $url = sprintf(self::PECL_REST_URL, $peclName); + logger()->debug("Fetching PECL release list for {$name} from REST API"); + $xml = default_shell()->executeCurl($url, retries: $downloader->getRetry()); + if ($xml === false) { + throw new DownloaderException("Failed to fetch PECL release list for {$name}"); + } + // Match VERSIONSTATE + preg_match_all('/(?P[^<]+)<\/v>(?P[^<]+)<\/s><\/r>/', $xml, $matches); + if (empty($matches['version'])) { + throw new DownloaderException("Failed to parse PECL release list for {$name}"); + } + $versions = []; + logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL"); + foreach ($matches['version'] as $i => $version) { + if ($matches['state'][$i] !== 'stable') { + continue; + } + $versions[$version] = $peclName . '-' . $version . '.tgz'; + } + if (empty($versions)) { + throw new DownloaderException("No stable releases found for {$name} on PECL"); + } + uksort($versions, 'version_compare'); + $filename = end($versions); + $version = array_key_last($versions); + return [$filename, $version, $versions]; + } + + /** + * Derive the lowercase PECL package / extract name from the artifact name. + * e.g. "ext-apcu" -> "apcu", "ext-ast" -> "ast" + */ + private function getExtractName(string $name): string + { + return strtolower(preg_replace('/^ext-/i', '', $name)); + } +} diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index fbf88321..83f9ca41 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -89,6 +89,7 @@ class ConfigValidator 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], + 'pecl' => [['name'], ['extract']], 'php-release' => [[], ['extract']], 'custom' => [[], ['func']], ]; diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 5b50d152..66dfb7ab 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -25,7 +25,7 @@ class DefaultShell extends Shell /** * Execute a cURL command to fetch data from a URL. */ - public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0): false|string + public function executeCurl(string $url, string $method = 'GET', array $headers = [], array $hooks = [], int $retries = 0, bool $compressed = false): false|string { foreach ($hooks as $hook) { $hook($method, $url, $headers); @@ -39,7 +39,8 @@ class DefaultShell extends Shell }; $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; - $cmd = SPC_CURL_EXEC . " -sfSL {$retry_arg} {$method_arg} {$header_arg} {$url_arg}"; + $compressed_arg = $compressed ? '--compressed' : ''; + $cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}"; $this->logCommandInfo($cmd); $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); @@ -72,7 +73,7 @@ class DefaultShell extends Shell $header_arg = implode(' ', array_map(fn ($v) => '"-H' . $v . '"', $headers)); $retry_arg = $retries > 0 ? "--retry {$retries}" : ''; $check = $this->console_putput ? '#' : 's'; - $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); + $cmd = clean_spaces(SPC_CURL_EXEC . " -{$check}fSL --max-time 3600 {$retry_arg} {$header_arg} -o {$path_arg} {$url_arg}"); $this->logCommandInfo($cmd); logger()->debug('[CURL DOWNLOAD] ' . $cmd); $this->passthru($cmd, $this->console_putput, capture_output: false, throw_on_error: true); @@ -93,7 +94,7 @@ class DefaultShell extends Shell $path_arg = escapeshellarg($path); $shallow_arg = $shallow ? '--depth 1 --single-branch' : ''; $submodules_arg = ($submodules === null && $shallow) ? '--recursive --shallow-submodules' : ($submodules === null ? '--recursive' : ''); - $cmd = clean_spaces("{$git} clone --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); + $cmd = clean_spaces("{$git} clone -c http.lowSpeedLimit=1 -c http.lowSpeedTime=3600 --config core.autocrlf=false --branch {$branch_arg} {$shallow_arg} {$submodules_arg} {$url_arg} {$path_arg}"); $this->logCommandInfo($cmd); logger()->debug("[GIT CLONE] {$cmd}"); $this->passthru($cmd, $this->console_putput); From 12d4009a21c418c6c303125e8901c32b5c79e0bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Mar 2026 16:32:16 +0800 Subject: [PATCH 047/178] Update PHP release handling to use configurable mirror and improve URL management --- config/artifact/php-src.yml | 4 +++ .../Artifact/Downloader/Type/PhpRelease.php | 28 +++++++++++++------ src/StaticPHP/Config/ConfigValidator.php | 2 +- 3 files changed, 25 insertions(+), 9 deletions(-) diff --git a/config/artifact/php-src.yml b/config/artifact/php-src.yml index 32bcb6cf..e304db9d 100644 --- a/config/artifact/php-src.yml +++ b/config/artifact/php-src.yml @@ -5,3 +5,7 @@ php-src: license: PHP-3.01 source: type: php-release + domain: 'https://www.php.net' + source-mirror: + type: php-release + domain: 'https://phpmirror.static-php.dev' diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index 372c7f50..b1fad70e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -10,9 +10,15 @@ use StaticPHP\Exception\DownloaderException; class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { - public const string PHP_API = 'https://www.php.net/releases/index.php?json&version={version}'; + public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; - public const string DOWNLOAD_URL = 'https://www.php.net/distributions/php-{version}.tar.xz'; + public const string API_URL = '/releases/index.php?json&version={version}'; + + public const string DOWNLOAD_URL = '/distributions/php-{version}.tar.xz'; + + public const string GIT_URL = 'https://github.com/php/php-src.git'; + + public const string GIT_REV = 'master'; private ?string $sha256 = ''; @@ -22,9 +28,9 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda // Handle 'git' version to clone from php-src repository if ($phpver === 'git') { $this->sha256 = null; - return (new Git())->download($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $downloader); + return (new Git())->download($name, ['url' => self::GIT_URL, 'rev' => self::GIT_REV], $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $downloader); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $version = $info['version']; foreach ($info['source'] as $source) { if (str_ends_with($source['filename'], '.tar.xz')) { @@ -36,7 +42,8 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda if (!isset($filename)) { throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); } - $url = str_replace('{version}', $version, self::DOWNLOAD_URL); + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= str_replace('{version}', $version, self::DOWNLOAD_URL); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); @@ -72,7 +79,7 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda // git version: delegate to Git checkUpdate with master branch return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $downloader); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); $new_version = $info['version']; return new CheckUpdateResult( old: $old_version, @@ -81,7 +88,7 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda ); } - protected function fetchPhpReleaseInfo(string $name, ArtifactDownloader $downloader): array + protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): array { $phpver = $downloader->getOption('with-php', '8.4'); // Handle 'git' version to clone from php-src repository @@ -90,8 +97,13 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); } + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= self::API_URL; + $url = str_replace('{version}', $phpver, $url); + logger()->debug("Fetching PHP release info for version {$phpver} from {$url}"); + // Fetch PHP release info first - $info = default_shell()->executeCurl(str_replace('{version}', $phpver, self::PHP_API), retries: $downloader->getRetry()); + $info = default_shell()->executeCurl($url, retries: $downloader->getRetry()); if ($info === false) { throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 83f9ca41..f011482c 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -90,7 +90,7 @@ class ConfigValidator 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], 'pecl' => [['name'], ['extract']], - 'php-release' => [[], ['extract']], + 'php-release' => [['domain'], ['extract']], 'custom' => [[], ['func']], ]; From 671ebd258284856fc5fa5770732233f2330b259d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 4 Mar 2026 16:32:49 +0800 Subject: [PATCH 048/178] Use gmp mirror site --- config/pkg/lib/gmp.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/lib/gmp.yml b/config/pkg/lib/gmp.yml index bdc13b55..c1469774 100644 --- a/config/pkg/lib/gmp.yml +++ b/config/pkg/lib/gmp.yml @@ -3,7 +3,7 @@ gmp: artifact: source: type: filelist - url: 'https://gmplib.org/download/gmp/' + url: 'https://ftp.gnu.org/gnu/gmp/' regex: '/href="(?gmp-(?[^"]+)\.tar\.xz)"/' source-mirror: type: url From 00c08e0c0ca27cb37848b1f1d57aa66c783c8966 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:11:41 +0800 Subject: [PATCH 049/178] Use no optional libs for libxml2 --- config/pkg/lib/libxml2.yml | 3 +-- src/Package/Library/libxml2.php | 12 ++++-------- src/StaticPHP/Artifact/ArtifactCache.php | 6 ++++++ 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/config/pkg/lib/libxml2.yml b/config/pkg/lib/libxml2.yml index db88e8b1..7e86b5af 100644 --- a/config/pkg/lib/libxml2.yml +++ b/config/pkg/lib/libxml2.yml @@ -10,9 +10,8 @@ libxml2: license: MIT depends@unix: - libiconv - suggests@unix: - - xz - zlib + - xz headers: - libxml2 pkg-configs: diff --git a/src/Package/Library/libxml2.php b/src/Package/Library/libxml2.php index 7c35d685..3f8b3e71 100644 --- a/src/Package/Library/libxml2.php +++ b/src/Package/Library/libxml2.php @@ -17,17 +17,13 @@ class libxml2 public function buildForLinux(LibraryPackage $lib): void { UnixCMakeExecutor::create($lib) - ->optionalPackage( - 'zlib', - '-DLIBXML2_WITH_ZLIB=ON ' . - "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a " . - "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", - '-DLIBXML2_WITH_ZLIB=OFF', - ) - ->optionalPackage('xz', ...cmake_boolean_args('LIBXML2_WITH_LZMA')) ->addConfigureArgs( '-DLIBXML2_WITH_ICONV=ON', '-DIconv_IS_BUILT_IN=OFF', + '-DLIBXML2_WITH_ZLIB=ON', + "-DZLIB_LIBRARY={$lib->getLibDir()}/libz.a", + "-DZLIB_INCLUDE_DIR={$lib->getIncludeDir()}", + '-DLIBXML2_WITH_LZMA=ON', '-DLIBXML2_WITH_ICU=OFF', // optional, but discouraged: https://gitlab.gnome.org/GNOME/libxml2/-/blob/master/README.md '-DLIBXML2_WITH_PYTHON=OFF', '-DLIBXML2_WITH_PROGRAMS=OFF', diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index dcd75ef7..2cdd0d0a 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -19,6 +19,7 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, + * time: int, * downloader: null|string * }, * binary: array{ @@ -29,6 +30,7 @@ class ArtifactCache * dirname?: string, * extract: null|'&custom'|string, * hash: null|string, + * time: int, * version?: null|string, * downloader: null|string * } @@ -108,6 +110,7 @@ class ArtifactCache 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -119,6 +122,7 @@ class ArtifactCache 'filename' => $download_result->filename, 'extract' => $download_result->extract, 'hash' => sha1_file(DOWNLOAD_PATH . '/' . $download_result->filename), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -130,6 +134,7 @@ class ArtifactCache 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => trim(exec('cd ' . escapeshellarg(DOWNLOAD_PATH . '/' . $download_result->dirname) . ' && ' . SPC_GIT_EXEC . ' rev-parse HEAD')), + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, @@ -141,6 +146,7 @@ class ArtifactCache 'dirname' => $download_result->dirname, 'extract' => $download_result->extract, 'hash' => null, + 'time' => time(), 'version' => $download_result->version, 'config' => $download_result->config, 'downloader' => $download_result->downloader, From f7277cc01238a4306e574853a16ab89447a72aa0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:17:13 +0800 Subject: [PATCH 050/178] Improve output formatting for update checks in CheckUpdateCommand --- src/StaticPHP/Command/CheckUpdateCommand.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 9a2d5f5c..4fac0f63 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -48,12 +48,10 @@ class CheckUpdateCommand extends BaseCommand foreach ($artifacts as $artifact) { $result = $downloader->checkUpdate($artifact, bare: $bare); if (!$result->needUpdate) { - $this->output->writeln("Artifact {$artifact} is already up to date (version: {$result->new})"); + $this->output->writeln("Artifact {$artifact} is already up to date ({$result->new})"); } else { - $this->output->writeln("Update available for artifact: {$artifact}"); [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; - $this->output->writeln(" Old version: {$old}"); - $this->output->writeln(" New version: {$new}"); + $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); } } return static::OK; From 715f33ac4dc1300b208996fc07fc9cd8f8c391ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:17:41 +0800 Subject: [PATCH 051/178] Add log filtering to prevent sensitive data leakage --- .../Downloader/Type/GitHubTokenSetupTrait.php | 2 ++ src/StaticPHP/Exception/ExceptionHandler.php | 2 +- src/StaticPHP/Runtime/Shell/Shell.php | 26 +++++++++---------- src/bootstrap.php | 2 +- src/globals/functions.php | 26 +++++++++++++++++++ 5 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php index 90c42507..34e350d4 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTokenSetupTrait.php @@ -16,10 +16,12 @@ trait GitHubTokenSetupTrait // GITHUB_TOKEN support if (($token = getenv('GITHUB_TOKEN')) !== false && ($user = getenv('GITHUB_USER')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' with user {$user} for authentication"); + spc_add_log_filter([$user, $token]); return ['Authorization: Basic ' . base64_encode("{$user}:{$token}")]; } if (($token = getenv('GITHUB_TOKEN')) !== false) { logger()->debug("Using 'GITHUB_TOKEN' for authentication"); + spc_add_log_filter($token); return ["Authorization: Bearer {$token}"]; } return []; diff --git a/src/StaticPHP/Exception/ExceptionHandler.php b/src/StaticPHP/Exception/ExceptionHandler.php index 9dddc910..053d82a3 100644 --- a/src/StaticPHP/Exception/ExceptionHandler.php +++ b/src/StaticPHP/Exception/ExceptionHandler.php @@ -115,7 +115,7 @@ class ExceptionHandler $msg = explode("\n", (string) $message); foreach ($msg as $v) { $line = str_pad($v, strlen($v) + $indent_space, ' ', STR_PAD_LEFT); - fwrite($spc_log, strip_ansi_colors($line) . PHP_EOL); + spc_write_log($spc_log, strip_ansi_colors($line) . PHP_EOL); if ($output_log) { InteractiveTerm::plain(ConsoleColor::$color($line) . '', 'error'); } diff --git a/src/StaticPHP/Runtime/Shell/Shell.php b/src/StaticPHP/Runtime/Shell/Shell.php index 2d0d90b8..f9f4f175 100644 --- a/src/StaticPHP/Runtime/Shell/Shell.php +++ b/src/StaticPHP/Runtime/Shell/Shell.php @@ -114,22 +114,22 @@ abstract class Shell if (!$this->enable_log_file) { return; } - // write executed command to log file using fwrite + // write executed command to log file using spc_write_log $log_file = fopen(SPC_SHELL_LOG, 'a'); - fwrite($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); - fwrite($log_file, "> Executing command: {$cmd}\n"); + spc_write_log($log_file, "\n>>>>>>>>>>>>>>>>>>>>>>>>>> [" . date('Y-m-d H:i:s') . "]\n"); + spc_write_log($log_file, "> Executing command: {$cmd}\n"); // get the backtrace to find the file and line number $backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 2); if (isset($backtrace[1]['file'], $backtrace[1]['line'])) { $file = $backtrace[1]['file']; $line = $backtrace[1]['line']; - fwrite($log_file, "> Called from: {$file} at line {$line}\n"); + spc_write_log($log_file, "> Called from: {$file} at line {$line}\n"); } - fwrite($log_file, "> Environment variables: {$this->getEnvString()}\n"); + spc_write_log($log_file, "> Environment variables: {$this->getEnvString()}\n"); if ($this->cd !== null) { - fwrite($log_file, "> Working dir: {$this->cd}\n"); + spc_write_log($log_file, "> Working dir: {$this->cd}\n"); } - fwrite($log_file, "\n"); + spc_write_log($log_file, "\n"); } /** @@ -154,7 +154,7 @@ abstract class Shell ): array { $file_res = null; if ($this->enable_log_file) { - // write executed command to the log file using fwrite + // write executed command to the log file using spc_write_log $file_res = fopen(SPC_SHELL_LOG, 'a'); } if ($console_output) { @@ -194,10 +194,10 @@ abstract class Shell foreach ([$pipes[1], $pipes[2]] as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; @@ -207,7 +207,7 @@ abstract class Shell // check exit code if ($throw_on_error && $status['exitcode'] !== 0) { if ($file_res !== null) { - fwrite($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); + spc_write_log($file_res, "Command exited with non-zero code: {$status['exitcode']}\n"); } throw new ExecutionException( cmd: $original_command ?? $cmd, @@ -238,10 +238,10 @@ abstract class Shell foreach ($read as $pipe) { while (($chunk = fread($pipe, 8192)) !== false && $chunk !== '') { if ($console_output) { - fwrite($console_res, $chunk); + spc_write_log($console_res, $chunk); } if ($file_res !== null) { - fwrite($file_res, $chunk); + spc_write_log($file_res, $chunk); } if ($capture_output) { $output_value .= $chunk; diff --git a/src/bootstrap.php b/src/bootstrap.php index 95384b71..7856c0b2 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -52,7 +52,7 @@ if (filter_var(getenv('SPC_ENABLE_LOG_FILE'), FILTER_VALIDATE_BOOLEAN)) { $log_file_fd = fopen(SPC_OUTPUT_LOG, 'a'); $ob_logger->addLogCallback(function ($level, $output) use ($log_file_fd) { if ($log_file_fd) { - fwrite($log_file_fd, strip_ansi_colors($output) . "\n"); + spc_write_log($log_file_fd, strip_ansi_colors($output) . "\n"); } return true; }); diff --git a/src/globals/functions.php b/src/globals/functions.php index 712cf621..ee279328 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -132,6 +132,32 @@ function patch_point(): string return ''; } +// Add log filter value(s) to prevent secret leak +function spc_add_log_filter(array|string $filter): void +{ + global $spc_log_filters; + if (!is_array($spc_log_filters)) { + $spc_log_filters = []; + } + if (is_string($filter)) { + if (!in_array($filter, $spc_log_filters, true)) { + $spc_log_filters[] = $filter; + } + } elseif (is_array($filter)) { + $spc_log_filters = array_values(array_unique(array_merge($spc_log_filters, $filter))); + } +} + +function spc_write_log(mixed $stream, string $data): false|int +{ + // get filter + global $spc_log_filters; + if (is_array($spc_log_filters)) { + $data = str_replace($spc_log_filters, '***', $data); + } + return fwrite($stream, $data); +} + function patch_point_interrupt(int $retcode, string $msg = ''): InterruptException { return new InterruptException(message: $msg, code: $retcode); From 5298ee4f971a510908026b518a532d6e6240e16f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 08:21:44 +0800 Subject: [PATCH 052/178] Use constant back due to config validation problem --- config/downloader.php | 32 ------------------- src/StaticPHP/Artifact/ArtifactDownloader.php | 25 ++++++++++++++- 2 files changed, 24 insertions(+), 33 deletions(-) delete mode 100644 config/downloader.php diff --git a/config/downloader.php b/config/downloader.php deleted file mode 100644 index 2d81a57e..00000000 --- a/config/downloader.php +++ /dev/null @@ -1,32 +0,0 @@ - */ -return [ - 'bitbuckettag' => BitBucketTag::class, - 'filelist' => FileList::class, - 'git' => Git::class, - 'ghrel' => GitHubRelease::class, - 'ghtar' => GitHubTarball::class, - 'ghtagtar' => GitHubTarball::class, - 'local' => LocalDir::class, - 'pie' => PIE::class, - 'pecl' => PECL::class, - 'url' => Url::class, - 'php-release' => PhpRelease::class, - 'hosted' => HostedPackageBin::class, -]; diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index b5e54078..8b64b60c 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -6,11 +6,19 @@ namespace StaticPHP\Artifact; use Psr\Log\LogLevel; use StaticPHP\Artifact\Downloader\DownloadResult; +use StaticPHP\Artifact\Downloader\Type\BitBucketTag; use StaticPHP\Artifact\Downloader\Type\CheckUpdateInterface; use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; use StaticPHP\Artifact\Downloader\Type\DownloadTypeInterface; +use StaticPHP\Artifact\Downloader\Type\FileList; use StaticPHP\Artifact\Downloader\Type\Git; +use StaticPHP\Artifact\Downloader\Type\GitHubRelease; +use StaticPHP\Artifact\Downloader\Type\GitHubTarball; +use StaticPHP\Artifact\Downloader\Type\HostedPackageBin; use StaticPHP\Artifact\Downloader\Type\LocalDir; +use StaticPHP\Artifact\Downloader\Type\PECL; +use StaticPHP\Artifact\Downloader\Type\PhpRelease; +use StaticPHP\Artifact\Downloader\Type\PIE; use StaticPHP\Artifact\Downloader\Type\Url; use StaticPHP\Artifact\Downloader\Type\ValidatorInterface; use StaticPHP\DI\ApplicationContext; @@ -31,6 +39,21 @@ use ZM\Logger\ConsoleColor; */ class ArtifactDownloader { + public const array DOWNLOADERS = [ + 'bitbuckettag' => BitBucketTag::class, + 'filelist' => FileList::class, + 'git' => Git::class, + 'ghrel' => GitHubRelease::class, + 'ghtar' => GitHubTarball::class, + 'ghtagtar' => GitHubTarball::class, + 'local' => LocalDir::class, + 'pie' => PIE::class, + 'pecl' => PECL::class, + 'url' => Url::class, + 'php-release' => PhpRelease::class, + 'hosted' => HostedPackageBin::class, + ]; + /** @var array> */ protected array $downloaders = []; @@ -198,7 +221,7 @@ class ArtifactDownloader $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; // load downloaders - $this->downloaders = require ROOT_DIR . '/config/downloader.php'; + $this->downloaders = self::DOWNLOADERS; } /** From abdaaab6e67672864b7d548c267ae1ff8fa7e871 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 11:11:02 +0800 Subject: [PATCH 053/178] Refactor CheckUpdateResult logic to simplify version comparison --- src/Package/Artifact/zig.php | 5 ++++- src/StaticPHP/Artifact/Downloader/Type/FileList.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/Git.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/PECL.php | 2 +- src/StaticPHP/Artifact/Downloader/Type/PIE.php | 2 +- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 0d334e5f..b42eee3a 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -72,6 +72,9 @@ class zig $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); $index_json = json_decode($index_json ?: '', true); $latest_version = null; + if (!is_array($index_json)) { + throw new DownloaderException('Failed to fetch Zig version index for update check'); + } foreach ($index_json as $version => $data) { if ($version !== 'master') { $latest_version = $version; @@ -84,7 +87,7 @@ class zig return new CheckUpdateResult( old: $old_version, new: $latest_version, - needUpdate: $old_version === null || version_compare($latest_version, $old_version, '>'), + needUpdate: $old_version === null || $latest_version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/FileList.php b/src/StaticPHP/Artifact/Downloader/Type/FileList.php index b3264533..b868210d 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/FileList.php +++ b/src/StaticPHP/Artifact/Downloader/Type/FileList.php @@ -32,7 +32,7 @@ class FileList implements DownloadTypeInterface, CheckUpdateInterface return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index 1ee9da4d..d5822e69 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -120,7 +120,7 @@ class Git implements DownloadTypeInterface, CheckUpdateInterface return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } throw new DownloaderException("No matching branch found for regex {$config['regex']}."); diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php index 0b14b05d..78ceed3a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PECL.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -22,7 +22,7 @@ class PECL implements DownloadTypeInterface, CheckUpdateInterface return new CheckUpdateResult( old: $old_version, new: $version, - needUpdate: $old_version === null || version_compare($version, $old_version, '>'), + needUpdate: $old_version === null || $version !== $old_version, ); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/PIE.php b/src/StaticPHP/Artifact/Downloader/Type/PIE.php index a84cffe5..14996c5a 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PIE.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PIE.php @@ -40,7 +40,7 @@ class PIE implements DownloadTypeInterface, CheckUpdateInterface return new CheckUpdateResult( old: $old_version, new: $new_version, - needUpdate: $old_version === null || version_compare($new_version, $old_version, '>'), + needUpdate: $old_version === null || $new_version !== $old_version, ); } From 84f6dab882544dd2f0011229e4196463fcc55025 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 5 Mar 2026 11:11:31 +0800 Subject: [PATCH 054/178] Add parallel update checking and improve artifact update handling --- src/StaticPHP/Artifact/ArtifactCache.php | 10 +++ src/StaticPHP/Artifact/ArtifactDownloader.php | 86 ++++++++++++++++++- .../Downloader/Type/CheckUpdateResult.php | 3 +- src/StaticPHP/Command/CheckUpdateCommand.php | 34 ++++++-- src/globals/defines.php | 2 +- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index cd9ad640..7626831a 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -282,6 +282,16 @@ class ArtifactCache logger()->debug("Removed binary cache entry for [{$artifact_name}] on platform [{$platform}]"); } + /** + * Get the names of all artifacts that have at least one downloaded entry (source or binary). + * + * @return array Artifact names + */ + public function getCachedArtifactNames(): array + { + return array_keys($this->cache); + } + /** * Save cache to file. */ diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index 8b64b60c..a9a25915 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -362,7 +362,8 @@ class ArtifactDownloader if ($result !== null) { return $result; } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking."); + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); } $cache = ApplicationContext::get(ArtifactCache::class); if ($prefer_source) { @@ -393,7 +394,33 @@ class ArtifactDownloader 'old_version' => $info['version'], ]); } - throw new WrongUsageException("Artifact '{$artifact_name}' downloader does not support update checking, exit."); + // logger()->warning("Artifact '{$artifact_name}' downloader does not support update checking, skipping."); + return new CheckUpdateResult(old: null, new: null, needUpdate: false, unsupported: true); + } + + /** + * Check updates for multiple artifacts, with optional parallel processing. + * + * @param array $artifact_names Artifact names to check + * @param bool $prefer_source Whether to prefer source over binary + * @param bool $bare Check without requiring artifact to be downloaded first + * @param null|callable $onResult Called immediately with (string $name, CheckUpdateResult) as each result arrives + * @return array Results keyed by artifact name + */ + public function checkUpdates(array $artifact_names, bool $prefer_source = false, bool $bare = false, ?callable $onResult = null): array + { + if ($this->parallel > 1 && count($artifact_names) > 1) { + return $this->checkUpdatesWithConcurrency($artifact_names, $prefer_source, $bare, $onResult); + } + $results = []; + foreach ($artifact_names as $name) { + $result = $this->checkUpdate($name, $prefer_source, $bare); + $results[$name] = $result; + if ($onResult !== null) { + ($onResult)($name, $result); + } + } + return $results; } public function getRetry(): int @@ -411,6 +438,61 @@ class ArtifactDownloader return $this->options[$name] ?? $default; } + private function checkUpdatesWithConcurrency(array $artifact_names, bool $prefer_source, bool $bare, ?callable $onResult): array + { + $results = []; + $fiber_pool = []; + $remaining = $artifact_names; + + Shell::passthruCallback(function () { + \Fiber::suspend(); + }); + + try { + while (!empty($remaining) || !empty($fiber_pool)) { + // fill pool + while (count($fiber_pool) < $this->parallel && !empty($remaining)) { + $name = array_shift($remaining); + $fiber = new \Fiber(function () use ($name, $prefer_source, $bare) { + return [$name, $this->checkUpdate($name, $prefer_source, $bare)]; + }); + $fiber->start(); + $fiber_pool[$name] = $fiber; + } + // check pool + foreach ($fiber_pool as $fiber_name => $fiber) { + if ($fiber->isTerminated()) { + // getReturn() re-throws if the fiber threw — propagates immediately + [$artifact_name, $result] = $fiber->getReturn(); + $results[$artifact_name] = $result; + if ($onResult !== null) { + ($onResult)($artifact_name, $result); + } + unset($fiber_pool[$fiber_name]); + } else { + $fiber->resume(); + } + } + } + } catch (\Throwable $e) { + // terminate all still-suspended fibers so their curl processes don't hang + foreach ($fiber_pool as $fiber) { + if (!$fiber->isTerminated()) { + try { + $fiber->throw($e); + } catch (\Throwable) { + // ignore — we only care about stopping them + } + } + } + throw $e; + } finally { + Shell::passthruCallback(null); + } + + return $results; + } + private function probeSourceCheckUpdate(Artifact $artifact, string $artifact_name): ?CheckUpdateResult { if (($callback = $artifact->getCustomSourceCheckUpdateCallback()) !== null) { diff --git a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php index 468b643b..7e46e4ad 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php +++ b/src/StaticPHP/Artifact/Downloader/Type/CheckUpdateResult.php @@ -8,7 +8,8 @@ readonly class CheckUpdateResult { public function __construct( public ?string $old, - public string $new, + public ?string $new, public bool $needUpdate, + public bool $unsupported = false, ) {} } diff --git a/src/StaticPHP/Command/CheckUpdateCommand.php b/src/StaticPHP/Command/CheckUpdateCommand.php index 4fac0f63..1663337c 100644 --- a/src/StaticPHP/Command/CheckUpdateCommand.php +++ b/src/StaticPHP/Command/CheckUpdateCommand.php @@ -4,7 +4,10 @@ declare(strict_types=1); namespace StaticPHP\Command; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactDownloader; +use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -17,9 +20,10 @@ class CheckUpdateCommand extends BaseCommand public function configure(): void { - $this->addArgument('artifact', InputArgument::REQUIRED, 'The name of the artifact(s) to check for updates, comma-separated'); + $this->addArgument('artifact', InputArgument::OPTIONAL, 'The name of the artifact(s) to check for updates, comma-separated (default: all downloaded artifacts)'); $this->addOption('json', null, null, 'Output result in JSON format'); $this->addOption('bare', null, null, 'Check update without requiring the artifact to be downloaded first (old version will be null)'); + $this->addOption('parallel', 'p', InputOption::VALUE_REQUIRED, 'Number of parallel update checks (default: 10)', 10); // --with-php option for checking updates with a specific PHP version context $this->addOption('with-php', null, InputOption::VALUE_REQUIRED, 'PHP version in major.minor format (default 8.4)', '8.4'); @@ -27,17 +31,27 @@ class CheckUpdateCommand extends BaseCommand public function handle(): int { - $artifacts = parse_comma_list($this->input->getArgument('artifact')); + $artifact_arg = $this->input->getArgument('artifact'); + if ($artifact_arg === null) { + $artifacts = ApplicationContext::get(ArtifactCache::class)->getCachedArtifactNames(); + if (empty($artifacts)) { + $this->output->writeln('No downloaded artifacts found.'); + return static::OK; + } + } else { + $artifacts = parse_comma_list($artifact_arg); + } try { $downloader = new ArtifactDownloader($this->input->getOptions()); $bare = (bool) $this->getOption('bare'); if ($this->getOption('json')) { + $results = $downloader->checkUpdates($artifacts, bare: $bare); $outputs = []; - foreach ($artifacts as $artifact) { - $result = $downloader->checkUpdate($artifact, bare: $bare); + foreach ($results as $artifact => $result) { $outputs[$artifact] = [ 'need-update' => $result->needUpdate, + 'unsupported' => $result->unsupported, 'old' => $result->old, 'new' => $result->new, ]; @@ -45,15 +59,17 @@ class CheckUpdateCommand extends BaseCommand $this->output->writeln(json_encode($outputs, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); return static::OK; } - foreach ($artifacts as $artifact) { - $result = $downloader->checkUpdate($artifact, bare: $bare); - if (!$result->needUpdate) { - $this->output->writeln("Artifact {$artifact} is already up to date ({$result->new})"); + $downloader->checkUpdates($artifacts, bare: $bare, onResult: function (string $artifact, CheckUpdateResult $result) { + if ($result->unsupported) { + $this->output->writeln("Artifact {$artifact} does not support update checking, skipped"); + } elseif (!$result->needUpdate) { + $ver = $result->new ? "({$result->new})" : ''; + $this->output->writeln("Artifact {$artifact} is already up to date {$ver}"); } else { [$old, $new] = [$result->old ?? 'unavailable', $result->new ?? 'unknown']; $this->output->writeln("Update available for {$artifact}: {$old} -> {$new}"); } - } + }); return static::OK; } catch (SPCException $e) { $e->setSimpleOutput(); diff --git a/src/globals/defines.php b/src/globals/defines.php index dbcb63f2..38490046 100644 --- a/src/globals/defines.php +++ b/src/globals/defines.php @@ -104,7 +104,7 @@ const SPC_DOWNLOAD_TYPE_DISPLAY_NAME = [ 'local' => 'local dir', 'pie' => 'PHP Installer for Extensions (PIE)', 'url' => 'url', - 'php-release' => 'php.net', + 'php-release' => 'PHP website release', 'custom' => 'custom downloader', ]; From 055bc7bc3cad7139f156633e9d5b9557fa09911a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 13:46:55 +0800 Subject: [PATCH 055/178] Add condition for ffi patch --- src/SPC/builder/extension/ffi.php | 10 ++++++++++ src/SPC/store/SourcePatcher.php | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/SPC/builder/extension/ffi.php b/src/SPC/builder/extension/ffi.php index 98547723..8e192bea 100644 --- a/src/SPC/builder/extension/ffi.php +++ b/src/SPC/builder/extension/ffi.php @@ -5,11 +5,21 @@ declare(strict_types=1); namespace SPC\builder\extension; use SPC\builder\Extension; +use SPC\builder\linux\SystemUtil; +use SPC\store\SourcePatcher; use SPC\util\CustomExt; #[CustomExt('ffi')] class ffi extends Extension { + public function patchBeforeBuildconf(): bool + { + if (PHP_OS_FAMILY === 'Linux' && SystemUtil::getOSRelease()['dist'] === 'centos') { + return SourcePatcher::patchFfiCentos7FixO3strncmp(); + } + return false; + } + public function getUnixConfigureArg(bool $shared = false): string { return '--with-ffi' . ($shared ? '=shared' : '') . ' --enable-zend-signals'; diff --git a/src/SPC/store/SourcePatcher.php b/src/SPC/store/SourcePatcher.php index 0068f53e..233a2e15 100644 --- a/src/SPC/store/SourcePatcher.php +++ b/src/SPC/store/SourcePatcher.php @@ -22,7 +22,7 @@ class SourcePatcher FileSystem::addSourceExtractHook('swoole', [__CLASS__, 'patchSwoole']); FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchPhpLibxml212']); FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchGDWin32']); - FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); + // FileSystem::addSourceExtractHook('php-src', [__CLASS__, 'patchFfiCentos7FixO3strncmp']); FileSystem::addSourceExtractHook('sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVWin32']); FileSystem::addSourceExtractHook('pdo_sqlsrv', [__CLASS__, 'patchSQLSRVPhp85']); From 8c4e3d58a38c14c6e9504f81cc2bc378a0cc597b Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Fri, 6 Mar 2026 15:25:38 +0900 Subject: [PATCH 056/178] Add php-src mirror and use gmp mirror site (#1048) --- config/source.json | 2 +- src/SPC/store/source/PhpSource.php | 43 +++++++++++++++++++----------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/config/source.json b/config/source.json index 114118bb..040197cb 100644 --- a/config/source.json +++ b/config/source.json @@ -361,7 +361,7 @@ }, "gmp": { "type": "filelist", - "url": "https://gmplib.org/download/gmp/", + "url": "https://ftp.gnu.org/gnu/gmp/", "regex": "/href=\"(?gmp-(?[^\"]+)\\.tar\\.xz)\"/", "provide-pre-built": true, "alt": { diff --git a/src/SPC/store/source/PhpSource.php b/src/SPC/store/source/PhpSource.php index 27e8bb89..02ba8755 100644 --- a/src/SPC/store/source/PhpSource.php +++ b/src/SPC/store/source/PhpSource.php @@ -6,11 +6,17 @@ namespace SPC\store\source; use JetBrains\PhpStorm\ArrayShape; use SPC\exception\DownloaderException; +use SPC\exception\SPCException; use SPC\store\Downloader; class PhpSource extends CustomSourceBase { - public const NAME = 'php-src'; + public const string NAME = 'php-src'; + + public const array WEB_PHP_DOMAINS = [ + 'https://www.php.net', + 'https://phpmirror.static-php.dev', + ]; public function fetch(bool $force = false, ?array $config = null, int $lock_as = SPC_DOWNLOAD_SOURCE): void { @@ -28,21 +34,26 @@ class PhpSource extends CustomSourceBase #[ArrayShape(['type' => 'string', 'path' => 'string', 'rev' => 'string', 'url' => 'string'])] public function getLatestPHPInfo(string $major_version): array { - // 查找最新的小版本号 - $info = json_decode(Downloader::curlExec( - url: "https://www.php.net/releases/index.php?json&version={$major_version}", - retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 - ), true); - if (!isset($info['version'])) { - throw new DownloaderException("Version {$major_version} not found."); + foreach (self::WEB_PHP_DOMAINS as $domain) { + try { + $info = json_decode(Downloader::curlExec( + url: "{$domain}/releases/index.php?json&version={$major_version}", + retries: (int) getenv('SPC_DOWNLOAD_RETRIES') ?: 0 + ), true); + if (!isset($info['version'])) { + throw new DownloaderException("Version {$major_version} not found."); + } + $version = $info['version']; + return [ + 'type' => 'url', + 'url' => "{$domain}/distributions/php-{$version}.tar.xz", + ]; + } catch (SPCException) { + logger()->warning('Failed to fetch latest PHP version for major version {$major_version} from {$domain}, trying next mirror if available.'); + continue; + } } - - $version = $info['version']; - - // 从官网直接下载 - return [ - 'type' => 'url', - 'url' => "https://www.php.net/distributions/php-{$version}.tar.xz", - ]; + // exception if all mirrors failed + throw new DownloaderException("Failed to fetch latest PHP version for major version {$major_version} from all tried mirrors."); } } From 32b7fee8d85555f15eb98f0ce7ee70532cbf5956 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 16:32:44 +0800 Subject: [PATCH 057/178] Fix version extraction to fallback on repository name if tag name is absent --- src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index a9283722..61517a9e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -42,12 +42,12 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface } if ($match_url === null) { $url = $rel['tarball_url'] ?? null; - $version = $rel['tag_name'] ?? null; + $version = $rel['tag_name'] ?? $rel['name'] ?? null; break; } if (preg_match("|{$match_url}|", $rel['tarball_url'] ?? '')) { $url = $rel['tarball_url']; - $version = $rel['tag_name'] ?? null; + $version = $rel['tag_name'] ?? $rel['name'] ?? null; break; } } From 368ce75261b44687c36b090370ccbe78b85bc1b9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:20:16 +0800 Subject: [PATCH 058/178] Fix configuration retrieval by using the extension's name instead of a formatted version --- 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 07bc6abd..06b62209 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -80,7 +80,7 @@ class PhpExtensionPackage extends Package } $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->getExtensionName()); - $ext_config = PackageConfig::get($name, 'php-extension', []); + $ext_config = PackageConfig::get($this->getName(), 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { 'Windows' => $ext_config['arg-type@windows'] ?? $ext_config['arg-type'] ?? 'enable', From d0b6a02432cb15e2156f27304f6878779283c7f2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:20:55 +0800 Subject: [PATCH 059/178] Add patchBeforeBuild method to modify Makefile CFLAGS and enhance build process --- src/Package/Library/bzip2.php | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php index 7f554ab4..403773da 100644 --- a/src/Package/Library/bzip2.php +++ b/src/Package/Library/bzip2.php @@ -6,19 +6,30 @@ namespace Package\Library; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; use StaticPHP\Package\LibraryPackage; use StaticPHP\Package\PackageBuilder; +use StaticPHP\Util\FileSystem; #[Library('bzip2')] class bzip2 { + #[PatchBeforeBuild] + public function patchBeforeBuild(LibraryPackage $lib): void + { + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); + } + #[BuildFor('Linux')] #[BuildFor('Darwin')] public function build(LibraryPackage $lib, PackageBuilder $builder): void { - shell()->cd($lib->getSourceDir())->initializeEnv($lib) - ->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") - ->exec("make -j{$builder->concurrency} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") + $shell = shell()->cd($lib->getSourceDir())->initializeEnv($lib); + $env = $shell->getEnvString(); + $cc_env = 'CC=' . escapeshellarg(getenv('CC') ?: 'cc') . ' AR=' . escapeshellarg(getenv('AR') ?: 'ar'); + + $shell->exec("make PREFIX='{$lib->getBuildRootPath()}' clean") + ->exec("make -j{$builder->concurrency} {$cc_env} {$env} PREFIX='{$lib->getBuildRootPath()}' libbz2.a") ->exec('cp libbz2.a ' . $lib->getLibDir()) ->exec('cp bzlib.h ' . $lib->getIncludeDir()); } From 5e84fed19a8d05c27a05d3874553cc9ce1085236 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:22:59 +0800 Subject: [PATCH 060/178] Add ext-brotli --- config/pkg/ext/ext-brotli.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 config/pkg/ext/ext-brotli.yml diff --git a/config/pkg/ext/ext-brotli.yml b/config/pkg/ext/ext-brotli.yml new file mode 100644 index 00000000..147ecb63 --- /dev/null +++ b/config/pkg/ext/ext-brotli.yml @@ -0,0 +1,13 @@ +ext-brotli: + type: php-extension + artifact: + source: + type: git + extract: php-src/ext/brotli + rev: master + url: 'https://github.com/kjdev/php-ext-brotli' + metadata: + license-files: [LICENSE] + license: MIT + depends: + - brotli From 58c02dfab376af7c8949a2ca8b52730cf6c788d6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:23:08 +0800 Subject: [PATCH 061/178] Add ext-bz2 --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 0149fe97..6a447444 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -1,5 +1,12 @@ ext-bcmath: type: php-extension +ext-bz2: + type: php-extension + depends: + - bzip2 + php-extension: + arg-type@unix: with-path + arg-type@windows: with ext-mbregex: type: php-extension depends: From fc807ec7c99cff0b2b856a409a8c0dc6cc7fcfb7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:24:48 +0800 Subject: [PATCH 062/178] Add ext-calendar --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 6a447444..7f60fa3d 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -7,6 +7,8 @@ ext-bz2: php-extension: arg-type@unix: with-path arg-type@windows: with +ext-calendar: + type: php-extension ext-mbregex: type: php-extension depends: From fbbed6d5c1be308615aecf563a4d56048ce564d2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 6 Mar 2026 19:27:22 +0800 Subject: [PATCH 063/178] Add ext-ctype --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 7f60fa3d..8b5a926d 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -9,6 +9,8 @@ ext-bz2: arg-type@windows: with ext-calendar: type: php-extension +ext-ctype: + type: php-extension ext-mbregex: type: php-extension depends: From 780232fa608a5ef9ed9b6190848d52f94db97149 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 7 Mar 2026 00:42:34 +0800 Subject: [PATCH 064/178] Enhance dependency resolution for virtual-target packages --- src/StaticPHP/Util/DependencyResolver.php | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index 2db74abd..9833ae10 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -45,6 +45,27 @@ class DependencyResolver } } + // Virtual-target packages (e.g. php-fpm) are built as part of their real parent's + // build step, so any dependency they declare must be available before the real parent + // is built. Promote those deps directly onto the real parent's dependency list so + // that the topological sort places them before the parent. + foreach ($dep_list_clean as $pkg_name => $pkg_item) { + if (PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { + continue; + } + foreach ($pkg_item['depends'] as $dep_name) { + if (isset($dep_list_clean[$dep_name]) && PackageConfig::get($dep_name, 'type') !== 'virtual-target') { + // $dep_name is the real parent; add all other deps of this virtual-target to it + $other_deps = array_values(array_filter($pkg_item['depends'], fn ($d) => $d !== $dep_name)); + if (!empty($other_deps)) { + $dep_list_clean[$dep_name]['depends'] = array_values(array_unique( + array_merge($dep_list_clean[$dep_name]['depends'], $other_deps) + )); + } + } + } + } + $resolved = self::doVisitPlat($packages, $dep_list_clean); // Build reverse dependency map if $why is requested From 07fd1bcd033b58adcf23891bcdc45439528078f7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 7 Mar 2026 00:42:49 +0800 Subject: [PATCH 065/178] Patch extension config.m4 files to use PKG_CHECK_MODULES_STATIC --- src/Package/Target/php/unix.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 888ae8bd..d485e2c1 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -47,6 +47,8 @@ trait unix // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) + shell()->cd($package->getSourceDir())->exec("grep -rl 'PKG_CHECK_MODULES(' ext/ | xargs sed -i 's/PKG_CHECK_MODULES(/PKG_CHECK_MODULES_STATIC(/g' || true"); } #[Stage] From 0548aba24880a901692242c27b0bdedce9258275 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 7 Mar 2026 21:20:34 +0800 Subject: [PATCH 066/178] Add ext-curl --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ src/Package/Library/curl.php | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 8b5a926d..074f6ef9 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -11,6 +11,15 @@ ext-calendar: type: php-extension ext-ctype: type: php-extension +ext-curl: + type: php-extension + depends: + - curl + depends@windows: + - ext-zlib + - ext-openssl + php-extension: + arg-type: with ext-mbregex: type: php-extension depends: diff --git a/src/Package/Library/curl.php b/src/Package/Library/curl.php index 283c765a..0edca93f 100644 --- a/src/Package/Library/curl.php +++ b/src/Package/Library/curl.php @@ -55,6 +55,12 @@ class curl // patch pkgconf $lib->patchPkgconfPrefix(['libcurl.pc']); + // curl's CMake embeds krb5 link flags directly without following Requires.private chain, + // so -lkrb5support (from mit-krb5.pc Libs.private) is missing from libcurl.pc. + $pc_path = "{$lib->getLibDir()}/pkgconfig/libcurl.pc"; + if (str_contains(FileSystem::readFile($pc_path), '-lgssapi_krb5')) { + FileSystem::replaceFileRegex($pc_path, '/-lcom_err$/m', '-lcom_err -lkrb5support'); + } shell()->cd("{$lib->getLibDir()}/cmake/CURL/") ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); } From b0b322071696daa0e3bd8062c7162b88469bd495 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:00:15 +0800 Subject: [PATCH 067/178] Fix zlib configure arg --- src/Package/Extension/zlib.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Package/Extension/zlib.php diff --git a/src/Package/Extension/zlib.php b/src/Package/Extension/zlib.php new file mode 100644 index 00000000..14ab656d --- /dev/null +++ b/src/Package/Extension/zlib.php @@ -0,0 +1,22 @@ += 80400 ? '' : ' --with-zlib-dir=' . $builder->getBuildRootPath(); + return '--with-zlib' . $zlib_dir; + } +} From 88af4a719f20f690f3638db39e2d81fe468aeacf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:00:33 +0800 Subject: [PATCH 068/178] Prefer cache extract path in getSourceDir method --- src/StaticPHP/Artifact/Artifact.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index 841775e3..0b5d8a6d 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -292,8 +292,11 @@ class Artifact */ public function getSourceDir(): string { - // defined in config - $extract = $this->config['source']['extract'] ?? null; + // Prefer cache extract path, fall back to config + $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + $extract = is_string($cache_info['extract'] ?? null) + ? $cache_info['extract'] + : ($this->config['source']['extract'] ?? null); if ($extract === null) { return FileSystem::convertPath(SOURCE_PATH . '/' . $this->name); From 19d6d669c07676dadeb5a26d29564927f2c801df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:01:29 +0800 Subject: [PATCH 069/178] Move arg-type def in config --- config/pkg/ext/ext-glfw.yml | 2 ++ src/Package/Extension/glfw.php | 8 -------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/config/pkg/ext/ext-glfw.yml b/config/pkg/ext/ext-glfw.yml index dc8844a8..1be1e75f 100644 --- a/config/pkg/ext/ext-glfw.yml +++ b/config/pkg/ext/ext-glfw.yml @@ -3,3 +3,5 @@ ext-glfw: artifact: glfw depends: - glfw + php-extension: + arg-type@unix: '--enable-glfw --with-glfw-dir=@build_root_path@' diff --git a/src/Package/Extension/glfw.php b/src/Package/Extension/glfw.php index 61d722e1..2a9c7ee5 100644 --- a/src/Package/Extension/glfw.php +++ b/src/Package/Extension/glfw.php @@ -6,7 +6,6 @@ namespace Package\Extension; use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; -use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PhpExtensionPackage; @@ -49,11 +48,4 @@ class glfw extends PhpExtensionPackage putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS=' . $extra_ldflags); } } - - #[CustomPhpConfigureArg('Darwin')] - #[CustomPhpConfigureArg('Linux')] - public function getUnixConfigureArg(bool $shared = false): string - { - return '--enable-glfw --with-glfw-dir=' . BUILD_ROOT_PATH; - } } From 2676875ccd573a298b8a161059c7000f816e498b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:01:44 +0800 Subject: [PATCH 070/178] Refactor PKG_CHECK_MODULES replacement and reuse make vars for configure --- src/Package/Target/php/unix.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index d485e2c1..d8236ebf 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -48,7 +48,9 @@ trait unix // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) - shell()->cd($package->getSourceDir())->exec("grep -rl 'PKG_CHECK_MODULES(' ext/ | xargs sed -i 's/PKG_CHECK_MODULES(/PKG_CHECK_MODULES_STATIC(/g' || true"); + foreach (glob("{$package->getSourceDir()}/ext/*/*.m4") as $m4file) { + FileSystem::replaceFileStr($m4file, 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + } } #[Stage] @@ -110,11 +112,15 @@ trait unix $static_extension_str = $this->makeStaticExtensionString($installer); + // reuse the same make vars so configure conftest links use the same LIBS (incl. -framework flags) + $vars = $this->makeVars($installer); + // run ./configure with args $this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([ 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'CPPFLAGS' => "-I{$package->getIncludeDir()}", 'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), + 'LIBS' => $vars['EXTRA_LIBS'] ?? '', ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } From 0c86d82b98846ed5ff235562fc11bbeb3440aac3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:02:04 +0800 Subject: [PATCH 071/178] Update getDistName method to use display-name from config --- src/StaticPHP/Package/PhpExtensionPackage.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 06b62209..bae11748 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -148,11 +148,11 @@ class PhpExtensionPackage extends Package /** * Get the dist name used for `--ri` check in smoke test. - * Reads from config `dist-name` field, defaults to extension name. + * Reads from config `display-name` field, defaults to extension name. */ public function getDistName(): string { - return $this->extension_config['dist-name'] ?? $this->getExtensionName(); + return $this->extension_config['display-name'] ?? $this->getExtensionName(); } /** @@ -166,7 +166,7 @@ class PhpExtensionPackage extends Package } $distName = $this->getDistName(); - // empty dist-name → no --ri check (e.g. password_argon2) + // empty display-name → no --ri check (e.g. password_argon2) if ($distName === '') { return; } From 8f10e0d0704cc638e54160c41602d62c49a3dfdd Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:02:15 +0800 Subject: [PATCH 072/178] Add before and after build hooks for phar extension to replace file strings --- src/Package/Extension/phar.php | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/Package/Extension/phar.php b/src/Package/Extension/phar.php index dd9b1dff..2220f727 100644 --- a/src/Package/Extension/phar.php +++ b/src/Package/Extension/phar.php @@ -9,6 +9,8 @@ use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; #[Extension('phar')] @@ -26,4 +28,24 @@ class phar { SourcePatcher::unpatchMicroPhar(); } + + #[BeforeStage('ext-phar', 'build')] + public function beforeBuildShared(PhpExtensionPackage $pkg): void + { + FileSystem::replaceFileStr( + "{$pkg->getSourceDir()}/config.m4", + ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'], + ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'] + ); + } + + #[AfterStage('ext-phar', 'build')] + public function afterBuildShared(PhpExtensionPackage $pkg): void + { + FileSystem::replaceFileStr( + "{$pkg->getSourceDir()}/config.m4", + ['${ext_dir}phar.1', '${ext_dir}phar.phar.1'], + ['$ext_dir/phar.1', '$ext_dir/phar.phar.1'] + ); + } } From ad0118718f9ce3629e4b5fefb98887b5e9d21ce7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 14:02:22 +0800 Subject: [PATCH 073/178] Update arg-type definition in builtin-extensions.yml to include specific options --- config/pkg/ext/builtin-extensions.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 074f6ef9..52f14924 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -56,7 +56,7 @@ ext-readline: support: Windows: wip BSD: wip - arg-type: with-path + arg-type: '--with-libedit --without-readline' build-shared: false build-static: true ext-zlib: From a9e6e4a22630024323a8c4bb163e5e3499cbf61a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 16:32:31 +0800 Subject: [PATCH 074/178] Add dba --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ src/Package/Extension/dba.php | 28 +++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 src/Package/Extension/dba.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 52f14924..b71e44ac 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -20,6 +20,12 @@ ext-curl: - ext-openssl php-extension: arg-type: with +ext-dba: + type: php-extension + suggests: + - qdbm + php-extension: + arg-type: custom ext-mbregex: type: php-extension depends: diff --git a/src/Package/Extension/dba.php b/src/Package/Extension/dba.php new file mode 100644 index 00000000..d16d979d --- /dev/null +++ b/src/Package/Extension/dba.php @@ -0,0 +1,28 @@ +getLibraryPackage('qdbm')) ? (" --with-qdbm={$qdbm->getBuildRootPath()}") : ''; + return '--enable-dba' . ($shared ? '=shared' : '') . $qdbm; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(PackageInstaller $installer): string + { + $qdbm = $installer->getLibraryPackage('qdbm') ? ' --with-qdbm' : ''; + return '--with-dba' . $qdbm; + } +} From 6d2c43d3e536dd3956ef47b9087ff71a80e2c4bc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 16:43:53 +0800 Subject: [PATCH 075/178] Add license metadata for ast extension --- config/pkg/ext/ext-ast.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/config/pkg/ext/ext-ast.yml b/config/pkg/ext/ext-ast.yml index 0684959d..776b82df 100644 --- a/config/pkg/ext/ext-ast.yml +++ b/config/pkg/ext/ext-ast.yml @@ -4,3 +4,6 @@ ext-ast: source: type: pecl name: ast + metadata: + license-files: [LICENSE] + license: BSD-3-Clause From 247a254af4aed1600448bdc49a7fc4697473a6e3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 17:46:14 +0800 Subject: [PATCH 076/178] Add ext-dio --- config/pkg/ext/ext-dio.yml | 9 +++++++++ src/Package/Extension/dio.php | 23 +++++++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 config/pkg/ext/ext-dio.yml create mode 100644 src/Package/Extension/dio.php diff --git a/config/pkg/ext/ext-dio.yml b/config/pkg/ext/ext-dio.yml new file mode 100644 index 00000000..b445940c --- /dev/null +++ b/config/pkg/ext/ext-dio.yml @@ -0,0 +1,9 @@ +ext-dio: + type: php-extension + artifact: + source: + type: pecl + name: dio + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/src/Package/Extension/dio.php b/src/Package/Extension/dio.php new file mode 100644 index 00000000..70ac387e --- /dev/null +++ b/src/Package/Extension/dio.php @@ -0,0 +1,23 @@ +getSourceDir()}/php_dio.h")) { + FileSystem::writeFile("{$this->getSourceDir()}/php_dio.h", FileSystem::readFile("{$this->getSourceDir()}/src/php_dio.h")); + } + } +} From b90356bc1d2311a75e0c79304e03ea8630150ff4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 17:47:09 +0800 Subject: [PATCH 077/178] Enhancement for bin/spc dev:info command --- src/StaticPHP/Artifact/ArtifactCache.php | 11 + .../Command/Dev/PackageInfoCommand.php | 246 +++++++++++++++++- src/StaticPHP/Registry/PackageLoader.php | 158 +++++++++++ 3 files changed, 404 insertions(+), 11 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactCache.php b/src/StaticPHP/Artifact/ArtifactCache.php index 7626831a..5a2c8bac 100644 --- a/src/StaticPHP/Artifact/ArtifactCache.php +++ b/src/StaticPHP/Artifact/ArtifactCache.php @@ -203,6 +203,17 @@ class ArtifactCache return $this->cache[$artifact_name]['binary'][$platform] ?? null; } + /** + * Get all binary cache entries for an artifact, keyed by platform string. + * + * @param string $artifact_name Artifact name + * @return array Map of platform → cache info (may be empty) + */ + public function getAllBinaryInfo(string $artifact_name): array + { + return $this->cache[$artifact_name]['binary'] ?? []; + } + /** * Get the full path to the cached file/directory. * diff --git a/src/StaticPHP/Command/Dev/PackageInfoCommand.php b/src/StaticPHP/Command/Dev/PackageInfoCommand.php index 7c869199..ba621c5e 100644 --- a/src/StaticPHP/Command/Dev/PackageInfoCommand.php +++ b/src/StaticPHP/Command/Dev/PackageInfoCommand.php @@ -4,10 +4,14 @@ declare(strict_types=1); namespace StaticPHP\Command\Dev; +use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Command\BaseCommand; use StaticPHP\Config\ArtifactConfig; use StaticPHP\Config\PackageConfig; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Registry\PackageLoader; use StaticPHP\Registry\Registry; +use StaticPHP\Runtime\SystemTarget; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; @@ -34,18 +38,26 @@ class PackageInfoCommand extends BaseCommand } $pkgConfig = PackageConfig::get($packageName); - $artifactConfig = ArtifactConfig::get($packageName); + // Resolve the actual artifact name: + // - string field → named reference (e.g. php → php-src) + // - array field → inline artifact, key is package name + // - null → no artifact, or may match by package name + $artifactField = $pkgConfig['artifact'] ?? null; + $artifactName = is_string($artifactField) ? $artifactField : $packageName; + $artifactConfig = ArtifactConfig::get($artifactName); $pkgInfo = Registry::getPackageConfigInfo($packageName); - $artifactInfo = Registry::getArtifactConfigInfo($packageName); + $artifactInfo = Registry::getArtifactConfigInfo($artifactName); + $annotationInfo = PackageLoader::getPackageAnnotationInfo($packageName); + $cacheInfo = $this->resolveCacheInfo($artifactName, $artifactConfig); if ($this->getOption('json')) { - return $this->outputJson($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + return $this->outputJson($packageName, $artifactName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo, $annotationInfo, $cacheInfo); } - return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo); + return $this->outputTerminal($packageName, $pkgConfig, $artifactConfig, $pkgInfo, $artifactInfo, $annotationInfo, $cacheInfo); } - private function outputJson(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo): int + private function outputJson(string $name, string $artifactName, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo, ?array $annotationInfo, ?array $cacheInfo): int { $data = [ 'name' => $name, @@ -55,15 +67,24 @@ class PackageInfoCommand extends BaseCommand ]; if ($artifactConfig !== null) { + $data['artifact_name'] = $artifactName !== $name ? $artifactName : null; $data['artifact_config_file'] = $artifactInfo ? $this->toRelativePath($artifactInfo['config']) : null; $data['artifact'] = $this->splitArtifactConfig($artifactConfig); } + if ($annotationInfo !== null) { + $data['annotations'] = $annotationInfo; + } + + if ($cacheInfo !== null) { + $data['cache'] = $cacheInfo; + } + $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 + private function outputTerminal(string $name, array $pkgConfig, ?array $artifactConfig, ?array $pkgInfo, ?array $artifactInfo, ?array $annotationInfo, ?array $cacheInfo): int { $type = $pkgConfig['type'] ?? 'unknown'; $registry = $pkgInfo['registry'] ?? 'unknown'; @@ -86,12 +107,15 @@ class PackageInfoCommand extends BaseCommand // 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)) { + $artifactField = $pkgConfig['artifact'] ?? null; + if (is_string($artifactField)) { + // Named reference: show the artifact name it points to + $this->output->writeln("── Artifact Config ── artifact: {$artifactField} file: {$artifactFile}"); + } elseif (is_array($artifactField)) { + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); $this->output->writeln(' (inline in package config)'); + } else { + $this->output->writeln("── Artifact Config ── file: {$artifactFile}"); } $split = $this->splitArtifactConfig($artifactConfig); @@ -107,9 +131,122 @@ class PackageInfoCommand extends BaseCommand $this->output->writeln(''); } + // Annotation section + $this->outputAnnotationSection($name, $annotationInfo); + + // Cache status section + $this->outputCacheSection($cacheInfo); + return static::SUCCESS; } + private function outputAnnotationSection(string $packageName, ?array $annotationInfo): void + { + if ($annotationInfo === null) { + $this->output->writeln('── Annotations ── (no annotation class registered)'); + $this->output->writeln(''); + return; + } + + $shortClass = $this->classBaseName($annotationInfo['class']); + $this->output->writeln("── Annotations ── class: {$shortClass}"); + $this->output->writeln(" {$annotationInfo['class']}"); + + // Method-level hooks + $methods = $annotationInfo['methods']; + if (!empty($methods)) { + $this->output->writeln(''); + $this->output->writeln(' Method hooks:'); + foreach ($methods as $methodName => $attrs) { + $attrList = implode(' ', array_map(fn ($a) => $this->formatAttr($a), $attrs)); + $this->output->writeln(" {$methodName}() {$attrList}"); + } + } + + // Before-stage hooks targeting this package (inbound) + $beforeStages = $annotationInfo['before_stages']; + if (!empty($beforeStages)) { + $this->output->writeln(''); + $this->output->writeln(' Before-stage hooks (inbound):'); + foreach ($beforeStages as $stage => $hooks) { + foreach ($hooks as $hook) { + $source = $this->classBaseName($hook['class']) . '::' . $hook['method'] . '()'; + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" {$stage} ← {$source}{$cond}"); + } + } + } + + // After-stage hooks targeting this package (inbound) + $afterStages = $annotationInfo['after_stages']; + if (!empty($afterStages)) { + $this->output->writeln(''); + $this->output->writeln(' After-stage hooks (inbound):'); + foreach ($afterStages as $stage => $hooks) { + foreach ($hooks as $hook) { + $source = $this->classBaseName($hook['class']) . '::' . $hook['method'] . '()'; + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" {$stage} ← {$source}{$cond}"); + } + } + } + + // Outbound hooks: stages this package's class registers on other packages (exclude self-hooks) + $outboundBefore = $annotationInfo['outbound_before_stages'] ?? []; + $outboundAfter = $annotationInfo['outbound_after_stages'] ?? []; + // Filter out entries targeting the same package — those are already shown inbound + $outboundBefore = array_filter($outboundBefore, fn ($pkg) => $pkg !== $packageName, ARRAY_FILTER_USE_KEY); + $outboundAfter = array_filter($outboundAfter, fn ($pkg) => $pkg !== $packageName, ARRAY_FILTER_USE_KEY); + if (!empty($outboundBefore) || !empty($outboundAfter)) { + $this->output->writeln(''); + $this->output->writeln(' Hooks on other packages (outbound):'); + foreach ($outboundBefore as $targetPkg => $stages) { + foreach ($stages as $stage => $hooks) { + foreach ($hooks as $hook) { + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" #[BeforeStage] → {$targetPkg} {$stage} {$hook['method']}(){$cond}"); + } + } + } + foreach ($outboundAfter as $targetPkg => $stages) { + foreach ($stages as $stage => $hooks) { + foreach ($hooks as $hook) { + $cond = $hook['only_when'] !== null ? " (only_when: {$hook['only_when']})" : ''; + $this->output->writeln(" #[AfterStage] → {$targetPkg} {$stage} {$hook['method']}(){$cond}"); + } + } + } + } + + $this->output->writeln(''); + } + + /** + * Format a single attribute entry (from annotation_map) as a colored inline string. + * + * @param array{attr: string, args: array} $attr + */ + private function formatAttr(array $attr): string + { + $name = $attr['attr']; + $args = $attr['args']; + if (empty($args)) { + return "#[{$name}]"; + } + $argStr = implode(', ', array_map( + fn ($v) => is_string($v) ? "'{$v}'" : (string) $v, + array_values($args) + )); + return "#[{$name}({$argStr})]"; + } + + /** Return the trailing class name component without the namespace. */ + private function classBaseName(string $fqcn): string + { + $parts = explode('\\', $fqcn); + return end($parts); + } + /** * Split artifact config into logical sections for cleaner display. * @@ -190,4 +327,91 @@ class PackageInfoCommand extends BaseCommand } return $normalized; } + + /** + * Build cache status data for display/JSON. + * Returns null when there is no artifact config for this package. + */ + private function resolveCacheInfo(string $name, ?array $artifactConfig): ?array + { + if ($artifactConfig === null) { + return null; + } + $cache = ApplicationContext::get(ArtifactCache::class); + $currentPlatform = SystemTarget::getCurrentPlatformString(); + $hasSource = array_key_exists('source', $artifactConfig) || array_key_exists('source-mirror', $artifactConfig); + $hasBinary = array_key_exists('binary', $artifactConfig) || array_key_exists('binary-mirror', $artifactConfig); + return [ + 'current_platform' => $currentPlatform, + 'has_source' => $hasSource, + 'has_binary' => $hasBinary, + 'source' => $hasSource ? [ + 'downloaded' => $cache->isSourceDownloaded($name), + 'info' => $cache->getSourceInfo($name), + ] : null, + 'binary' => $hasBinary ? $cache->getAllBinaryInfo($name) : null, + ]; + } + + private function outputCacheSection(?array $cacheInfo): void + { + if ($cacheInfo === null) { + $this->output->writeln('── Cache Status ── (no artifact config)'); + $this->output->writeln(''); + return; + } + + $platform = $cacheInfo['current_platform']; + $this->output->writeln("── Cache Status ── current platform: {$platform}"); + + // Source + $this->output->writeln(''); + $this->output->writeln(' source:'); + if (!$cacheInfo['has_source']) { + $this->output->writeln(' ─ not applicable'); + } elseif ($cacheInfo['source']['downloaded'] && $cacheInfo['source']['info'] !== null) { + $this->output->writeln(' ✓ downloaded ' . $this->formatCacheEntry($cacheInfo['source']['info'])); + } else { + $this->output->writeln(' ✗ not downloaded'); + } + + // Binary + $this->output->writeln(''); + $this->output->writeln(' binary:'); + if (!$cacheInfo['has_binary']) { + $this->output->writeln(' ─ not applicable'); + } elseif (empty($cacheInfo['binary'])) { + $this->output->writeln(" ✗ {$platform} (current — not cached)"); + } else { + $allBinary = $cacheInfo['binary']; + foreach ($allBinary as $binPlatform => $binInfo) { + $isCurrent = $binPlatform === $platform; + $tag = $isCurrent ? ' (current)' : ''; + if ($binInfo !== null) { + $this->output->writeln(" ✓ {$binPlatform}{$tag} " . $this->formatCacheEntry($binInfo)); + } else { + $this->output->writeln(" ✗ {$binPlatform}{$tag}"); + } + } + // Show current platform if not already listed + if (!array_key_exists($platform, $allBinary)) { + $this->output->writeln(" ✗ {$platform} (current — not cached)"); + } + } + + $this->output->writeln(''); + } + + private function formatCacheEntry(array $info): string + { + $type = $info['cache_type'] ?? '?'; + $version = $info['version'] !== null ? " {$info['version']}" : ''; + $time = isset($info['time']) ? ' ' . date('Y-m-d H:i', (int) $info['time']) : ''; + $file = match ($type) { + 'archive', 'file' => isset($info['filename']) ? " {$info['filename']}" : '', + 'git', 'local' => isset($info['dirname']) ? " {$info['dirname']}" : '', + default => '', + }; + return "[{$type}]{$version}{$time}{$file}"; + } } diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index 573ad7da..421403c9 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -40,6 +40,42 @@ class PackageLoader /** @var array Track loaded classes to prevent duplicates */ private static array $loaded_classes = []; + /** + * Annotation metadata keyed by package name, capturing the defining class and its method-level attributes. + * + * @var array}>>}> + */ + private static array $annotation_map = []; + + /** + * Source metadata for #[BeforeStage] hooks, keyed by target package name → stage name. + * + * @var array>> + */ + private static array $before_stage_meta = []; + + /** + * Source metadata for #[AfterStage] hooks, keyed by target package name → stage name. + * + * @var array>> + */ + private static array $after_stage_meta = []; + + /** + * Reverse index of #[BeforeStage] hooks, keyed by registering class → target package → stage. + * Enables O(1) "outbound hook" lookup: what stages does a given class hook into on other packages? + * + * @var array>>> + */ + private static array $class_before_stage_meta = []; + + /** + * Reverse index of #[AfterStage] hooks, keyed by registering class → target package → stage. + * + * @var array>>> + */ + private static array $class_after_stage_meta = []; + public static function initPackageInstances(): void { if (self::$packages !== null) { @@ -213,8 +249,19 @@ class PackageLoader Validate::class => $pkg->setValidateCallback([$instance_class, $method->getName()]), default => null, }; + + // Capture annotation metadata for inspection (dev:info, future event-trace commands) + $meta_attr = self::annotationShortName($method_attribute->getName()); + if ($meta_attr !== null) { + self::$annotation_map[$pkg->getName()]['methods'][$method->getName()][] = [ + 'attr' => $meta_attr, + 'args' => self::annotationArgs($method_instance), + ]; + } } } + // Record which class defines this package (set once; IS_REPEATABLE may loop more than once) + self::$annotation_map[$pkg->getName()]['class'] ??= $class_name; // register package self::$packages[$pkg->getName()] = $pkg; } @@ -260,6 +307,63 @@ class PackageLoader return self::$after_stages; } + /** + * Get annotation metadata for a specific package. + * + * Returns null if no annotation class was loaded for this package (config-only package). + * The returned structure includes the defining class name, per-method attribute list, + * inbound BeforeStage/AfterStage hooks targeting this package, and outbound hooks that + * this package's class registers on other packages. + * + * @return null|array{ + * class: string, + * methods: array}>>, + * before_stages: array>, + * after_stages: array>, + * outbound_before_stages: array>>, + * outbound_after_stages: array>> + * } + */ + public static function getPackageAnnotationInfo(string $name): ?array + { + $class_info = self::$annotation_map[$name] ?? null; + if ($class_info === null) { + return null; + } + $class = $class_info['class']; + return [ + 'class' => $class, + 'methods' => $class_info['methods'], + 'before_stages' => self::$before_stage_meta[$name] ?? [], + 'after_stages' => self::$after_stage_meta[$name] ?? [], + 'outbound_before_stages' => self::$class_before_stage_meta[$class] ?? [], + 'outbound_after_stages' => self::$class_after_stage_meta[$class] ?? [], + ]; + } + + /** + * Get all annotation metadata keyed by package name. + * Useful for future event-trace commands or cross-package inspection. + * + * @return array + */ + public static function getAllAnnotations(): array + { + $result = []; + foreach (self::$annotation_map as $name => $info) { + $class = $info['class']; + $result[$name] = [ + 'class' => $class, + 'methods' => $info['methods'], + 'before_stages' => self::$before_stage_meta[$name] ?? [], + 'after_stages' => self::$after_stage_meta[$name] ?? [], + 'outbound_before_stages' => self::$class_before_stage_meta[$class] ?? [], + 'outbound_after_stages' => self::$class_after_stage_meta[$class] ?? [], + ]; + } + return $result; + } + public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { // match condition @@ -385,6 +489,16 @@ class PackageLoader } $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; self::$before_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + $registering_class = get_class($instance_class); + self::$before_stage_meta[$package_name][$stage][] = [ + 'class' => $registering_class, + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + self::$class_before_stage_meta[$registering_class][$package_name][$stage][] = [ + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; } private static function addAfterStage(\ReflectionMethod $method, ?Package $pkg, mixed $instance_class, object $method_instance): void @@ -400,5 +514,49 @@ class PackageLoader } $package_name = $method_instance->package_name === '' ? $pkg->getName() : $method_instance->package_name; self::$after_stages[$package_name][$stage][] = [[$instance_class, $method->getName()], $method_instance->only_when_package_resolved]; + $registering_class = get_class($instance_class); + self::$after_stage_meta[$package_name][$stage][] = [ + 'class' => $registering_class, + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + self::$class_after_stage_meta[$registering_class][$package_name][$stage][] = [ + 'method' => $method->getName(), + 'only_when' => $method_instance->only_when_package_resolved, + ]; + } + + /** + * Map a fully-qualified attribute class name to a short display name for metadata storage. + * Returns null for attributes that are not tracked in the annotation map. + */ + private static function annotationShortName(string $attr): ?string + { + return match ($attr) { + Stage::class => 'Stage', + BuildFor::class => 'BuildFor', + PatchBeforeBuild::class => 'PatchBeforeBuild', + CustomPhpConfigureArg::class => 'CustomPhpConfigureArg', + InitPackage::class => 'InitPackage', + ResolveBuild::class => 'ResolveBuild', + Info::class => 'Info', + Validate::class => 'Validate', + default => null, + }; + } + + /** + * Extract the meaningful constructor arguments from an attribute instance as a key-value array. + * + * @return array + */ + private static function annotationArgs(object $inst): array + { + return match (true) { + $inst instanceof Stage => array_filter(['function' => $inst->function], fn ($v) => $v !== null), + $inst instanceof BuildFor => ['os' => $inst->os], + $inst instanceof CustomPhpConfigureArg => array_filter(['os' => $inst->os], fn ($v) => $v !== ''), + default => [], + }; } } From 424228d81e39a31d29162f2a55c03427fa42ef34 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 22:33:00 +0800 Subject: [PATCH 078/178] Add ext-dom, ext-xml --- config/pkg/ext/builtin-extensions.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index b71e44ac..8f773900 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -26,6 +26,14 @@ ext-dba: - qdbm php-extension: arg-type: custom +ext-dom: + type: php-extension + depends: + - libxml2 + - ext-xml + php-extension: + arg-type: '--enable-dom@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with ext-mbregex: type: php-extension depends: @@ -65,6 +73,17 @@ ext-readline: arg-type: '--with-libedit --without-readline' build-shared: false build-static: true +ext-xml: + type: php-extension + depends: + - libxml2 + depends@windows: + - libxml2 + - ext-iconv + php-extension: + arg-type: '--enable-xml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true ext-zlib: type: php-extension depends: From 1f768ffc644e362058f81e7ac8c866020738dbb1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sun, 8 Mar 2026 22:33:44 +0800 Subject: [PATCH 079/178] Mark transitive PHP extension dependencies of static extensions as static --- src/Package/Target/php.php | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 54d41dc5..351622c9 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -27,6 +27,7 @@ use StaticPHP\Registry\PackageLoader; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Toolchain\ToolchainManager; +use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\V2CompatLayer; @@ -215,6 +216,25 @@ class php extends TargetPackage } } + // Mark transitive PHP extension dependencies of static extensions as static too + if (!empty($static_extensions)) { + $static_ext_pkgs = array_map(fn ($x) => "ext-{$x}", $static_extensions); + $transitive_deps = DependencyResolver::resolve($static_ext_pkgs); + foreach ($transitive_deps as $dep_name) { + if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { + continue; + } + $dep_instance = PackageLoader::getPackage($dep_name); + if (!$dep_instance instanceof PhpExtensionPackage || $dep_instance->isBuildStatic() || $dep_instance->isBuildShared()) { + continue; + } + $dep_config = PackageConfig::get($dep_name, 'php-extension', []); + if (($dep_config['build-static'] ?? true) !== false) { + $dep_instance->setBuildStatic(); + } + } + } + // building shared extensions need embed SAPI if (!empty($shared_extensions) && !$package->getBuildOption('build-embed', false) && $package->getName() === 'php') { $installer->addBuildPackage('php-embed'); @@ -266,7 +286,8 @@ class php extends TargetPackage $installer->isPackageResolved('php-embed') ? 'embed' : null, $installer->isPackageResolved('frankenphp') ? 'frankenphp' : null, ]); - $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && $x->isBuildStatic()); + $static_extensions = array_filter($installer->getResolvedPackages(), fn ($x) => $x instanceof PhpExtensionPackage && + $x->isBuildStatic()); $shared_extensions = parse_extension_list($package->getBuildOption('build-shared') ?? []); $install_packages = array_filter($installer->getResolvedPackages(), fn ($x) => $x->getType() !== 'php-extension' && $x->getName() !== 'php' && !str_starts_with($x->getName(), 'php-')); return [ From 77e129881a6d663464e85bd27666990428b15fd5 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 11:04:18 +0800 Subject: [PATCH 080/178] Move all interactive input to construct --- src/Package/Target/php/unix.php | 22 ++++++- src/StaticPHP/Artifact/ArtifactDownloader.php | 24 ++++---- .../Command/InstallPackageCommand.php | 4 +- src/StaticPHP/Config/ConfigValidator.php | 2 + src/StaticPHP/Doctor/Doctor.php | 12 ++-- src/StaticPHP/Doctor/Item/LinuxMuslCheck.php | 8 +-- src/StaticPHP/Doctor/Item/PkgConfigCheck.php | 4 +- .../Doctor/Item/Re2cVersionCheck.php | 4 +- .../Doctor/Item/WindowsToolCheck.php | 14 ++--- src/StaticPHP/Doctor/Item/ZigCheck.php | 4 +- src/StaticPHP/Package/PackageInstaller.php | 59 ++++++++++++------- 11 files changed, 96 insertions(+), 61 deletions(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index d8236ebf..40961b5e 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -443,7 +443,27 @@ trait unix $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); - $package->runStage([$this, 'smokeTestForUnix']); + } + + #[Stage('postInstall')] + public function postInstall(TargetPackage $package, PackageInstaller $installer): void + { + if ($package->getName() === 'frankenphp') { + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); + return; + } + if ($package->getName() !== 'php') { + return; + } + if (SystemTarget::isUnix()) { + if ($installer->interactive) { + InteractiveTerm::indicateProgress('Running PHP smoke tests'); + } + $package->runStage([$this, 'smokeTestForUnix']); + if ($installer->interactive) { + InteractiveTerm::finish('PHP smoke tests passed'); + } + } } /** diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index a9a25915..6cc57439 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -106,7 +106,7 @@ class ArtifactDownloader * no-shallow-clone?: bool * } $options Downloader options */ - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { // Allow setting concurrency via options $this->parallel = max(1, (int) ($options['parallel'] ?? 1)); @@ -273,12 +273,10 @@ class ArtifactDownloader /** * Download all artifacts, with optional parallel processing. - * - * @param bool $interactive Enable interactive mode with Ctrl+C handling */ - public function download(bool $interactive = true): void + public function download(): void { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(function () { InteractiveTerm::advance(); }); @@ -311,7 +309,7 @@ class ArtifactDownloader $count = count($this->artifacts); $artifacts_str = implode(',', array_map(fn ($x) => '' . ConsoleColor::yellow($x->getName()), $this->artifacts)); // mute the first line if not interactive - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice("Downloading {$count} artifacts: {$artifacts_str} ..."); } try { @@ -329,19 +327,19 @@ class ArtifactDownloader $skipped = []; foreach ($this->artifacts as $artifact) { ++$current; - if ($this->downloadWithType($artifact, $current, $count, interactive: $interactive) === SPC_DOWNLOAD_STATUS_SKIPPED) { + if ($this->downloadWithType($artifact, $current, $count) === SPC_DOWNLOAD_STATUS_SKIPPED) { $skipped[] = $artifact->getName(); continue; } $this->_before_files = FileSystem::scanDirFiles(DOWNLOAD_PATH, false, true, true) ?: []; } - if ($interactive) { + if ($this->interactive) { $skip_msg = !empty($skipped) ? ' (Skipped ' . count($skipped) . ' artifacts for being already downloaded)' : ''; InteractiveTerm::success("Downloaded all {$count} artifacts.{$skip_msg}\n", true); } } } finally { - if ($interactive) { + if ($this->interactive) { Shell::passthruCallback(null); keyboard_interrupt_unregister(); } @@ -537,7 +535,7 @@ class ArtifactDownloader return $dl->checkUpdate($artifact_name, $platform_config, null, $this); } - private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false, bool $interactive = true): int + private function downloadWithType(Artifact $artifact, int $current, int $total, bool $parallel = false): int { $queue = $this->generateQueue($artifact); // already downloaded @@ -558,7 +556,7 @@ class ArtifactDownloader }; $try_h = $try ? 'Try downloading' : 'Downloading'; logger()->info("{$try_h} artifact '{$artifact->getName()}' {$item['display']} ..."); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::indicateProgress("[{$current}/{$total}] Downloading artifact " . ConsoleColor::green($artifact->getName()) . " {$item['display']} from {$type_display_name} ..."); } // is valid download type @@ -597,13 +595,13 @@ class ArtifactDownloader } // process lock ApplicationContext::get(ArtifactCache::class)->lock($artifact, $item['lock'], $lock, SystemTarget::getCurrentPlatformString()); - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { $ver = $lock->hasVersion() ? (' (' . ConsoleColor::yellow($lock->version) . ')') : ''; InteractiveTerm::finish('Downloaded ' . ($verified ? 'and verified ' : '') . 'artifact ' . ConsoleColor::green($artifact->getName()) . $ver . " {$item['display']} ."); } return SPC_DOWNLOAD_STATUS_SUCCESS; } catch (DownloaderException|ExecutionException $e) { - if ($parallel === false && $interactive) { + if ($parallel === false && $this->interactive) { InteractiveTerm::finish("Download artifact {$artifact->getName()} {$item['display']} failed !", false); InteractiveTerm::error("Failed message: {$e->getMessage()}", true); } diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index 23032261..b5fb8d2c 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -34,9 +34,9 @@ class InstallPackageCommand extends BaseCommand public function handle(): int { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true]); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], false); $installer->addInstallPackage($this->input->getArgument('package')); - $installer->run(true, true); + $installer->run(true); return static::SUCCESS; } } diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index f011482c..4a4f75db 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -16,6 +16,7 @@ class ConfigValidator public const array PACKAGE_FIELD_TYPES = [ // package fields 'type' => ConfigType::STRING, + 'description' => ConfigType::STRING, 'depends' => ConfigType::LIST_ARRAY, // @ 'suggests' => ConfigType::LIST_ARRAY, // @ 'artifact' => [self::class, 'validateArtifactField'], // STRING or OBJECT @@ -43,6 +44,7 @@ class ConfigValidator public const array PACKAGE_FIELDS = [ 'type' => true, + 'description' => false, 'depends' => false, // @ 'suggests' => false, // @ 'artifact' => false, diff --git a/src/StaticPHP/Doctor/Doctor.php b/src/StaticPHP/Doctor/Doctor.php index fc69cc2a..1239a30c 100644 --- a/src/StaticPHP/Doctor/Doctor.php +++ b/src/StaticPHP/Doctor/Doctor.php @@ -18,7 +18,7 @@ use function Laravel\Prompts\confirm; readonly class Doctor { - public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT) + public function __construct(private ?OutputInterface $output = null, private int $auto_fix = FIX_POLICY_PROMPT, public readonly bool $interactive = true) { // debug shows all loaded doctor items $items = DoctorLoader::getDoctorItems(); @@ -53,13 +53,13 @@ readonly class Doctor * Check all valid check items. * @return bool true if all checks passed, false otherwise */ - public function checkAll(bool $interactive = true): bool + public function checkAll(): bool { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Starting doctor checks ...'); } foreach ($this->getValidCheckList() as $check) { - if (!$this->checkItem($check, $interactive)) { + if (!$this->checkItem($check)) { return false; } } @@ -72,7 +72,7 @@ readonly class Doctor * @param CheckItem|string $check The check item to be checked * @return bool True if the check passed or was fixed, false otherwise */ - public function checkItem(CheckItem|string $check, bool $interactive = true): bool + public function checkItem(CheckItem|string $check): bool { if (is_string($check)) { $found = null; @@ -88,7 +88,7 @@ readonly class Doctor } $check = $found; } - $prepend = $interactive ? ' - ' : ''; + $prepend = $this->interactive ? ' - ' : ''; $this->output?->write("{$prepend}Checking {$check->item_name} ... "); // call check diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index b01b7b7b..df3b5241 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -59,8 +59,8 @@ class LinuxMuslCheck #[FixItem('fix-musl-wrapper')] public function fixMusl(): bool { - $downloader = new ArtifactDownloader(); - $downloader->add('musl-wrapper')->download(false); + $downloader = new ArtifactDownloader(interactive: false); + $downloader->add('musl-wrapper')->download(); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); $extractor->extract('musl-wrapper'); @@ -96,9 +96,9 @@ class LinuxMuslCheck Shell::passthruCallback(function () { InteractiveTerm::advance(); }); - $downloader = new ArtifactDownloader(); + $downloader = new ArtifactDownloader(interactive: false); $extractor = new ArtifactExtractor(ApplicationContext::get(ArtifactCache::class)); - $downloader->add('musl-toolchain')->download(false); + $downloader->add('musl-toolchain')->download(); $extractor->extract('musl-toolchain'); $pkg_root = PKG_ROOT_PATH . '/musl-toolchain'; f_passthru("{$prefix}cp -rf {$pkg_root}/* /usr/local/musl"); diff --git a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php index 4a0ba498..88865163 100644 --- a/src/StaticPHP/Doctor/Item/PkgConfigCheck.php +++ b/src/StaticPHP/Doctor/Item/PkgConfigCheck.php @@ -45,9 +45,9 @@ class PkgConfigCheck public function fix(): bool { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller(['dl-binary-only' => true]); + $installer = new PackageInstaller(['dl-binary-only' => true], interactive: false); $installer->addInstallPackage('pkg-config'); - $installer->run(false, true); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php index fce3350b..3316be3f 100644 --- a/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php +++ b/src/StaticPHP/Doctor/Item/Re2cVersionCheck.php @@ -30,9 +30,9 @@ class Re2cVersionCheck #[FixItem('build-re2c')] public function buildRe2c(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('re2c'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php index e6a042d3..08e140f4 100644 --- a/src/StaticPHP/Doctor/Item/WindowsToolCheck.php +++ b/src/StaticPHP/Doctor/Item/WindowsToolCheck.php @@ -107,7 +107,7 @@ class WindowsToolCheck { $installer = new PackageInstaller(); $installer->addInstallPackage('strawberry-perl'); - $installer->run(false); + $installer->run(true); GlobalEnvManager::addPathIfNotExists(PKG_ROOT_PATH . '\strawberry-perl'); return true; } @@ -116,27 +116,27 @@ class WindowsToolCheck public function installSDK(): bool { FileSystem::removeDir(getenv('PHP_SDK_PATH')); - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('php-sdk-binary-tools'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-nasm')] public function installNasm(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('nasm'); - $installer->run(false); + $installer->run(true); return true; } #[FixItem('install-vswhere')] public function installVSWhere(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('vswhere'); - $installer->run(false); + $installer->run(true); return true; } } diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index c3a6aa9f..baa6d4cb 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -34,9 +34,9 @@ class ZigCheck #[FixItem('install-zig')] public function installZig(): bool { - $installer = new PackageInstaller(); + $installer = new PackageInstaller(interactive: false); $installer->addInstallPackage('zig'); - $installer->run(false); + $installer->run(true); return $installer->isPackageInstalled('zig'); } } diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 628900fa..d8d745f6 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -46,7 +46,7 @@ class PackageInstaller /** @var null|BuildRootTracker buildroot file tracker for debugging purpose */ protected ?BuildRootTracker $tracker = null; - public function __construct(protected array $options = []) + public function __construct(protected array $options = [], public readonly bool $interactive = true) { ApplicationContext::set(PackageInstaller::class, $this); $builder = new PackageBuilder($options); @@ -143,7 +143,7 @@ class PackageInstaller /** * Run the package installation process. */ - public function run(bool $interactive = true, bool $disable_delay_msg = false): void + public function run(bool $disable_delay_msg = false): void { // apply build toolchain envs GlobalEnvManager::afterInit(); @@ -153,7 +153,7 @@ class PackageInstaller $this->resolvePackages(); } - if ($interactive && !$disable_delay_msg) { + if ($this->interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output $this->printInstallerInfo(); @@ -167,14 +167,17 @@ class PackageInstaller // check download if ($this->download) { $downloaderOptions = DownloaderOptions::extractFromConsoleOptions($this->options, 'dl'); - $downloader = new ArtifactDownloader([...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))]); - $downloader->addArtifacts($this->getArtifacts())->download($interactive); + $downloader = new ArtifactDownloader( + [...$downloaderOptions, 'source-only' => implode(',', array_map(fn ($x) => $x->getName(), $this->build_packages))], + $this->interactive + ); + $downloader->addArtifacts($this->getArtifacts())->download(); } else { logger()->notice('Skipping download (--no-download option enabled)'); } // extract sources - $this->extractSourceArtifacts(interactive: $interactive); + $this->extractSourceArtifacts(); // validate packages foreach ($this->packages as $package) { @@ -183,7 +186,7 @@ class PackageInstaller } // build/install packages - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Building/Installing packages ...'); keyboard_interrupt_register(function () { InteractiveTerm::finish('Build/Install process interrupted by user!', false); @@ -198,7 +201,7 @@ class PackageInstaller $has_source = $package->hasSource(); if (!$is_to_build && $should_use_binary) { // install binary - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -210,17 +213,17 @@ class PackageInstaller } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); } try { @@ -243,22 +246,20 @@ class PackageInstaller } catch (\Throwable $e) { // Stop tracking on error $this->tracker?->stopTracking(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } } } - $this->dumpLicenseFiles($this->packages); - if ($interactive) { - InteractiveTerm::success('Exported package licenses', true); - } + // perform after-install actions and emit post-install events + $this->emitPostInstallEvents(); } public function isBuildPackage(Package|string $package): bool @@ -311,6 +312,17 @@ class PackageInstaller return false; } + public function emitPostInstallEvents(): void + { + foreach ($this->packages as $package) { + if ($package->hasStage('postInstall')) { + $package->runStage('postInstall'); + } + } + + $this->dumpLicenseFiles($this->packages); + } + /** * Returns the download status of all artifacts for the resolved packages. * @@ -368,7 +380,7 @@ class PackageInstaller /** * Extract all artifacts for resolved packages. */ - public function extractSourceArtifacts(bool $interactive = true): void + public function extractSourceArtifacts(): void { FileSystem::createDir(SOURCE_PATH); $packages = array_values($this->packages); @@ -403,7 +415,7 @@ class PackageInstaller } // Extract each artifact - if ($interactive) { + if ($this->interactive) { InteractiveTerm::notice('Extracting source for ' . count($artifacts) . ' artifacts: ' . implode(',', array_map(fn ($x) => ConsoleColor::yellow($x->getName()), $artifacts)) . ' ...'); InteractiveTerm::indicateProgress('Extracting artifacts'); } @@ -411,7 +423,7 @@ class PackageInstaller try { V2CompatLayer::beforeExtsExtractHook(); foreach ($artifacts as $artifact) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::setMessage('Extracting source: ' . ConsoleColor::green($artifact->getName())); } if (($pkg = array_search($artifact->getName(), $pkg_artifact_map, true)) !== false) { @@ -423,12 +435,12 @@ class PackageInstaller } } V2CompatLayer::afterExtsExtractHook(); - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Extracted all sources successfully.'); echo PHP_EOL; } } catch (\Throwable $e) { - if ($interactive) { + if ($this->interactive) { InteractiveTerm::finish('Artifact extraction failed!', false); echo PHP_EOL; } @@ -525,6 +537,9 @@ class PackageInstaller } } $dumper->dump(BUILD_ROOT_PATH . '/license'); + if ($this->interactive) { + InteractiveTerm::success('Exported package licenses', true); + } } /** From b185d27ad75f6f583e8160a45ac488d2f84c51da Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 14:30:48 +0800 Subject: [PATCH 081/178] Add ext-ds --- config/pkg/ext/ext-ds.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/pkg/ext/ext-ds.yml diff --git a/config/pkg/ext/ext-ds.yml b/config/pkg/ext/ext-ds.yml new file mode 100644 index 00000000..0c0a4b3c --- /dev/null +++ b/config/pkg/ext/ext-ds.yml @@ -0,0 +1,9 @@ +ext-ds: + type: php-extension + artifact: + source: + type: pecl + name: ds + metadata: + license-files: [LICENSE] + license: MIT From 8cc5877f3c4a74936a7eb6a6a8bdcecc7bc86262 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 14:35:42 +0800 Subject: [PATCH 082/178] Add ext-ev,ext-sockets --- config/pkg/ext/builtin-extensions.yml | 2 ++ config/pkg/ext/ext-ev.yml | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-ev.yml diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 8f773900..67d879a1 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -73,6 +73,8 @@ ext-readline: arg-type: '--with-libedit --without-readline' build-shared: false build-static: true +ext-sockets: + type: php-extension ext-xml: type: php-extension depends: diff --git a/config/pkg/ext/ext-ev.yml b/config/pkg/ext/ext-ev.yml new file mode 100644 index 00000000..174e5f84 --- /dev/null +++ b/config/pkg/ext/ext-ev.yml @@ -0,0 +1,13 @@ +ext-ev: + type: php-extension + artifact: + source: + type: pecl + name: ev + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - ext-sockets + php-extension: + arg-type@windows: with From a678d908d50fa86b1ab0f1966f2e7f14fb26bcc9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 14:45:36 +0800 Subject: [PATCH 083/178] Add ext-event --- config/pkg/ext/ext-event.yml | 19 ++++++++++++++ src/Package/Extension/event.php | 46 +++++++++++++++++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 config/pkg/ext/ext-event.yml create mode 100644 src/Package/Extension/event.php diff --git a/config/pkg/ext/ext-event.yml b/config/pkg/ext/ext-event.yml new file mode 100644 index 00000000..dd9c1c8e --- /dev/null +++ b/config/pkg/ext/ext-event.yml @@ -0,0 +1,19 @@ +ext-event: + type: php-extension + artifact: + source: + type: url + url: 'https://bitbucket.org/osmanov/pecl-event/get/3.1.4.tar.gz' + extract: php-src/ext/event + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libevent + - ext-openssl + suggests: + - ext-sockets + php-extension: + support: + Windows: wip + arg-type: custom diff --git a/src/Package/Extension/event.php b/src/Package/Extension/event.php new file mode 100644 index 00000000..db119274 --- /dev/null +++ b/src/Package/Extension/event.php @@ -0,0 +1,46 @@ +getBuilder()->getBuildRootPath()}"; + if ($installer->getLibraryPackage('openssl')) { + $arg .= " --with-event-openssl={$this->getBuilder()->getBuildRootPath()}"; + } + if ($installer->getPhpExtensionPackage('ext-sockets')) { + $arg .= ' --enable-event-sockets'; + } else { + $arg .= ' --disable-event-sockets'; + } + return $arg; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-event')] + #[PatchDescription('Prevent event extension compile error on macOS')] + public function patchBeforeMake(PackageInstaller $installer): void + { + // Prevent event extension compile error on macOS + if (SystemTarget::getTargetOS() === 'Darwin') { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + FileSystem::replaceFileRegex("{$php_src}/main/php_config.h", '/^#define HAVE_OPENPTY 1$/m', ''); + } + } +} From 552a8a1ea291c92af1f43d8f189c959699112ad3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:00:06 +0800 Subject: [PATCH 084/178] Add ext-excimer (closes #1019) --- config/pkg/ext/ext-excimer.yml | 9 +++++++++ src/Package/Extension/excimer.php | 19 +++++++++++++++++++ src/StaticPHP/Util/System/UnixUtil.php | 6 +----- 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 config/pkg/ext/ext-excimer.yml create mode 100644 src/Package/Extension/excimer.php diff --git a/config/pkg/ext/ext-excimer.yml b/config/pkg/ext/ext-excimer.yml new file mode 100644 index 00000000..3d085888 --- /dev/null +++ b/config/pkg/ext/ext-excimer.yml @@ -0,0 +1,9 @@ +ext-excimer: + type: php-extension + artifact: + source: + type: pecl + name: excimer + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/src/Package/Extension/excimer.php b/src/Package/Extension/excimer.php new file mode 100644 index 00000000..9780a2ec --- /dev/null +++ b/src/Package/Extension/excimer.php @@ -0,0 +1,19 @@ + Date: Mon, 9 Mar 2026 15:02:23 +0800 Subject: [PATCH 085/178] Add ext-exif --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 67d879a1..c5d832f8 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -34,6 +34,8 @@ ext-dom: php-extension: arg-type: '--enable-dom@shared_suffix@ --with-libxml=@build_root_path@' arg-type@windows: with +ext-exif: + type: php-extension ext-mbregex: type: php-extension depends: From cf2e1d9819a1fd98d8fef7d9ed0ea008b69cb65d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:13:07 +0800 Subject: [PATCH 086/178] Add ext-ffi --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ src/Package/Artifact/php_src.php | 11 ---------- src/Package/Extension/ffi.php | 29 +++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 11 deletions(-) create mode 100644 src/Package/Extension/ffi.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index c5d832f8..a9f357f3 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -36,6 +36,13 @@ ext-dom: arg-type@windows: with ext-exif: type: php-extension +ext-ffi: + type: php-extension + depends@unix: + - libffi + php-extension: + arg-type@unix: '--with-ffi=@shared_suffix@ --enable-zend-signals' + arg-type@windows: with ext-mbregex: type: php-extension depends: diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php index ae9488d6..119f9056 100644 --- a/src/Package/Artifact/php_src.php +++ b/src/Package/Artifact/php_src.php @@ -7,7 +7,6 @@ namespace Package\Artifact; use Package\Target\php; use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SourcePatcher; @@ -52,16 +51,6 @@ class php_src } } - #[AfterSourceExtract('php-src')] - #[PatchDescription('Patch FFI extension on CentOS 7 with -O3 optimization (strncmp issue)')] - public function patchFfiCentos7FixO3strncmp(): void - { - spc_skip_if(!($ver = SystemTarget::getLibcVersion()) || version_compare($ver, '2.17', '>')); - $ver_id = php::getPHPVersionID(return_null_if_failed: true); - spc_skip_if($ver_id === null || $ver_id < 80316); - SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); - } - #[AfterSourceExtract('php-src')] #[PatchDescription('Add LICENSE file to IMAP extension if missing')] public function patchImapLicense(): void diff --git a/src/Package/Extension/ffi.php b/src/Package/Extension/ffi.php new file mode 100644 index 00000000..dc5287e0 --- /dev/null +++ b/src/Package/Extension/ffi.php @@ -0,0 +1,29 @@ +')); + $ver_id = php::getPHPVersionID(return_null_if_failed: true); + spc_skip_if($ver_id === null || $ver_id < 80316); + spc_skip_if(LinuxUtil::getOSRelease()['dist'] !== 'centos'); + SourcePatcher::patchFile('ffi_centos7_fix_O3_strncmp.patch', SOURCE_PATH . '/php-src'); + } +} From 659b75cedd10c056371eee502775b79aab48d76d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:18:03 +0800 Subject: [PATCH 087/178] Remove redundant dependency for specific virtual target (php-fpm) --- src/StaticPHP/Util/DependencyResolver.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Util/DependencyResolver.php b/src/StaticPHP/Util/DependencyResolver.php index 9833ae10..129468f9 100644 --- a/src/StaticPHP/Util/DependencyResolver.php +++ b/src/StaticPHP/Util/DependencyResolver.php @@ -45,12 +45,20 @@ class DependencyResolver } } + // Build a lookup set of explicitly requested packages for the promotion step below. + $input_package_set = []; + foreach ($packages as $pkg) { + $input_package_set[is_string($pkg) ? $pkg : $pkg->getName()] = true; + } + // Virtual-target packages (e.g. php-fpm) are built as part of their real parent's // build step, so any dependency they declare must be available before the real parent // is built. Promote those deps directly onto the real parent's dependency list so // that the topological sort places them before the parent. + // Only applies to virtual-targets that are in the input request — if a virtual-target + // is not being built, its deps must not be injected into the parent. foreach ($dep_list_clean as $pkg_name => $pkg_item) { - if (PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { + if (!isset($input_package_set[$pkg_name]) || PackageConfig::get($pkg_name, 'type') !== 'virtual-target') { continue; } foreach ($pkg_item['depends'] as $dep_name) { From 8fdfcf8fcd3ec4eb5246fc84d428d8ee8c17192e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:29:12 +0800 Subject: [PATCH 088/178] Fix suggested extensions not passing when using `--with-suggests` --- src/Package/Target/php.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 351622c9..48499810 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -219,7 +219,7 @@ class php extends TargetPackage // Mark transitive PHP extension dependencies of static extensions as static too if (!empty($static_extensions)) { $static_ext_pkgs = array_map(fn ($x) => "ext-{$x}", $static_extensions); - $transitive_deps = DependencyResolver::resolve($static_ext_pkgs); + $transitive_deps = DependencyResolver::resolve($static_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); foreach ($transitive_deps as $dep_name) { if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { continue; From 38715bba21a8d821e3178728796647f243ee5c45 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:29:38 +0800 Subject: [PATCH 089/178] Add ext-fileinfo,ext-filter,ext-ftp --- config/pkg/ext/builtin-extensions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index a9f357f3..ee92025c 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -43,6 +43,14 @@ ext-ffi: php-extension: arg-type@unix: '--with-ffi=@shared_suffix@ --enable-zend-signals' arg-type@windows: with +ext-fileinfo: + type: php-extension +ext-filter: + type: php-extension +ext-ftp: + type: php-extension + suggests: + - ext-openssl ext-mbregex: type: php-extension depends: From 61d50cd28bef972c1b6f80b19d03182cf2a348e1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:55:01 +0800 Subject: [PATCH 090/178] Add ext-gd --- config/pkg/ext/builtin-extensions.yml | 13 +++++++++++++ src/Package/Extension/gd.php | 26 ++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/Package/Extension/gd.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index ee92025c..2d7888a2 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -51,6 +51,19 @@ ext-ftp: type: php-extension suggests: - ext-openssl +ext-gd: + type: php-extension + depends: + - zlib + - libpng + - ext-zlib + suggests: + - libavif + - libwebp + - libjpeg + - freetype + php-extension: + arg-type: custom ext-mbregex: type: php-extension depends: diff --git a/src/Package/Extension/gd.php b/src/Package/Extension/gd.php new file mode 100644 index 00000000..5e815b5d --- /dev/null +++ b/src/Package/Extension/gd.php @@ -0,0 +1,26 @@ +getLibraryPackage('freetype') ? ' --with-freetype' : ''; + $arg .= $installer->getLibraryPackage('libjpeg') ? ' --with-jpeg' : ''; + $arg .= $installer->getLibraryPackage('libwebp') ? ' --with-webp' : ''; + $arg .= $installer->getLibraryPackage('libavif') ? ' --with-avif' : ''; + return $arg; + } +} From 4a572a1372b2ba75eaec8851efab92e00f0e762d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 15:59:27 +0800 Subject: [PATCH 091/178] Add ext-gettext --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 2d7888a2..79ca0537 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -64,6 +64,12 @@ ext-gd: - freetype php-extension: arg-type: custom +ext-gettext: + type: php-extension + depends: + - gettext + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From 7856f7e03a575b24ec222b2f0196207d5a2d7d57 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 16:30:00 +0800 Subject: [PATCH 092/178] Add ext-gmp --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 79ca0537..742358eb 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -70,6 +70,12 @@ ext-gettext: - gettext php-extension: arg-type: with-path +ext-gmp: + type: php-extension + depends: + - gmp + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From 404195a38bbb9af9217edcc2b4abb14d5bdd6fae Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 9 Mar 2026 16:30:35 +0800 Subject: [PATCH 093/178] Add ext-gmssl --- config/pkg/ext/ext-gmssl.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-gmssl.yml diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml new file mode 100644 index 00000000..7ed8981d --- /dev/null +++ b/config/pkg/ext/ext-gmssl.yml @@ -0,0 +1,12 @@ +ext-gmssl: + type: php-extension + artifact: + source: + type: ghtar + repo: gmssl/GmSSL-PHP + extract: php-src/ext/gmssl + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - gmssl From b89a29d5f31457550dd263f5125c001ca98d479a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 13:37:09 +0800 Subject: [PATCH 094/178] Add ext-grpc --- config/pkg/ext/ext-grpc.yml | 14 +++++++ src/Package/Extension/grpc.php | 70 ++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100644 config/pkg/ext/ext-grpc.yml create mode 100644 src/Package/Extension/grpc.php diff --git a/config/pkg/ext/ext-grpc.yml b/config/pkg/ext/ext-grpc.yml new file mode 100644 index 00000000..ff5bae7b --- /dev/null +++ b/config/pkg/ext/ext-grpc.yml @@ -0,0 +1,14 @@ +ext-grpc: + type: php-extension + artifact: + source: + type: pecl + name: grpc + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - grpc + lang: cpp + php-extension: + arg-type@unix: enable-path diff --git a/src/Package/Extension/grpc.php b/src/Package/Extension/grpc.php new file mode 100644 index 00000000..c3b08f16 --- /dev/null +++ b/src/Package/Extension/grpc.php @@ -0,0 +1,70 @@ +getSourceDir()}/src/php/ext/grpc/call.c", + 'zend_exception_get_default(TSRMLS_C),', + 'zend_ce_exception,', + ); + + // custom config.m4 content for grpc extension, to prevent building libgrpc.a again + $config_m4 = <<<'M4' +PHP_ARG_ENABLE(grpc, [whether to enable grpc support], [AS_HELP_STRING([--enable-grpc], [Enable grpc support])]) + +if test "$PHP_GRPC" != "no"; then + PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/include) + PHP_ADD_INCLUDE(PHP_EXT_SRCDIR()/src/php/ext/grpc) + GRPC_LIBDIR=@@build_lib_path@@ + PHP_ADD_LIBPATH($GRPC_LIBDIR) + PHP_ADD_LIBRARY(grpc,,GRPC_SHARED_LIBADD) + LIBS="-lpthread $LIBS" + PHP_ADD_LIBRARY(pthread) + + case $host in + *darwin*) + PHP_ADD_LIBRARY(c++,1,GRPC_SHARED_LIBADD) + ;; + *) + PHP_ADD_LIBRARY(stdc++,1,GRPC_SHARED_LIBADD) + PHP_ADD_LIBRARY(rt,,GRPC_SHARED_LIBADD) + PHP_ADD_LIBRARY(rt) + ;; + esac + + PHP_NEW_EXTENSION(grpc, @grpc_c_files@, $ext_shared, , -DGRPC_POSIX_FORK_ALLOW_PTHREAD_ATFORK=1) + PHP_SUBST(GRPC_SHARED_LIBADD) + PHP_INSTALL_HEADERS([ext/grpc], [php_grpc.h]) +fi +M4; + $replace = get_pack_replace(); + // load grpc c files from src/php/ext/grpc + $c_files = glob("{$this->getSourceDir()}/src/php/ext/grpc/*.c"); + $replace['@grpc_c_files@'] = implode(" \\\n ", array_map(fn ($f) => 'src/php/ext/grpc/' . basename($f), $c_files)); + $config_m4 = str_replace(array_keys($replace), array_values($replace), $config_m4); + file_put_contents("{$this->getSourceDir()}/config.m4", $config_m4); + + copy("{$this->getSourceDir()}/src/php/ext/grpc/php_grpc.h", "{$this->getSourceDir()}/php_grpc.h"); + } +} From 465549f97dcfc1c706d9b1d2429f0e9c0f25b1f1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:41:01 +0800 Subject: [PATCH 095/178] Forward-port #1056 on v3 --- src/Package/Library/postgresql.php | 3 +-- src/Package/Target/php/frankenphp.php | 1 + .../Runtime/Executor/UnixAutoconfExecutor.php | 14 ++++---------- src/StaticPHP/Util/System/UnixUtil.php | 6 +++++- 4 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 18893d0e..682b79e2 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -119,8 +119,7 @@ class postgresql extends LibraryPackage // remove dynamic libs shell()->cd($this->getSourceDir() . '/build') - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so.*") - ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so") + ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.so*") ->exec("rm -rf {$this->getBuildRootPath()}/lib/*.dylib"); FileSystem::replaceFileStr("{$this->getLibDir()}/pkgconfig/libpq.pc", '-lldap', '-lldap -llber'); diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 8b2fb81d..51304d66 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -92,6 +92,7 @@ trait frankenphp '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . + '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index 41bc6e78..c59859cf 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -20,8 +20,6 @@ class UnixAutoconfExecutor extends Executor protected array $configure_args = []; - protected array $ignore_args = []; - protected PackageInstaller $installer; public function __construct(protected LibraryPackage $package, ?PackageInstaller $installer = null) @@ -40,6 +38,8 @@ class UnixAutoconfExecutor extends Executor if (!$this->package->hasStage('build')) { throw new SPCInternalException("Package {$this->package->getName()} does not have a build stage defined."); } + + $this->configure_args = $this->getDefaultConfigureArgs(); } /** @@ -48,18 +48,12 @@ class UnixAutoconfExecutor extends Executor public function configure(...$args): static { // remove all the ignored args - $args = array_merge($args, $this->getDefaultConfigureArgs(), $this->configure_args); - $args = array_diff($args, $this->ignore_args); + $args = array_merge($args, $this->configure_args); $configure_args = implode(' ', $args); InteractiveTerm::setMessage('Building package: ' . ConsoleColor::yellow($this->package->getName()) . ' (./configure)'); return $this->seekLogFileOnException(fn () => $this->shell->exec("./configure {$configure_args}")); } - public function getConfigureArgsString(): string - { - return implode(' ', array_merge($this->getDefaultConfigureArgs(), $this->configure_args)); - } - /** * Run make * @@ -134,7 +128,7 @@ class UnixAutoconfExecutor extends Executor */ public function removeConfigureArgs(...$args): static { - $this->ignore_args = [...$this->ignore_args, ...$args]; + $this->configure_args = array_diff($this->configure_args, $args); return $this; } diff --git a/src/StaticPHP/Util/System/UnixUtil.php b/src/StaticPHP/Util/System/UnixUtil.php index 7d7ddfe2..4a41c524 100644 --- a/src/StaticPHP/Util/System/UnixUtil.php +++ b/src/StaticPHP/Util/System/UnixUtil.php @@ -74,7 +74,11 @@ abstract class UnixUtil throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); } // https://github.com/ziglang/zig/issues/24662 - if (SystemTarget::getTargetOS() !== 'Linux' || ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + $toolchain = ApplicationContext::get(ToolchainInterface::class); + if ($toolchain instanceof ZigToolchain) { + return '-Wl,--export-dynamic'; // needs release 0.16, can be removed then + } + if (SystemTarget::getTargetOS() !== 'Linux') { return "-Wl,-exported_symbols_list,{$symbol_file}"; } return "-Wl,--dynamic-list={$symbol_file}"; From e0d2ee91f7b318e8a8b893d41a7222f1a406a87d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:52:29 +0800 Subject: [PATCH 096/178] Add ext-gmp --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 79ca0537..742358eb 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -70,6 +70,12 @@ ext-gettext: - gettext php-extension: arg-type: with-path +ext-gmp: + type: php-extension + depends: + - gmp + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From bc26e3d37c27a2059562e25c56a5fd1720380b3d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:52:36 +0800 Subject: [PATCH 097/178] Add ext-gmssl --- config/pkg/ext/ext-gmssl.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-gmssl.yml diff --git a/config/pkg/ext/ext-gmssl.yml b/config/pkg/ext/ext-gmssl.yml new file mode 100644 index 00000000..7ed8981d --- /dev/null +++ b/config/pkg/ext/ext-gmssl.yml @@ -0,0 +1,12 @@ +ext-gmssl: + type: php-extension + artifact: + source: + type: ghtar + repo: gmssl/GmSSL-PHP + extract: php-src/ext/gmssl + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - gmssl From 2d906a8145323f9e5c94eaa537a1a9f6fd89ae30 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:56:09 +0800 Subject: [PATCH 098/178] Add ext-iconv --- config/pkg/ext/builtin-extensions.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 742358eb..eeee7b05 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -76,6 +76,13 @@ ext-gmp: - gmp php-extension: arg-type: with-path +ext-iconv: + type: php-extension + depends@unix: + - libiconv + php-extension: + arg-type@unix: with-path + arg-type@windows: with ext-mbregex: type: php-extension depends: From e73bad9d239dee36c64f8934f35a7b5b641bb4b1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:59:48 +0800 Subject: [PATCH 099/178] Add ext-igbinary --- config/pkg/ext/ext-igbinary.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-igbinary.yml diff --git a/config/pkg/ext/ext-igbinary.yml b/config/pkg/ext/ext-igbinary.yml new file mode 100644 index 00000000..1a80831b --- /dev/null +++ b/config/pkg/ext/ext-igbinary.yml @@ -0,0 +1,12 @@ +ext-igbinary: + type: php-extension + artifact: + source: + type: pecl + name: igbinary + metadata: + license-files: [COPYING] + license: BSD-3-Clause + suggests: + - ext-session + - ext-apcu From 1400dc649ffddc414217257384c6ec8a69be9953 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 16:59:54 +0800 Subject: [PATCH 100/178] Add ext-session --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index eeee7b05..a0ef8ac8 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -122,6 +122,8 @@ ext-readline: arg-type: '--with-libedit --without-readline' build-shared: false build-static: true +ext-session: + type: php-extension ext-sockets: type: php-extension ext-xml: From d7eb33ff1ee4df908a0f5a3a8fe675e95dcf5c23 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:01:23 +0800 Subject: [PATCH 101/178] Forward-port #1057 --- 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 51304d66..f513242b 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -91,8 +91,8 @@ trait frankenphp '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.CustomBinaryName=frankenphp\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . - '-X \'github.com/caddyserver/caddy/v2.CustomBinaryName=frankenphp ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, From e31aeabf122404e66aa982c9ec8e5529e27da232 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:19:01 +0800 Subject: [PATCH 102/178] Add ext-imagick --- config/pkg/ext/ext-imagick.yml | 13 +++++++++++++ src/Package/Extension/imagick.php | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 config/pkg/ext/ext-imagick.yml create mode 100644 src/Package/Extension/imagick.php diff --git a/config/pkg/ext/ext-imagick.yml b/config/pkg/ext/ext-imagick.yml new file mode 100644 index 00000000..e6f9843e --- /dev/null +++ b/config/pkg/ext/ext-imagick.yml @@ -0,0 +1,13 @@ +ext-imagick: + type: php-extension + artifact: + source: + type: pecl + name: imagick + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - imagemagick + php-extension: + arg-type: custom diff --git a/src/Package/Extension/imagick.php b/src/Package/Extension/imagick.php new file mode 100644 index 00000000..2d2aa0aa --- /dev/null +++ b/src/Package/Extension/imagick.php @@ -0,0 +1,21 @@ +getBuildRootPath() . $disable_omp; + } +} From f83565b0589c39974057081128f0f3c49aaa8345 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:25:11 +0800 Subject: [PATCH 103/178] Add ext-intl --- config/pkg/ext/builtin-extensions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index a0ef8ac8..49c73282 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -83,6 +83,10 @@ ext-iconv: php-extension: arg-type@unix: with-path arg-type@windows: with +ext-intl: + type: php-extension + depends@unix: + - icu ext-mbregex: type: php-extension depends: From d8dda09fb6e0765a54a0fe575cda17d5a8e2cb3c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:38:34 +0800 Subject: [PATCH 104/178] Add ext-ldap --- config/pkg/ext/builtin-extensions.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 49c73282..38161759 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -87,6 +87,16 @@ ext-intl: type: php-extension depends@unix: - icu +ext-ldap: + type: php-extension + depends: + - ldap + suggests: + - gmp + - libsodium + - ext-openssl + php-extension: + arg-type: with-path ext-mbregex: type: php-extension depends: From c5b11f47c335b2c68ae29275d113b49ffd1a8518 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:41:33 +0800 Subject: [PATCH 105/178] Add ext-libxml --- config/pkg/ext/builtin-extensions.yml | 8 ++++++++ src/StaticPHP/Package/PhpExtensionPackage.php | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 38161759..7bbb3454 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -97,6 +97,14 @@ ext-ldap: - ext-openssl php-extension: arg-type: with-path +ext-libxml: + type: php-extension + depends: + - ext-xml + php-extension: + build-with-php: true + build-shared: false + arg-type: none ext-mbregex: type: php-extension depends: diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index bae11748..7064d041 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -94,7 +94,7 @@ class PhpExtensionPackage extends Package 'enable-path' => $shared ? "--enable-{$name}=shared,{$escapedPath}" : "--enable-{$name}={$escapedPath}", 'with' => $shared ? "--with-{$name}=shared" : "--with-{$name}", 'with-path' => $shared ? "--with-{$name}=shared,{$escapedPath}" : "--with-{$name}={$escapedPath}", - 'custom' => '', + 'custom', 'none' => '', default => $arg_type, }; // customize argument from config string From fa7de0642a9d485dbdfc30c57b47173754f39caf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 10 Mar 2026 22:47:37 +0800 Subject: [PATCH 106/178] Add ext-lz4 --- config/pkg/ext/ext-lz4.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-lz4.yml diff --git a/config/pkg/ext/ext-lz4.yml b/config/pkg/ext/ext-lz4.yml new file mode 100644 index 00000000..8a3bb4db --- /dev/null +++ b/config/pkg/ext/ext-lz4.yml @@ -0,0 +1,15 @@ +ext-lz4: + type: php-extension + artifact: + source: + type: ghtagtar + repo: kjdev/php-ext-lz4 + extract: php-src/ext/lz4 + metadata: + license-files: [LICENSE] + license: MIT + depends: + - liblz4 + php-extension: + arg-type@unix: '--enable-lz4=@shared_suffix@ --with-lz4-includedir=@build_root_path@' + arg-type@windows: '--enable-lz4' From f414bd289cafcc15ff22aa1ac1155e4ae95cbe03 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 08:18:25 +0800 Subject: [PATCH 107/178] Add ext-maxminddb --- config/pkg/ext/ext-maxminddb.yml | 13 +++++++++++++ src/Package/Extension/maxminddb.php | 30 +++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 config/pkg/ext/ext-maxminddb.yml create mode 100644 src/Package/Extension/maxminddb.php diff --git a/config/pkg/ext/ext-maxminddb.yml b/config/pkg/ext/ext-maxminddb.yml new file mode 100644 index 00000000..59d7e4e3 --- /dev/null +++ b/config/pkg/ext/ext-maxminddb.yml @@ -0,0 +1,13 @@ +ext-maxminddb: + type: php-extension + artifact: + source: + type: pecl + name: maxminddb + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - libmaxminddb + php-extension: + arg-type: with diff --git a/src/Package/Extension/maxminddb.php b/src/Package/Extension/maxminddb.php new file mode 100644 index 00000000..bda8d34c --- /dev/null +++ b/src/Package/Extension/maxminddb.php @@ -0,0 +1,30 @@ +getSourceDir()}/config.m4")) { + return; + } + // move ext/maxminddb/ext/* to ext/maxminddb/ + $files = FileSystem::scanDirFiles("{$this->getSourceDir()}/ext", false, true); + foreach ($files as $file) { + rename("{$this->getSourceDir()}/ext/{$file}", "{$this->getSourceDir()}/{$file}"); + } + } +} From f6a9dac504f26f1301bda5265de3edf6c1880a45 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 11:07:25 +0800 Subject: [PATCH 108/178] Fix grpc build error with RPATH --- src/Package/Library/grpc.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Package/Library/grpc.php b/src/Package/Library/grpc.php index 86cddcc0..0e2d191a 100644 --- a/src/Package/Library/grpc.php +++ b/src/Package/Library/grpc.php @@ -48,6 +48,7 @@ class grpc '-DgRPC_ZLIB_PROVIDER=package', '-DgRPC_CARES_PROVIDER=package', '-DgRPC_SSL_PROVIDER=package', + '-DCMAKE_SKIP_INSTALL_RPATH=ON', ); if (PHP_OS_FAMILY === 'Linux' && $toolchain->isStatic() && !LinuxUtil::isMuslDist()) { From cbfeefc8089f80309003ea100a431d763f2239f6 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 15:12:21 +0800 Subject: [PATCH 109/178] Add ext-inotify --- config/pkg/ext/ext-inotify.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 config/pkg/ext/ext-inotify.yml diff --git a/config/pkg/ext/ext-inotify.yml b/config/pkg/ext/ext-inotify.yml new file mode 100644 index 00000000..0956f9e4 --- /dev/null +++ b/config/pkg/ext/ext-inotify.yml @@ -0,0 +1,9 @@ +ext-inotify: + type: php-extension + artifact: + source: + type: pecl + name: inotify + metadata: + license-files: [LICENSE] + license: PHP-3.01 From f35f133115ffd0b58580d9e9b3ad8e4acfe48d2b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 15:29:00 +0800 Subject: [PATCH 110/178] Add ext-memcache --- config/pkg/ext/ext-memcache.yml | 14 ++++++ src/Package/Extension/memcache.php | 75 ++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 config/pkg/ext/ext-memcache.yml create mode 100644 src/Package/Extension/memcache.php diff --git a/config/pkg/ext/ext-memcache.yml b/config/pkg/ext/ext-memcache.yml new file mode 100644 index 00000000..9db51c05 --- /dev/null +++ b/config/pkg/ext/ext-memcache.yml @@ -0,0 +1,14 @@ +ext-memcache: + type: php-extension + artifact: + source: + type: pecl + name: memcache + metadata: + license-files: [LICENSE] + license: PHP-3.0 + depends: + - ext-zlib + - ext-session + php-extension: + arg-type: '--enable-memcache@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/src/Package/Extension/memcache.php b/src/Package/Extension/memcache.php new file mode 100644 index 00000000..a9c58b76 --- /dev/null +++ b/src/Package/Extension/memcache.php @@ -0,0 +1,75 @@ +isBuildStatic()) { + return false; + } + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'if test -d $abs_srcdir/src ; then', + 'if test -d $abs_srcdir/main ; then' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"' + ); + // add for in-tree building + file_put_contents( + "{$this->getSourceDir()}/php_memcache.h", + <<<'EOF' +#ifndef PHP_MEMCACHE_H +#define PHP_MEMCACHE_H + +extern zend_module_entry memcache_module_entry; +#define phpext_memcache_ptr &memcache_module_entry + +#endif +EOF + ); + return true; + } + + #[BeforeStage('ext-memcache', [self::class, 'configureForUnix'])] + #[PatchDescription('Fix memcache extension compile error when building as shared')] + public function patchBeforeSharedConfigure(): bool + { + if (!$this->isBuildShared()) { + return false; + } + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'if test -d $abs_srcdir/main ; then', + 'if test -d $abs_srcdir/src ; then', + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config9.m4", + 'export CPPFLAGS="$CPPFLAGS $INCLUDES -I$abs_srcdir/main"', + 'export CPPFLAGS="$CPPFLAGS $INCLUDES"', + ); + return true; + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17'; + return $parent; + } +} From 59a8b65f6f1d537b2a9ccb02a5ec5804cdef7080 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 16:14:05 +0800 Subject: [PATCH 111/178] Add ext-memcache,ext-msgpack --- config/pkg/ext/ext-memcached.yml | 23 ++++++++++++++++++++++ config/pkg/ext/ext-msgpack.yml | 14 ++++++++++++++ src/Package/Extension/memcached.php | 30 +++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) create mode 100644 config/pkg/ext/ext-memcached.yml create mode 100644 config/pkg/ext/ext-msgpack.yml create mode 100644 src/Package/Extension/memcached.php diff --git a/config/pkg/ext/ext-memcached.yml b/config/pkg/ext/ext-memcached.yml new file mode 100644 index 00000000..329227f2 --- /dev/null +++ b/config/pkg/ext/ext-memcached.yml @@ -0,0 +1,23 @@ +ext-memcached: + type: php-extension + artifact: + source: + type: pecl + name: memcached + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libmemcached + depends@unix: + - libmemcached + - fastlz + - ext-session + - ext-zlib + suggests: + - zstd + - ext-igbinary + - ext-msgpack + - ext-session + php-extension: + arg-type: '--enable-memcached@shared_suffix@ --with-zlib-dir=@build_root_path@' diff --git a/config/pkg/ext/ext-msgpack.yml b/config/pkg/ext/ext-msgpack.yml new file mode 100644 index 00000000..8b230c31 --- /dev/null +++ b/config/pkg/ext/ext-msgpack.yml @@ -0,0 +1,14 @@ +ext-msgpack: + type: php-extension + artifact: + source: + type: pecl + name: msgpack + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-session + php-extension: + arg-type@unix: with + arg-type@windows: enable diff --git a/src/Package/Extension/memcached.php b/src/Package/Extension/memcached.php new file mode 100644 index 00000000..0453e8ec --- /dev/null +++ b/src/Package/Extension/memcached.php @@ -0,0 +1,30 @@ +getLibraryPackage('zlib')->getBuildRootPath() . ' ' . + '--with-libmemcached-dir=' . $installer->getLibraryPackage('libmemcached')->getBuildRootPath() . ' ' . + '--disable-memcached-sasl ' . + '--enable-memcached-json ' . + ($installer->getLibraryPackage('zstd') ? '--with-zstd ' : '') . + ($installer->getPhpExtensionPackage('ext-igbinary') ? '--enable-memcached-igbinary ' : '') . + ($installer->getPhpExtensionPackage('ext-session') ? '--enable-memcached-session ' : '') . + ($installer->getPhpExtensionPackage('ext-msgpack') ? '--enable-memcached-msgpack ' : '') . + '--with-system-fastlz'; + } +} From 32bb0aadce48efd461a05007a27f954e3ec58333 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 16:37:25 +0800 Subject: [PATCH 112/178] Add ext-mysqli,ext-mysqlnd --- config/pkg/ext/builtin-extensions.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 7bbb3454..00d1d593 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -119,6 +119,21 @@ ext-mbstring: type: php-extension php-extension: arg-type: custom +ext-mysqli: + type: php-extension + depends: + - ext-mysqlnd + php-extension: + arg-type: with + build-with-php: true +ext-mysqlnd: + type: php-extension + depends: + - zlib + php-extension: + arg-type@unix: enable + arg-type@windows: with + build-with-php: true ext-openssl: type: php-extension depends: From 13ab3e2b6c7b5b5683fc3bb6f87d94a8266a775f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 17:10:48 +0800 Subject: [PATCH 113/178] Fix transitive extension dependency not enabled bug --- src/Package/Target/php.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 48499810..38e2ad91 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -216,14 +216,21 @@ class php extends TargetPackage } } - // Mark transitive PHP extension dependencies of static extensions as static too - if (!empty($static_extensions)) { - $static_ext_pkgs = array_map(fn ($x) => "ext-{$x}", $static_extensions); - $transitive_deps = DependencyResolver::resolve($static_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); + // Mark transitive PHP extension dependencies of static/shared extensions as static too. + // For static extensions: their ext deps must also be static. + // For shared extensions: their ext deps that are not themselves shared must be compiled + // into the static PHP build so their headers and symbols are available when linking the .so. + $all_input_ext_pkgs = array_map(fn ($x) => "ext-{$x}", array_values(array_unique([...$static_extensions, ...$shared_extensions]))); + if (!empty($all_input_ext_pkgs)) { + $transitive_deps = DependencyResolver::resolve($all_input_ext_pkgs, include_suggests: (bool) $package->getBuildOption('with-suggests', false)); foreach ($transitive_deps as $dep_name) { if (!str_starts_with($dep_name, 'ext-') || !PackageLoader::hasPackage($dep_name)) { continue; } + $dep_extname = substr($dep_name, 4); + if (in_array($dep_extname, $shared_extensions)) { + continue; // already designated as shared + } $dep_instance = PackageLoader::getPackage($dep_name); if (!$dep_instance instanceof PhpExtensionPackage || $dep_instance->isBuildStatic() || $dep_instance->isBuildShared()) { continue; From e523fff0ab4d6bc98280a30cc494cfb5473b4757 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 11 Mar 2026 17:12:27 +0800 Subject: [PATCH 114/178] Add ext-mysqlnd_ed25519 --- config/pkg/ext/ext-mysqlnd_ed25519.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 config/pkg/ext/ext-mysqlnd_ed25519.yml diff --git a/config/pkg/ext/ext-mysqlnd_ed25519.yml b/config/pkg/ext/ext-mysqlnd_ed25519.yml new file mode 100644 index 00000000..e7aa3de8 --- /dev/null +++ b/config/pkg/ext/ext-mysqlnd_ed25519.yml @@ -0,0 +1,18 @@ +ext-mysqlnd_ed25519: + type: php-extension + artifact: + source: + type: pie + repo: mariadb/mysqlnd_ed25519 + extract: php-src/ext/mysqlnd_ed25519 + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-mysqlnd + - libsodium + suggests: + - openssl + php-extension: + arg-type: '--with-mysqlnd_ed25519=@shared_suffix@' + build-static: false From 91ee94f3497c8c2546631823d5680b239e5130ba Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 14:19:38 +0800 Subject: [PATCH 115/178] Add ext-mongodb --- config/pkg/ext/ext-mongodb.yml | 21 +++++++++++++++++ src/Package/Extension/mongodb.php | 38 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 config/pkg/ext/ext-mongodb.yml create mode 100644 src/Package/Extension/mongodb.php diff --git a/config/pkg/ext/ext-mongodb.yml b/config/pkg/ext/ext-mongodb.yml new file mode 100644 index 00000000..7cbdbb14 --- /dev/null +++ b/config/pkg/ext/ext-mongodb.yml @@ -0,0 +1,21 @@ +ext-mongodb: + type: php-extension + artifact: + source: + type: ghrel + repo: mongodb/mongo-php-driver + match: mongodb.+\.tgz + extract: php-src/ext/mongodb + metadata: + license-files: [LICENSE] + license: PHP-3.01 + suggests: + - icu + - openssl + - zstd + - zlib + frameworks: + - CoreFoundation + - Security + php-extension: + arg-type: custom diff --git a/src/Package/Extension/mongodb.php b/src/Package/Extension/mongodb.php new file mode 100644 index 00000000..3434491d --- /dev/null +++ b/src/Package/Extension/mongodb.php @@ -0,0 +1,38 @@ +getLibraryPackage('openssl')) { + $arg .= '--with-mongodb-ssl=openssl'; + } + $arg .= $installer->getLibraryPackage('icu') ? ' --with-mongodb-icu=yes ' : ' --with-mongodb-icu=no '; + $arg .= $installer->getLibraryPackage('zstd') ? ' --with-mongodb-zstd=yes ' : ' --with-mongodb-zstd=no '; + // $arg .= $installer->getLibraryPackage('snappy') ? ' --with-mongodb-snappy=yes ' : ' --with-mongodb-snappy=no '; + $arg .= $installer->getLibraryPackage('zlib') ? ' --with-mongodb-zlib=yes ' : ' --with-mongodb-zlib=bundled '; + return clean_spaces($arg); + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17'; + return $parent; + } +} From 54f53fd1049f62e0759aea9115698f12c7090a42 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 14:29:05 +0800 Subject: [PATCH 116/178] Add ext-mysqlnd_parsec --- config/pkg/ext/ext-mysqlnd_parsec.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 config/pkg/ext/ext-mysqlnd_parsec.yml diff --git a/config/pkg/ext/ext-mysqlnd_parsec.yml b/config/pkg/ext/ext-mysqlnd_parsec.yml new file mode 100644 index 00000000..903d65c4 --- /dev/null +++ b/config/pkg/ext/ext-mysqlnd_parsec.yml @@ -0,0 +1,17 @@ +ext-mysqlnd_parsec: + type: php-extension + artifact: + source: + type: pie + repo: mariadb/mysqlnd_parsec + extract: php-src/ext/mysqlnd_parsec + metadata: + license-files: [LICENSE] + license: BSD-3-Clause + depends: + - ext-mysqlnd + - libsodium + - openssl + php-extension: + arg-type: '--enable-mysqlnd_parsec' + build-static: false From c7f611fe80af3819e7f6cea74b7ca8c89df9d339 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 14:33:39 +0800 Subject: [PATCH 117/178] Add ext-odbc --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 00d1d593..7a14dcef 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -134,6 +134,12 @@ ext-mysqlnd: arg-type@unix: enable arg-type@windows: with build-with-php: true +ext-odbc: + type: php-extension + depends@unix: + - unixodbc + php-extension: + arg-type@unix: '--with-unixODBC@shared_path_suffix@' ext-openssl: type: php-extension depends: From 6f372a74a261564b40efe95f111e0a4ef65cc6ac Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 15:08:02 +0800 Subject: [PATCH 118/178] Remove check for php_micro.c file existence in SourcePatcher --- src/StaticPHP/Util/SourcePatcher.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 6a16f041..b4e2e1c7 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -209,9 +209,6 @@ class SourcePatcher $patch_dir = $tmp_dir; } $php_package = PackageLoader::getTargetPackage('php'); - if (!file_exists("{$php_package->getSourceDir()}/sapi/micro/php_micro.c")) { - return false; - } $ver_file = "{$php_package->getSourceDir()}/main/php_version.h"; if (!file_exists($ver_file)) { throw new PatchException('php-src patcher (original micro patches)', 'Patch failed, cannot find php source files'); From 371a1af5724a23cfd46d1d75ae0ae0669430ec97 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 15:08:11 +0800 Subject: [PATCH 119/178] Add ext-opcache --- config/pkg/ext/builtin-extensions.yml | 7 +++ src/Package/Extension/opcache.php | 76 +++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 src/Package/Extension/opcache.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 7a14dcef..da6ec1e9 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -140,6 +140,13 @@ ext-odbc: - unixodbc php-extension: arg-type@unix: '--with-unixODBC@shared_path_suffix@' +ext-opcache: + type: php-extension + php-extension: + arg-type@unix: custom + arg-type@windows: enable + zend-extension: true + display-name: 'Zend Opcache' ext-openssl: type: php-extension depends: diff --git a/src/Package/Extension/opcache.php b/src/Package/Extension/opcache.php new file mode 100644 index 00000000..93cb0a9f --- /dev/null +++ b/src/Package/Extension/opcache.php @@ -0,0 +1,76 @@ += 8.0 !'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-opcache')] + #[PatchDescription('Fix static opcache build for PHP 8.2.0 to 8.4.x')] + public function patchBeforeBuildconf(PackageInstaller $installer): bool + { + $version = php::getPHPVersion(); + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + if (file_exists("{$php_src}/.opcache_patched")) { + return false; + } + // if 8.2.0 <= PHP_VERSION < 8.2.23, we need to patch from legacy patch file + if (version_compare($version, '8.2.0', '>=') && version_compare($version, '8.2.23', '<')) { + SourcePatcher::patchFile('spc_fix_static_opcache_before_80222.patch', $php_src); + } + // if 8.3.0 <= PHP_VERSION < 8.3.11, we need to patch from legacy patch file + elseif (version_compare($version, '8.3.0', '>=') && version_compare($version, '8.3.11', '<')) { + SourcePatcher::patchFile('spc_fix_static_opcache_before_80310.patch', $php_src); + } + // if 8.3.12 <= PHP_VERSION < 8.5.0-dev, we need to patch from legacy patch file + elseif (version_compare($version, '8.5.0-dev', '<')) { + SourcePatcher::patchPhpSrc(items: ['static_opcache']); + } + // PHP 8.5.0-dev and later supports static opcache without patching + else { + return false; + } + return file_put_contents($php_src . '/.opcache_patched', '1') !== false; + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string + { + $phpVersionID = php::getPHPVersionID(); + $opcache_jit = ' --enable-opcache-jit'; + if ((SystemTarget::getTargetOS() === 'Linux' && + SystemTarget::getLibc() === 'musl' && + $builder->getOption('enable-zts') && + SystemTarget::getTargetArch() === 'x86_64' && + $phpVersionID < 80500) || + $builder->getOption('disable-opcache-jit') + ) { + $opcache_jit = ' --disable-opcache-jit'; + } + return '--enable-opcache' . ($shared ? '=shared' : '') . $opcache_jit; + } +} From 528469514bc0412a86dd7112fcefb4645e9cc466 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 15:38:56 +0800 Subject: [PATCH 120/178] Add ext-opentelemetry --- config/pkg/ext/ext-opentelemetry.yml | 9 +++++++++ src/Package/Extension/opentelemetry.php | 21 +++++++++++++++++++++ 2 files changed, 30 insertions(+) create mode 100644 config/pkg/ext/ext-opentelemetry.yml create mode 100644 src/Package/Extension/opentelemetry.php diff --git a/config/pkg/ext/ext-opentelemetry.yml b/config/pkg/ext/ext-opentelemetry.yml new file mode 100644 index 00000000..5caebef2 --- /dev/null +++ b/config/pkg/ext/ext-opentelemetry.yml @@ -0,0 +1,9 @@ +ext-opentelemetry: + type: php-extension + artifact: + source: + type: pecl + name: opentelemetry + metadata: + license-files: [LICENSE] + license: Apache-2.0 diff --git a/src/Package/Extension/opentelemetry.php b/src/Package/Extension/opentelemetry.php new file mode 100644 index 00000000..632d1257 --- /dev/null +++ b/src/Package/Extension/opentelemetry.php @@ -0,0 +1,21 @@ + Date: Thu, 12 Mar 2026 16:04:25 +0800 Subject: [PATCH 121/178] Add ext-parallel --- config/pkg/ext/ext-parallel.yml | 9 ++++++++ src/Package/Extension/parallel.php | 35 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+) create mode 100644 config/pkg/ext/ext-parallel.yml create mode 100644 src/Package/Extension/parallel.php diff --git a/config/pkg/ext/ext-parallel.yml b/config/pkg/ext/ext-parallel.yml new file mode 100644 index 00000000..a3e91efe --- /dev/null +++ b/config/pkg/ext/ext-parallel.yml @@ -0,0 +1,9 @@ +ext-parallel: + type: php-extension + artifact: + source: + type: pecl + name: parallel + metadata: + license-files: [LICENSE] + license: PHP-3.01 diff --git a/src/Package/Extension/parallel.php b/src/Package/Extension/parallel.php new file mode 100644 index 00000000..0ec58595 --- /dev/null +++ b/src/Package/Extension/parallel.php @@ -0,0 +1,35 @@ +getOption('enable-zts')) { + throw new WrongUsageException('ext-parallel must be built with ZTS builds. Use "--enable-zts" option!'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-parallel')] + #[PatchDescription('Fix parallel m4 hardcoded PHP_VERSION check')] + public function patchBeforeBuildconf(): bool + { + FileSystem::replaceFileRegex("{$this->getSourceDir()}/config.m4", '/PHP_VERSION=.*/m', ''); + return true; + } +} From cbc8feebfdfe0233178e6866b8f6eea494aea0eb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 16:54:01 +0800 Subject: [PATCH 122/178] Add patch for SPC_MICRO_PATCHES and update configure.ac handling --- src/Package/Target/php/unix.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 40961b5e..de439acb 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -32,10 +32,14 @@ use ZM\Logger\ConsoleColor; trait unix { #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] + #[PatchDescription('Patch SPC_MICRO_PATCHES defined patches (e.g. cli_checks, disable_huge_page)')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] public function patchBeforeBuildconf(TargetPackage $package): void { + // php-src patches from micro (reads SPC_MICRO_PATCHES env var) + SourcePatcher::patchPhpSrc(); + // patch configure.ac for musl and musl-toolchain $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); @@ -47,6 +51,7 @@ trait unix // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); + // also patch extension config.m4 files (they call PKG_CHECK_MODULES directly, not via php.m4) foreach (glob("{$package->getSourceDir()}/ext/*/*.m4") as $m4file) { FileSystem::replaceFileStr($m4file, 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); From 9713b7693558079543f1dd7ebe4ebcd29f5515b2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 17:09:59 +0800 Subject: [PATCH 123/178] Add patch to modify info.c for release builds to hide configure command --- src/Package/Target/php/unix.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index de439acb..e636883e 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -161,6 +161,25 @@ trait unix shell()->cd(SOURCE_PATH . '/php-src')->exec('sed -i "s|//lib|/lib|g" Makefile'); } + #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] + #[PatchDescription('Patch info.c to hide configure command in release builds')] + public function patchInfoCForRelease(): void + { + 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 makeForUnix(TargetPackage $package, PackageInstaller $installer): void { From 9d65c491e7e483dcef86951107e2595374cd520b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 21:56:28 +0800 Subject: [PATCH 124/178] Add ext-password-argon2 --- config/pkg/ext/builtin-extensions.yml | 8 +++++ src/Package/Extension/password_argon2.php | 37 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 src/Package/Extension/password_argon2.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 00d1d593..76b20644 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -144,6 +144,14 @@ ext-openssl: arg-type: custom arg-type@windows: with build-with-php: true +ext-password-argon2: + type: php-extension + depends: + - libargon2 + - ext-openssl + php-extension: + arg-type: custom + display-name: '' ext-phar: type: php-extension depends: diff --git a/src/Package/Extension/password_argon2.php b/src/Package/Extension/password_argon2.php new file mode 100644 index 00000000..77122405 --- /dev/null +++ b/src/Package/Extension/password_argon2.php @@ -0,0 +1,37 @@ +execWithResult(BUILD_ROOT_PATH . '/bin/php -n -r "assert(defined(\'PASSWORD_ARGON2I\'));"'); + if ($ret !== 0) { + throw new ValidationException('extension ' . $this->getName() . ' failed sanity check', validation_module: 'password_argon2 function check'); + } + } + + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string + { + if ($installer->getLibraryPackage('openssl') !== null) { + if (php::getPHPVersionID() >= 80500 || (php::getPHPVersionID() >= 80400 && !$builder->getOption('enable-zts'))) { + return '--without-password-argon2'; // use --with-openssl-argon2 in openssl extension instead + } + } + return '--with-password-argon2'; + } +} From 7a690fd9a3237fa104f8c2ce1d7b8685b771ad1f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:06:11 +0800 Subject: [PATCH 125/178] Add ext-pcov --- config/pkg/ext/ext-pcov.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 config/pkg/ext/ext-pcov.yml diff --git a/config/pkg/ext/ext-pcov.yml b/config/pkg/ext/ext-pcov.yml new file mode 100644 index 00000000..3fac61d0 --- /dev/null +++ b/config/pkg/ext/ext-pcov.yml @@ -0,0 +1,12 @@ +ext-pcov: + type: php-extension + artifact: + source: + type: pecl + name: pcov + metadata: + license-files: [LICENSE] + license: PHP-3.01 + php-extension: + build-static: false + build-shared: true From 74865025bd868f03a3788040f5b2847241aed683 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:06:42 +0800 Subject: [PATCH 126/178] Add ext-pcntl,ext-pdo,ext-pdo_mysql,ext-pdo_odbc --- config/pkg/ext/builtin-extensions.yml | 22 +++++++++++++++++ src/Package/Extension/pdo_odbc.php | 35 +++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/Package/Extension/pdo_odbc.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 2fdec855..78d3d282 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -165,6 +165,28 @@ ext-password-argon2: php-extension: arg-type: custom display-name: '' +ext-pcntl: + type: php-extension +ext-pdo: + type: php-extension +ext-pdo_mysql: + type: php-extension + depends: + - ext-pdo + - ext-mysqlnd + php-extension: + arg-type: with +ext-pdo_odbc: + type: php-extension + depends: + - ext-pdo + - ext-odbc + depends@unix: + - unixodbc + - ext-pdo + - ext-odbc + php-extension: + arg-type: custom ext-phar: type: php-extension depends: diff --git a/src/Package/Extension/pdo_odbc.php b/src/Package/Extension/pdo_odbc.php new file mode 100644 index 00000000..f8835d13 --- /dev/null +++ b/src/Package/Extension/pdo_odbc.php @@ -0,0 +1,35 @@ +getSourceDir()}/config.m4", 'PDO_ODBC_LDFLAGS="$pdo_odbc_def_ldflags', 'PDO_ODBC_LDFLAGS="-liconv $pdo_odbc_def_ldflags'); + } + + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getUnixConfigureArg(bool $shared): string + { + return '--with-pdo-odbc=' . ($shared ? 'shared,' : '') . 'unixODBC,' . BUILD_ROOT_PATH; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared): string + { + return '--with-pdo-odbc'; + } +} From f85f29e628ea5396d4de39e3303e5666b135a69a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:22:02 +0800 Subject: [PATCH 127/178] Add ext-pgsql,ext-pdo_pgsql --- config/pkg/ext/builtin-extensions.yml | 15 +++++++++ src/Package/Extension/pgsql.php | 48 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/Package/Extension/pgsql.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 78d3d282..33287bd8 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -187,6 +187,21 @@ ext-pdo_odbc: - ext-odbc php-extension: arg-type: custom +ext-pdo_pgsql: + type: php-extension + depends@unix: + - ext-pdo + - ext-pgsql + - postgresql + php-extension: + arg-type@unix: with-path + arg-type@windows: '--with-pdo-pgsql=yes' +ext-pgsql: + type: php-extension + depends@unix: + - postgresql + php-extension: + arg-type: custom ext-phar: type: php-extension depends: diff --git a/src/Package/Extension/pgsql.php b/src/Package/Extension/pgsql.php new file mode 100644 index 00000000..6e2b8f0b --- /dev/null +++ b/src/Package/Extension/pgsql.php @@ -0,0 +1,48 @@ += 80400) { + $libfiles = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true])->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), $builder->getOption('with-suggests'))['libs']; + $libfiles = str_replace("{$builder->getLibDir()}/lib", '-l', $libfiles); + $libfiles = str_replace('.a', '', $libfiles); + return '--with-pgsql' . ($shared ? '=shared' : '') . + ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libfiles . '"'; + } + return '--with-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared, PackageBuilder $builder): string + { + if (php::getPHPVersionID() >= 80400) { + return '--with-pgsql'; + } + return "--with-pgsql={$builder->getBuildRootPath()}"; + } + + public function getSharedExtensionEnv(): array + { + $parent = parent::getSharedExtensionEnv(); + $parent['CFLAGS'] .= ' -std=c17 -Wno-int-conversion'; + return $parent; + } +} From 6af55323b34f1ff7ef2c4cb9b29422e550518351 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:27:42 +0800 Subject: [PATCH 128/178] Add ext-sqlite3,ext-pdo_sqlite --- config/pkg/ext/builtin-extensions.yml | 16 ++++++++++++++++ src/Package/Extension/pdo_sqlite.php | 25 +++++++++++++++++++++++++ 2 files changed, 41 insertions(+) create mode 100644 src/Package/Extension/pdo_sqlite.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 33287bd8..6d239633 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -196,6 +196,14 @@ ext-pdo_pgsql: php-extension: arg-type@unix: with-path arg-type@windows: '--with-pdo-pgsql=yes' +ext-pdo_sqlite: + type: php-extension + depends: + - ext-pdo + - ext-sqlite3 + - sqlite + php-extension: + arg-type: with ext-pgsql: type: php-extension depends@unix: @@ -221,6 +229,14 @@ ext-session: type: php-extension ext-sockets: type: php-extension +ext-sqlite3: + type: php-extension + depends: + - sqlite + php-extension: + arg-type@unix: with-path + arg-type@windows: with + build-with-php: true ext-xml: type: php-extension depends: diff --git a/src/Package/Extension/pdo_sqlite.php b/src/Package/Extension/pdo_sqlite.php new file mode 100644 index 00000000..b0429f62 --- /dev/null +++ b/src/Package/Extension/pdo_sqlite.php @@ -0,0 +1,25 @@ +getTargetPackage('php')->getSourceDir()}/configure", + '/sqlite3_column_table_name=yes/', + 'sqlite3_column_table_name=no' + ); + } +} From 54e301d55cbdd542648e3b3941aebc8d2a0b0e13 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:32:36 +0800 Subject: [PATCH 129/178] Add ext-sqlsrv,ext-pdo_sqlsrv --- config/pkg/ext/ext-pdo_sqlsrv.yml | 14 ++++++++++++++ config/pkg/ext/ext-sqlsrv.yml | 15 +++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 config/pkg/ext/ext-pdo_sqlsrv.yml create mode 100644 config/pkg/ext/ext-sqlsrv.yml diff --git a/config/pkg/ext/ext-pdo_sqlsrv.yml b/config/pkg/ext/ext-pdo_sqlsrv.yml new file mode 100644 index 00000000..6d57333b --- /dev/null +++ b/config/pkg/ext/ext-pdo_sqlsrv.yml @@ -0,0 +1,14 @@ +ext-pdo_sqlsrv: + type: php-extension + artifact: + source: + type: pecl + name: pdo_sqlsrv + metadata: + license-files: [LICENSE] + license: MIT + depends: + - ext-pdo + - ext-sqlsrv + php-extension: + arg-type: with diff --git a/config/pkg/ext/ext-sqlsrv.yml b/config/pkg/ext/ext-sqlsrv.yml new file mode 100644 index 00000000..603d7a93 --- /dev/null +++ b/config/pkg/ext/ext-sqlsrv.yml @@ -0,0 +1,15 @@ +ext-sqlsrv: + type: php-extension + artifact: + source: + type: pecl + name: sqlsrv + metadata: + license-files: [LICENSE] + license: MIT + depends@linux: + - unixodbc + - ext-pcntl + depends@macos: + - unixodbc + lang: cpp From 63d28bdc014423379e3baab80a72388e83ac36cf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:33:28 +0800 Subject: [PATCH 130/178] Add ext-posix --- config/pkg/ext/builtin-extensions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 6d239633..bdb97507 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -214,6 +214,8 @@ ext-phar: type: php-extension depends: - zlib +ext-posix: + type: php-extension ext-readline: type: php-extension depends: From 067749ab1b1b0255b1c22f5bacd03a4715859f27 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:44:57 +0800 Subject: [PATCH 131/178] Add ext-protobuf --- config/pkg/ext/ext-protobuf.yml | 9 +++++++++ src/Package/Extension/protobuf.php | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/ext/ext-protobuf.yml create mode 100644 src/Package/Extension/protobuf.php diff --git a/config/pkg/ext/ext-protobuf.yml b/config/pkg/ext/ext-protobuf.yml new file mode 100644 index 00000000..020059d3 --- /dev/null +++ b/config/pkg/ext/ext-protobuf.yml @@ -0,0 +1,9 @@ +ext-protobuf: + type: php-extension + artifact: + source: + type: pecl + name: protobuf + metadata: + license-files: [LICENSE] + license: BSD-3-Clause diff --git a/src/Package/Extension/protobuf.php b/src/Package/Extension/protobuf.php new file mode 100644 index 00000000..2c3dd036 --- /dev/null +++ b/src/Package/Extension/protobuf.php @@ -0,0 +1,28 @@ +getPhpExtensionPackage('ext-grpc'); + // protobuf conflicts with grpc + if ($grpc?->isBuildStatic()) { + throw new ValidationException('protobuf conflicts with grpc, please remove grpc or protobuf extension'); + } + } +} From a288533fc372787db479f8f13b0fccd5f52148bf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Thu, 12 Mar 2026 22:58:59 +0800 Subject: [PATCH 132/178] Add ext-rar --- config/pkg/ext/ext-rar.yml | 12 ++++++++++++ src/Package/Extension/rar.php | 27 +++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 config/pkg/ext/ext-rar.yml create mode 100644 src/Package/Extension/rar.php diff --git a/config/pkg/ext/ext-rar.yml b/config/pkg/ext/ext-rar.yml new file mode 100644 index 00000000..1770788a --- /dev/null +++ b/config/pkg/ext/ext-rar.yml @@ -0,0 +1,12 @@ +ext-rar: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/static-php/php-rar.git' + rev: issue-php82 + extract: php-src/ext/rar + metadata: + license-files: [LICENSE] + license: PHP-3.01 + lang: cpp diff --git a/src/Package/Extension/rar.php b/src/Package/Extension/rar.php new file mode 100644 index 00000000..2fc20ed1 --- /dev/null +++ b/src/Package/Extension/rar.php @@ -0,0 +1,27 @@ += 15.0)')] + public function patchBeforeBuildconf(): void + { + // workaround for newer Xcode clang (>= 15.0) + if (SystemTarget::getTargetOS() === 'Darwin') { + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", '-Wall -fvisibility=hidden', '-Wall -Wno-incompatible-function-pointer-types -fvisibility=hidden'); + } + } +} From 935fbbd31a7ca157c15f319dda23f86ab9d85297 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 13 Mar 2026 10:14:05 +0800 Subject: [PATCH 133/178] Add ext-rdkafka --- config/pkg/ext/ext-rdkafka.yml | 15 +++++++++ src/Package/Extension/rdkafka.php | 55 +++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 config/pkg/ext/ext-rdkafka.yml create mode 100644 src/Package/Extension/rdkafka.php diff --git a/config/pkg/ext/ext-rdkafka.yml b/config/pkg/ext/ext-rdkafka.yml new file mode 100644 index 00000000..1f26e49c --- /dev/null +++ b/config/pkg/ext/ext-rdkafka.yml @@ -0,0 +1,15 @@ +ext-rdkafka: + type: php-extension + artifact: + source: + type: ghtar + repo: arnaud-lb/php-rdkafka + extract: php-src/ext/rdkafka + metadata: + license-files: [LICENSE] + license: MIT + depends: + - librdkafka + lang: cpp + php-extension: + arg-type: custom diff --git a/src/Package/Extension/rdkafka.php b/src/Package/Extension/rdkafka.php new file mode 100644 index 00000000..4bb28ee5 --- /dev/null +++ b/src/Package/Extension/rdkafka.php @@ -0,0 +1,55 @@ +getSourceDir()}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\n", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm \$RDKAFKA_LIBS\n"); + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", "-L\$RDKAFKA_DIR/\$PHP_LIBDIR -lm\"\n", '-L$RDKAFKA_DIR/$PHP_LIBDIR -lm $RDKAFKA_LIBS"'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", 'PHP_CHECK_LIBRARY($LIBNAME,$LIBSYMBOL,', 'AC_CHECK_LIB([$LIBNAME], [$LIBSYMBOL],'); + return true; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-rdkafka')] + #[PatchDescription('Patch rdkafka extension source code to fix build errors with inline builds')] + public function patchBeforeMake(): bool + { + // when compiling rdkafka with inline builds, it shows some errors, I don't know why. + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/rdkafka.c", + "#ifdef HAS_RD_KAFKA_TRANSACTIONS\n#include \"kafka_error_exception.h\"\n#endif", + '#include "kafka_error_exception.h"' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/kafka_error_exception.h", + ['#ifdef HAS_RD_KAFKA_TRANSACTIONS', '#endif'], + '' + ); + return true; + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): string + { + $pkgconf_libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->getExtensionConfig($this); + return '--with-rdkafka=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath() . " RDKAFKA_LIBS=\"{$pkgconf_libs['libs']}\""; + } +} From 6ed620683f9099d9b3ba7ce9b2f1464eaad73f79 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 13 Mar 2026 16:53:28 +0800 Subject: [PATCH 134/178] Add ext-redis --- config/pkg/ext/ext-redis.yml | 21 +++++++++++++++ src/Package/Extension/redis.php | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 config/pkg/ext/ext-redis.yml create mode 100644 src/Package/Extension/redis.php diff --git a/config/pkg/ext/ext-redis.yml b/config/pkg/ext/ext-redis.yml new file mode 100644 index 00000000..c05b4ee2 --- /dev/null +++ b/config/pkg/ext/ext-redis.yml @@ -0,0 +1,21 @@ +ext-redis: + type: php-extension + artifact: + source: + type: pecl + name: redis + metadata: + license-files: [LICENSE] + license: PHP-3.01 + suggests: + - ext-session + - ext-igbinary + - ext-msgpack + suggests@unix: + - ext-session + - ext-igbinary + - ext-msgpack + - zstd + - liblz4 + php-extension: + arg-type: custom diff --git a/src/Package/Extension/redis.php b/src/Package/Extension/redis.php new file mode 100644 index 00000000..bfc5cc5e --- /dev/null +++ b/src/Package/Extension/redis.php @@ -0,0 +1,47 @@ +isBuildStatic()) { + $arg .= $installer->getPhpExtensionPackage('session')?->isBuildStatic() ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary')?->isBuildStatic() ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + $arg .= $installer->getPhpExtensionPackage('msgpack')?->isBuildStatic() ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; + } else { + $arg .= $installer->getPhpExtensionPackage('session') ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + $arg .= $installer->getPhpExtensionPackage('msgpack') ? ' --enable-redis-msgpack' : ' --disable-redis-msgpack'; + } + if ($zstd = $installer->getLibraryPackage('zstd')) { + $arg .= ' --enable-redis-zstd --with-libzstd="' . $zstd->getBuildRootPath() . '"'; + } + if ($liblz4 = $installer->getLibraryPackage('liblz4')) { + $arg .= ' --enable-redis-lz4 --with-liblz4="' . $liblz4->getBuildRootPath() . '"'; + } + return $arg; + } + + #[CustomPhpConfigureArg('Windows')] + public function getWindowsConfigureArg(bool $shared, PackageInstaller $installer): string + { + $arg = '--enable-redis'; + $arg .= $installer->getPhpExtensionPackage('session') ? ' --enable-redis-session' : ' --disable-redis-session'; + $arg .= $installer->getPhpExtensionPackage('igbinary') ? ' --enable-redis-igbinary' : ' --disable-redis-igbinary'; + return $arg; + } +} From 271013f2d651148c72a90f09948840e27e519abe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 13 Mar 2026 17:02:35 +0800 Subject: [PATCH 135/178] Add ext-shmop, fix path slashes --- config/pkg/ext/builtin-extensions.yml | 4 ++++ src/StaticPHP/Config/PackageConfig.php | 1 + src/StaticPHP/Util/FileSystem.php | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index bdb97507..9b39ae8f 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -229,6 +229,10 @@ ext-readline: build-static: true ext-session: type: php-extension +ext-shmop: + type: php-extension + php-extension: + build-with-php: true ext-sockets: type: php-extension ext-sqlite3: diff --git a/src/StaticPHP/Config/PackageConfig.php b/src/StaticPHP/Config/PackageConfig.php index c4f22a52..0e2d0af1 100644 --- a/src/StaticPHP/Config/PackageConfig.php +++ b/src/StaticPHP/Config/PackageConfig.php @@ -23,6 +23,7 @@ class PackageConfig if (!is_dir($dir)) { throw new WrongUsageException("Directory {$dir} does not exist, cannot load pkg.json config."); } + $dir = rtrim($dir, '/'); $loaded = []; $files = FileSystem::scanDirFiles($dir, false); if (is_array($files)) { diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index c8da5353..3015b489 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -481,7 +481,7 @@ class FileSystem public static function fullpath(string $path, string $relative_path_base): string { if (FileSystem::isRelativePath($path)) { - $path = $relative_path_base . DIRECTORY_SEPARATOR . $path; + $path = rtrim($relative_path_base, '/') . DIRECTORY_SEPARATOR . $path; } if (!file_exists($path)) { throw new FileSystemException("Path does not exist: {$path}"); From e30a10f60f9c2729fdb858dd713beb0767d420cf Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:04:35 +0800 Subject: [PATCH 136/178] Add ext-simdjson, add SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS env var --- config/env.ini | 6 +++ config/pkg/ext/ext-simdjson.yml | 10 +++++ src/Package/Extension/simdjson.php | 70 ++++++++++++++++++++++++++++++ src/Package/Target/php/unix.php | 1 + 4 files changed, 87 insertions(+) create mode 100644 config/pkg/ext/ext-simdjson.yml create mode 100644 src/Package/Extension/simdjson.php diff --git a/config/env.ini b/config/env.ini index 9e295d79..3143efaf 100644 --- a/config/env.ini +++ b/config/env.ini @@ -121,6 +121,8 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --disable SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_CXXFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE ${SPC_DEFAULT_CXX_FLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" @@ -155,5 +157,9 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --enable- SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_C_FLAGS}" +; EXTRA_CXXFLAGS for `configure` and `make` php +SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fpie -Werror=unknown-warning-option ${SPC_DEFAULT_CXX_FLAGS}" +; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.dylib +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" ; minimum compatible macOS version (LLVM vars, availability not guaranteed) MACOSX_DEPLOYMENT_TARGET=12.0 diff --git a/config/pkg/ext/ext-simdjson.yml b/config/pkg/ext/ext-simdjson.yml new file mode 100644 index 00000000..37eeb5f1 --- /dev/null +++ b/config/pkg/ext/ext-simdjson.yml @@ -0,0 +1,10 @@ +ext-simdjson: + type: php-extension + artifact: + source: + type: pecl + name: simdjson + metadata: + license-files: [LICENSE] + license: Apache-2.0 + lang: cpp diff --git a/src/Package/Extension/simdjson.php b/src/Package/Extension/simdjson.php new file mode 100644 index 00000000..e04c415a --- /dev/null +++ b/src/Package/Extension/simdjson.php @@ -0,0 +1,70 @@ +getTargetPackage('php'); + $php_ver = php::getPHPVersionID(); + FileSystem::replaceFileRegex( + "{$this->getSourceDir()}/config.m4", + '/php_version=(`.*`)$/m', + "php_version={$php_ver}" + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + 'if test -z "$PHP_CONFIG"; then', + 'if false; then' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.w32", + "'yes',", + 'PHP_SIMDJSON_SHARED,' + ); + return true; + } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + if (ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + $extra = getenv('SPC_COMPILER_EXTRA'); + if (!str_contains((string) $extra, '-lstdc++')) { + f_putenv('SPC_COMPILER_EXTRA=' . clean_spaces($extra . ' -lstdc++')); + } + $env['CFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + $env['CXXFLAGS'] .= ' -Xclang -target-feature -Xclang +evex512'; + } + return $env; + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-simdjson')] + public function patchBeforeMake(): void + { + if (!ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain) { + return; + } + $extra_cflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags . ' -Xclang -target-feature -Xclang +evex512')); + $extra_cxxflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS') ?: ''; + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim($extra_cxxflags . ' -Xclang -target-feature -Xclang +evex512')); + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index e636883e..f16d879f 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -766,6 +766,7 @@ trait unix return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), + 'EXTRA_CXXFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS'), 'EXTRA_LDFLAGS_PROGRAM' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . "{$config['ldflags']} {$static} {$pie}", 'EXTRA_LDFLAGS' => $config['ldflags'], 'EXTRA_LIBS' => $libs, From fe302bf8b9059136ac0de4dd80df8407980bf4df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:17:15 +0800 Subject: [PATCH 137/178] Add ext-simplexml --- config/pkg/ext/builtin-extensions.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 9b39ae8f..c10ee59e 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -233,6 +233,14 @@ ext-shmop: type: php-extension php-extension: build-with-php: true +ext-simplexml: + type: php-extension + depends: + - ext-xml + php-extension: + arg-type@unix: '--enable-simplexml@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true ext-sockets: type: php-extension ext-sqlite3: From 15e7678615617c49fb821c23da857eff4f2d1d36 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:17:53 +0800 Subject: [PATCH 138/178] Add missing xml-related patches for windows --- src/Package/Target/php/windows.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index e77b88cd..74e746e2 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -22,6 +22,13 @@ use ZM\Logger\ConsoleColor; trait windows { + #[BeforeStage('php', [self::class, 'buildconfForWindows'])] + #[PatchDescription('Patch for fixing win32 xml related extensions builds')] + public function beforeBuildconfWin(TargetPackage $package): void + { + FileSystem::replaceFileStr("{$package->getSourceDir()}/win32/build/config.w32", 'dllmain.c ', ''); + } + #[Stage] public function buildconfForWindows(TargetPackage $package): void { From 1670b61ed7492ddb679b6c3139ebd767c2699c2a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:26:53 +0800 Subject: [PATCH 139/178] Add ext-snappy --- config/pkg/ext/ext-snappy.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 config/pkg/ext/ext-snappy.yml diff --git a/config/pkg/ext/ext-snappy.yml b/config/pkg/ext/ext-snappy.yml new file mode 100644 index 00000000..7ddec261 --- /dev/null +++ b/config/pkg/ext/ext-snappy.yml @@ -0,0 +1,18 @@ +ext-snappy: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/kjdev/php-ext-snappy' + rev: master + extract: php-src/ext/snappy + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - snappy + suggests: + - ext-apcu + lang: cpp + php-extension: + arg-type@unix: '--enable-snappy --with-snappy-includedir=@build_root_path@' From 3f812fe5fcf1bc9f8c685502b2d86b464d11d088 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:48:26 +0800 Subject: [PATCH 140/178] Fix filename generation for GitHub tarballs to handle missing tag names --- src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 61517a9e..e473c0ca 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -61,7 +61,7 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface $filename = $matches['filename']; } else { $basename = $basename ?? basename($repo); - $filename = "{$basename}-" . ($rel_type === 'releases' ? $data['tag_name'] : $data['name']) . '.tar.gz'; + $filename = "{$basename}-" . ($rel_type === 'releases' ? ($data['tag_name'] ?? $data['name']) : $data['name']) . '.tar.gz'; } return [$url, $filename]; } From 21e2a0194c6d0db98371c7fa2c9d21beba7c03df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:48:50 +0800 Subject: [PATCH 141/178] Add ext-snmp --- config/pkg/ext/builtin-extensions.yml | 6 +++++ src/Package/Extension/snmp.php | 34 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/Package/Extension/snmp.php diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index c10ee59e..62c225fe 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -241,6 +241,12 @@ ext-simplexml: arg-type@unix: '--enable-simplexml@shared_suffix@ --with-libxml=@build_root_path@' arg-type@windows: with build-with-php: true +ext-snmp: + type: php-extension + depends: + - net-snmp + php-extension: + arg-type: with ext-sockets: type: php-extension ext-sqlite3: diff --git a/src/Package/Extension/snmp.php b/src/Package/Extension/snmp.php new file mode 100644 index 00000000..d161c602 --- /dev/null +++ b/src/Package/Extension/snmp.php @@ -0,0 +1,34 @@ +getSourceDir()}/config.m4"); + } + $libs = implode(' ', PkgConfigUtil::getLibsArray('netsnmp')); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + 'PHP_EVAL_LIBLINE([$SNMP_LIBS], [SNMP_SHARED_LIBADD])', + "SNMP_LIBS=\"{$libs}\"\nPHP_EVAL_LIBLINE([\$SNMP_LIBS], [SNMP_SHARED_LIBADD])" + ); + return true; + } +} From ba253ea2a593fcb43bb7cce9500aff4ed22f7b79 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:57:43 +0800 Subject: [PATCH 142/178] Add ext-soap --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 62c225fe..51a02423 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -247,6 +247,15 @@ ext-snmp: - net-snmp php-extension: arg-type: with +ext-soap: + type: php-extension + depends: + - ext-xml + - ext-session + php-extension: + arg-type@unix: '--enable-soap@shared_suffix@ --with-libxml=@build_root_path@' + arg-type@windows: with + build-with-php: true ext-sockets: type: php-extension ext-sqlite3: From d79128cdbf246b4ba1ac7d080ffc01860433abba Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:57:51 +0800 Subject: [PATCH 143/178] Add ext-sodium --- config/pkg/ext/builtin-extensions.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 51a02423..34c6f8bf 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -258,6 +258,12 @@ ext-soap: build-with-php: true ext-sockets: type: php-extension +ext-sodium: + type: php-extension + depends: + - libsodium + php-extension: + arg-type: with ext-sqlite3: type: php-extension depends: From 65c3263b25427eb6f71a1b32545899c6f8276de2 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 16 Mar 2026 16:59:27 +0800 Subject: [PATCH 144/178] Add ext-spx --- config/pkg/ext/ext-spx.yml | 14 ++++++++++ src/Package/Extension/spx.php | 52 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 config/pkg/ext/ext-spx.yml create mode 100644 src/Package/Extension/spx.php diff --git a/config/pkg/ext/ext-spx.yml b/config/pkg/ext/ext-spx.yml new file mode 100644 index 00000000..a379cdd4 --- /dev/null +++ b/config/pkg/ext/ext-spx.yml @@ -0,0 +1,14 @@ +ext-spx: + type: php-extension + artifact: + source: + type: pie + repo: noisebynorthwest/php-spx + extract: php-src/ext/spx + metadata: + license-files: [LICENSE] + license: GPL-3.0-or-later + depends: + - ext-zlib + php-extension: + arg-type: '--enable-SPX@shared_suffix@' diff --git a/src/Package/Extension/spx.php b/src/Package/Extension/spx.php new file mode 100644 index 00000000..bb230ec9 --- /dev/null +++ b/src/Package/Extension/spx.php @@ -0,0 +1,52 @@ +getSourceDir()}/config.m4", + 'CFLAGS="$CFLAGS -Werror -Wall -O3 -pthread -std=gnu90"', + 'CFLAGS="$CFLAGS -pthread"' + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/src/php_spx.h", + "extern zend_module_entry spx_module_entry;\n", + "extern zend_module_entry spx_module_entry;;\n#define phpext_spx_ptr &spx_module_entry\n" + ); + FileSystem::copy("{$this->getSourceDir()}/src/php_spx.h", "{$this->getSourceDir()}/php_spx.h"); + return true; + } + + #[BeforeStage('php', [php::class, 'configureForUnix'], 'ext-spx')] + #[PatchDescription('Fix spx extension compile error when configuring')] + public function patchBeforeConfigure(): void + { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/Makefile.frag", + '@cp -r assets/web-ui/*', + "@cp -r {$this->getSourceDir()}/assets/web-ui/*", + ); + } + + public function getSharedExtensionEnv(): array + { + $env = parent::getSharedExtensionEnv(); + $env['SPX_SHARED_LIBADD'] = $env['LIBS']; + return $env; + } +} From b89e941ab26390eb8cdcfd20d43e0e285a423438 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:24:57 +0800 Subject: [PATCH 145/178] Add ext-ssh2 --- config/pkg/ext/ext-ssh2.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-ssh2.yml diff --git a/config/pkg/ext/ext-ssh2.yml b/config/pkg/ext/ext-ssh2.yml new file mode 100644 index 00000000..14c9bf32 --- /dev/null +++ b/config/pkg/ext/ext-ssh2.yml @@ -0,0 +1,15 @@ +ext-ssh2: + type: php-extension + artifact: + source: + type: pecl + name: ssh2 + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libssh2 + - ext-openssl + - ext-zlib + php-extension: + arg-type: with-path From 02d40d197b0645059d2b4c4047e25dcdec22da3a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:25:18 +0800 Subject: [PATCH 146/178] Add ext-sysvmsg,ext-sysvsem,ext-sysvshm --- config/pkg/ext/builtin-extensions.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 34c6f8bf..3f146687 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -272,6 +272,23 @@ ext-sqlite3: arg-type@unix: with-path arg-type@windows: with build-with-php: true +ext-sysvmsg: + type: php-extension + php-extension: + support: + Windows: 'no' + BSD: wip +ext-sysvsem: + type: php-extension + php-extension: + support: + Windows: 'no' + BSD: wip +ext-sysvshm: + type: php-extension + php-extension: + support: + BSD: wip ext-xml: type: php-extension depends: From 170371abf754289773b0e9d43bae05c05f969f1e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:27:16 +0800 Subject: [PATCH 147/178] Add ext-tidy --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 3f146687..f55cf631 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -289,6 +289,15 @@ ext-sysvshm: php-extension: support: BSD: wip +ext-tidy: + type: php-extension + depends: + - tidy + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path ext-xml: type: php-extension depends: From bfb6fcd436bb3ac256c0d027e2ea7679e6ebb70f Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 11:27:47 +0800 Subject: [PATCH 148/178] Add ext-tokenizer --- config/pkg/ext/builtin-extensions.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index f55cf631..65da4295 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -298,6 +298,10 @@ ext-tidy: Windows: wip BSD: wip arg-type: with-path +ext-tokenizer: + type: php-extension + php-extension: + build-with-php: true ext-xml: type: php-extension depends: From deef11c86a9c7fcae76532186a17928404392a60 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 12:27:14 +0800 Subject: [PATCH 149/178] Add ext-trader --- config/pkg/ext/ext-trader.yml | 14 ++++++++++++++ src/Package/Extension/trader.php | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/ext/ext-trader.yml create mode 100644 src/Package/Extension/trader.php diff --git a/config/pkg/ext/ext-trader.yml b/config/pkg/ext/ext-trader.yml new file mode 100644 index 00000000..8e16afbb --- /dev/null +++ b/config/pkg/ext/ext-trader.yml @@ -0,0 +1,14 @@ +ext-trader: + type: php-extension + artifact: + source: + type: pecl + name: trader + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + php-extension: + support: + BSD: wip + Windows: wip + arg-type: enable diff --git a/src/Package/Extension/trader.php b/src/Package/Extension/trader.php new file mode 100644 index 00000000..546b073a --- /dev/null +++ b/src/Package/Extension/trader.php @@ -0,0 +1,23 @@ +getSourceDir()}/config.m4", 'PHP_TA', 'PHP_TRADER'); + return true; + } +} From 25bec6b9747a7c98a2b95081f7e077e3fd828e3c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 12:30:02 +0800 Subject: [PATCH 150/178] Add ext-uuid --- config/pkg/ext/ext-uuid.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/pkg/ext/ext-uuid.yml diff --git a/config/pkg/ext/ext-uuid.yml b/config/pkg/ext/ext-uuid.yml new file mode 100644 index 00000000..68080531 --- /dev/null +++ b/config/pkg/ext/ext-uuid.yml @@ -0,0 +1,16 @@ +ext-uuid: + type: php-extension + artifact: + source: + type: pecl + name: uuid + metadata: + license-files: [LICENSE] + license: LGPL-2.1-only + depends: + - libuuid + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path From 22c5403e98d6b576dd21e8f7cd919d8a2141b159 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 12:57:43 +0800 Subject: [PATCH 151/178] Allow unstable for PECL downloads --- src/StaticPHP/Artifact/Downloader/Type/PECL.php | 2 +- src/StaticPHP/Config/ConfigValidator.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Artifact/Downloader/Type/PECL.php b/src/StaticPHP/Artifact/Downloader/Type/PECL.php index 78ceed3a..df2da341 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PECL.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PECL.php @@ -54,7 +54,7 @@ class PECL implements DownloadTypeInterface, CheckUpdateInterface $versions = []; logger()->debug('Matched ' . count($matches['version']) . " releases for {$name} from PECL"); foreach ($matches['version'] as $i => $version) { - if ($matches['state'][$i] !== 'stable') { + if ($matches['state'][$i] !== 'stable' && ($config['prefer-stable'] ?? true) === true) { continue; } $versions[$version] = $peclName . '-' . $version . '.tgz'; diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 4a4f75db..256e47fd 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -91,7 +91,7 @@ class ConfigValidator 'bitbuckettag' => [['repo'], ['extract']], 'local' => [['dirname'], ['extract']], 'pie' => [['repo'], ['extract']], - 'pecl' => [['name'], ['extract']], + 'pecl' => [['name'], ['extract', 'prefer-stable']], 'php-release' => [['domain'], ['extract']], 'custom' => [[], ['func']], ]; From 2327f32e4176492eaea7199d70d840fdef3886bb Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 13:04:19 +0800 Subject: [PATCH 152/178] Add ext-uv --- config/pkg/ext/ext-uv.yml | 18 ++++++++++++++++++ src/Package/Extension/uv.php | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+) create mode 100644 config/pkg/ext/ext-uv.yml create mode 100644 src/Package/Extension/uv.php diff --git a/config/pkg/ext/ext-uv.yml b/config/pkg/ext/ext-uv.yml new file mode 100644 index 00000000..f1a3031b --- /dev/null +++ b/config/pkg/ext/ext-uv.yml @@ -0,0 +1,18 @@ +ext-uv: + type: php-extension + artifact: + source: + type: pecl + name: uv + prefer-stable: false + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - libuv + - ext-sockets + php-extension: + support: + Windows: wip + BSD: wip + arg-type: with-path diff --git a/src/Package/Extension/uv.php b/src/Package/Extension/uv.php new file mode 100644 index 00000000..869f4ad9 --- /dev/null +++ b/src/Package/Extension/uv.php @@ -0,0 +1,36 @@ +getSourceDir()}/Makefile", '/^(LDFLAGS =.*)$/m', '$1 -luv -ldl -lrt -pthread'); + return true; + } +} From 20b693d1fa4678b88faac1ccb724bbe192c0e4b8 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 13:16:47 +0800 Subject: [PATCH 153/178] Add ext-xdebug --- config/pkg/ext/ext-xdebug.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 config/pkg/ext/ext-xdebug.yml diff --git a/config/pkg/ext/ext-xdebug.yml b/config/pkg/ext/ext-xdebug.yml new file mode 100644 index 00000000..0374e573 --- /dev/null +++ b/config/pkg/ext/ext-xdebug.yml @@ -0,0 +1,14 @@ +ext-xdebug: + type: php-extension + artifact: + source: + type: pie + repo: xdebug/xdebug + metadata: + license-files: [LICENSE] + license: Xdebug-1.03 + php-extension: + zend-extension: true + build-static: false + build-shared: true + build-with-php: false From ca15ccd4d15ffc03e077fc0c82af62b4c49ab958 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 13:27:37 +0800 Subject: [PATCH 154/178] Add ext-xhprof --- config/pkg/ext/ext-xhprof.yml | 18 ++++++++++++++++ src/Package/Extension/xhprof.php | 35 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 config/pkg/ext/ext-xhprof.yml create mode 100644 src/Package/Extension/xhprof.php diff --git a/config/pkg/ext/ext-xhprof.yml b/config/pkg/ext/ext-xhprof.yml new file mode 100644 index 00000000..b075f65b --- /dev/null +++ b/config/pkg/ext/ext-xhprof.yml @@ -0,0 +1,18 @@ +ext-xhprof: + type: php-extension + artifact: + source: + type: pecl + name: xhprof + extract: php-src/ext/xhprof-src + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - ext-ctype + php-extension: + support: + Windows: wip + BSD: wip + arg-type: enable + build-with-php: true diff --git a/src/Package/Extension/xhprof.php b/src/Package/Extension/xhprof.php new file mode 100644 index 00000000..91c23fac --- /dev/null +++ b/src/Package/Extension/xhprof.php @@ -0,0 +1,35 @@ +getTargetPackage('php')->getSourceDir(); + $link = "{$php_src}/ext/xhprof"; + if (!is_link($link)) { + shell()->cd("{$php_src}/ext")->exec('ln -s xhprof-src/extension xhprof'); + + // patch config.m4 + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/extension/config.m4", + 'if test -f $phpincludedir/ext/pcre/php_pcre.h; then', + 'if test -f $abs_srcdir/ext/pcre/php_pcre.h; then' + ); + return true; + } + return false; + } +} From 5d309ee998b40e4f817f27a478e5854309a79667 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:13:47 +0800 Subject: [PATCH 155/178] Add ext-zip --- config/pkg/ext/ext-zip.yml | 17 +++++++++++++++++ src/Package/Extension/zip.php | 20 ++++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 config/pkg/ext/ext-zip.yml create mode 100644 src/Package/Extension/zip.php diff --git a/config/pkg/ext/ext-zip.yml b/config/pkg/ext/ext-zip.yml new file mode 100644 index 00000000..a5a9e4b5 --- /dev/null +++ b/config/pkg/ext/ext-zip.yml @@ -0,0 +1,17 @@ +ext-zip: + type: php-extension + artifact: + source: + type: pecl + name: zip + extract: ext-zip + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends@unix: + - libzip + php-extension: + support: + BSD: wip + arg-type: custom + arg-type@windows: enable diff --git a/src/Package/Extension/zip.php b/src/Package/Extension/zip.php new file mode 100644 index 00000000..dc2d29c5 --- /dev/null +++ b/src/Package/Extension/zip.php @@ -0,0 +1,20 @@ + Date: Tue, 17 Mar 2026 15:13:59 +0800 Subject: [PATCH 156/178] Add ext-xlswriter --- config/pkg/ext/ext-xlswriter.yml | 18 ++++++++++++++++++ src/Package/Extension/xlswriter.php | 25 +++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 config/pkg/ext/ext-xlswriter.yml create mode 100644 src/Package/Extension/xlswriter.php diff --git a/config/pkg/ext/ext-xlswriter.yml b/config/pkg/ext/ext-xlswriter.yml new file mode 100644 index 00000000..24d2fa3c --- /dev/null +++ b/config/pkg/ext/ext-xlswriter.yml @@ -0,0 +1,18 @@ +ext-xlswriter: + type: php-extension + artifact: + source: + type: pecl + name: xlswriter + metadata: + license-files: [LICENSE] + license: BSD-2-Clause + depends: + - ext-zlib + - ext-zip + suggests: + - openssl + php-extension: + support: + BSD: wip + arg-type: custom diff --git a/src/Package/Extension/xlswriter.php b/src/Package/Extension/xlswriter.php new file mode 100644 index 00000000..b2f25716 --- /dev/null +++ b/src/Package/Extension/xlswriter.php @@ -0,0 +1,25 @@ +getLibraryPackage('openssl')) { + $arg .= ' --with-openssl=' . $installer->getLibraryPackage('openssl')->getBuildRootPath(); + } + return $arg; + } +} From 63bee0db1399403e79bf4f7d7b0106d92c5b75fc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:21:16 +0800 Subject: [PATCH 157/178] Add ext-xmlwriter,ext-xmlreader --- config/pkg/ext/builtin-extensions.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 65da4295..f7b322fd 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -313,6 +313,20 @@ ext-xml: arg-type: '--enable-xml@shared_suffix@ --with-libxml=@build_root_path@' arg-type@windows: with build-with-php: true +ext-xmlreader: + type: php-extension + depends: + - libxml2 + php-extension: + arg-type: '--enable-xmlreader@shared_suffix@ --with-libxml=@build_root_path@' + build-with-php: true +ext-xmlwriter: + type: php-extension + depends: + - libxml2 + php-extension: + arg-type: '--enable-xmlwriter@shared_suffix@ --with-libxml=@build_root_path@' + build-with-php: true ext-zlib: type: php-extension depends: From 0101e6c52b5f8b8f1655e61a14bd0a858e72d9df Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:23:20 +0800 Subject: [PATCH 158/178] Add ext-xsl --- config/pkg/ext/builtin-extensions.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index f7b322fd..b938182c 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -327,6 +327,15 @@ ext-xmlwriter: php-extension: arg-type: '--enable-xmlwriter@shared_suffix@ --with-libxml=@build_root_path@' build-with-php: true +ext-xsl: + type: php-extension + depends: + - libxslt + - ext-xml + - ext-dom + php-extension: + arg-type: with-path + build-with-php: true ext-zlib: type: php-extension depends: From 0a60ebad17abc02696417811e41281aa89997e53 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:30:54 +0800 Subject: [PATCH 159/178] Add ext-xz --- config/pkg/ext/ext-xz.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-xz.yml diff --git a/config/pkg/ext/ext-xz.yml b/config/pkg/ext/ext-xz.yml new file mode 100644 index 00000000..0d625ad2 --- /dev/null +++ b/config/pkg/ext/ext-xz.yml @@ -0,0 +1,15 @@ +ext-xz: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/codemasher/php-ext-xz' + rev: main + extract: php-src/ext/xz + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - xz + php-extension: + arg-type: with-path From 83c266a71326fdca4ec00f2d43bfca1664f2dc62 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:41:00 +0800 Subject: [PATCH 160/178] Add ext-yac --- config/pkg/ext/ext-yac.yml | 14 ++++++++++++++ src/Package/Extension/yac.php | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 config/pkg/ext/ext-yac.yml create mode 100644 src/Package/Extension/yac.php diff --git a/config/pkg/ext/ext-yac.yml b/config/pkg/ext/ext-yac.yml new file mode 100644 index 00000000..e10bea06 --- /dev/null +++ b/config/pkg/ext/ext-yac.yml @@ -0,0 +1,14 @@ +ext-yac: + type: php-extension + artifact: + source: + type: pecl + name: yac + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends@unix: + - fastlz + - ext-igbinary + php-extension: + arg-type@unix: '--enable-yac@shared_suffix@ --enable-igbinary --enable-json --with-system-fastlz' diff --git a/src/Package/Extension/yac.php b/src/Package/Extension/yac.php new file mode 100644 index 00000000..4bf2cf66 --- /dev/null +++ b/src/Package/Extension/yac.php @@ -0,0 +1,25 @@ +getSourceDir()}/storage/allocator/yac_allocator.h", 'defined(HAVE_SHM_MMAP_ANON)', 'defined(YAC_ALLOCATOR_H)'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/serializer/igbinary.c", '#ifdef YAC_ENABLE_IGBINARY', '#if 1'); + FileSystem::replaceFileStr("{$this->getSourceDir()}/serializer/json.c", '#if YAC_ENABLE_JSON', '#if 1'); + return true; + } +} From 738c61b682dab25dd5d04161682c096f8302110d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:50:12 +0800 Subject: [PATCH 161/178] Add ext-zstd --- config/pkg/ext/ext-zstd.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 config/pkg/ext/ext-zstd.yml diff --git a/config/pkg/ext/ext-zstd.yml b/config/pkg/ext/ext-zstd.yml new file mode 100644 index 00000000..1f004f13 --- /dev/null +++ b/config/pkg/ext/ext-zstd.yml @@ -0,0 +1,15 @@ +ext-zstd: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/kjdev/php-ext-zstd' + rev: master + extract: php-src/ext/zstd + metadata: + license-files: [LICENSE] + license: MIT + depends: + - zstd + php-extension: + arg-type: '--enable-zstd --with-libzstd=@build_root_path@' From 98a618f1cd23572a66e3f6018b2dfa5c3392fd72 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Tue, 17 Mar 2026 15:54:11 +0800 Subject: [PATCH 162/178] Add ext-yaml --- config/pkg/ext/ext-yaml.yml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 config/pkg/ext/ext-yaml.yml diff --git a/config/pkg/ext/ext-yaml.yml b/config/pkg/ext/ext-yaml.yml new file mode 100644 index 00000000..a60b6254 --- /dev/null +++ b/config/pkg/ext/ext-yaml.yml @@ -0,0 +1,16 @@ +ext-yaml: + type: php-extension + artifact: + source: + type: git + url: 'https://github.com/php/pecl-file_formats-yaml' + rev: php7 + extract: php-src/ext/yaml + metadata: + license-files: [LICENSE] + license: MIT + depends: + - libyaml + php-extension: + arg-type@unix: with-path + arg-type@windows: with From b1a59dad791f4c0e8db29314ad27f100da5e26c1 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 12:06:48 +0800 Subject: [PATCH 163/178] Make PhpExtensionPackage::getSharedExtensionLoadString public --- src/StaticPHP/Package/PhpExtensionPackage.php | 56 +++++++++---------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 7064d041..baaa2753 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -327,6 +327,34 @@ class PhpExtensionPackage extends Package } } + /** + * 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. + */ + public 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; + } + /** * Splits a given string of library flags into static and shared libraries. * @@ -354,34 +382,6 @@ class PhpExtensionPackage extends Package 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: Wed, 18 Mar 2026 12:07:19 +0800 Subject: [PATCH 164/178] Fix zig-cc strlcpy missing issue with swoole+openssl --- src/StaticPHP/Toolchain/ZigToolchain.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php index 344ce3e9..e817abd7 100644 --- a/src/StaticPHP/Toolchain/ZigToolchain.php +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -67,6 +67,9 @@ class ZigToolchain implements UnixToolchainInterface $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=php_cv_have_avx512=no php_cv_have_avx512vbmi=no {$extra_vars}"); } + // zig-cc/clang treats strlcpy/strlcat as compiler builtins, so configure link tests pass (HAVE_STRLCPY=1) + $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=ac_cv_func_strlcpy=no ac_cv_func_strlcat=no {$extra_vars}"); } public function getCompilerInfo(): ?string From a24fae7a55e995543ee8a19cf004414920430419 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 12:07:39 +0800 Subject: [PATCH 165/178] Add ext-swoole --- config/pkg/ext/ext-swoole.yml | 72 +++++++++++++++ src/Package/Extension/swoole.php | 150 +++++++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 config/pkg/ext/ext-swoole.yml create mode 100644 src/Package/Extension/swoole.php diff --git a/config/pkg/ext/ext-swoole.yml b/config/pkg/ext/ext-swoole.yml new file mode 100644 index 00000000..b6499e85 --- /dev/null +++ b/config/pkg/ext/ext-swoole.yml @@ -0,0 +1,72 @@ +ext-swoole: + type: php-extension + artifact: + source: + type: ghtar + repo: swoole/swoole-src + extract: php-src/ext/swoole + match: v6\.+ + prefer-stable: true + metadata: + license-files: [LICENSE] + license: Apache-2.0 + depends: + - libcares + - brotli + - nghttp2 + - zlib + - ext-openssl + - ext-curl + suggests: + - zstd + - ext-sockets + - ext-swoole-hook-pgsql + - ext-swoole-hook-mysql + - ext-swoole-hook-sqlite + - ext-swoole-hook-odbc + suggests@linux: + - zstd + - liburing + - ext-sockets + - ext-swoole-hook-pgsql + - ext-swoole-hook-mysql + - ext-swoole-hook-sqlite + - ext-swoole-hook-odbc + lang: cpp + php-extension: + arg-type: custom +ext-swoole-hook-mysql: + type: php-extension + depends: + - ext-mysqlnd + - ext-pdo + - ext-pdo_mysql + suggests: + - ext-mysqli + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-odbc: + type: php-extension + depends: + - ext-pdo + - unixodbc + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-pgsql: + type: php-extension + depends: + - ext-pgsql + - ext-pdo + php-extension: + arg-type: none + display-name: swoole +ext-swoole-hook-sqlite: + type: php-extension + depends: + - ext-sqlite3 + - ext-pdo + php-extension: + arg-type: none + display-name: swoole diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php new file mode 100644 index 00000000..c269ba54 --- /dev/null +++ b/src/Package/Extension/swoole.php @@ -0,0 +1,150 @@ +getPhpExtensionPackage('swoole-hook-odbc') && $installer->getPhpExtensionPackage('pdo_odbc')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-odbc provides pdo_odbc, if you enable odbc hook for swoole, you must remove pdo_odbc extension.'); + } + // swoole-hook-pgsql conflicts with pdo_pgsql + if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && $installer->getPhpExtensionPackage('pdo_pgsql')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-pgsql provides pdo_pgsql, if you enable pgsql hook for swoole, you must remove pdo_pgsql extension.'); + } + // swoole-hook-sqlite conflicts with pdo_sqlite + if ($installer->getPhpExtensionPackage('swoole-hook-sqlite') && $installer->getPhpExtensionPackage('pdo_sqlite')?->isBuildStatic()) { + throw new WrongUsageException('swoole-hook-sqlite provides pdo_sqlite, if you enable sqlite hook for swoole, you must remove pdo_sqlite extension.'); + } + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Fix maximum version check for Swoole 6.2')] + public function patchBeforeMake(): void + { + FileSystem::replaceFileStr($this->getSourceDir() . '/ext-src/php_swoole_private.h', 'PHP_VERSION_ID > 80500', 'PHP_VERSION_ID >= 80600'); + } + + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-swoole')] + #[PatchDescription('Fix swoole with event extension conflict bug on macOS')] + public function patchBeforeMake2(): void + { + if (SystemTarget::getTargetOS() === 'Darwin') { + // Fix swoole with event extension conflict bug + $util_path = shell()->execWithResult('xcrun --show-sdk-path', false)[1][0] . '/usr/include/util.h'; + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/thirdparty/php/standard/proc_open.cc", + 'include ', + "include \"{$util_path}\"", + ); + } + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string + { + // enable swoole + $arg = '--enable-swoole' . ($shared ? '=shared' : ''); + + // commonly used feature: coroutine-time + $arg .= ' --enable-swoole-coro-time --with-pic'; + + $arg .= $builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; + + // required features: curl, openssl (but curl hook is buggy for php 8.0) + $arg .= php::getPHPVersionID() >= 80100 ? ' --enable-swoole-curl' : ' --disable-swoole-curl'; + $arg .= ' --enable-openssl'; + + // additional features that only require libraries + $arg .= $installer->getLibraryPackage('libcares') ? ' --enable-cares' : ''; + $arg .= $installer->getLibraryPackage('brotli') ? (' --enable-brotli --with-brotli-dir=' . BUILD_ROOT_PATH) : ''; + $arg .= $installer->getLibraryPackage('nghttp2') ? (' --with-nghttp2-dir=' . BUILD_ROOT_PATH) : ''; + $arg .= $installer->getLibraryPackage('zstd') ? ' --enable-zstd' : ''; + $arg .= $installer->getLibraryPackage('liburing') ? ' --enable-iouring' : ''; + $arg .= $installer->getPhpExtensionPackage('sockets') ? ' --enable-sockets' : ''; + + // enable additional features that require the pdo extension, but conflict with pdo_* extensions + // to make sure everything works as it should, this is done in fake addon extensions + $arg .= $installer->getPhpExtensionPackage('swoole-hook-pgsql') ? ' --enable-swoole-pgsql' : ' --disable-swoole-pgsql'; + $arg .= $installer->getPhpExtensionPackage('swoole-hook-mysql') ? ' --enable-mysqlnd' : ' --disable-mysqlnd'; + $arg .= $installer->getPhpExtensionPackage('swoole-hook-sqlite') ? ' --enable-swoole-sqlite' : ' --disable-swoole-sqlite'; + if ($installer->getPhpExtensionPackage('swoole-hook-odbc')) { + $config = new SPCConfigUtil()->getLibraryConfig($installer->getLibraryPackage('unixodbc')); + $arg .= " --with-swoole-odbc=unixODBC,{$builder->getBuildRootPath()} SWOOLE_ODBC_LIBS=\"{$config['libs']}\""; + } + + // Get version from source directory + $ver = null; + $file = SOURCE_PATH . '/php-src/ext/swoole/include/swoole_version.h'; + // Match #define SWOOLE_VERSION "5.1.3" + $pattern = '/#define SWOOLE_VERSION "(.+)"/'; + if (preg_match($pattern, file_get_contents($file), $matches)) { + $ver = $matches[1]; + } + + if ($ver && $ver >= '6.1.0') { + $arg .= ' --enable-swoole-stdext'; + } + + if (SystemTarget::getTargetOS() === 'Darwin') { + $arg .= ' ac_cv_lib_pthread_pthread_barrier_init=no'; + } + + return $arg; + } + + #[AfterStage('php', [php::class, 'smokeTestCliForUnix'], 'ext-swoole-hook-mysql')] + public function mysqlTest(PackageInstaller $installer): void + { + [$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $this->getSharedExtensionLoadString() . ' --ri "swoole"', false); + $out = implode('', $out); + if ($ret !== 0) { + throw new ValidationException("extension {$this->getName()} failed compile check: php-cli returned {$ret}", validation_module: 'extension swoole_hook_mysql sanity check'); + } + // mysqlnd + if ($installer->getPhpExtensionPackage('swoole-hook-mysql') && !str_contains($out, 'mysqlnd')) { + throw new ValidationException('swoole mysql hook is not enabled correctly.', validation_module: 'Extension swoole mysql hook availability check'); + } + // coroutine_odbc + if ($installer->getPhpExtensionPackage('swoole-hook-odbc') && !str_contains($out, 'coroutine_odbc')) { + throw new ValidationException('swoole odbc hook is not enabled correctly.', validation_module: 'Extension swoole odbc hook availability check'); + } + // coroutine_pgsql + if ($installer->getPhpExtensionPackage('swoole-hook-pgsql') && !str_contains($out, 'coroutine_pgsql')) { + throw new ValidationException( + 'swoole pgsql hook is not enabled correctly.', + validation_module: 'Extension swoole pgsql hook availability check' + ); + } + // coroutine_sqlite + if ($installer->getPhpExtensionPackage('swoole-hook-sqlite') && !str_contains($out, 'coroutine_sqlite')) { + throw new ValidationException( + 'swoole sqlite hook is not enabled correctly.', + validation_module: 'Extension swoole sqlite hook availability check' + ); + } + } +} From 1ee8bc7d3483a5ebd7d25f73cfdaffa8bde72008 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 12:36:22 +0800 Subject: [PATCH 166/178] Add ext-swow --- config/pkg/ext/ext-swow.yml | 18 ++++++++++++++ src/Package/Extension/swow.php | 44 ++++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 config/pkg/ext/ext-swow.yml create mode 100644 src/Package/Extension/swow.php diff --git a/config/pkg/ext/ext-swow.yml b/config/pkg/ext/ext-swow.yml new file mode 100644 index 00000000..11592cd0 --- /dev/null +++ b/config/pkg/ext/ext-swow.yml @@ -0,0 +1,18 @@ +ext-swow: + type: php-extension + artifact: + source: + extract: php-src/ext/swow-src + type: ghtar + repo: swow/swow + prefer-stable: true + metadata: + license: Apache-2.0 + license-files: [LICENSE] + suggests: + - openssl + - curl + - ext-openssl + - ext-curl + php-extension: + arg-type: custom diff --git a/src/Package/Extension/swow.php b/src/Package/Extension/swow.php new file mode 100644 index 00000000..333a3ed7 --- /dev/null +++ b/src/Package/Extension/swow.php @@ -0,0 +1,44 @@ +getLibraryPackage('openssl') ? ' --enable-swow-ssl' : ' --disable-swow-ssl'; + $arg .= $installer->getLibraryPackage('curl') ? ' --enable-swow-curl' : ' --disable-swow-curl'; + return $arg; + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-swow')] + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-swow')] + public function patchBeforeBuildconf(PackageInstaller $installer): bool + { + $php_src = $installer->getTargetPackage('php')->getSourceDir(); + if (php::getPHPVersionID() >= 80000 && !is_link("{$php_src}/ext/swow")) { + if (PHP_OS_FAMILY === 'Windows') { + f_passthru("cd {$php_src}/ext && mklink /D swow swow-src\\ext"); + } else { + f_passthru("cd {$php_src}/ext && ln -s swow-src/ext swow"); + } + } + // replace AC_DEFUN([SWOW_PKG_CHECK_MODULES] to AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC] + FileSystem::replaceFileStr($this->getSourceDir() . '/ext/config.m4', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES]', 'AC_DEFUN([SWOW_PKG_CHECK_MODULES_STATIC]'); + return false; + } +} From c1f2fd49a6fdf92cbf680b15d5b3faa8977f6660 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 13:30:29 +0800 Subject: [PATCH 167/178] Add ext-imap --- config/pkg/ext/ext-imap.yml | 15 +++++++ src/Package/Extension/imap.php | 55 +++++++++++++++++++++++ src/Package/Library/imap.php | 65 +++++++++++++++------------- src/StaticPHP/Util/SPCConfigUtil.php | 4 +- 4 files changed, 109 insertions(+), 30 deletions(-) create mode 100644 config/pkg/ext/ext-imap.yml create mode 100644 src/Package/Extension/imap.php diff --git a/config/pkg/ext/ext-imap.yml b/config/pkg/ext/ext-imap.yml new file mode 100644 index 00000000..a6c18dac --- /dev/null +++ b/config/pkg/ext/ext-imap.yml @@ -0,0 +1,15 @@ +ext-imap: + type: php-extension + artifact: + source: + type: pecl + name: imap + metadata: + license-files: [LICENSE] + license: PHP-3.01 + depends: + - imap + suggests: + - ext-openssl + php-extension: + arg-type: custom diff --git a/src/Package/Extension/imap.php b/src/Package/Extension/imap.php new file mode 100644 index 00000000..e9879b48 --- /dev/null +++ b/src/Package/Extension/imap.php @@ -0,0 +1,55 @@ +getOption('enable-zts')) { + throw new WrongUsageException('ext-imap is not thread safe, do not build it with ZTS builds'); + } + } + + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-imap')] + public function patchBeforeBuildconf(PackageInstaller $installer): void + { + if ($installer->getLibraryPackage('openssl')) { + // sometimes imap with openssl does not contain zlib (required by openssl) + // we need to add it manually + FileSystem::replaceFileStr("{$this->getSourceDir()}/config.m4", 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD"', 'TST_LIBS="$DLIBS $IMAP_SHARED_LIBADD -lz"'); + } + // c-client is built with PASSWDTYPE=nul so libcrypt is not referenced. + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/config.m4", + " PHP_CHECK_LIBRARY(crypt, crypt,\n [\n PHP_ADD_LIBRARY(crypt,, IMAP_SHARED_LIBADD)\n AC_DEFINE(HAVE_LIBCRYPT,1,[ ])\n ])", + ' dnl Skipped: crypt check not needed (c-client built with PASSWDTYPE=nul)' + ); + } + + #[CustomPhpConfigureArg('Darwin')] + #[CustomPhpConfigureArg('Linux')] + public function getUnixConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string + { + $arg = "--with-imap={$builder->getBuildRootPath()}"; + if (($ssl = $installer->getLibraryPackage('openssl')) !== null) { + $arg .= " --with-imap-ssl={$ssl->getBuildRootPath()}"; + } + return $arg; + } +} diff --git a/src/Package/Library/imap.php b/src/Package/Library/imap.php index 607d78ee..3c9f261c 100644 --- a/src/Package/Library/imap.php +++ b/src/Package/Library/imap.php @@ -4,8 +4,6 @@ declare(strict_types=1); namespace Package\Library; -use Package\Target\php; -use StaticPHP\Attribute\Package\AfterStage; use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; @@ -19,15 +17,6 @@ use StaticPHP\Util\SourcePatcher; #[Library('imap')] class imap { - #[AfterStage('php', [php::class, 'patchUnixEmbedScripts'], 'imap')] - #[PatchDescription('Fix missing -lcrypt in php-config libs on glibc systems')] - public function afterPatchScripts(): void - { - if (SystemTarget::getLibc() === 'glibc') { - FileSystem::replaceFileRegex(BUILD_BIN_PATH . '/php-config', '/^libs="(.*)"$/m', 'libs="$1 -lcrypt"'); - } - } - #[PatchBeforeBuild] #[PatchDescription('Patch imap build system for Linux and macOS compatibility')] public function patchBeforeBuild(LibraryPackage $lib): void @@ -66,14 +55,24 @@ class imap } $libcVer = SystemTarget::getLibcVersion(); $extraLibs = $libcVer && version_compare($libcVer, '2.17', '<=') ? 'EXTRALDFLAGS="-ldl -lrt -lpthread"' : ''; - shell()->cd($lib->getSourceDir()) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec("yes | make slx {$ssl_options} EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + try { + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + // PASSWDTYPE=nul avoids any crypt() symbol reference in c-client.a; + // zig-cc 0.15+ uses paths_first strategy and cannot find libcrypt outside of buildroot. + ->exec("yes | make slx {$ssl_options} PASSWDTYPE=nul EXTRACFLAGS='-fPIC -Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types' {$extraLibs}"); + } catch (\Throwable $e) { + // slx target also builds bundled tools (mtest, etc.) which may fail to link -lcrypt dynamically + // (e.g. with zig-cc). We only need c-client.a, so tolerate the failure if it was built. + if (!file_exists("{$lib->getSourceDir()}/c-client/c-client.a")) { + throw $e; + } + } try { shell() ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") @@ -94,16 +93,24 @@ class imap $ssl_options = 'SSLTYPE=none'; } $out = shell()->execWithResult('echo "-include $(xcrun --show-sdk-path)/usr/include/poll.h -include $(xcrun --show-sdk-path)/usr/include/time.h -include $(xcrun --show-sdk-path)/usr/include/utime.h"')[1][0]; - shell()->cd($lib->getSourceDir()) - ->exec('make clean') - ->exec('touch ip6') - ->exec('chmod +x tools/an') - ->exec('chmod +x tools/ua') - ->exec('chmod +x src/osdep/unix/drivers') - ->exec('chmod +x src/osdep/unix/mkauths') - ->exec( - "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" - ); + try { + shell()->cd($lib->getSourceDir()) + ->exec('make clean') + ->exec('touch ip6') + ->exec('chmod +x tools/an') + ->exec('chmod +x tools/ua') + ->exec('chmod +x src/osdep/unix/drivers') + ->exec('chmod +x src/osdep/unix/mkauths') + ->exec( + "echo y | make osx {$ssl_options} EXTRACFLAGS='-Wno-implicit-function-declaration -Wno-incompatible-function-pointer-types {$out}'" + ); + } catch (\Throwable $e) { + // osx target also builds bundled tools (mtest, etc.) which may fail to link. + // We only need c-client.a, so tolerate the failure if it was built. + if (!file_exists("{$lib->getSourceDir()}/c-client/c-client.a")) { + throw $e; + } + } try { shell() ->exec("cp -rf {$lib->getSourceDir()}/c-client/c-client.a {$lib->getLibDir()}/libc-client.a") diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 32ef3bc6..8b6fe6b3 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -389,7 +389,9 @@ class SPCConfigUtil } if (in_array('imap', $packages) && SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'glibc') { - $lib_names[] = '-lcrypt'; + if (file_exists(BUILD_LIB_PATH . '/libcrypt.a')) { + $lib_names[] = '-lcrypt'; + } } if (!$use_short_libs) { $lib_names = array_map(fn ($l) => $this->getFullLibName($l), $lib_names); From c81146bf18a65468ad6d4cd006c292e9ce880d1a Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 15:00:44 +0800 Subject: [PATCH 168/178] Add ncursesw --- config/artifact/ncurses.yml | 9 ++++++ config/pkg/lib/ncurses.yml | 14 ++++----- src/Package/Library/ncurses.php | 56 ++++++++++++++++++++------------- 3 files changed, 49 insertions(+), 30 deletions(-) create mode 100644 config/artifact/ncurses.yml diff --git a/config/artifact/ncurses.yml b/config/artifact/ncurses.yml new file mode 100644 index 00000000..52c8f59f --- /dev/null +++ b/config/artifact/ncurses.yml @@ -0,0 +1,9 @@ +ncurses: + binary: hosted + metadata: + license-files: + - COPYING + source: + type: filelist + url: 'https://ftp.gnu.org/pub/gnu/ncurses/' + regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' diff --git a/config/pkg/lib/ncurses.yml b/config/pkg/lib/ncurses.yml index cbc1ba67..6ce20f91 100644 --- a/config/pkg/lib/ncurses.yml +++ b/config/pkg/lib/ncurses.yml @@ -1,12 +1,10 @@ ncurses: type: library - artifact: - source: - type: filelist - url: 'https://ftp.gnu.org/pub/gnu/ncurses/' - regex: '/href="(?ncurses-(?[^"]+)\.tar\.gz)"/' - binary: hosted - metadata: - license-files: [COPYING] + artifact: ncurses static-libs@unix: - libncurses.a +ncursesw: + type: library + artifact: ncurses + static-libs@unix: + - libncursesw.a diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index c7c39dc1..dd591a6f 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -13,6 +13,7 @@ use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; #[Library('ncurses')] +#[Library('ncursesw')] class ncurses { #[BuildFor('Darwin')] @@ -21,37 +22,48 @@ class ncurses { $dirdiff = new DirDiff(BUILD_BIN_PATH); - UnixAutoconfExecutor::create($package) + $ac = UnixAutoconfExecutor::create($package) ->appendEnv([ 'LDFLAGS' => $toolchain->isStatic() ? '-static' : '', - ]) - ->configure( - '--enable-overwrite', - '--with-curses-h', - '--enable-pc-files', - '--enable-echo', - '--disable-widec', - '--with-normal', - '--with-ticlib', - '--without-tests', - '--without-dlsym', - '--without-debug', - '--enable-symlinks', - "--bindir={$package->getBinDir()}", - "--includedir={$package->getIncludeDir()}", - "--libdir={$package->getLibDir()}", - "--prefix={$package->getBuildRootPath()}", - ) + ]); + $wide = $package->getName() === 'ncurses' ? ['--disable-widec'] : []; + // Include standard system terminfo paths as fallback so binaries linking this ncurses + // (e.g. htop) can find terminfo on any target system without needing TERMINFO_DIRS set. + $terminfo_dirs = implode(':', [ + "{$package->getBuildRootPath()}/share/terminfo", + '/etc/terminfo', + '/lib/terminfo', + '/usr/share/terminfo', + ]); + $ac->configure( + '--enable-overwrite', + '--with-curses-h', + '--enable-pc-files', + '--enable-echo', + '--with-normal', + '--with-ticlib', + '--without-tests', + '--without-dlsym', + '--without-debug', + '--enable-symlinks', + "--with-terminfo-dirs={$terminfo_dirs}", + "--bindir={$package->getBinDir()}", + "--includedir={$package->getIncludeDir()}", + "--libdir={$package->getLibDir()}", + "--prefix={$package->getBuildRootPath()}", + ...$wide, + ) ->make(); $new_files = $dirdiff->getIncrementFiles(true); foreach ($new_files as $file) { @unlink(BUILD_BIN_PATH . '/' . $file); } - shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); - shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); + // shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf share/terminfo'); + // shell()->cd(BUILD_ROOT_PATH)->exec('rm -rf lib/terminfo'); - $pkgconf_list = ['form.pc', 'menu.pc', 'ncurses++.pc', 'ncurses.pc', 'panel.pc', 'tic.pc']; + $suffix = $package->getName() === 'ncursesw' ? 'w' : ''; + $pkgconf_list = ["form{$suffix}.pc", "menu{$suffix}.pc", "ncurses++{$suffix}.pc", "ncurses{$suffix}.pc", "panel{$suffix}.pc", "tic{$suffix}.pc"]; $package->patchPkgconfPrefix($pkgconf_list); foreach ($pkgconf_list as $pkgconf) { From 0b0ecd17c324cc98b144ae5bfcadfaaa21024265 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 15:00:58 +0800 Subject: [PATCH 169/178] Allow curl building static executable --- config/pkg/{lib => target}/curl.yml | 4 +++- src/Package/{Library => Target}/curl.php | 10 ++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) rename config/pkg/{lib => target}/curl.yml (91%) rename src/Package/{Library => Target}/curl.php (93%) diff --git a/config/pkg/lib/curl.yml b/config/pkg/target/curl.yml similarity index 91% rename from config/pkg/lib/curl.yml rename to config/pkg/target/curl.yml index f183b21e..4daba8c1 100644 --- a/config/pkg/lib/curl.yml +++ b/config/pkg/target/curl.yml @@ -1,5 +1,5 @@ curl: - type: library + type: target artifact: source: type: ghrel @@ -29,5 +29,7 @@ curl: - SystemConfiguration headers: - curl + static-bins@unix: + - curl static-libs@unix: - libcurl.a diff --git a/src/Package/Library/curl.php b/src/Package/Target/curl.php similarity index 93% rename from src/Package/Library/curl.php rename to src/Package/Target/curl.php index 0edca93f..dbfa8f7a 100644 --- a/src/Package/Library/curl.php +++ b/src/Package/Target/curl.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace Package\Library; +namespace Package\Target; use StaticPHP\Attribute\Package\BuildFor; -use StaticPHP\Attribute\Package\Library; use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixCMakeExecutor; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; -#[Library('curl')] +#[Target('curl')] class curl { #[PatchBeforeBuild] @@ -48,7 +48,7 @@ class curl ->optionalPackage('idn2', ...cmake_boolean_args('CURL_USE_IDN2')) ->optionalPackage('libcares', '-DENABLE_ARES=ON') ->addConfigureArgs( - '-DBUILD_CURL_EXE=OFF', + '-DBUILD_CURL_EXE=ON', '-DBUILD_LIBCURL_DOCS=OFF', ) ->build(); @@ -63,5 +63,7 @@ class curl } shell()->cd("{$lib->getLibDir()}/cmake/CURL/") ->exec("sed -ie 's|\"/lib/libcurl.a\"|\"{$lib->getLibDir()}/libcurl.a\"|g' CURLTargets-release.cmake"); + + $lib->setOutput('Static curl executable path', BUILD_BIN_PATH . '/curl'); } } From 9e2a5ce188e64c2b93194628bae7f1f457acabca Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Wed, 18 Mar 2026 15:01:06 +0800 Subject: [PATCH 170/178] Add target htop --- config/pkg/target/htop.yml | 10 ++++++++++ src/Package/Target/htop.php | 29 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 config/pkg/target/htop.yml create mode 100644 src/Package/Target/htop.php diff --git a/config/pkg/target/htop.yml b/config/pkg/target/htop.yml new file mode 100644 index 00000000..fcefa70a --- /dev/null +++ b/config/pkg/target/htop.yml @@ -0,0 +1,10 @@ +htop: + type: target + artifact: + source: + type: ghrel + repo: htop-dev/htop + match: htop.+\.tar\.xz + prefer-stable: true + depends: + - ncursesw diff --git a/src/Package/Target/htop.php b/src/Package/Target/htop.php new file mode 100644 index 00000000..3539d3a7 --- /dev/null +++ b/src/Package/Target/htop.php @@ -0,0 +1,29 @@ +removeConfigureArgs('--disable-shared', '--enable-static') + ->exec('./autogen.sh') + ->addConfigureArgs($toolchain->isStatic() ? '--enable-static' : '--disable-static') + ->configure() + ->make(with_clean: false); + } +} From beeb0b87211bbb826b88bd6ed2c2371ca8072afe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:29:45 +0800 Subject: [PATCH 171/178] Handle failure in fetching Zig version index --- src/Package/Artifact/zig.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index b42eee3a..95520aa4 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -26,6 +26,9 @@ class zig $index_json = default_shell()->executeCurl('https://ziglang.org/download/index.json', retries: $downloader->getRetry()); $index_json = json_decode($index_json ?: '', true); $latest_version = null; + if ($index_json === null) { + throw new DownloaderException('Failed to fetch Zig version index'); + } foreach ($index_json as $version => $data) { if ($version !== 'master') { $latest_version = $version; From b0522205dabe2f9364fd0667132c407a4b006246 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:30:34 +0800 Subject: [PATCH 172/178] Add support for package environment variables and path injection --- src/StaticPHP/Config/ConfigValidator.php | 9 ++++++++ src/StaticPHP/Package/PackageInstaller.php | 25 ++++++++++++++++++++++ src/StaticPHP/Util/GlobalEnvManager.php | 11 ++++++++++ 3 files changed, 45 insertions(+) diff --git a/src/StaticPHP/Config/ConfigValidator.php b/src/StaticPHP/Config/ConfigValidator.php index 256e47fd..919de86d 100644 --- a/src/StaticPHP/Config/ConfigValidator.php +++ b/src/StaticPHP/Config/ConfigValidator.php @@ -40,6 +40,9 @@ class ConfigValidator 'static-libs' => ConfigType::LIST_ARRAY, // @ 'pkg-configs' => ConfigType::LIST_ARRAY, 'static-bins' => ConfigType::LIST_ARRAY, // @ + 'path' => ConfigType::LIST_ARRAY, // @ + 'env' => ConfigType::ASSOC_ARRAY, // @ + 'append-env' => ConfigType::ASSOC_ARRAY, // @ ]; public const array PACKAGE_FIELDS = [ @@ -60,6 +63,9 @@ class ConfigValidator 'static-libs' => false, // @ 'pkg-configs' => false, 'static-bins' => false, // @ + 'path' => false, // @ + 'env' => false, // @ + 'append-env' => false, // @ ]; public const array SUFFIX_ALLOWED_FIELDS = [ @@ -68,6 +74,9 @@ class ConfigValidator 'headers', 'static-libs', 'static-bins', + 'path', + 'env', + 'append-env', ]; public const array PHP_EXTENSION_FIELDS = [ diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index d8d745f6..417c4e1b 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -9,6 +9,7 @@ use StaticPHP\Artifact\ArtifactCache; use StaticPHP\Artifact\ArtifactDownloader; use StaticPHP\Artifact\ArtifactExtractor; use StaticPHP\Artifact\DownloaderOptions; +use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Registry\PackageLoader; @@ -580,6 +581,30 @@ class PackageInstaller foreach ($resolved_packages as $pkg_name) { $this->packages[$pkg_name] = PackageLoader::getPackage($pkg_name); } + + foreach ($this->packages as $package) { + $this->injectPackageEnvs($package); + } + } + + private function injectPackageEnvs(Package $package): void + { + $name = $package->getName(); + + $paths = PackageConfig::get($name, 'path', []); + foreach ($paths as $path) { + GlobalEnvManager::addPathIfNotExists(FileSystem::replacePathVariable($path)); + } + + $envs = PackageConfig::get($name, 'env', []); + foreach ($envs as $k => $v) { + GlobalEnvManager::putenv("{$k}=" . FileSystem::replacePathVariable((string) $v)); + } + + $append_envs = PackageConfig::get($name, 'append-env', []); + foreach ($append_envs as $k => $v) { + GlobalEnvManager::appendEnv($k, FileSystem::replacePathVariable((string) $v)); + } } private function handlePhpTargetPackage(TargetPackage $package): void diff --git a/src/StaticPHP/Util/GlobalEnvManager.php b/src/StaticPHP/Util/GlobalEnvManager.php index 86fcc652..5b4b16b2 100644 --- a/src/StaticPHP/Util/GlobalEnvManager.php +++ b/src/StaticPHP/Util/GlobalEnvManager.php @@ -112,6 +112,17 @@ class GlobalEnvManager } } + public static function appendEnv(string $key, string $value): void + { + $existing = getenv($key); + if ($existing !== false && $existing !== '') { + $separator = SystemTarget::isUnix() ? ':' : ';'; + self::putenv("{$key}={$value}{$separator}{$existing}"); + } else { + self::putenv("{$key}={$value}"); + } + } + /** * Initialize the toolchain after the environment variables are set. * The toolchain or environment availability check is done here. From 9d748a6e0892ae1721f244331ff1188604231930 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:30:52 +0800 Subject: [PATCH 173/178] Add rust target --- config/pkg/target/rust.yml | 6 +++ src/Package/Artifact/rust.php | 85 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+) create mode 100644 config/pkg/target/rust.yml create mode 100644 src/Package/Artifact/rust.php diff --git a/config/pkg/target/rust.yml b/config/pkg/target/rust.yml new file mode 100644 index 00000000..f2e10738 --- /dev/null +++ b/config/pkg/target/rust.yml @@ -0,0 +1,6 @@ +rust: + type: target + artifact: + binary: custom + path: + - '{pkg_root_path}/rust/bin' diff --git a/src/Package/Artifact/rust.php b/src/Package/Artifact/rust.php new file mode 100644 index 00000000..e5c9f525 --- /dev/null +++ b/src/Package/Artifact/rust.php @@ -0,0 +1,85 @@ +executeCurl('https://static.rust-lang.org/dist/channel-rust-stable.toml', retries: $downloader->getRetry()); + // parse toml by regex since we want to avoid adding a toml parser dependency just for this + $cnt = preg_match_all('/^version = "([^"]+)"$/m', $toml_config ?: '', $matches); + if (!$cnt) { + throw new DownloaderException('Failed to parse Rust version from channel config'); + } + $versions = $matches[1]; + // strip version num \d.\d.\d (some version number is like "x.x.x (abcdefg 1970-01-01)" + $versions = array_filter(array_map(fn ($v) => preg_match('/^(\d+\.\d+\.\d+)/', $v, $m) ? $m[1] : null, $versions)); + usort($versions, 'version_compare'); + $latest_version = end($versions); + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Rust version'); + } + + // merge download link + $download_url = "https://static.rust-lang.org/dist/rust-{$latest_version}-{$arch}-unknown-linux-{$distro}.tar.xz"; + $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . basename($download_url); + default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); + return DownloadResult::archive(basename($path), ['url' => $download_url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/rust-install', verified: false, version: $latest_version); + } + + #[CustomBinaryCheckUpdate('rust', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $toml_config = default_shell()->executeCurl('https://static.rust-lang.org/dist/channel-rust-stable.toml', retries: $downloader->getRetry()); + $cnt = preg_match_all('/^version = "([^"]+)"$/m', $toml_config ?: '', $matches); + if (!$cnt) { + throw new DownloaderException('Failed to parse Rust version from channel config'); + } + $versions = array_filter(array_map(fn ($v) => preg_match('/^(\d+\.\d+\.\d+)/', $v, $m) ? $m[1] : null, $matches[1])); + usort($versions, 'version_compare'); + $latest_version = end($versions); + if (!$latest_version) { + throw new DownloaderException('Could not determine latest Rust version'); + } + return new CheckUpdateResult( + old: $old_version, + new: $latest_version, + needUpdate: $old_version === null || $latest_version !== $old_version, + ); + } + + #[AfterBinaryExtract('rust', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function postExtractRust(string $target_path): void + { + $prefix = PKG_ROOT_PATH . '/rust'; + shell()->exec("cd {$target_path} && ./install.sh --prefix={$prefix}"); + } +} From b0eff0ba6e39f69e2637907b0476458fa5b70023 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:31:05 +0800 Subject: [PATCH 174/178] Add protoc target --- config/pkg/target/protoc.yml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 config/pkg/target/protoc.yml diff --git a/config/pkg/target/protoc.yml b/config/pkg/target/protoc.yml new file mode 100644 index 00000000..b45fb335 --- /dev/null +++ b/config/pkg/target/protoc.yml @@ -0,0 +1,8 @@ +protoc: + type: target + artifact: + binary: + linux-x86_64: { type: ghrel, repo: protocolbuffers/protobuf, match: 'protoc-([0-9.]+)-linux-x86_64\.zip', extract: '{pkg_root_path}/protoc' } + linux-aarch64: { type: ghrel, repo: protocolbuffers/protobuf, match: 'protoc-([0-9.]+)-linux-aarch_64\.zip', extract: '{pkg_root_path}/protoc' } + path: + - '{pkg_root_path}/protoc/bin' From c6207d8c7c2d7aa0fe2a16c8c21c921e8d012e92 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:31:24 +0800 Subject: [PATCH 175/178] Fix interactive install-pkg command --- src/StaticPHP/Command/InstallPackageCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/InstallPackageCommand.php b/src/StaticPHP/Command/InstallPackageCommand.php index b5fb8d2c..864fd379 100644 --- a/src/StaticPHP/Command/InstallPackageCommand.php +++ b/src/StaticPHP/Command/InstallPackageCommand.php @@ -34,7 +34,7 @@ class InstallPackageCommand extends BaseCommand public function handle(): int { ApplicationContext::set('elephant', true); - $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], false); + $installer = new PackageInstaller([...$this->input->getOptions(), 'dl-prefer-binary' => true], true); $installer->addInstallPackage($this->input->getArgument('package')); $installer->run(true); return static::SUCCESS; From 11376cc6ade8c9e960f72ad6d8f73dc736e51f26 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 09:31:43 +0800 Subject: [PATCH 176/178] Use env and path injection instead of old style --- config/pkg/target/go-xcaddy.yml | 6 ++++++ src/Package/Target/go_xcaddy.php | 26 -------------------------- 2 files changed, 6 insertions(+), 26 deletions(-) delete mode 100644 src/Package/Target/go_xcaddy.php diff --git a/config/pkg/target/go-xcaddy.yml b/config/pkg/target/go-xcaddy.yml index 89cb4cd0..deafb37d 100644 --- a/config/pkg/target/go-xcaddy.yml +++ b/config/pkg/target/go-xcaddy.yml @@ -2,5 +2,11 @@ go-xcaddy: type: target artifact: binary: custom + env: + GOROOT: '{pkg_root_path}/go-xcaddy' + GOBIN: '{pkg_root_path}/go-xcaddy/bin' + GOPATH: '{pkg_root_path}/go-xcaddy/go' + path@unix: + - '{pkg_root_path}/go-xcaddy/bin' static-bins: - xcaddy diff --git a/src/Package/Target/go_xcaddy.php b/src/Package/Target/go_xcaddy.php deleted file mode 100644 index 0f8c7553..00000000 --- a/src/Package/Target/go_xcaddy.php +++ /dev/null @@ -1,26 +0,0 @@ - Date: Fri, 20 Mar 2026 11:20:42 +0800 Subject: [PATCH 177/178] Add homebrew llvm version toolchain support --- config/env.ini | 2 ++ src/StaticPHP/Doctor/Item/MacOSToolCheck.php | 15 +++++++++++++ .../Toolchain/ClangBrewToolchain.php | 21 +++++++++++++++++++ src/StaticPHP/Toolchain/ToolchainManager.php | 5 ++++- 4 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 src/StaticPHP/Toolchain/ClangBrewToolchain.php diff --git a/config/env.ini b/config/env.ini index 3143efaf..94797118 100644 --- a/config/env.ini +++ b/config/env.ini @@ -134,6 +134,8 @@ OPENSSLDIR="" ; build target: macho or macho (possibly we could support macho-universal in the future) ; Currently we do not support universal and cross-compilation for macOS. SPC_TARGET=native-macos +; Whether to use brew version of llvm or system version (valid options: 'system', 'brew', default: 'system') +SPC_USE_LLVM=system ; compiler environments (default value is defined by selected toolchain) CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php index b69528ad..9d51b83a 100644 --- a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -7,6 +7,7 @@ namespace StaticPHP\Doctor\Item; use StaticPHP\Attribute\Doctor\CheckItem; use StaticPHP\Attribute\Doctor\FixItem; use StaticPHP\Doctor\CheckResult; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\System\MacOSUtil; class MacOSToolCheck @@ -58,6 +59,20 @@ class MacOSToolCheck return CheckResult::ok(); } + #[CheckItem('if homebrew llvm are installed', limit_os: 'Darwin')] + public function checkBrewLLVM(): ?CheckResult + { + if (getenv('SPC_USE_LLVM') === 'brew') { + $homebrew_prefix = getenv('HOMEBREW_PREFIX') ?: (SystemTarget::getTargetArch() === 'aarch64' ? '/opt/homebrew' : '/usr/local/homebrew'); + + if (MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"]) === null) { + return CheckResult::fail('Homebrew llvm is not installed', 'brew', ['missing' => ['llvm']]); + } + return CheckResult::ok(); + } + return null; + } + #[CheckItem('if bison version is 3.0 or later', limit_os: 'Darwin')] public function checkBisonVersion(array $command_path = []): ?CheckResult { diff --git a/src/StaticPHP/Toolchain/ClangBrewToolchain.php b/src/StaticPHP/Toolchain/ClangBrewToolchain.php new file mode 100644 index 00000000..5d8963ef --- /dev/null +++ b/src/StaticPHP/Toolchain/ClangBrewToolchain.php @@ -0,0 +1,21 @@ + ZigToolchain::class, 'Windows' => MSVCToolchain::class, - 'Darwin' => ClangNativeToolchain::class, + 'Darwin' => match (getenv('SPC_USE_LLVM') ?: 'system') { + 'brew' => ClangBrewToolchain::class, + default => ClangNativeToolchain::class, + }, default => throw new WrongUsageException('Unsupported OS family: ' . PHP_OS_FAMILY), }; } From dc79ac9c9a9efb5dac6963a3fe93497516c8065b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 20 Mar 2026 11:32:15 +0800 Subject: [PATCH 178/178] Correct doctor fix --- src/StaticPHP/Doctor/Item/MacOSToolCheck.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php index 9d51b83a..54d62e44 100644 --- a/src/StaticPHP/Doctor/Item/MacOSToolCheck.php +++ b/src/StaticPHP/Doctor/Item/MacOSToolCheck.php @@ -65,10 +65,10 @@ class MacOSToolCheck if (getenv('SPC_USE_LLVM') === 'brew') { $homebrew_prefix = getenv('HOMEBREW_PREFIX') ?: (SystemTarget::getTargetArch() === 'aarch64' ? '/opt/homebrew' : '/usr/local/homebrew'); - if (MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"]) === null) { - return CheckResult::fail('Homebrew llvm is not installed', 'brew', ['missing' => ['llvm']]); + if (($path = MacOSUtil::findCommand('clang', ["{$homebrew_prefix}/opt/llvm/bin"])) === null) { + return CheckResult::fail('Homebrew llvm is not installed', 'build-tools', ['missing' => ['llvm']]); } - return CheckResult::ok(); + return CheckResult::ok($path); } return null; }