From 9f80d6b2a82a82903c271673af3886df6a62c3ec Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 15:57:15 +0800 Subject: [PATCH 01/41] Add dev:gen-ext-test-matrix command --- .../Command/Dev/GenExtTestMatrixCommand.php | 184 ++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 2 files changed, 186 insertions(+) create mode 100644 src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php new file mode 100644 index 000000000..2587df7d5 --- /dev/null +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -0,0 +1,184 @@ + ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], + 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-2025', 'os_key' => 'Windows'], + ]; + + protected bool $no_motd = true; + + public function handle(): int + { + if (!spc_mode(SPC_MODE_SOURCE)) { + $this->output->writeln('This command is only available in source mode.'); + return static::USER_ERROR; + } + + $all = PackageConfig::getAll(); + + // Separate into regular and virtual extensions (build-static:false excluded globally) + $all_regular = []; + $all_virtual = []; + foreach ($all as $pkg_name => $config) { + if (($config['type'] ?? '') !== 'php-extension') { + continue; + } + if (($config['php-extension']['build-static'] ?? null) === false) { + continue; + } + if (($config['php-extension']['arg-type'] ?? '') === 'none') { + $all_virtual[$pkg_name] = $config; + } else { + $all_regular[$pkg_name] = $config; + } + } + + $entries = []; + + foreach (self::OS_RUNNERS as $os => $os_info) { + $os_key = $os_info['os_key']; + + // Filter by OS support + $os_regular = array_filter($all_regular, fn ($c) => $this->supportsOS($c, $os_key)); + $os_virtual = array_filter($all_virtual, fn ($c) => $this->supportsOS($c, $os_key)); + + // Pool: all ext-* names available on this OS (regular + virtual) + $pool_set = array_fill_keys( + array_merge(array_keys($os_regular), array_keys($os_virtual)), + true + ); + + // Compute ext_deps for every pool member: union of depends + suggests, limited to pool + $ext_deps = []; + foreach (array_merge($os_regular, $os_virtual) as $pkg_name => $config) { + $raw = array_merge( + $this->resolvePlatformList($config, 'depends', $os), + $this->resolvePlatformList($config, 'suggests', $os), + ); + $ext_deps[$pkg_name] = array_values(array_filter( + $raw, + fn ($d) => isset($pool_set[$d]) && $d !== $pkg_name + )); + } + + // Which regular exts are reachable as a dep/suggest from another regular ext? + $depended_on = []; + foreach ($os_regular as $pkg_name => $_) { + foreach ($ext_deps[$pkg_name] as $dep) { + $depended_on[$dep] = true; + } + } + + // Process order: roots (not depended on) first, then non-roots; each group alpha-sorted + $roots = []; + $non_roots = []; + foreach (array_keys($os_regular) as $pkg_name) { + if (isset($depended_on[$pkg_name])) { + $non_roots[] = $pkg_name; + } else { + $roots[] = $pkg_name; + } + } + sort($roots); + sort($non_roots); + + // DFS to collect dependency chains; true orphans (no ext-* relations) are batched + $covered = []; + $groups = []; + $orphans = []; + + foreach (array_merge($roots, $non_roots) as $ext) { + if (isset($covered[$ext])) { + continue; + } + $chain = $this->dfsCollect($ext, $ext_deps, $pool_set, $covered); + if (count($chain) === 1 && empty($ext_deps[$ext])) { + $orphans[] = $this->displayName($ext); + } else { + $groups[] = implode(',', array_map($this->displayName(...), $chain)); + } + } + + // All orphans become a single batched matrix entry + if (!empty($orphans)) { + sort($orphans); + $groups[] = implode(',', $orphans); + } + + sort($groups); + foreach ($groups as $group) { + $entries[] = [ + 'runner' => $os_info['runner'], + 'os' => $os, + 'arch' => $os_info['arch'], + 'extension' => $group, + 'build-args' => '"' . $group . '" ' . self::BUILD_TARGETS, + ]; + } + } + + $this->output->write(json_encode($entries, JSON_UNESCAPED_SLASHES)); + return static::SUCCESS; + } + + /** + * DFS-collect the dependency chain starting from $ext. + * Marks all visited nodes in $covered to prevent duplicates and handle cycles. + */ + private function dfsCollect(string $ext, array $ext_deps, array $pool_set, array &$covered): array + { + if (isset($covered[$ext])) { + return []; + } + $covered[$ext] = true; + $chain = [$ext]; + foreach ($ext_deps[$ext] ?? [] as $dep) { + if (!isset($covered[$dep]) && isset($pool_set[$dep])) { + $chain = array_merge($chain, $this->dfsCollect($dep, $ext_deps, $pool_set, $covered)); + } + } + return $chain; + } + + private function supportsOS(array $config, string $os_key): bool + { + $os_list = $config['php-extension']['os'] ?? null; + return $os_list === null || in_array($os_key, $os_list, true); + } + + private function displayName(string $pkg_name): string + { + return str_starts_with($pkg_name, 'ext-') ? substr($pkg_name, 4) : $pkg_name; + } + + /** + * Resolve the value of a platform-specific array field, applying the suffix fallback chain. + * + * Fallback rules (same as PackageConfig::get): + * linux : @linux → @unix → (base) + * macos : @macos → @unix → (base) + * windows : @windows → (base) + */ + private function resolvePlatformList(array $config, string $field, string $platform): array + { + return match ($platform) { + 'linux' => $config["{$field}@linux"] ?? $config["{$field}@unix"] ?? $config[$field] ?? [], + 'macos' => $config["{$field}@macos"] ?? $config["{$field}@unix"] ?? $config[$field] ?? [], + 'windows' => $config["{$field}@windows"] ?? $config[$field] ?? [], + default => $config[$field] ?? [], + }; + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index 06234f769..cc10554e4 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -13,6 +13,7 @@ use StaticPHP\Command\Dev\EnvCommand; use StaticPHP\Command\Dev\GenDepsDataCommand; use StaticPHP\Command\Dev\GenExtDocsCommand; +use StaticPHP\Command\Dev\GenExtTestMatrixCommand; use StaticPHP\Command\Dev\IsInstalledCommand; use StaticPHP\Command\Dev\LintConfigCommand; use StaticPHP\Command\Dev\PackageInfoCommand; @@ -85,6 +86,7 @@ public function __construct() new PackageInfoCommand(), new GenExtDocsCommand(), new GenDepsDataCommand(), + new GenExtTestMatrixCommand(), ]); // add additional commands from registries From 4507c8feb4eb548fc308348e3e9b949208efaedd Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 16:47:34 +0800 Subject: [PATCH 02/41] Fix zig check on fresh installed spc --- src/StaticPHP/Package/PackageInstaller.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 750ddbaf0..9cd0a1067 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -350,7 +350,10 @@ public function isPackageInstalled(Package|string $package_name): bool } // Fallback: if the download cache is missing (e.g. download failed or cache was cleared), // still check whether the files are physically present in buildroot. - if ($package instanceof LibraryPackage) { + // Note: TargetPackage extends LibraryPackage, but target packages (e.g. zig) have no + // static-libs/headers configured, so isInstalled() would trivially return true for them. + // Only apply this fallback to pure library packages. + if ($package instanceof LibraryPackage && !($package instanceof TargetPackage)) { return $package->isInstalled(); } return false; From 6b6025d3f0dab388f628d29011cf134e4d35a955 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 17:41:12 +0800 Subject: [PATCH 03/41] Add parallel download --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index 2587df7d5..ffc94b527 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -11,7 +11,7 @@ #[AsCommand('dev:gen-ext-test-matrix', 'Generate GitHub Actions extension test matrix JSON', [], true)] class GenExtTestMatrixCommand extends BaseCommand { - private const BUILD_TARGETS = '--build-cli --build-cgi --build-micro'; + private const BUILD_TARGETS = '--build-cli --build-cgi --build-micro --dl-parallel=10'; private const OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], From a8a851659f7ef80a7d6920d47f3c277eaec580d4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 19:07:06 +0800 Subject: [PATCH 04/41] Fix memcache build on PHP 8.5 --- src/Package/Extension/memcache.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Package/Extension/memcache.php b/src/Package/Extension/memcache.php index a9c58b768..3fac40228 100644 --- a/src/Package/Extension/memcache.php +++ b/src/Package/Extension/memcache.php @@ -17,6 +17,20 @@ class memcache extends PhpExtensionPackage #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-memcache')] public function patchBeforeBuildconf(): bool { + // PHP 8.5 moved php_smart_string*.h from ext/standard/ to Zend/ + foreach (['src/memcache_pool.h', 'src/memcache_pool.c', 'src/memcache_session.c', 'src/memcache_ascii_protocol.c', 'src/memcache_binary_protocol.c'] as $file) { + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/{$file}", + '#include "ext/standard/php_smart_string_public.h"', + '#include "Zend/zend_smart_string_public.h"', + ); + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/{$file}", + '#include "ext/standard/php_smart_string.h"', + '#include "Zend/zend_smart_string.h"', + ); + } + if (!$this->isBuildStatic()) { return false; } From a980b0a1dff329876a7c99fffdb681807ac07a30 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 19:07:39 +0800 Subject: [PATCH 05/41] Use -i instead of -I (-I used by hardcoded INI) --- src/StaticPHP/Artifact/DownloaderOptions.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Artifact/DownloaderOptions.php b/src/StaticPHP/Artifact/DownloaderOptions.php index de2e91bf9..3e00d3b36 100644 --- a/src/StaticPHP/Artifact/DownloaderOptions.php +++ b/src/StaticPHP/Artifact/DownloaderOptions.php @@ -48,6 +48,7 @@ public static function getConsoleOptions(string $prefix = ''): array $shortU = $prefix ? null : 'U'; $shortG = $prefix ? null : 'G'; $shortL = $prefix ? null : 'L'; + $shortI = $prefix ? null : 'i'; return [ // php version option @@ -62,7 +63,7 @@ public static function getConsoleOptions(string $prefix = ''): array // download behavior options new InputOption("{$p}parallel", $shortP, InputOption::VALUE_REQUIRED, 'Number of parallel downloads (default 1)', '1'), new InputOption("{$p}retry", $shortR, InputOption::VALUE_REQUIRED, 'Number of download retries on failure (default 0)', '0'), - new InputOption("{$p}ignore-cache", null, InputOption::VALUE_OPTIONAL, 'Ignore some caches when downloading, comma separated, e.g "php-src,curl,openssl"', false), + new InputOption("{$p}ignore-cache", $shortI, InputOption::VALUE_OPTIONAL, 'Ignore some caches when downloading, comma separated, e.g "php-src,curl,openssl"', false), new InputOption("{$p}no-alt", null, null, 'Do not use alternative mirror download artifacts for sources'), new InputOption("{$p}no-shallow-clone", null, null, 'Do not clone shallowly repositories when downloading sources'), From 143ae4b8c44c0c5e8d3a22c9ef42cbb56dcbfba0 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 19:08:00 +0800 Subject: [PATCH 06/41] Use -i instead of -I (-I used by hardcoded INI) --- docs/en/guide/cli-reference.md | 2 +- docs/zh/guide/cli-reference.md | 24 ++++++++++++------------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/en/guide/cli-reference.md b/docs/en/guide/cli-reference.md index a02c6ec53..5a162f97a 100644 --- a/docs/en/guide/cli-reference.md +++ b/docs/en/guide/cli-reference.md @@ -36,7 +36,7 @@ spc download [artifacts] [options] | `--binary-only` | | Only download binary artifacts | | `--parallel=` | `-P` | Number of parallel downloads (default: `1`) | | `--retry=` | `-R` | Number of retries on failure (default: `0`) | -| `--ignore-cache=` | | Force re-download the specified artifacts | +| `--ignore-cache=` | `-i` | Force re-download the specified artifacts | | `--no-alt` | | Do not use alternative mirror URLs | | `--no-shallow-clone` | | Do not clone git repositories shallowly | | `--custom-url=` | `-U` | Override the download URL for a source | diff --git a/docs/zh/guide/cli-reference.md b/docs/zh/guide/cli-reference.md index 961e0affa..7c0d5d33a 100644 --- a/docs/zh/guide/cli-reference.md +++ b/docs/zh/guide/cli-reference.md @@ -22,23 +22,23 @@ spc download [artifacts] [options] ### 选项 -| 选项 | 缩写 | 说明 | -|---|---|---| +| 选项 | 缩写 | 说明 | +|---|------|---| | `--for-extensions=` | `-e` | 按扩展名下载其所需的制品 | | `--for-libs=` | `-l` | 按库名下载其所需的制品 | -| `--for-packages=` | | 按包名下载其所需的制品 | -| `--without-suggests` | | 使用 `--for-extensions` 时跳过建议包 | -| `--clean` | | 下载前删除旧的下载缓存 | -| `--with-php=` | | PHP 版本,格式为 `major.minor`(默认 `8.4`)| +| `--for-packages=` | | 按包名下载其所需的制品 | +| `--without-suggests` | | 使用 `--for-extensions` 时跳过建议包 | +| `--clean` | | 下载前删除旧的下载缓存 | +| `--with-php=` | | PHP 版本,格式为 `major.minor`(默认 `8.4`)| | `--prefer-binary` | `-p` | 优先使用预编译二进制 | -| `--prefer-source` | | 优先使用源码包 | -| `--source-only` | | 仅下载源码制品 | -| `--binary-only` | | 仅下载二进制制品 | +| `--prefer-source` | | 优先使用源码包 | +| `--source-only` | | 仅下载源码制品 | +| `--binary-only` | | 仅下载二进制制品 | | `--parallel=` | `-P` | 并行下载数(默认 `1`)| | `--retry=` | `-R` | 失败重试次数(默认 `0`)| -| `--ignore-cache=` | | 强制重新下载指定制品 | -| `--no-alt` | | 不使用镜像站 | -| `--no-shallow-clone` | | 不使用浅层克隆 | +| `--ignore-cache=` | `-i` | 强制重新下载指定制品 | +| `--no-alt` | | 不使用镜像站 | +| `--no-shallow-clone` | | 不使用浅层克隆 | | `--custom-url=` | `-U` | 覆盖指定源的下载地址 | | `--custom-git=` | `-G` | 覆盖为自定义 git 仓库 | | `--custom-local=` | `-L` | 使用本地路径作为制品来源 | From a19c4470bb20e86fbb52c716d565122c8461eb21 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 19:11:31 +0800 Subject: [PATCH 07/41] Fix frameworks in unix cmake executor wrongly used by linux --- src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index ecf17693c..82fa468ae 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -230,7 +230,7 @@ private function getDefaultCMakeArgs(): array // EXE linker flags: base system libs + framework flags for target packages $exeLinkerFlags = SystemTarget::getRuntimeLibs(); - if ($this->package instanceof TargetPackage) { + if ($this->package instanceof TargetPackage && SystemTarget::getTargetOS() === 'Darwin') { $resolvedNames = array_keys($this->installer->getResolvedPackages()); $resolvedNames[] = $this->package->getName(); $fwFlags = new SPCConfigUtil()->getFrameworksString($resolvedNames); From 641ad8becff995cc73c6fbabd4822a61c800401d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 19:27:11 +0800 Subject: [PATCH 08/41] Add patch for PHP 8.2 compatibility in imap extension --- src/Package/Extension/imap.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Package/Extension/imap.php b/src/Package/Extension/imap.php index e9879b48b..df53cfe70 100644 --- a/src/Package/Extension/imap.php +++ b/src/Package/Extension/imap.php @@ -9,6 +9,7 @@ use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\Package\Validate; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -26,6 +27,19 @@ public function validate(PackageBuilder $builder): void } } + #[BeforeStage('php', [php::class, 'makeCliForUnix'], 'ext-imap')] + #[PatchDescription('Fix imap zend_zval_value_name() call for PHP 8.2 compatibility')] + public function patchBeforeMake(): void + { + // zend_zval_value_name() was introduced in PHP 8.3; PHP 8.2 imap backported the call but not the declaration + // replace with the equivalent PHP 8.2-compatible function + FileSystem::replaceFileStr( + "{$this->getSourceDir()}/php_imap.c", + 'zend_zval_value_name(data)', + 'zend_zval_type_name(data)' + ); + } + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-imap')] public function patchBeforeBuildconf(PackageInstaller $installer): void { From 5529e66a619fea8d074b1328ff4a389f2b018bf7 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 19:33:19 +0800 Subject: [PATCH 09/41] Add dom deps for xmlreader --- config/pkg/ext/builtin-extensions.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 52478e2c3..750745cba 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -348,6 +348,7 @@ ext-xmlreader: type: php-extension depends: - ext-xml + - ext-dom php-extension: arg-type: enable build-with-php: true From be039802c027ce8c7d3a1ef042b1672fd02a0dbe Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 20:54:41 +0800 Subject: [PATCH 10/41] Add exclude for gen-ext-test-matrix --- .../Command/Dev/GenExtTestMatrixCommand.php | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index ffc94b527..0f938ef72 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -11,13 +11,20 @@ #[AsCommand('dev:gen-ext-test-matrix', 'Generate GitHub Actions extension test matrix JSON', [], true)] class GenExtTestMatrixCommand extends BaseCommand { - private const BUILD_TARGETS = '--build-cli --build-cgi --build-micro --dl-parallel=10'; + private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro --dl-parallel=10'; - private const OS_RUNNERS = [ + private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-2025', 'os_key' => 'Windows'], ]; + /** + * Extensions excluded from specific OS matrix entries. + */ + private const array OS_EXCLUDE = [ + 'linux' => ['ext-glfw'], + ]; + protected bool $no_motd = true; public function handle(): int @@ -52,8 +59,9 @@ public function handle(): int $os_key = $os_info['os_key']; // Filter by OS support - $os_regular = array_filter($all_regular, fn ($c) => $this->supportsOS($c, $os_key)); - $os_virtual = array_filter($all_virtual, fn ($c) => $this->supportsOS($c, $os_key)); + $os_exclude = array_fill_keys(self::OS_EXCLUDE[$os] ?? [], true); + $os_regular = array_filter($all_regular, fn ($c, $k) => $this->supportsOS($c, $os_key) && !isset($os_exclude[$k]), ARRAY_FILTER_USE_BOTH); + $os_virtual = array_filter($all_virtual, fn ($c, $k) => $this->supportsOS($c, $os_key) && !isset($os_exclude[$k]), ARRAY_FILTER_USE_BOTH); // Pool: all ext-* names available on this OS (regular + virtual) $pool_set = array_fill_keys( From 1d1f58f3a1e47d226f76cf7c83d9d4879dab0fd9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 20:55:23 +0800 Subject: [PATCH 11/41] Disable parallel downloading --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index 0f938ef72..3741b2aa2 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -11,7 +11,7 @@ #[AsCommand('dev:gen-ext-test-matrix', 'Generate GitHub Actions extension test matrix JSON', [], true)] class GenExtTestMatrixCommand extends BaseCommand { - private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro --dl-parallel=10'; + private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro'; private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], From 5e40982e856268de48d3c340f3822cdfc46221c3 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 20:58:33 +0800 Subject: [PATCH 12/41] Use windows-latest --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index 3741b2aa2..1cf19a087 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -15,7 +15,7 @@ class GenExtTestMatrixCommand extends BaseCommand private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], - 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-2025', 'os_key' => 'Windows'], + 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-latest', 'os_key' => 'Windows'], ]; /** From 37b5f89ee146c28a584691adf210ebd54de39248 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 21:07:49 +0800 Subject: [PATCH 13/41] Add extra build flags for specific extensions --- .../Command/Dev/GenExtTestMatrixCommand.php | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index 1cf19a087..bbae70c83 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -22,7 +22,15 @@ class GenExtTestMatrixCommand extends BaseCommand * Extensions excluded from specific OS matrix entries. */ private const array OS_EXCLUDE = [ - 'linux' => ['ext-glfw'], + 'linux' => ['glfw'], + ]; + + /** + * Extra build flags appended when a matrix entry contains any of the listed extensions. + * Key: extension display name (without ext- prefix). Value: extra flags string. + */ + private const array EXTRA_BUILD_FLAGS = [ + 'parallel' => '--enable-zts', ]; protected bool $no_motd = true; @@ -59,7 +67,7 @@ public function handle(): int $os_key = $os_info['os_key']; // Filter by OS support - $os_exclude = array_fill_keys(self::OS_EXCLUDE[$os] ?? [], true); + $os_exclude = array_fill_keys(array_map(fn ($n) => 'ext-' . $n, self::OS_EXCLUDE[$os] ?? []), true); $os_regular = array_filter($all_regular, fn ($c, $k) => $this->supportsOS($c, $os_key) && !isset($os_exclude[$k]), ARRAY_FILTER_USE_BOTH); $os_virtual = array_filter($all_virtual, fn ($c, $k) => $this->supportsOS($c, $os_key) && !isset($os_exclude[$k]), ARRAY_FILTER_USE_BOTH); @@ -128,12 +136,13 @@ public function handle(): int sort($groups); foreach ($groups as $group) { + $extra = $this->extraBuildFlags($group); $entries[] = [ 'runner' => $os_info['runner'], 'os' => $os, 'arch' => $os_info['arch'], 'extension' => $group, - 'build-args' => '"' . $group . '" ' . self::BUILD_TARGETS, + 'build-args' => '"' . $group . '" ' . self::BUILD_TARGETS . ($extra !== '' ? ' ' . $extra : ''), ]; } } @@ -172,6 +181,22 @@ private function displayName(string $pkg_name): string return str_starts_with($pkg_name, 'ext-') ? substr($pkg_name, 4) : $pkg_name; } + /** + * Returns any extra build flags required for an extension group string. + * Checks whether any extension in the comma-separated group matches EXTRA_BUILD_FLAGS. + */ + private function extraBuildFlags(string $group): string + { + $names = explode(',', $group); + $flags = []; + foreach (self::EXTRA_BUILD_FLAGS as $ext => $extra) { + if (in_array($ext, $names, true)) { + $flags[] = $extra; + } + } + return implode(' ', $flags); + } + /** * Resolve the value of a platform-specific array field, applying the suffix fallback chain. * From 4695f846f5c48b1e93dc4c1ed4c8b5c139f22ab9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 21:23:29 +0800 Subject: [PATCH 14/41] Add macOS test --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index bbae70c83..f336bc8cf 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -16,6 +16,7 @@ class GenExtTestMatrixCommand extends BaseCommand private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-latest', 'os_key' => 'Windows'], + 'macos' => ['arch' => 'aarch64', 'runner' => 'macos-15', 'os_key' => 'macos'], ]; /** From dcf1c5942cbd96359e8a727af2cadac558299aa9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:12:06 +0800 Subject: [PATCH 15/41] Add macOS test, conflicts --- .../Command/Dev/GenExtTestMatrixCommand.php | 56 ++++++++++++++++++- 1 file changed, 53 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index f336bc8cf..f133793be 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -16,7 +16,7 @@ class GenExtTestMatrixCommand extends BaseCommand private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], 'windows' => ['arch' => 'x86_64', 'runner' => 'windows-latest', 'os_key' => 'Windows'], - 'macos' => ['arch' => 'aarch64', 'runner' => 'macos-15', 'os_key' => 'macos'], + 'macos' => ['arch' => 'aarch64', 'runner' => 'macos-15', 'os_key' => 'Darwin'], ]; /** @@ -34,6 +34,14 @@ class GenExtTestMatrixCommand extends BaseCommand 'parallel' => '--enable-zts', ]; + /** + * Pairs of extensions that cannot be built together in the same matrix entry. + */ + private const array CONFLICTS = [ + ['grpc', 'protobuf'], + ['swow', 'swoole'], + ]; + protected bool $no_motd = true; public function handle(): int @@ -129,10 +137,12 @@ public function handle(): int } } - // All orphans become a single batched matrix entry + // Batch orphans, splitting conflicting extensions into separate entries if (!empty($orphans)) { sort($orphans); - $groups[] = implode(',', $orphans); + foreach ($this->splitOrphansByConflicts($orphans) as $batch) { + $groups[] = implode(',', $batch); + } } sort($groups); @@ -182,6 +192,46 @@ private function displayName(string $pkg_name): string return str_starts_with($pkg_name, 'ext-') ? substr($pkg_name, 4) : $pkg_name; } + /** + * Split orphans into batches such that no two conflicting extensions share a batch. + * Uses a greedy graph-coloring approach. + * + * @param string[] $orphans display names, pre-sorted + * @return string[][] array of batches, each batch is an array of display names + */ + private function splitOrphansByConflicts(array $orphans): array + { + $adjacency = []; + foreach (self::CONFLICTS as [$a, $b]) { + $adjacency[$a][$b] = true; + $adjacency[$b][$a] = true; + } + + $batches = []; + foreach ($orphans as $ext) { + $placed = false; + foreach ($batches as &$batch) { + $conflict = false; + foreach ($batch as $member) { + if (isset($adjacency[$ext][$member])) { + $conflict = true; + break; + } + } + if (!$conflict) { + $batch[] = $ext; + $placed = true; + break; + } + } + unset($batch); + if (!$placed) { + $batches[] = [$ext]; + } + } + return $batches; + } + /** * Returns any extra build flags required for an extension group string. * Checks whether any extension in the comma-separated group matches EXTRA_BUILD_FLAGS. From 73bf9ff93f72574ed4aac6794be4fcfca482f032 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:13:50 +0800 Subject: [PATCH 16/41] Add libzstd.lib for windows builds --- config/pkg/lib/zstd.yml | 1 + src/Package/Library/zstd.php | 1 + 2 files changed, 2 insertions(+) diff --git a/config/pkg/lib/zstd.yml b/config/pkg/lib/zstd.yml index c1d15cf6e..be0061617 100644 --- a/config/pkg/lib/zstd.yml +++ b/config/pkg/lib/zstd.yml @@ -19,3 +19,4 @@ zstd: - libzstd.a static-libs@windows: - zstd.lib + - libzstd.lib diff --git a/src/Package/Library/zstd.php b/src/Package/Library/zstd.php index 4b4a490f1..a5df737e5 100644 --- a/src/Package/Library/zstd.php +++ b/src/Package/Library/zstd.php @@ -26,6 +26,7 @@ public function buildWin(LibraryPackage $package): void ) ->build(); FileSystem::copy($package->getLibDir() . '\zstd_static.lib', $package->getLibDir() . '/zstd.lib'); + FileSystem::copy($package->getLibDir() . '\zstd_static.lib', $package->getLibDir() . '/libzstd.lib'); } #[BuildFor('Linux')] From fd4bf90a7095ace01b97d42c08d7484836499924 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:23:14 +0800 Subject: [PATCH 17/41] Add standalone list --- .../Command/Dev/GenExtTestMatrixCommand.php | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index f133793be..859ad3727 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -42,6 +42,14 @@ class GenExtTestMatrixCommand extends BaseCommand ['swow', 'swoole'], ]; + /** + * Extensions that must always appear alone in their own matrix entry. + * Use display names (without ext- prefix). + */ + private const array STANDALONE = [ + 'grpc', + ]; + protected bool $no_motd = true; public function handle(): int @@ -124,14 +132,19 @@ public function handle(): int $covered = []; $groups = []; $orphans = []; + $standalone_set = array_fill_keys(self::STANDALONE, true); foreach (array_merge($roots, $non_roots) as $ext) { if (isset($covered[$ext])) { continue; } $chain = $this->dfsCollect($ext, $ext_deps, $pool_set, $covered); - if (count($chain) === 1 && empty($ext_deps[$ext])) { - $orphans[] = $this->displayName($ext); + $display = $this->displayName($ext); + if (isset($standalone_set[$display])) { + // Always emit standalone extensions as their own single entry + $groups[] = $display; + } elseif (count($chain) === 1 && empty($ext_deps[$ext])) { + $orphans[] = $display; } else { $groups[] = implode(',', array_map($this->displayName(...), $chain)); } From 01607a06c99bc02e5944161411b699f1d3a22731 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:32:50 +0800 Subject: [PATCH 18/41] Add spx compatible command arg --- src/Package/Extension/zlib.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Package/Extension/zlib.php b/src/Package/Extension/zlib.php index 14ab656d7..d04c00a03 100644 --- a/src/Package/Extension/zlib.php +++ b/src/Package/Extension/zlib.php @@ -8,15 +8,16 @@ use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Package\PackageBuilder; +use StaticPHP\Package\PackageInstaller; #[Extension('zlib')] class zlib { #[CustomPhpConfigureArg('Darwin')] #[CustomPhpConfigureArg('Linux')] - public function unixConfigureArg(PackageBuilder $builder): string + public function unixConfigureArg(PackageBuilder $builder, PackageInstaller $installer): string { - $zlib_dir = php::getPHPVersionID() >= 80400 ? '' : ' --with-zlib-dir=' . $builder->getBuildRootPath(); - return '--with-zlib' . $zlib_dir; + $zlib_dir = (php::getPHPVersionID() >= 80400 && !$installer->getPhpExtensionPackage('spx')) ? '' : " --with-zlib-dir={$builder->getBuildRootPath()}"; + return "--with-zlib{$zlib_dir}"; } } From ee21199ebbfda6f6705eef8cea73c9a4ff72c53d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:33:06 +0800 Subject: [PATCH 19/41] Remove hosted binary temporarily --- config/artifact/ncurses.yml | 1 - config/pkg/lib/brotli.yml | 1 - config/pkg/lib/bzip2.yml | 1 - config/pkg/lib/libcares.yml | 1 - config/pkg/lib/libedit.yml | 1 - config/pkg/lib/libiconv.yml | 1 - config/pkg/lib/libpng.yml | 1 - config/pkg/lib/libsodium.yml | 1 - config/pkg/lib/libssh2.yml | 1 - config/pkg/lib/libunistring.yml | 1 - config/pkg/lib/openssl.yml | 1 - config/pkg/lib/xz.yml | 1 - config/pkg/lib/zlib.yml | 1 - 13 files changed, 13 deletions(-) diff --git a/config/artifact/ncurses.yml b/config/artifact/ncurses.yml index 52c8f59ff..173d4c326 100644 --- a/config/artifact/ncurses.yml +++ b/config/artifact/ncurses.yml @@ -1,5 +1,4 @@ ncurses: - binary: hosted metadata: license-files: - COPYING diff --git a/config/pkg/lib/brotli.yml b/config/pkg/lib/brotli.yml index c88b93b5a..19c7981d3 100644 --- a/config/pkg/lib/brotli.yml +++ b/config/pkg/lib/brotli.yml @@ -5,7 +5,6 @@ brotli: type: ghtagtar repo: google/brotli match: 'v1\.\d.*' - binary: hosted metadata: license-files: [LICENSE] license: MIT diff --git a/config/pkg/lib/bzip2.yml b/config/pkg/lib/bzip2.yml index f9e1870d8..e4e98363a 100644 --- a/config/pkg/lib/bzip2.yml +++ b/config/pkg/lib/bzip2.yml @@ -8,7 +8,6 @@ bzip2: type: filelist url: 'https://sourceware.org/pub/bzip2/' regex: '/href="(?bzip2-(?[^"]+)\.tar\.gz)"/' - binary: hosted metadata: license-files: ['@/bzip2.txt'] license: bzip2-1.0.6 diff --git a/config/pkg/lib/libcares.yml b/config/pkg/lib/libcares.yml index 5d3f8c9d1..a2eb9e80b 100644 --- a/config/pkg/lib/libcares.yml +++ b/config/pkg/lib/libcares.yml @@ -10,7 +10,6 @@ libcares: type: filelist url: 'https://c-ares.org/download/' regex: '/href="\/download\/(?c-ares-(?[^"]+)\.tar\.gz)"/' - binary: hosted metadata: license-files: [LICENSE.md] headers@unix: diff --git a/config/pkg/lib/libedit.yml b/config/pkg/lib/libedit.yml index 02d6dd810..0f1ed78b7 100644 --- a/config/pkg/lib/libedit.yml +++ b/config/pkg/lib/libedit.yml @@ -5,7 +5,6 @@ libedit: type: filelist url: 'https://thrysoee.dk/editline/' regex: '/href="(?libedit-(?[^"]+)\.tar\.gz)"/' - binary: hosted metadata: license-files: [COPYING] license: BSD-3-Clause diff --git a/config/pkg/lib/libiconv.yml b/config/pkg/lib/libiconv.yml index 7fc3bddad..58cd77b08 100644 --- a/config/pkg/lib/libiconv.yml +++ b/config/pkg/lib/libiconv.yml @@ -5,7 +5,6 @@ libiconv: type: filelist url: 'https://ftp.gnu.org/gnu/libiconv/' regex: '/href="(?libiconv-(?[^"]+)\.tar\.gz)"/' - binary: hosted metadata: license-files: [COPYING.LIB] license: LGPL-2.0-or-later diff --git a/config/pkg/lib/libpng.yml b/config/pkg/lib/libpng.yml index 083cf430a..a58bdfa5f 100644 --- a/config/pkg/lib/libpng.yml +++ b/config/pkg/lib/libpng.yml @@ -6,7 +6,6 @@ libpng: repo: pnggroup/libpng match: v1\.6\.\d+ query: '?per_page=150' - binary: hosted metadata: license-files: [LICENSE] license: PNG diff --git a/config/pkg/lib/libsodium.yml b/config/pkg/lib/libsodium.yml index 4bd41363b..9497381ba 100644 --- a/config/pkg/lib/libsodium.yml +++ b/config/pkg/lib/libsodium.yml @@ -6,7 +6,6 @@ libsodium: repo: jedisct1/libsodium match: 'libsodium-(?!1\.0\.21)\d+(\.\d+)*\.tar\.gz' prefer-stable: true - binary: hosted metadata: license-files: [LICENSE] pkg-configs: diff --git a/config/pkg/lib/libssh2.yml b/config/pkg/lib/libssh2.yml index 2916e3a9c..724474162 100644 --- a/config/pkg/lib/libssh2.yml +++ b/config/pkg/lib/libssh2.yml @@ -6,7 +6,6 @@ libssh2: repo: libssh2/libssh2 match: libssh2.+\.tar\.gz prefer-stable: true - binary: hosted metadata: license-files: [COPYING] license: BSD-3-Clause diff --git a/config/pkg/lib/libunistring.yml b/config/pkg/lib/libunistring.yml index 2b2ffd334..13a66d42e 100644 --- a/config/pkg/lib/libunistring.yml +++ b/config/pkg/lib/libunistring.yml @@ -5,7 +5,6 @@ libunistring: type: filelist url: 'https://ftp.gnu.org/gnu/libunistring/' regex: '/href="(?libunistring-(?[^"]+)\.tar\.gz)"/' - binary: hosted metadata: license-files: [COPYING.LIB] license: LGPL-3.0-or-later diff --git a/config/pkg/lib/openssl.yml b/config/pkg/lib/openssl.yml index f526725b6..cbbd0984c 100644 --- a/config/pkg/lib/openssl.yml +++ b/config/pkg/lib/openssl.yml @@ -10,7 +10,6 @@ openssl: type: filelist url: 'https://www.openssl.org/source/' regex: '/href="(?openssl-(?3\.[^"]+)\.tar\.gz)"/' - binary: hosted metadata: license-files: [LICENSE.txt] license: OpenSSL diff --git a/config/pkg/lib/xz.yml b/config/pkg/lib/xz.yml index 3be1815d8..04b931840 100644 --- a/config/pkg/lib/xz.yml +++ b/config/pkg/lib/xz.yml @@ -6,7 +6,6 @@ xz: repo: tukaani-project/xz match: xz.+\.tar\.xz prefer-stable: true - binary: hosted metadata: license-files: [COPYING] license: 0BSD diff --git a/config/pkg/lib/zlib.yml b/config/pkg/lib/zlib.yml index b4e71364e..3ab591805 100644 --- a/config/pkg/lib/zlib.yml +++ b/config/pkg/lib/zlib.yml @@ -5,7 +5,6 @@ zlib: type: ghrel repo: madler/zlib match: zlib.+\.tar\.gz - binary: hosted metadata: license-files: ['@/zlib.txt'] license: Zlib-Custom From c374163a0545c18b61a6cb244e00ed42bcf1d0b9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:37:05 +0800 Subject: [PATCH 20/41] Fix windows mpir build --- src/Package/Library/mpir.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Library/mpir.php b/src/Package/Library/mpir.php index 951d218ad..8063b7826 100644 --- a/src/Package/Library/mpir.php +++ b/src/Package/Library/mpir.php @@ -33,7 +33,7 @@ public function build(LibraryPackage $lib): void { $vs_ver_dir = ApplicationContext::get('mpir_vs_ver_dir'); cmd()->cd("{$lib->getSourceDir()}{$vs_ver_dir}\\lib_mpir_gc") - ->exec('msbuild lib_mpir_gc.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64'); + ->exec('msbuild lib_mpir_gc.vcxproj /t:Rebuild /p:Configuration=Release /p:Platform=x64 /p:WindowsTargetPlatformVersion=10.0'); FileSystem::createDir($lib->getLibDir()); FileSystem::createDir($lib->getIncludeDir()); FileSystem::copy("{$lib->getSourceDir()}{$vs_ver_dir}\\lib_mpir_gc\\x64\\Release\\mpir_a.lib", "{$lib->getLibDir()}\\mpir_a.lib"); From 61747f6f4e6560a93a118bc1401e559fbc63d1e4 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:51:17 +0800 Subject: [PATCH 21/41] Add frameworks for glfw --- config/pkg/lib/glfw.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/pkg/lib/glfw.yml b/config/pkg/lib/glfw.yml index f7d015492..7bd46e4a8 100644 --- a/config/pkg/lib/glfw.yml +++ b/config/pkg/lib/glfw.yml @@ -1,6 +1,12 @@ glfw: type: library artifact: glfw + frameworks: + - Cocoa + - CoreFoundation + - CoreVideo + - IOKit + - QuartzCore headers: - GLFW/glfw3.h - GLFW/glfw3native.h From 1b30c98fbdb6bf09e5d9de6c295c6b20c3c2507c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 22:51:49 +0800 Subject: [PATCH 22/41] Make glfw standalone though --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index 859ad3727..1271136e6 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -48,6 +48,7 @@ class GenExtTestMatrixCommand extends BaseCommand */ private const array STANDALONE = [ 'grpc', + 'glfw', ]; protected bool $no_motd = true; From e4201a28ca13ae182d0a44f259397bd0cfcf861b Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Fri, 8 May 2026 23:11:48 +0800 Subject: [PATCH 23/41] Add 'imagick' and 'intl' to standalone array --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index 1271136e6..d7f95e15b 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -49,6 +49,8 @@ class GenExtTestMatrixCommand extends BaseCommand private const array STANDALONE = [ 'grpc', 'glfw', + 'imagick', + 'intl', ]; protected bool $no_motd = true; From 04df87cd5fc299f9570216624c0187b390565668 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 08:15:45 +0800 Subject: [PATCH 24/41] Make intl use c++17 --- src/Package/Extension/intl.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Package/Extension/intl.php b/src/Package/Extension/intl.php index f5e17c1fe..dd7d3ffd9 100644 --- a/src/Package/Extension/intl.php +++ b/src/Package/Extension/intl.php @@ -16,7 +16,7 @@ class intl extends PhpExtensionPackage { #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-intl')] - #[PatchDescription('Fix intl config.w32: replace hardcoded true with PHP_INTL_SHARED for static build support')] + #[PatchDescription('Fix intl config.w32: replace hardcoded true with PHP_INTL_SHARED for static build support; add /std:c++17 required by ICU 73+')] public function patchBeforeBuildconfForWindows(PackageInstaller $installer): void { $php_src = $installer->getTargetPackage('php')->getSourceDir(); @@ -25,5 +25,11 @@ public function patchBeforeBuildconfForWindows(PackageInstaller $installer): voi 'EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", true,', 'EXTENSION("intl", "php_intl.c intl_convert.c intl_convertcpp.cpp intl_error.c ", PHP_INTL_SHARED,' ); + // ICU 73+ headers (char16ptr.h etc.) unconditionally include which requires C++17. + FileSystem::replaceFileStr( + "{$php_src}/ext/intl/config.w32", + 'ADD_FLAG("CFLAGS_INTL", "/EHsc', + 'ADD_FLAG("CFLAGS_INTL", "/std:c++17 /EHsc' + ); } } From 4f9a555bf3aeb69a086ff7d5d1353feda765acad Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 08:16:11 +0800 Subject: [PATCH 25/41] ds use git (pecl have buggy config.w32) --- config/pkg/ext/ext-ds.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/config/pkg/ext/ext-ds.yml b/config/pkg/ext/ext-ds.yml index 0c0a4b3c4..779582574 100644 --- a/config/pkg/ext/ext-ds.yml +++ b/config/pkg/ext/ext-ds.yml @@ -2,8 +2,10 @@ ext-ds: type: php-extension artifact: source: - type: pecl - name: ds + type: git + url: 'https://github.com/php-ds/ext-ds.git' + rev: master + extract: php-src/ext/ds metadata: license-files: [LICENSE] license: MIT From bf326de9854421c32c83ca59c12657a1fc1ef7ab Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 08:17:03 +0800 Subject: [PATCH 26/41] Fix extracting hosted type for zip archive caused overwriting --- src/StaticPHP/Artifact/ArtifactExtractor.php | 97 ++++++++++++++------ 1 file changed, 70 insertions(+), 27 deletions(-) diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 8b73243a4..987ec554e 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -242,7 +242,10 @@ protected function extractBinary(Artifact $artifact): int } logger()->info("Extracting binary [{$name}] to {$target_path}..."); - $this->doStandardExtract($name, $cache_info, $target_path); + // When a binary artifact targets the shared buildroot, merge into it instead of wiping it. + // Wiping buildroot would destroy files installed by packages processed earlier in the build queue. + $merge = (FileSystem::convertPath($target_path) === FileSystem::convertPath(BUILD_ROOT_PATH)); + $this->doStandardExtract($name, $cache_info, $target_path, $merge); $artifact->emitAfterBinaryExtract($target_path, $platform); logger()->debug("Emitted after-binary-extract hooks for [{$name}]"); @@ -256,8 +259,10 @@ protected function extractBinary(Artifact $artifact): int /** * Standard extraction: extract entire archive to target directory. + * + * @param bool $merge when true, merge extracted files into existing target dir instead of wiping it */ - protected function doStandardExtract(string $name, array $cache_info, string $target_path): void + protected function doStandardExtract(string $name, array $cache_info, string $target_path, bool $merge = false): void { $source_file = $this->cache->getCacheFullPath($cache_info); $cache_type = $cache_info['cache_type']; @@ -265,7 +270,7 @@ protected function doStandardExtract(string $name, array $cache_info, string $ta // Validate source file exists before extraction $this->validateSourceFile($name, $source_file, $cache_type); - $this->extractWithType($cache_type, $source_file, $target_path); + $this->extractWithType($cache_type, $source_file, $target_path, $merge); } /** @@ -443,10 +448,10 @@ protected function copyFileOrDir(string $src, string $dst): void * @param string $source_file Path to source file or directory * @param string $target_path Target extraction path */ - protected function extractWithType(string $cache_type, string $source_file, string $target_path): void + protected function extractWithType(string $cache_type, string $source_file, string $target_path, bool $merge = false): void { match ($cache_type) { - 'archive' => $this->extractArchive($source_file, $target_path), + 'archive' => $this->extractArchive($source_file, $target_path, $merge), 'file' => $this->copyFile($source_file, $target_path), 'git' => FileSystem::copyDir(FileSystem::convertPath($source_file), $target_path), 'local' => symlink(FileSystem::convertPath($source_file), $target_path), @@ -458,8 +463,10 @@ protected function extractWithType(string $cache_type, string $source_file, stri * Extract archive file to target directory. * * Supports: tar, tar.gz, tgz, tar.bz2, tar.xz, txz, zip, exe + * + * @param bool $merge when true, merge zip contents into existing target dir instead of wiping it */ - protected function extractArchive(string $filename, string $target): void + protected function extractArchive(string $filename, string $target, bool $merge = false): void { $target = FileSystem::convertPath($target); $filename = FileSystem::convertPath($filename); @@ -476,7 +483,7 @@ protected function extractArchive(string $filename, string $target): void 'Windows' => match ($extname) { 'tar' => default_shell()->executeTarExtract($filename, $target, 'none'), 'xz', 'txz', 'gz', 'tgz', 'bz2' => default_shell()->execute7zExtract($filename, $target), - 'zip' => $this->unzipWithStrip($filename, $target), + 'zip' => $this->unzipWithStrip($filename, $target, $merge), 'exe' => $this->copyFile($filename, $target), default => throw new FileSystemException("Unknown archive format: {$filename}"), }, @@ -485,7 +492,7 @@ protected function extractArchive(string $filename, string $target): void 'gz', 'tgz' => default_shell()->executeTarExtract($filename, $target, 'gz'), 'bz2' => default_shell()->executeTarExtract($filename, $target, 'bz2'), 'xz', 'txz' => default_shell()->executeTarExtract($filename, $target, 'xz'), - 'zip' => $this->unzipWithStrip($filename, $target), + 'zip' => $this->unzipWithStrip($filename, $target, $merge), 'exe' => $this->copyFile($filename, $target), default => throw new FileSystemException("Unknown archive format: {$filename}"), }, @@ -496,7 +503,7 @@ protected function extractArchive(string $filename, string $target): void /** * Unzip file with stripping top-level directory. */ - protected function unzipWithStrip(string $zip_file, string $extract_path): bool + protected function unzipWithStrip(string $zip_file, string $extract_path, bool $merge = false): bool { $temp_dir = FileSystem::convertPath(sys_get_temp_dir() . '/spc_unzip_' . bin2hex(random_bytes(16))); $zip_file = FileSystem::convertPath($zip_file); @@ -517,15 +524,22 @@ protected function unzipWithStrip(string $zip_file, string $extract_path): bool throw new FileSystemException('Cannot scan unzip temp dir: ' . $temp_dir); } - // If extract path already exists, remove it - if (is_dir($extract_path)) { - FileSystem::removeDir($extract_path); + if (!$merge) { + // Replace mode: wipe the target directory before extracting + if (is_dir($extract_path)) { + FileSystem::removeDir($extract_path); + } } - // If only one dir, move its contents to extract_path + // If only one dir, move/merge its contents to extract_path $subdir = FileSystem::convertPath("{$temp_dir}/{$contents[0]}"); if (count($contents) === 1 && is_dir($subdir)) { - $this->moveFileOrDir($subdir, $extract_path); + if ($merge) { + $this->mergeDirContent($subdir, $extract_path); + FileSystem::removeDir($subdir); + } else { + $this->moveFileOrDir($subdir, $extract_path); + } } else { // Else, if it contains only one dir, strip dir and copy other files $dircount = 0; @@ -550,26 +564,36 @@ protected function unzipWithStrip(string $zip_file, string $extract_path): bool throw new FileSystemException("Cannot scan unzip temp sub-dir: {$dir[0]}"); } foreach ($sub_contents as $sub_item) { - $this->moveFileOrDir( - FileSystem::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"), - FileSystem::convertPath("{$extract_path}/{$sub_item}") - ); + $src = FileSystem::convertPath("{$temp_dir}/{$dir[0]}/{$sub_item}"); + $dst = FileSystem::convertPath("{$extract_path}/{$sub_item}"); + if ($merge && is_dir($src)) { + $this->mergeDirContent($src, $dst); + } else { + $this->moveFileOrDir($src, $dst); + } } } else { foreach ($dir as $item) { - $this->moveFileOrDir( - FileSystem::convertPath("{$temp_dir}/{$item}"), - FileSystem::convertPath("{$extract_path}/{$item}") - ); + $src = FileSystem::convertPath("{$temp_dir}/{$item}"); + $dst = FileSystem::convertPath("{$extract_path}/{$item}"); + if ($merge) { + $this->mergeDirContent($src, $dst); + } else { + $this->moveFileOrDir($src, $dst); + } } } - // Move top-level files to extract_path + // Move or copy top-level files to extract_path foreach ($top_files as $top_file) { - $this->moveFileOrDir( - FileSystem::convertPath("{$temp_dir}/{$top_file}"), - FileSystem::convertPath("{$extract_path}/{$top_file}") - ); + $src = FileSystem::convertPath("{$temp_dir}/{$top_file}"); + $dst = FileSystem::convertPath("{$extract_path}/{$top_file}"); + if ($merge) { + FileSystem::createDir(dirname($dst)); + copy($src, $dst); + } else { + $this->moveFileOrDir($src, $dst); + } } } @@ -595,6 +619,25 @@ protected function replacePathVariables(string $path): string return str_replace(array_keys($replacement), array_values($replacement), $path); } + private function mergeDirContent(string $src_dir, string $dest_dir): void + { + FileSystem::createDir($dest_dir); + $items = FileSystem::scanDirFiles($src_dir, false, true, true); + if ($items === false || empty($items)) { + return; + } + foreach ($items as $item) { + $src_item = FileSystem::convertPath("{$src_dir}/{$item}"); + $dest_item = FileSystem::convertPath("{$dest_dir}/{$item}"); + if (is_dir($src_item)) { + $this->mergeDirContent($src_item, $dest_item); + } else { + FileSystem::createDir(dirname($dest_item)); + copy($src_item, $dest_item); + } + } + } + /** * Move file or directory, handling cross-device scenarios * Uses rename() if possible, falls back to copy+delete for cross-device moves From bf308e89a59cca7dbf7608c2205f99a0749945d9 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 09:49:53 +0800 Subject: [PATCH 27/41] Remove duplicate phar patch for micro --- src/Package/Target/php/unix.php | 59 ++++++++++++------------------ src/Package/Target/php/windows.php | 17 +-------- 2 files changed, 26 insertions(+), 50 deletions(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index e1052fbdc..bd2871148 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -266,43 +266,32 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { - $phar_patched = false; - try { - if ($installer->isPackageResolved('ext-phar')) { - $phar_patched = true; - SourcePatcher::patchMicroPhar(self::getPHPVersionID()); - } - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); - // apply --with-micro-fake-cli option - $vars = $this->makeVars($installer); - $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - - $dst = BUILD_BIN_PATH . '/micro.sfx'; - $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); - // patch after UPX-ed micro.sfx (Linux only) - if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { - // cut binary with readelf to remove UPX extra segment - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); - } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); - } - $package->setOutput('Binary path for micro SAPI', $dst); - } finally { - if ($phar_patched) { - SourcePatcher::unpatchMicroPhar(); + 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 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); } #[Stage] diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 311db0333..d2f863327 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -293,21 +293,8 @@ public function makeMicroForWindows(TargetPackage $package, PackageBuilder $buil $fake_cli = $package->getBuildOption('with-micro-fake-cli', false) ? ' /DPHP_MICRO_FAKE_CLI' : ''; - // phar patch for micro - $phar_patched = false; - if ($installer->isPackageResolved('ext-phar')) { - $phar_patched = true; - SourcePatcher::patchMicroPhar(self::getPHPVersionID()); - } - - try { - cmd()->cd($package->getSourceDir()) - ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); - } finally { - if ($phar_patched) { - SourcePatcher::unpatchMicroPhar(); - } - } + cmd()->cd($package->getSourceDir()) + ->exec("nmake /nologo {$debug_overrides}LIBS_MICRO=\"ws2_32.lib shell32.lib {$extra_libs}\" CFLAGS_MICRO=\"/DZEND_ENABLE_STATIC_TSRMLS_CACHE=1{$fake_cli}\" EXTRA_LD_FLAGS_PROGRAM= micro"); $this->deployWindowsBinary($builder, $package, 'php-micro'); } From b09cc968e6f879222a2fb973c298147c00529269 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 10:19:16 +0800 Subject: [PATCH 28/41] Add curl execute output log --- src/StaticPHP/Runtime/Shell/DefaultShell.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 8f77f62d7..6c32b1554 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -44,6 +44,7 @@ public function executeCurl(string $url, string $method = 'GET', array $headers $cmd = SPC_CURL_EXEC . " -sfSL --max-time 3600 {$retry_arg} {$compressed_arg} {$method_arg} {$header_arg} {$url_arg}"; $this->logCommandInfo($cmd); + logger()->debug("[CURL EXECUTE] {$cmd}"); $result = $this->passthru($cmd, capture_output: true, throw_on_error: false); $ret = $result['code']; $output = $result['output']; From cd803c75c5be9b802267ba4bf3dad68cc7edb482 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 10:44:04 +0800 Subject: [PATCH 29/41] Add filtering options for extensions, libs, and OS in test matrix generation --- .../Command/Dev/GenExtTestMatrixCommand.php | 54 ++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index d7f95e15b..ac0a61012 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -7,6 +7,7 @@ use StaticPHP\Command\BaseCommand; use StaticPHP\Config\PackageConfig; use Symfony\Component\Console\Attribute\AsCommand; +use Symfony\Component\Console\Input\InputOption; #[AsCommand('dev:gen-ext-test-matrix', 'Generate GitHub Actions extension test matrix JSON', [], true)] class GenExtTestMatrixCommand extends BaseCommand @@ -53,8 +54,20 @@ class GenExtTestMatrixCommand extends BaseCommand 'intl', ]; + /** + * Maximum number of orphan extensions per matrix entry. + */ + private const int ORPHAN_BATCH_SIZE = 15; + protected bool $no_motd = true; + public function configure(): void + { + $this->addOption('for-extensions', null, InputOption::VALUE_OPTIONAL, 'Filter by extension display names, comma-separated', '') + ->addOption('for-libs', null, InputOption::VALUE_OPTIONAL, 'Filter by lib names (depends+suggests), comma-separated', '') + ->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Filter by OS (Linux/Darwin/Windows), comma-separated', ''); + } + public function handle(): int { if (!spc_mode(SPC_MODE_SOURCE)) { @@ -62,6 +75,11 @@ public function handle(): int return static::USER_ERROR; } + $parse_option = fn (string $name): array => array_values(array_filter(array_map('trim', explode(',', (string) $this->input->getOption($name))))); + $filter_extensions = $parse_option('for-extensions'); + $filter_libs = $parse_option('for-libs'); + $filter_os_keys = $parse_option('os'); + $all = PackageConfig::getAll(); // Separate into regular and virtual extensions (build-static:false excluded globally) @@ -81,9 +99,14 @@ public function handle(): int } } + $os_runners = empty($filter_os_keys) + ? self::OS_RUNNERS + : array_filter(self::OS_RUNNERS, fn ($info) => in_array($info['os_key'], $filter_os_keys, true)); + $entries = []; + $all_ext_lib_deps = []; - foreach (self::OS_RUNNERS as $os => $os_info) { + foreach ($os_runners as $os => $os_info) { $os_key = $os_info['os_key']; // Filter by OS support @@ -99,6 +122,7 @@ public function handle(): int // Compute ext_deps for every pool member: union of depends + suggests, limited to pool $ext_deps = []; + $os_lib_deps = []; foreach (array_merge($os_regular, $os_virtual) as $pkg_name => $config) { $raw = array_merge( $this->resolvePlatformList($config, 'depends', $os), @@ -108,7 +132,12 @@ public function handle(): int $raw, fn ($d) => isset($pool_set[$d]) && $d !== $pkg_name )); + $os_lib_deps[$this->displayName($pkg_name)] = array_values(array_filter( + $raw, + fn ($d) => !str_starts_with($d, 'ext-') + )); } + $all_ext_lib_deps[$os] = $os_lib_deps; // Which regular exts are reachable as a dep/suggest from another regular ext? $depended_on = []; @@ -174,6 +203,26 @@ public function handle(): int } } + if (!empty($filter_extensions)) { + $entries = array_values(array_filter($entries, function (array $entry) use ($filter_extensions): bool { + $names = explode(',', $entry['extension']); + return count(array_intersect($names, $filter_extensions)) > 0; + })); + } + + if (!empty($filter_libs)) { + $entries = array_values(array_filter($entries, function (array $entry) use ($filter_libs, $all_ext_lib_deps): bool { + $names = explode(',', $entry['extension']); + $lib_deps = $all_ext_lib_deps[$entry['os']] ?? []; + foreach ($names as $name) { + if (count(array_intersect($lib_deps[$name] ?? [], $filter_libs)) > 0) { + return true; + } + } + return false; + })); + } + $this->output->write(json_encode($entries, JSON_UNESCAPED_SLASHES)); return static::SUCCESS; } @@ -227,6 +276,9 @@ private function splitOrphansByConflicts(array $orphans): array foreach ($orphans as $ext) { $placed = false; foreach ($batches as &$batch) { + if (count($batch) >= self::ORPHAN_BATCH_SIZE) { + continue; + } $conflict = false; foreach ($batch as $member) { if (isset($adjacency[$ext][$member])) { From e930873e60701c05db84db364334f5de77a60f2c Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 11:00:26 +0800 Subject: [PATCH 30/41] Add tier 2 support for actions runner test --- .../Command/Dev/GenExtTestMatrixCommand.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index ac0a61012..b3a7e1fab 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -20,6 +20,14 @@ class GenExtTestMatrixCommand extends BaseCommand 'macos' => ['arch' => 'aarch64', 'runner' => 'macos-15', 'os_key' => 'Darwin'], ]; + /** + * Tier 2 runners: Linux aarch64 + macOS x86_64, no Windows. + */ + private const array OS_RUNNERS_TIER2 = [ + 'linux' => ['arch' => 'aarch64', 'runner' => 'ubuntu-24.04-arm', 'os_key' => 'Linux'], + 'macos' => ['arch' => 'x86_64', 'runner' => 'macos-15-intel', 'os_key' => 'Darwin'], + ]; + /** * Extensions excluded from specific OS matrix entries. */ @@ -65,7 +73,8 @@ public function configure(): void { $this->addOption('for-extensions', null, InputOption::VALUE_OPTIONAL, 'Filter by extension display names, comma-separated', '') ->addOption('for-libs', null, InputOption::VALUE_OPTIONAL, 'Filter by lib names (depends+suggests), comma-separated', '') - ->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Filter by OS (Linux/Darwin/Windows), comma-separated', ''); + ->addOption('os', null, InputOption::VALUE_OPTIONAL, 'Filter by OS (Linux/Darwin/Windows), comma-separated', '') + ->addOption('tier2', null, InputOption::VALUE_NONE, 'Use Tier 2 runners (Linux aarch64 + macOS x86_64, no Windows)'); } public function handle(): int @@ -79,6 +88,9 @@ public function handle(): int $filter_extensions = $parse_option('for-extensions'); $filter_libs = $parse_option('for-libs'); $filter_os_keys = $parse_option('os'); + $tier2 = (bool) $this->input->getOption('tier2'); + + $base_runners = $tier2 ? self::OS_RUNNERS_TIER2 : self::OS_RUNNERS; $all = PackageConfig::getAll(); @@ -100,8 +112,8 @@ public function handle(): int } $os_runners = empty($filter_os_keys) - ? self::OS_RUNNERS - : array_filter(self::OS_RUNNERS, fn ($info) => in_array($info['os_key'], $filter_os_keys, true)); + ? $base_runners + : array_filter($base_runners, fn ($info) => in_array($info['os_key'], $filter_os_keys, true)); $entries = []; $all_ext_lib_deps = []; From af3c6a6d0868fbdd72681e682ee1e9b2c072479e Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 11:05:28 +0800 Subject: [PATCH 31/41] Move build command here --- src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php index b3a7e1fab..8621277cd 100644 --- a/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php +++ b/src/StaticPHP/Command/Dev/GenExtTestMatrixCommand.php @@ -12,7 +12,7 @@ #[AsCommand('dev:gen-ext-test-matrix', 'Generate GitHub Actions extension test matrix JSON', [], true)] class GenExtTestMatrixCommand extends BaseCommand { - private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro'; + private const string BUILD_TARGETS = '--build-cli --build-cgi --build-micro --with-suggests -vvv'; private const array OS_RUNNERS = [ 'linux' => ['arch' => 'x86_64', 'runner' => 'ubuntu-latest', 'os_key' => 'Linux'], @@ -210,7 +210,7 @@ public function handle(): int 'os' => $os, 'arch' => $os_info['arch'], 'extension' => $group, - 'build-args' => '"' . $group . '" ' . self::BUILD_TARGETS . ($extra !== '' ? ' ' . $extra : ''), + 'build-args' => './bin/spc build "' . $group . '" ' . self::BUILD_TARGETS . ($extra !== '' ? ' ' . $extra : ''), ]; } } From e6642459b80d06f2b824a116b904bb8be03f7218 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 12:16:48 +0800 Subject: [PATCH 32/41] Fix opentelemetry strict flag --- src/Package/Extension/opentelemetry.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Package/Extension/opentelemetry.php b/src/Package/Extension/opentelemetry.php index 632d12571..6c106f7f5 100644 --- a/src/Package/Extension/opentelemetry.php +++ b/src/Package/Extension/opentelemetry.php @@ -7,15 +7,21 @@ use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\Extension; +use StaticPHP\Toolchain\Interface\ToolchainInterface; +use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\GlobalEnvManager; #[Extension('opentelemetry')] class opentelemetry { #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-opentelemetry')] - public function patchBeforeMake(): void + public function patchBeforeMake(ToolchainInterface $toolchain): void { - // add -Wno-strict-prototypes - GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -Wno-strict-prototypes'); + $extra_cflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') ?: ''; + $extra_cflags .= ' -Wno-strict-prototypes'; + if ($toolchain instanceof ZigToolchain) { + $extra_cflags .= ' -Wno-unknown-warning-option'; + } + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS=' . trim($extra_cflags)); } } From 184a091fd916cf0c5aab3d9a1df6984e19d8f07d Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 14:12:50 +0800 Subject: [PATCH 33/41] Implicitly define swow dependencies (due to extension generating and windows swow curl bug) --- config/pkg/ext/ext-swow.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/config/pkg/ext/ext-swow.yml b/config/pkg/ext/ext-swow.yml index 11592cd0b..eb25bc815 100644 --- a/config/pkg/ext/ext-swow.yml +++ b/config/pkg/ext/ext-swow.yml @@ -9,10 +9,5 @@ ext-swow: metadata: license: Apache-2.0 license-files: [LICENSE] - suggests: - - openssl - - curl - - ext-openssl - - ext-curl php-extension: arg-type: custom From 2ed4b102608c328c20a567588308d76de4935fcc Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 14:13:09 +0800 Subject: [PATCH 34/41] Make brotli and zstd as dep --- config/pkg/target/curl.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/pkg/target/curl.yml b/config/pkg/target/curl.yml index 78064510c..8140bf728 100644 --- a/config/pkg/target/curl.yml +++ b/config/pkg/target/curl.yml @@ -16,6 +16,8 @@ curl: - zlib - libssh2 - nghttp2 + - brotli + - zstd suggests@unix: - libssh2 - brotli @@ -27,9 +29,6 @@ curl: - ldap - idn2 - krb5 - suggests@windows: - - brotli - - zstd frameworks: - CoreFoundation - CoreServices From 7b79767355d2910394f3ca3603daaa5eff840bfa Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 14:13:47 +0800 Subject: [PATCH 35/41] Add retry mechanism to Git clone and GitHub release fetching methods --- src/Package/Artifact/go_win.php | 4 ++-- src/Package/Artifact/go_xcaddy.php | 4 ++-- src/StaticPHP/Artifact/Downloader/Type/Git.php | 4 ++-- .../Artifact/Downloader/Type/GitHubRelease.php | 12 ++++++------ .../Artifact/Downloader/Type/GitHubTarball.php | 14 +++++++------- src/StaticPHP/Runtime/Shell/DefaultShell.php | 18 ++++++++++++++++-- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/Package/Artifact/go_win.php b/src/Package/Artifact/go_win.php index 44cf61b96..e06e615ab 100644 --- a/src/Package/Artifact/go_win.php +++ b/src/Package/Artifact/go_win.php @@ -23,13 +23,13 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $pkgroot = PKG_ROOT_PATH; // get version - [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: ''); if ($version === '') { throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } // find SHA256 hash from download page - $page = default_shell()->executeCurl('https://go.dev/dl/'); + $page = default_shell()->executeCurl('https://go.dev/dl/', retries: $downloader->getRetry()); if ($page === '' || $page === false) { throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); } diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index ca61bd465..51ccfb87b 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -39,11 +39,11 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult }; // get version and hash - [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text') ?: ''); + [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: ''); if ($version === '') { throw new DownloaderException('Failed to get latest Go version from https://go.dev/VERSION?m=text'); } - $page = default_shell()->executeCurl('https://go.dev/dl/'); + $page = default_shell()->executeCurl('https://go.dev/dl/', retries: $downloader->getRetry()); if ($page === '' || $page === false) { throw new DownloaderException('Failed to get Go download page from https://go.dev/dl/'); } diff --git a/src/StaticPHP/Artifact/Downloader/Type/Git.php b/src/StaticPHP/Artifact/Downloader/Type/Git.php index d5822e697..ad561c472 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/Git.php +++ b/src/StaticPHP/Artifact/Downloader/Type/Git.php @@ -20,7 +20,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo // direct branch clone if (isset($config['rev'])) { - default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null); + default_shell()->executeGitClone($config['url'], $config['rev'], $path, $shallow, $config['submodules'] ?? null, $downloader->getRetry()); $shell = PHP_OS_FAMILY === 'Windows' ? cmd(false) : shell(false); $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]) : ''; @@ -66,7 +66,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $version = array_key_first($matched_version_branch); $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); + default_shell()->executeGitClone($config['url'], $branch, $path, $shallow, $config['submodules'] ?? null, $downloader->getRetry()); 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)."); diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php index 15626089a..20db71275 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubRelease.php @@ -21,13 +21,13 @@ class GitHubRelease implements DownloadTypeInterface, ValidatorInterface, CheckU private ?string $version = null; - public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null): array + public function getGitHubReleases(string $name, string $repo, bool $prefer_stable = true, ?string $query = null, int $retries = 0): array { logger()->debug("Fetching {$name} GitHub releases from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); - $data2 = default_shell()->executeCurl($url, headers: $headers); + $data2 = default_shell()->executeCurl($url, headers: $headers, retries: $retries); $data = json_decode($data2 ?: '', true); if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); @@ -46,13 +46,13 @@ public function getGitHubReleases(string $name, string $repo, bool $prefer_stabl * Get the latest GitHub release assets for a given repository. * match_asset is provided, only return the asset that matches the regex. */ - public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null): array + public function getLatestGitHubRelease(string $name, string $repo, bool $prefer_stable, string $match_asset, ?string $query = null, int $retries = 0): array { logger()->debug("Fetching {$name} GitHub release from {$repo}"); $url = str_replace('{repo}', $repo, self::API_URL); $url .= ($query ?? ''); $headers = $this->getGitHubTokenHeaders(); - $data2 = default_shell()->executeCurl($url, headers: $headers); + $data2 = default_shell()->executeCurl($url, headers: $headers, retries: $retries); $data = json_decode($data2 ?: '', true); if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub release API info for {$repo} from {$url}"); @@ -84,7 +84,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo if (!isset($config['match'])) { throw new DownloaderException("GitHubRelease downloader requires 'match' config for {$name}"); } - $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null); + $rel = $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null, $downloader->getRetry()); // download file using curl $asset_url = str_replace(['{repo}', '{id}'], [$config['repo'], $rel['id']], self::ASSET_URL); @@ -124,7 +124,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A 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); + $this->getLatestGitHubRelease($name, $config['repo'], $config['prefer-stable'] ?? true, $config['match'], $config['query'] ?? null, $downloader->getRetry()); $new_version = $this->version ?? $old_version ?? ''; return new CheckUpdateResult( old: $old_version, diff --git a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php index 37ba5ca7a..c9c657bc8 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php +++ b/src/StaticPHP/Artifact/Downloader/Type/GitHubTarball.php @@ -22,11 +22,11 @@ class GitHubTarball implements DownloadTypeInterface, CheckUpdateInterface * Get the GitHub tarball URL for a given repository and release type. * If match_url is provided, only return the tarball that matches the regex. */ - public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null): array + public function getGitHubTarballInfo(string $name, string $repo, string $rel_type, bool $prefer_stable = true, ?string $match_url = null, ?string $basename = null, ?string $query = null, int $retries = 0): array { if ($rel_type === 'releases' && $match_url === null && $query === null && $prefer_stable) { $api_url = str_replace(['{repo}', '{rel_type}'], [$repo, 'releases/latest'], self::API_URL); - $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders()); + $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders(), retries: $retries); $data = json_decode($data ?: '', true); if (!is_array($data) || empty($data['tarball_url'])) { throw new DownloaderException("Failed to get GitHub latest release for {$repo} from {$api_url}"); @@ -36,7 +36,7 @@ public function getGitHubTarballInfo(string $name, string $repo, string $rel_typ } else { $api_url = str_replace(['{repo}', '{rel_type}'], [$repo, $rel_type], self::API_URL); $api_url .= ($query ?? ''); - $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders()); + $data = default_shell()->executeCurl($api_url, headers: $this->getGitHubTokenHeaders(), retries: $retries); $data = json_decode($data ?: '', true); if (!is_array($data)) { throw new DownloaderException("Failed to get GitHub tarball URL for {$repo} from {$api_url}"); @@ -65,7 +65,7 @@ public function getGitHubTarballInfo(string $name, string $repo, string $rel_typ } $this->version = $version ?? null; } - $head = default_shell()->executeCurl($rel_url, 'HEAD', headers: $this->getGitHubTokenHeaders()) ?: ''; + $head = default_shell()->executeCurl($rel_url, 'HEAD', headers: $this->getGitHubTokenHeaders(), retries: $retries) ?: ''; preg_match('/^content-disposition:\s+attachment;\s*filename=("?)(?.+\.tar\.gz)\1/im', $head, $matches); if ($matches) { $filename = $matches['filename']; @@ -84,9 +84,9 @@ public function download(string $name, array $config, ArtifactDownloader $downlo 'ghtagtar' => 'tags', default => throw new DownloaderException("Invalid GitHubTarball type for {$name}"), }; - [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null); + [$url, $filename] = $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null, $downloader->getRetry()); $path = DOWNLOAD_PATH . "/{$filename}"; - default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders()); + default_shell()->executeCurlDownload($url, $path, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry()); return DownloadResult::archive($filename, $config, $config['extract'] ?? null, version: $this->version, downloader: static::class); } @@ -97,7 +97,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A '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); + $this->getGitHubTarballInfo($name, $config['repo'], $rel_type, $config['prefer-stable'] ?? true, $config['match'] ?? null, $name, $config['query'] ?? null, $downloader->getRetry()); $new_version = $this->version ?? $old_version ?? ''; return new CheckUpdateResult( old: $old_version, diff --git a/src/StaticPHP/Runtime/Shell/DefaultShell.php b/src/StaticPHP/Runtime/Shell/DefaultShell.php index 6c32b1554..f20fca339 100644 --- a/src/StaticPHP/Runtime/Shell/DefaultShell.php +++ b/src/StaticPHP/Runtime/Shell/DefaultShell.php @@ -84,7 +84,7 @@ public function executeCurlDownload(string $url, string $path, array $headers = /** * Execute a Git clone command to clone a repository. */ - public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null): void + public function executeGitClone(string $url, string $branch, string $path, bool $shallow = true, ?array $submodules = null, int $retries = 0): void { $path = FileSystem::convertPath($path); if (file_exists($path)) { @@ -99,7 +99,21 @@ public function executeGitClone(string $url, string $branch, string $path, bool $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); + try { + $this->passthru($cmd, $this->console_putput); + } catch (InterruptException $e) { + throw $e; + } catch (\Throwable $e) { + if ($retries > 0) { + logger()->warning("Git clone failed, retrying... ({$retries} retries left)"); + if (is_dir($path)) { + FileSystem::removeDir($path); + } + $this->executeGitClone($url, $branch, $path, $shallow, $submodules, $retries - 1); + return; + } + throw $e; + } if ($submodules !== null) { $depth_flag = $shallow ? '--depth 1' : ''; foreach ($submodules as $submodule) { From 629b5b6b2d1ff8b7314caf040b691701823745ff Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 15:54:01 +0800 Subject: [PATCH 36/41] Add test-bot --- .github/workflows/tests.yml | 219 ++++++++----- src/StaticPHP/Command/Dev/TestBotCommand.php | 312 +++++++++++++++++++ src/StaticPHP/ConsoleApplication.php | 2 + 3 files changed, 450 insertions(+), 83 deletions(-) create mode 100644 src/StaticPHP/Command/Dev/TestBotCommand.php diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0b2979f13..57817e742 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -1,8 +1,8 @@ -name: Tests +name: v3 Tests on: pull_request: - branches: [ "main", "v3" ] + branches: [ "v3" ] types: [ opened, synchronize, reopened ] paths: - 'src/**' @@ -103,114 +103,167 @@ jobs: run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist - name: "Run PHPUnit Tests" - run: SPC_LIBC=glibc vendor/bin/phpunit tests/ --no-coverage + run: vendor/bin/phpunit tests/ --no-coverage - define-matrix: - if: false # TODO: enable when refactoring workflows - name: "Define Matrix" + check-gate: + name: "Check: need-test label" runs-on: ubuntu-latest outputs: - php: ${{ steps.gendef.outputs.php }} - os: ${{ steps.gendef.outputs.os }} + enabled: ${{ steps.gate.outputs.enabled }} steps: - - name: "Checkout" - uses: actions/checkout@v4 + - name: Check label + id: gate + run: | + LABELS='${{ toJSON(github.event.pull_request.labels.*.name) }}' + if echo "$LABELS" | grep -q '"need-test"'; then + echo "enabled=true" >> "$GITHUB_OUTPUT" + else + echo "enabled=false" >> "$GITHUB_OUTPUT" + fi + + test-bot: + name: "Test Bot: analyze PR" + needs: check-gate + if: needs.check-gate.outputs.enabled == 'true' + runs-on: ubuntu-latest + permissions: + pull-requests: write + contents: read + outputs: + need_test: ${{ steps.bot.outputs.need_test }} + gen_matrix_args: ${{ steps.bot.outputs.gen_matrix_args }} + gen_matrix_args_tier2: ${{ steps.bot.outputs.gen_matrix_args_tier2 }} + php_versions: ${{ steps.bot.outputs.php_versions }} + tier2: ${{ steps.bot.outputs.tier2 }} + steps: + - uses: actions/checkout@v4 - - name: "Setup PHP" + - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: 8.4 + php-version: '8.4' extensions: curl, openssl, mbstring + ini-values: memory_limit=-1 + tools: composer + + - name: Install dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --no-dev - - name: Define - id: gendef + - name: Run dev:test-bot + id: bot + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - PHP_VERSIONS=$(php src/globals/test-extensions.php php) - OS_VERSIONS=$(php src/globals/test-extensions.php os) - echo 'php='"$PHP_VERSIONS" >> "$GITHUB_OUTPUT" - echo 'os='"$OS_VERSIONS" >> "$GITHUB_OUTPUT" + BOT_JSON=$(php -d opcache.enable_cli=0 bin/spc dev:test-bot \ + --pr=${{ github.event.pull_request.number }} \ + --repo=${{ github.repository }} 2>/dev/null) + + echo "need_test=$(echo "$BOT_JSON" | jq -r '.need_test')" >> "$GITHUB_OUTPUT" + echo "gen_matrix_args=$(echo "$BOT_JSON" | jq -r '.gen_matrix_args')" >> "$GITHUB_OUTPUT" + echo "gen_matrix_args_tier2=$(echo "$BOT_JSON" | jq -r '.gen_matrix_args_tier2')" >> "$GITHUB_OUTPUT" + echo "php_versions=$(echo "$BOT_JSON" | jq -c '.php_versions')" >> "$GITHUB_OUTPUT" + echo "tier2=$(echo "$BOT_JSON" | jq -r '.tier2')" >> "$GITHUB_OUTPUT" + + COMMENT_BODY=$(echo "$BOT_JSON" | jq -r '.comment_body') + MARKER="" + + # Find existing bot comment id + EXISTING_ID=$(gh api \ + repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments \ + --jq "[.[] | select(.body | startswith(\"$MARKER\")) | .id] | first // empty") + + if [ -n "$EXISTING_ID" ]; then + gh api --method PATCH \ + repos/${{ github.repository }}/issues/comments/"$EXISTING_ID" \ + -f body="$COMMENT_BODY" + else + gh pr comment ${{ github.event.pull_request.number }} \ + --repo ${{ github.repository }} \ + --body "$COMMENT_BODY" + fi + + gen-matrix: + name: "Generate test matrix" + needs: test-bot + if: needs.test-bot.outputs.need_test == 'true' + runs-on: ubuntu-latest + outputs: + matrix: ${{ steps.build.outputs.matrix }} + steps: + - uses: actions/checkout@v4 + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + extensions: curl, openssl, mbstring + ini-values: memory_limit=-1 + tools: composer + + - name: Install dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --no-dev - build: - if: false - name: "Build PHP Test (PHP ${{ matrix.php }} ${{ matrix.os }})" - runs-on: ${{ matrix.os }} - needs: [define-matrix, php-cs-fixer, phpstan, phpunit] + - name: Build matrix + id: build + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GEN_MATRIX_ARGS: ${{ needs.test-bot.outputs.gen_matrix_args }} + GEN_MATRIX_ARGS_TIER2: ${{ needs.test-bot.outputs.gen_matrix_args_tier2 }} + PHP_VERSIONS: ${{ needs.test-bot.outputs.php_versions }} + TIER2: ${{ needs.test-bot.outputs.tier2 }} + run: | + # Tier1 matrix + MATRIX1=$(bin/spc dev:gen-ext-test-matrix $GEN_MATRIX_ARGS 2>/dev/null) + + # Merge Tier2 if requested + if [ "$TIER2" = "true" ] && [ -n "$GEN_MATRIX_ARGS_TIER2" ]; then + MATRIX2=$(bin/spc dev:gen-ext-test-matrix $GEN_MATRIX_ARGS_TIER2 2>/dev/null) + COMBINED=$(jq -n --argjson m1 "$MATRIX1" --argjson m2 "$MATRIX2" '$m1 + $m2') + else + COMBINED=$MATRIX1 + fi + + # Expand PHP versions: cartesian product of entries × php_versions + FINAL=$(echo "$COMBINED" | jq --argjson versions "$PHP_VERSIONS" \ + '[.[] | . as $entry | $versions[] | $entry + {"php-version": .}]') + + echo "matrix=$(echo "$FINAL" | jq -c '{"combo": .}')" >> "$GITHUB_OUTPUT" + + ext-test: + name: "Ext test: ${{ matrix.combo.extension }} (PHP ${{ matrix.combo.php-version }} · ${{ matrix.combo.os }}-${{ matrix.combo.arch }})" + needs: gen-matrix + runs-on: ${{ matrix.combo.runner }} timeout-minutes: 120 strategy: - matrix: - php: ${{ fromJSON(needs.define-matrix.outputs.php) }} - os: ${{ fromJSON(needs.define-matrix.outputs.os) }} fail-fast: false + matrix: ${{ fromJSON(needs.gen-matrix.outputs.matrix) }} steps: - - name: "Update runner packages" - if: ${{ startsWith(matrix.os, 'ubuntu-') }} - run: sudo apt-get update && sudo apt-get install -y ca-certificates + - uses: actions/checkout@v4 - - name: "Checkout" - uses: actions/checkout@v4 - - - name: "Setup PHP" + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: 8.4 - tools: pecl, composer extensions: curl, openssl, mbstring ini-values: memory_limit=-1 + tools: composer env: phpts: nts - - name: "Cache composer packages" - id: composer-cache - uses: actions/cache@v4 - with: - path: vendor - key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-php- + - name: Install dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --no-dev - # Cache downloaded source - - id: cache-download - uses: actions/cache@v4 - with: - path: downloads - key: php-dependencies-${{ matrix.os }} - - - name: "Install Dependencies" - run: composer update -vvv --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist --no-plugins - - - name: "Run Build Tests (doctor)" - run: php src/globals/test-extensions.php doctor_cmd ${{ matrix.os }} ${{ matrix.php }} - - - name: "Prepare UPX for Windows" - if: ${{ startsWith(matrix.os, 'windows-') }} - run: | - php src/globals/test-extensions.php install_upx_cmd ${{ matrix.os }} ${{ matrix.php }} - echo "UPX_CMD=$(php src/globals/test-extensions.php upx)" >> $env:GITHUB_ENV - - - name: "Prepare UPX for Linux" - if: ${{ startsWith(matrix.os, 'ubuntu-') }} + - name: Build + env: + SPC_USE_SUDO: "yes" + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - php src/globals/test-extensions.php install_upx_cmd ${{ matrix.os }} ${{ matrix.php }} - echo "UPX_CMD=$(php src/globals/test-extensions.php upx)" >> $GITHUB_ENV - - - name: "Run Build Tests (download)" - run: php src/globals/test-extensions.php download_cmd ${{ matrix.os }} ${{ matrix.php }} + ./bin/spc doctor --auto-fix + ${{ matrix.combo.build-args }} - - name: "Run Build Tests (build)" - run: php src/globals/test-extensions.php build_cmd ${{ matrix.os }} ${{ matrix.php }} - - - name: "Run Build Tests (build - embed for non-windows)" - if: ${{ !startsWith(matrix.os, 'windows-') }} - run: php src/globals/test-extensions.php build_embed_cmd ${{ matrix.os }} ${{ matrix.php }} - - - name: "Upload logs" - if: ${{ always() && hashFiles('log/**') != '' }} - uses: actions/upload-artifact@v7 + - name: Upload logs + if: always() && hashFiles('log/**') != '' + uses: actions/upload-artifact@v4 with: - name: build-logs-${{ matrix.os }}-${{ matrix.php }} + name: logs-${{ matrix.combo.os }}-${{ matrix.combo.arch }}-${{ matrix.combo.extension }}-php${{ matrix.combo.php-version }} path: log - -# - name: Setup tmate session -# if: ${{ failure() }} -# uses: mxschmitt/action-tmate@v3 diff --git a/src/StaticPHP/Command/Dev/TestBotCommand.php b/src/StaticPHP/Command/Dev/TestBotCommand.php new file mode 100644 index 000000000..0d2378187 --- /dev/null +++ b/src/StaticPHP/Command/Dev/TestBotCommand.php @@ -0,0 +1,312 @@ + 'Linux', + 'test/windows' => 'Windows', + 'test/macos' => 'Darwin', + ]; + + private const string TIER2_LABEL = 'test/tier2'; + + /** PHP version labels → version string (8.5 is always included as default) */ + private const array PHP_VERSION_LABELS = [ + 'test/php-83' => '8.3', + 'test/php-84' => '8.4', + ]; + + private const string DEFAULT_PHP_VERSION = '8.5'; + + protected bool $no_motd = true; + + public function configure(): void + { + $this->addOption('pr', null, InputOption::VALUE_REQUIRED, 'Pull request number') + ->addOption('repo', null, InputOption::VALUE_REQUIRED, 'Repository in owner/repo format (e.g. owner/repo)') + ->addOption('mock-files', null, InputOption::VALUE_REQUIRED, 'Comma-separated file paths to simulate PR changed files (skips GitHub API, for local testing)', '') + ->addOption('mock-labels', null, InputOption::VALUE_REQUIRED, 'Comma-separated labels to simulate PR labels (skips GitHub API, for local testing)', ''); + } + + public function handle(): int + { + $mock_files_raw = (string) $this->input->getOption('mock-files'); + $mock_labels_raw = (string) $this->input->getOption('mock-labels'); + $is_mock = $mock_files_raw !== '' || $mock_labels_raw !== ''; + + if ($is_mock) { + // Local testing mode: skip all GitHub API calls + $changed_files = array_map( + fn ($f) => ['filename' => trim($f)], + array_filter(explode(',', $mock_files_raw)) + ); + $label_names = array_map('trim', array_filter(explode(',', $mock_labels_raw))); + } else { + $pr = (int) $this->input->getOption('pr'); + $repo = (string) $this->input->getOption('repo'); + + if ($pr <= 0 || $repo === '') { + $this->output->writeln('Either --mock-files/--mock-labels (local test) or --pr and --repo (live) are required.'); + return static::USER_ERROR; + } + + $headers = array_merge( + $this->getGitHubTokenHeaders(), + ['Accept: application/vnd.github+json', 'X-GitHub-Api-Version: 2022-11-28'], + ); + + // Fetch changed files (paginated, up to 300) + $changed_files = $this->fetchPaginatedFiles($repo, $pr, $headers); + + // Fetch current labels on the PR/issue + $labels_raw = $this->apiGet( + sprintf('%s/repos/%s/issues/%d/labels', self::API_BASE, $repo, $pr), + $headers + ); + $label_names = array_column($labels_raw ?? [], 'name'); + } + + // Analyze changed files → extensions, libs, targets + [$extensions, $libs, $targets] = $this->analyzeChangedFiles($changed_files); + + // Resolve active platform OS keys (used as filters, not as trigger) + $os_keys = []; + foreach (self::PLATFORM_LABELS as $label => $os_key) { + if (in_array($label, $label_names, true)) { + $os_keys[] = $os_key; + } + } + $tier2 = in_array(self::TIER2_LABEL, $label_names, true); + $need_test = in_array('need-test', $label_names, true); + + // Resolve PHP versions (default always included) + $php_versions = [self::DEFAULT_PHP_VERSION]; + foreach (self::PHP_VERSION_LABELS as $label => $version) { + if (in_array($label, $label_names, true)) { + $php_versions[] = $version; + } + } + $php_versions = array_unique($php_versions); + sort($php_versions); + + // Build gen_matrix_args whenever need-test is set. + // Platform labels narrow the OS scope; absent = no --os filter (all platforms). + $gen_matrix_args = ''; + $gen_matrix_args_tier2 = ''; + if ($need_test) { + $flag_parts = []; + if (!empty($extensions)) { + $flag_parts[] = '--for-extensions=' . implode(',', $extensions); + } + if (!empty($libs)) { + $flag_parts[] = '--for-libs=' . implode(',', $libs); + } + if (!empty($os_keys)) { + $flag_parts[] = '--os=' . implode(',', $os_keys); + } + $gen_matrix_args = implode(' ', $flag_parts); + + if ($tier2) { + // Tier2 covers Linux + macOS only (never Windows) + $tier2_os = array_values(array_filter( + !empty($os_keys) ? $os_keys : ['Linux', 'Darwin'], + fn ($k) => $k !== 'Windows' + )); + if (!empty($tier2_os)) { + $tier2_parts = array_values(array_filter($flag_parts, fn ($f) => !str_starts_with($f, '--os='))); + $tier2_parts[] = '--os=' . implode(',', $tier2_os); + $tier2_parts[] = '--tier2'; + $gen_matrix_args_tier2 = implode(' ', $tier2_parts); + } + } + } + + $comment_body = $this->buildCommentBody( + $extensions, + $libs, + $targets, + $label_names, + $os_keys, + $tier2, + $php_versions, + $need_test, + ); + + $result = [ + 'need_test' => $need_test, + 'extensions' => array_values($extensions), + 'libs' => array_values($libs), + 'targets' => array_values($targets), + 'gen_matrix_args' => $gen_matrix_args, + 'gen_matrix_args_tier2' => $gen_matrix_args_tier2, + 'php_versions' => array_values($php_versions), + 'tier2' => $tier2, + 'comment_body' => $comment_body, + ]; + + $this->output->write(json_encode($result, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)); + return static::SUCCESS; + } + + /** + * Fetch all changed files for a PR across up to 3 pages (max 300 files). + */ + private function fetchPaginatedFiles(string $repo, int $pr, array $headers): array + { + $files = []; + for ($page = 1; $page <= 3; ++$page) { + $url = sprintf('%s/repos/%s/pulls/%d/files?per_page=100&page=%d', self::API_BASE, $repo, $pr, $page); + $batch = $this->apiGet($url, $headers); + if (empty($batch)) { + break; + } + $files = array_merge($files, $batch); + if (count($batch) < 100) { + break; + } + } + return $files; + } + + /** + * Perform a GET request and return decoded JSON array, or null on failure. + */ + private function apiGet(string $url, array $headers): ?array + { + $data = default_shell()->executeCurl($url, headers: $headers); + $decoded = json_decode($data ?: '', true); + return is_array($decoded) ? $decoded : null; + } + + /** + * Analyze changed file paths and classify them into extensions, libs, and targets. + * + * @return array{string[], string[], string[]} + */ + private function analyzeChangedFiles(array $files): array + { + $extensions = []; + $libs = []; + $targets = []; + + foreach ($files as $file) { + $path = $file['filename'] ?? ''; + + if (preg_match('#^src/Package/Extension/([^/]+)\.php$#', $path, $m)) { + $name = strtolower($m[1]); + $extensions[$name] = $name; + } elseif (preg_match('#^config/pkg/ext/ext-([^/]+)\.yml$#', $path, $m)) { + $extensions[$m[1]] = $m[1]; + } elseif (preg_match('#^src/Package/Library/([^/]+)\.php$#', $path, $m)) { + $name = strtolower($m[1]); + $libs[$name] = $name; + } elseif (preg_match('#^config/pkg/lib/([^/]+)\.yml$#', $path, $m)) { + $libs[$m[1]] = $m[1]; + } elseif (preg_match('#^src/Package/Target/([^/]+)\.php$#', $path, $m)) { + $name = strtolower($m[1]); + $targets[$name] = $name; + } elseif (preg_match('#^config/pkg/target/([^/]+)\.yml$#', $path, $m)) { + $targets[$m[1]] = $m[1]; + } + } + + sort($extensions); + sort($libs); + sort($targets); + + return [$extensions, $libs, $targets]; + } + + private function buildCommentBody( + array $extensions, + array $libs, + array $targets, + array $label_names, + array $os_keys, + bool $tier2, + array $php_versions, + bool $need_test, + ): string { + $fmt = static fn (array $items): string => !empty($items) + ? '`' . implode('`, `', $items) . '`' + : '_none_'; + + $detected = sprintf( + '**Detected**: Extensions: %s | Libraries: %s | Targets: %s', + $fmt($extensions), + $fmt($libs), + $fmt($targets), + ); + + // Case 1: need-test absent → invite the author to add it + if (!$need_test) { + return implode("\n", [ + '', + '**StaticPHP Test Bot**', + '', + $detected, + '', + 'To trigger extension build tests on this PR, add the `need-test` label:', + '', + '**Gate**: `need-test`', + '**Platform filter** (optional, default all): `test/linux` `test/windows` `test/macos` · `test/tier2`', + '**PHP version** (optional, default 8.5): `test/php-83` `test/php-84`', + ]); + } + + // Case 2: need-test present → show what will run + // os_keys empty = no filter = all platforms + $effective_os = !empty($os_keys) + ? $os_keys + : array_values(self::PLATFORM_LABELS); // all OS keys + + $platform_parts = []; + foreach (self::PLATFORM_LABELS as $_label => $os_key) { + if (!in_array($os_key, $effective_os, true)) { + continue; + } + $platform_parts[] = match ($os_key) { + 'Linux' => 'Linux x86_64', + 'Darwin' => 'macOS arm64', + 'Windows' => 'Windows x86_64', + default => $os_key, + }; + } + if ($tier2) { + if (in_array('Linux', $effective_os, true)) { + $platform_parts[] = 'Linux aarch64 (Tier2)'; + } + if (in_array('Darwin', $effective_os, true)) { + $platform_parts[] = 'macOS x86_64 (Tier2)'; + } + } + + $php_str = implode(', ', array_map(fn ($v) => "PHP {$v}", $php_versions)) . ' NTS'; + $active_test_labels = array_values(array_filter($label_names, fn ($l) => str_starts_with($l, 'test/'))); + $labels_str = !empty($active_test_labels) ? '`' . implode('`, `', $active_test_labels) . '`' : '_none_'; + + return implode("\n", [ + '', + '**StaticPHP Test Bot**', + '', + $detected, + '**Active labels**: ' . $labels_str, + '**Config**: ' . implode(' + ', $platform_parts) . ' | ' . $php_str, + ]); + } +} diff --git a/src/StaticPHP/ConsoleApplication.php b/src/StaticPHP/ConsoleApplication.php index cc10554e4..cf305e05b 100644 --- a/src/StaticPHP/ConsoleApplication.php +++ b/src/StaticPHP/ConsoleApplication.php @@ -19,6 +19,7 @@ use StaticPHP\Command\Dev\PackageInfoCommand; use StaticPHP\Command\Dev\PackLibCommand; use StaticPHP\Command\Dev\ShellCommand; +use StaticPHP\Command\Dev\TestBotCommand; use StaticPHP\Command\DoctorCommand; use StaticPHP\Command\DownloadCommand; use StaticPHP\Command\DumpExtensionsCommand; @@ -87,6 +88,7 @@ public function __construct() new GenExtDocsCommand(), new GenDepsDataCommand(), new GenExtTestMatrixCommand(), + new TestBotCommand(), ]); // add additional commands from registries From ceade306b81b442f030d7e8f8c875bfa884ed233 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 15:55:55 +0800 Subject: [PATCH 37/41] phpstan fix --- src/StaticPHP/Command/Dev/TestBotCommand.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/StaticPHP/Command/Dev/TestBotCommand.php b/src/StaticPHP/Command/Dev/TestBotCommand.php index 0d2378187..dc34444df 100644 --- a/src/StaticPHP/Command/Dev/TestBotCommand.php +++ b/src/StaticPHP/Command/Dev/TestBotCommand.php @@ -283,6 +283,7 @@ private function buildCommentBody( $platform_parts[] = match ($os_key) { 'Linux' => 'Linux x86_64', 'Darwin' => 'macOS arm64', + /* @phpstan-ignore-next-line */ 'Windows' => 'Windows x86_64', default => $os_key, }; From 270f131f545d6e5bad08d5a4c567eb014f434119 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 16:01:44 +0800 Subject: [PATCH 38/41] Add label event --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 57817e742..78a8a8977 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -3,7 +3,7 @@ name: v3 Tests on: pull_request: branches: [ "v3" ] - types: [ opened, synchronize, reopened ] + types: [ opened, synchronize, reopened, labeled, unlabeled ] paths: - 'src/**' - 'config/**' From 6ad4b6a4af6fcee574a0a128228a134aeeed5e17 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 16:07:22 +0800 Subject: [PATCH 39/41] Add concurrency settings to GitHub Actions workflow --- .github/workflows/tests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 78a8a8977..3efa06df9 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -15,6 +15,10 @@ on: permissions: read-all +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 9dbb178bf3575f4f538dcc46e603904d2c42db9c Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Sat, 9 May 2026 16:11:26 +0800 Subject: [PATCH 40/41] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .github/workflows/tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 3efa06df9..5f1f93ecd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -263,7 +263,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | ./bin/spc doctor --auto-fix - ${{ matrix.combo.build-args }} + ${{ matrix.combo.build-args }} --dl-with-php=${{ matrix.combo.php-version }} - name: Upload logs if: always() && hashFiles('log/**') != '' From e63d49ec136516040bf7534e7bf9971792d22418 Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Sat, 9 May 2026 16:30:57 +0800 Subject: [PATCH 41/41] Disable pdo pgsql hook for swow temporarily --- src/Package/Extension/swow.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Package/Extension/swow.php b/src/Package/Extension/swow.php index 3ebe3d582..c49bc191b 100644 --- a/src/Package/Extension/swow.php +++ b/src/Package/Extension/swow.php @@ -20,7 +20,7 @@ class swow extends PhpExtensionPackage #[CustomPhpConfigureArg('Windows')] public function configureArg(PackageInstaller $installer): string { - $arg = '--enable-swow'; + $arg = '--enable-swow --disable-swow-pdo-pgsql'; $arg .= $installer->getLibraryPackage('openssl') ? ' --enable-swow-ssl' : ' --disable-swow-ssl'; $arg .= $installer->getLibraryPackage('curl') ? ' --enable-swow-curl' : ' --disable-swow-curl'; return $arg;