From 67c6661fcc56b9205d9b603fdfedc9f98bff480c Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 7 Aug 2026 22:02:18 +0200 Subject: [PATCH 01/12] php 8.6 compatibility with all bundled extensions --- README-zh.md | 4 +- README.md | 4 +- config/pkg/ext/builtin-extensions.yml | 2 +- .../Command/SwitchPhpVersionCommand.php | 5 +- src/Package/Extension/gd.php | 15 +++ src/Package/Extension/intl.php | 11 ++ src/Package/Extension/pdo_pgsql.php | 26 +++++ src/Package/Extension/pgsql.php | 20 ++-- src/Package/Target/php.php | 2 +- src/Package/Target/php/unix.php | 21 ++-- .../Artifact/Downloader/Type/PhpRelease.php | 100 ++++++++++++++---- src/StaticPHP/Command/Dev/TestBotCommand.php | 1 + src/StaticPHP/Util/SourcePatcher.php | 8 +- src/globals/test-extensions.php | 5 +- 14 files changed, 180 insertions(+), 44 deletions(-) create mode 100644 src/Package/Extension/pdo_pgsql.php diff --git a/README-zh.md b/README-zh.md index cb5700fd4..156077cf6 100755 --- a/README-zh.md +++ b/README-zh.md @@ -16,7 +16,7 @@ ## 特性 -- :elephant: 支持多个 PHP 版本 - PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- :elephant: 支持多个 PHP 版本 - PHP 8.1, 8.2, 8.3, 8.4, 8.5, 8.6(预发布) - :handbag: 构建零依赖的单文件 PHP 可执行程序 - :hamburger: 构建 **[phpmicro](https://github.com/static-php/phpmicro)** 自解压可执行文件(将 PHP 二进制和源码合并为单个文件) - :pill: 自动构建环境检查器,支持自动修复 @@ -63,7 +63,7 @@ chmod +x ./spc 首先,创建 `craft.yml` 文件,并从 [扩展列表](https://static-php.dev/en/guide/extensions.html) 或 [命令生成器](https://static-php.dev/en/guide/cli-generator.html) 指定要包含的扩展: ```yml -# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5 +# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6 (pre-release) php-version: 8.5 # Put your extension list here extensions: "apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" diff --git a/README.md b/README.md index 1617737ec..566954a16 100755 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ## Features -- :elephant: Support multiple PHP versions - PHP 8.1, 8.2, 8.3, 8.4, 8.5 +- :elephant: Support multiple PHP versions - PHP 8.1, 8.2, 8.3, 8.4, 8.5, 8.6 (pre-release) - :handbag: Build single-file PHP executable with zero dependencies - :hamburger: Build **[phpmicro](https://github.com/static-php/phpmicro)** self-extracting executables (combines PHP binary and source code into one file) - :pill: Automatic build environment checker with auto-fix capabilities @@ -63,7 +63,7 @@ chmod +x ./spc First, create a `craft.yml` file and specify which extensions you want to include from [extension list](https://static-php.dev/en/guide/extensions.html) or [command generator](https://static-php.dev/en/guide/cli-generator.html): ```yml -# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5 +# PHP version support: 8.1, 8.2, 8.3, 8.4, 8.5, 8.6 (pre-release) php-version: 8.5 # Put your extension list here extensions: "apcu,bcmath,calendar,ctype,curl,dba,dom,exif,fileinfo,filter,gd,iconv,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,readline,redis,session,simplexml,sockets,sodium,sqlite3,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib" diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index c97b32dd2..a2a649dfd 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -226,7 +226,7 @@ ext-pdo_pgsql: - ext-pgsql - postgresql php-extension: - arg-type@unix: with-path + arg-type@unix: custom arg-type@windows: '--with-pdo-pgsql=yes' ext-pdo_sqlite: type: php-extension diff --git a/src/Package/Command/SwitchPhpVersionCommand.php b/src/Package/Command/SwitchPhpVersionCommand.php index a6594713d..76416c79a 100644 --- a/src/Package/Command/SwitchPhpVersionCommand.php +++ b/src/Package/Command/SwitchPhpVersionCommand.php @@ -104,6 +104,7 @@ public function handle(): int * Accepts: * - Major.Minor format, e.g. 7.4 * - Full version format, e.g. 8.4.5, 8.3.12, etc. + * - Pre-release format, e.g. 8.6.0RC1 */ private function isValidPhpVersion(string $version): bool { @@ -112,8 +113,8 @@ private function isValidPhpVersion(string $version): bool return true; } - // Check full version format (e.g., 8.4.5) - if (preg_match('/^\d+\.\d+\.\d+$/', $version)) { + // Check full version format (e.g., 8.4.5, 8.6.0RC1) + if (preg_match('/^\d+\.\d+\.\d+(?:(?:alpha|beta|RC)\d+)?$/', $version)) { return true; } diff --git a/src/Package/Extension/gd.php b/src/Package/Extension/gd.php index 5e815b5da..daaf8da6b 100644 --- a/src/Package/Extension/gd.php +++ b/src/Package/Extension/gd.php @@ -4,14 +4,29 @@ namespace Package\Extension; +use Package\Target\php; +use StaticPHP\Attribute\Package\BeforeStage; use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\FileSystem; #[Extension('gd')] class gd extends PhpExtensionPackage { + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-gd')] + #[PatchDescription('Fix libgd iconv_t fallback typedef guard for shared builds (PHP 8.6+)')] + public function patchBeforeBuildconf(): void + { + FileSystem::replaceFileStr( + "{$this->getBuildDir()}/libgd/gdkanji.c", + "#ifndef HAVE_ICONV_T_DEF\ntypedef void *iconv_t;", + "#ifndef HAVE_ICONV\ntypedef void *iconv_t;", + ); + } + #[CustomPhpConfigureArg('Darwin')] #[CustomPhpConfigureArg('Linux')] public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): string diff --git a/src/Package/Extension/intl.php b/src/Package/Extension/intl.php index dd7d3ffd9..8637a0198 100644 --- a/src/Package/Extension/intl.php +++ b/src/Package/Extension/intl.php @@ -11,10 +11,21 @@ use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\GlobalEnvManager; #[Extension('intl')] class intl extends PhpExtensionPackage { + // php.h defines vsnprintf as ap_php_vsnprintf, breaking std::vsnprintf in libc++ + #[BeforeStage('php', [php::class, 'makeForUnix'], 'ext-intl')] + public function forceLibcxxLocaleBeforePhpHeaders(): void + { + $cxxflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS') ?: ''; + if (!str_contains($cxxflags, '-include locale')) { + GlobalEnvManager::putenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS=' . trim("{$cxxflags} -include locale")); + } + } + #[BeforeStage('php', [php::class, 'buildconfForWindows'], 'ext-intl')] #[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 diff --git a/src/Package/Extension/pdo_pgsql.php b/src/Package/Extension/pdo_pgsql.php new file mode 100644 index 000000000..85ca1f5e1 --- /dev/null +++ b/src/Package/Extension/pdo_pgsql.php @@ -0,0 +1,26 @@ += 80400) { + return '--with-pdo-pgsql' . ($shared ? '=shared' : '') . pgsql::libpqConfigureVars($builder, $installer); + } + return '--with-pdo-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); + } +} diff --git a/src/Package/Extension/pgsql.php b/src/Package/Extension/pgsql.php index 5b7f50a06..a064caaf3 100644 --- a/src/Package/Extension/pgsql.php +++ b/src/Package/Extension/pgsql.php @@ -13,6 +13,7 @@ use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\TargetPackage; +use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SPCConfigUtil; @@ -24,16 +25,20 @@ class pgsql extends PhpExtensionPackage public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string { if (php::getPHPVersionID() >= 80400) { - $libfiles = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true])->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()))['libs']; - $libfiles = str_replace("{$builder->getLibDir()}/lib", '-l', $libfiles); - $libfiles = str_replace('.a', '', $libfiles); - return '--with-pgsql' . ($shared ? '=shared' : '') . - ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . - ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libfiles . '"'; + return '--with-pgsql' . ($shared ? '=shared' : '') . self::libpqConfigureVars($builder, $installer); } return '--with-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); } + /** These override pkg-config, so they must carry libpq itself too */ + public static function libpqConfigureVars(PackageBuilder $builder, PackageInstaller $installer): string + { + $closure = DependencyResolver::getResolvedPackageClosure(['postgresql'], array_keys($installer->getResolvedPackages())); + $libfiles = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->configWithResolvedPackages($closure)['libs']; + return ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libfiles . '"'; + } + #[CustomPhpConfigureArg('Windows')] public function getWindowsConfigureArg(bool $shared, PackageBuilder $builder): string { @@ -67,7 +72,8 @@ public function patchConfigW32ForWindows(TargetPackage $package): void public function getSharedExtensionEnv(): array { $parent = parent::getSharedExtensionEnv(); - $parent['CFLAGS'] .= ' -std=c17 -Wno-int-conversion'; + // gnu17, not c17: PHP 8.6 headers use typeof + $parent['CFLAGS'] .= ' -std=gnu17 -Wno-int-conversion'; return $parent; } } diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 37e59feab..ee505d80b 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -50,7 +50,7 @@ class php extends TargetPackage use frankenphp; /** @var string[] Supported major PHP versions */ - public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; + public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5', '8.6']; /** * Get PHP version ID from php_version.h diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index e306647f5..5a64ccc83 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -41,14 +41,16 @@ public function patchBeforeBuildconf(TargetPackage $package): void // php-src patches from micro (reads SPC_MICRO_PATCHES env var) SourcePatcher::patchPhpSrc(); - // patch configure.ac for musl and musl-toolchain + // patch libc detection for musl and musl-toolchain $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/configure.ac', - 'if command -v ldd >/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1', - 'if ' . ($musl ? 'true' : 'false') - ); + foreach (['configure.ac', 'build/php.m4'] as $libc_probe_file) { + FileSystem::replaceFileStr( + SOURCE_PATH . "/php-src/{$libc_probe_file}", + 'command -v ldd >/dev/null && ldd --version 2>&1 | grep ^musl >/dev/null 2>&1', + $musl ? 'true' : 'false' + ); + } // let php m4 tools use static pkg-config FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); @@ -134,6 +136,11 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $static_extension_str = $this->makeStaticExtensionString($installer); + $configure_str = "{$cmd} {$args} {$static_extension_str}"; + if ($version_id >= 80600) { + $configure_str = str_replace('--with-pic', '--enable-pic', $configure_str); + } + // reuse the same make vars so configure conftest links use the same LIBS (incl. -framework flags) $vars = $this->makeVars($installer); @@ -143,7 +150,7 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta 'CPPFLAGS' => "-I{$package->getIncludeDir()}", 'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), 'LIBS' => $vars['EXTRA_LIBS'] ?? '', - ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); + ])->exec($configure_str), $package->getSourceDir()); } #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index 8368c409f..949c040ac 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -10,6 +10,8 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { + use GitHubTokenSetupTrait; + public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; public const string API_URL = '/releases/index.php?json&version={version}'; @@ -20,6 +22,10 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda public const string GIT_REV = 'master'; + public const string GITHUB_TAGS_API = 'https://api.github.com/repos/php/php-src/git/matching-refs/tags/{prefix}'; + + public const string GITHUB_ARCHIVE_URL = 'https://github.com/php/php-src/archive/refs/tags/{tag}.tar.gz'; + private ?string $sha256 = ''; public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult @@ -30,20 +36,7 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $this->sha256 = null; return (new Git())->download($name, ['url' => self::GIT_URL, 'rev' => self::GIT_REV], $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); - $version = $info['version']; - foreach ($info['source'] as $source) { - if (str_ends_with($source['filename'], '.tar.xz')) { - $this->sha256 = $source['sha256']; - $filename = $source['filename']; - break; - } - } - if (!isset($filename)) { - throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); - } - $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; - $url .= str_replace('{version}', $version, self::DOWNLOAD_URL); + ['version' => $version, 'url' => $url, 'filename' => $filename, 'sha256' => $this->sha256] = $this->resolveRelease($name, $config, $downloader); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); @@ -79,8 +72,7 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A // git version: delegate to Git checkUpdate with master branch return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); } - $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); - $new_version = $info['version']; + $new_version = $this->resolveRelease($name, $config, $downloader)['version']; return new CheckUpdateResult( old: $old_version, new: $new_version, @@ -88,7 +80,75 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A ); } - protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): array + /** @return array{version: string, url: string, filename: string, sha256: null|string} */ + protected function resolveRelease(string $name, array $config, ArtifactDownloader $downloader): array + { + $phpver = $downloader->getOption('with-php', '8.5'); + $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); + if ($info === null) { + return $this->resolvePrereleaseFromGitTags($phpver, $downloader); + } + + $version = $info['version']; + $filename = null; + $sha256 = ''; + foreach ($info['source'] ?? [] as $source) { + if (str_ends_with($source['filename'] ?? '', '.tar.xz')) { + $sha256 = $source['sha256'] ?? ''; + $filename = $source['filename']; + break; + } + } + if ($filename === null) { + throw new DownloaderException("No suitable source tarball found for PHP version {$version}"); + } + $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; + $url .= str_replace('{version}', $version, self::DOWNLOAD_URL); + return ['version' => $version, 'url' => $url, 'filename' => $filename, 'sha256' => $sha256]; + } + + /** @return array{version: string, url: string, filename: string, sha256: null|string} */ + protected function resolvePrereleaseFromGitTags(string $phpver, ArtifactDownloader $downloader): array + { + $is_branch = preg_match('/^\d+\.\d+$/', $phpver) === 1; + $prefix = $is_branch ? "php-{$phpver}." : "php-{$phpver}"; + $url = str_replace('{prefix}', $prefix, self::GITHUB_TAGS_API); + logger()->debug("PHP version {$phpver} is not published on php.net, looking it up in php-src tags from {$url}"); + + $data = default_shell()->executeCurl($url, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry()); + if ($data === false) { + throw new DownloaderException("Failed to fetch php-src git tags for PHP version {$phpver}"); + } + $data = json_decode($data, true); + if (!is_array($data)) { + throw new DownloaderException("Invalid php-src git tag list received for PHP version {$phpver}"); + } + + $pattern = '/^php-(' . preg_quote($phpver, '/') . ($is_branch ? '\.\d+' : '') . '(?:(?:alpha|beta|RC)\d+)?)$/'; + $versions = []; + foreach ($data as $ref) { + $tag = substr((string) ($ref['ref'] ?? ''), strlen('refs/tags/')); + if (preg_match($pattern, $tag, $match) === 1) { + $versions[] = $match[1]; + } + } + if (empty($versions)) { + throw new DownloaderException("PHP version {$phpver} is not available on php.net nor tagged in php-src."); + } + usort($versions, version_compare(...)); + $version = end($versions); + + logger()->notice("PHP {$version} is a pre-release, downloading its source archive from php-src git tag."); + return [ + 'version' => $version, + 'url' => str_replace('{tag}', "php-{$version}", self::GITHUB_ARCHIVE_URL), + 'filename' => "php-{$version}.tar.gz", + 'sha256' => null, + ]; + } + + /** @return null|array null when php.net does not publish this version (yet) */ + protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): ?array { $phpver = $downloader->getOption('with-php', '8.5'); // Handle 'git' version to clone from php-src repository @@ -108,9 +168,13 @@ protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDown throw new DownloaderException("Failed to fetch PHP release info for version {$phpver}"); } $info = json_decode($info, true); - if (!is_array($info) || !isset($info['version'])) { + if (!is_array($info)) { throw new DownloaderException("Invalid PHP release info received for version {$phpver}"); } + if (!isset($info['version'])) { + logger()->debug("php.net has no release for PHP version {$phpver}: " . ($info['error'] ?? 'no version in response')); + return null; + } return $info; } } diff --git a/src/StaticPHP/Command/Dev/TestBotCommand.php b/src/StaticPHP/Command/Dev/TestBotCommand.php index 72f882ec2..86c71040d 100644 --- a/src/StaticPHP/Command/Dev/TestBotCommand.php +++ b/src/StaticPHP/Command/Dev/TestBotCommand.php @@ -33,6 +33,7 @@ class TestBotCommand extends BaseCommand private const array PHP_VERSION_LABELS = [ 'test/php-83' => '8.3', 'test/php-84' => '8.4', + 'test/php-86' => '8.6', ]; private const string DEFAULT_PHP_VERSION = '8.5'; diff --git a/src/StaticPHP/Util/SourcePatcher.php b/src/StaticPHP/Util/SourcePatcher.php index 430a44edc..49c8d9c42 100644 --- a/src/StaticPHP/Util/SourcePatcher.php +++ b/src/StaticPHP/Util/SourcePatcher.php @@ -263,13 +263,17 @@ public static function patchPhpSrc(?array $items = null): bool $spc_micro_patches = array_filter($spc_micro_patches, fn ($item) => trim((string) $item) !== ''); $patch_list = $spc_micro_patches; $patches = []; - $serial = ['80', '81', '82', '83', '84', '85']; + $serial = ['80', '81', '82', '83', '84', '85', '86']; + $start = array_search($major_ver, $serial, true); + if ($start === false) { + $start = count($serial) - 1; + } foreach ($patch_list as $patchName) { if (file_exists("{$patch_dir}/{$patchName}.patch")) { $patches[] = "{$patch_dir}/{$patchName}.patch"; continue; } - for ($i = array_search($major_ver, $serial, true); $i >= 0; --$i) { + for ($i = $start; $i >= 0; --$i) { $tryMajMin = $serial[$i]; if (!file_exists("{$patch_dir}/{$patchName}_{$tryMajMin}.patch")) { continue; diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 2e6e807e4..bab801301 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -11,13 +11,14 @@ // --------------------------------- edit area --------------------------------- -// test php version (8.1 ~ 8.4 available, multiple for matrix) +// test php version (8.1 ~ 8.6 available, multiple for matrix) $test_php_version = [ // '8.1', // '8.2', - '8.3', + // '8.3', // '8.4', '8.5', + '8.6', // 'git', ]; From 7fccae7df4fd5f52867266b2885abe3b48527711 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 8 Aug 2026 11:59:06 +0200 Subject: [PATCH 02/12] more fixes for 8.6 --- docs/deps-craft-yml.md | 3 + src/Package/Extension/swoole.php | 25 +++++- src/Package/Target/php.php | 39 ++++++++- src/Package/Target/php/unix.php | 39 +++++++-- src/StaticPHP/Command/CraftCommand.php | 4 +- src/StaticPHP/Package/PhpExtensionPackage.php | 81 ++++++++++++++++++- 6 files changed, 178 insertions(+), 13 deletions(-) diff --git a/docs/deps-craft-yml.md b/docs/deps-craft-yml.md index 1716d9f37..c9bc041fe 100644 --- a/docs/deps-craft-yml.md +++ b/docs/deps-craft-yml.md @@ -26,6 +26,9 @@ build-options: enable-zts: false # Disable smoke test, or for specific SAPIs comma-separated (default: false) no-smoke-test: false + # Do not abort when a shared extension fails to build or to load: skip it, drop its .so, and + # record it in buildroot/skipped-shared-extensions.json (default: false) + allow-shared-ext-failure: false # PHP configuration options (same as --with-config-file-path) with-config-file-path: "" # PHP configuration options (same as --with-config-file-scan-dir) diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php index 20257a226..deddee9c8 100644 --- a/src/Package/Extension/swoole.php +++ b/src/Package/Extension/swoole.php @@ -18,7 +18,9 @@ use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\SPCConfigUtil; +use ZM\Logger\ConsoleColor; #[Extension('swoole')] class swoole extends PhpExtensionPackage @@ -71,7 +73,7 @@ public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, Packa $arg = '--enable-swoole' . ($shared ? '=shared' : ''); // commonly used feature: coroutine-time - $arg .= ' --enable-swoole-coro-time --with-pic'; + $arg .= ' --enable-swoole-coro-time'; $arg .= $builder->getOption('enable-zts') ? ' --enable-swoole-thread --disable-thread-context' : ' --disable-swoole-thread --enable-thread-context'; @@ -119,7 +121,26 @@ public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, Packa } #[AfterStage('php', [php::class, 'smokeTestCliForUnix'], 'ext-swoole-hook-mysql')] - public function mysqlTest(PackageInstaller $installer): void + public function mysqlTest(PackageInstaller $installer, PackageBuilder $builder): void + { + // allow-shared-ext-failure already quarantined swoole and dropped its .so; there is nothing left to check + if ($this->isSharedSkipped() || $installer->getPhpExtensionPackage('swoole-hook-mysql')?->isSharedSkipped()) { + return; + } + + try { + $this->assertHooksEnabled($installer); + } catch (ValidationException $e) { + // only shared, separately-built extensions are skippable; static and build-with-php ones stay fatal + if (!$builder->getOption('allow-shared-ext-failure', false) || !$this->isBuildShared() || $this->isBuildWithPhp()) { + throw $e; + } + $this->markSharedSkipped('load', $e); + InteractiveTerm::error('SKIPPING shared extension ' . ConsoleColor::red($this->getExtensionName()) . ' — hook check failed (allow-shared-ext-failure): ' . $e->getMessage()); + } + } + + private function assertHooksEnabled(PackageInstaller $installer): void { [$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $this->getSharedExtensionLoadString() . ' --ri "swoole"', false); $out = implode('', $out); diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index ee505d80b..b168c801c 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -161,6 +161,7 @@ public function init(TargetPackage $package): void // embed build options if ($package->getName() === 'php' || $package->getName() === 'php-embed') { $package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); + $package->addBuildOption('allow-shared-ext-failure', null, null, 'Do not abort when a shared extension fails to build or to load; skip it, drop its .so, and record it in buildroot/skipped-shared-extensions.json'); $package->addBuildOption('maintainer-skip-build', null, null, '(maintainer only) skip embed build if exists'); } @@ -397,6 +398,7 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void // clean old modules that may conflict with the new php build FileSystem::removeDir(BUILD_MODULES_PATH); + FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/skipped-shared-extensions.json'); } /** @@ -454,7 +456,7 @@ public function syncExtensionSources(PackageInstaller $installer): void } #[Stage('postInstall')] - public function postInstall(TargetPackage $package, PackageInstaller $installer): void + public function postInstall(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { if ($package->getName() === 'frankenphp') { if (SystemTarget::getTargetOS() === 'Windows') { @@ -484,6 +486,41 @@ public function postInstall(TargetPackage $package, PackageInstaller $installer) InteractiveTerm::finish('PHP smoke tests passed'); } } + $this->writeSkippedSharedExtensionsManifest($package, $installer, $builder); + } + + /** + * Record the shared extensions quarantined by --allow-shared-ext-failure. + * Written unconditionally (even with an empty list) so consumers can tell + * "nothing was skipped" apart from "spc is too old to report". + */ + private function writeSkippedSharedExtensionsManifest(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + { + $skipped = []; + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if (($record = $ext->getSharedSkipRecord()) === null) { + continue; + } + $skipped[] = [ + 'package' => $ext->getName(), + 'extension' => $ext->getExtensionName(), + 'phase' => $record['phase'], + 'exception' => $record['exception'], + 'message' => $record['message'], + ]; + } + + FileSystem::writeFile(BUILD_ROOT_PATH . '/skipped-shared-extensions.json', json_encode([ + 'schema' => 1, + 'generated_at' => date('c'), + 'php_version_id' => self::getPHPVersionID(return_null_if_failed: true), + 'allow_shared_ext_failure' => (bool) $builder->getOption('allow-shared-ext-failure', false), + 'skipped' => $skipped, + ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + + if ($skipped !== []) { + $package->setOutput('Skipped shared extensions', implode(', ', array_column($skipped, 'extension'))); + } } private function makeStaticExtensionString(PackageInstaller $installer): string diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 5a64ccc83..b48f0919a 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -363,7 +363,7 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta } #[Stage] - public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void + public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void { // collect shared extensions /** @var PhpExtensionPackage[] $shared_extensions */ @@ -389,11 +389,27 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes'); } + $allow_failure = (bool) $builder->getOption('allow-shared-ext-failure', false); + if ($allow_failure) { + logger()->warning('allow-shared-ext-failure is ON: shared extension build/load failures will be skipped, not fatal.'); + } + try { logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); - $extension->buildShared(); + InteractiveTerm::setMessage('Building shared extension: ' . ConsoleColor::yellow($extension->getName())); + try { + $extension->buildShared(); + } catch (\Throwable $e) { + InteractiveTerm::error('Building shared extension failed: ' . ConsoleColor::red($extension->getName())); + if (!$allow_failure) { + throw $e; + } + $extension->markSharedSkipped('build', $e); + InteractiveTerm::error('SKIPPING shared extension ' . ConsoleColor::red($extension->getName()) . ' (allow-shared-ext-failure): ' . $e->getMessage()); + continue; + } + InteractiveTerm::success('Built shared extension: ' . ConsoleColor::green($extension->getName()), true); } } finally { // restore php-config @@ -517,7 +533,7 @@ public function patchUnixEmbedScripts(): void } #[Stage] - public function smokeTestCliForUnix(PackageInstaller $installer): void + public function smokeTestCliForUnix(PackageInstaller $installer, PackageBuilder $builder): void { InteractiveTerm::setMessage('Running basic php-cli smoke test'); [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); @@ -526,10 +542,23 @@ public function smokeTestCliForUnix(PackageInstaller $installer): void throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); } + $allow_failure = (bool) $builder->getOption('allow-shared-ext-failure', false); $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); foreach ($exts as $ext) { + if ($ext->isSharedSkipped()) { + continue; + } InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); - $ext->runSmokeTestCliUnix(); + try { + $ext->runSmokeTestCliUnix(); + } catch (\Throwable $e) { + // only shared, separately-built extensions are skippable; static and build-with-php ones stay fatal + if (!$allow_failure || !$ext->isBuildShared() || $ext->isBuildWithPhp()) { + throw $e; + } + $ext->markSharedSkipped('load', $e); + InteractiveTerm::error('SKIPPING shared extension ' . ConsoleColor::red($ext->getExtensionName()) . ' — failed to load (allow-shared-ext-failure): ' . $e->getMessage()); + } } } diff --git a/src/StaticPHP/Command/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php index 4ec67e6bf..f53aadc83 100644 --- a/src/StaticPHP/Command/CraftCommand.php +++ b/src/StaticPHP/Command/CraftCommand.php @@ -146,8 +146,8 @@ private function validateAndParseCraftFile(string $craft_file): array } // check php-version - if (isset($craft['php-version']) && !preg_match('/^(\d+)(\.\d+)?(\.\d+)?$/', strval($craft['php-version']))) { - throw new ValidationException("Craft file '{$craft_file}' has invalid 'php-version' field, it should be in format of '8.0.0'."); + if (isset($craft['php-version']) && !preg_match('/^(\d+)(\.\d+)?(\.\d+)?(?:(?:alpha|beta|RC)\d+)?$/', strval($craft['php-version']))) { + throw new ValidationException("Craft file '{$craft_file}' has invalid 'php-version' field, it should be in format of '8.0.0' or '8.6.0RC1'."); } // check php extensions field diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 0c40daa96..aa93b2292 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -31,6 +31,9 @@ class PhpExtensionPackage extends Package protected bool $build_with_php = false; + /** @var null|array{phase: string, exception: string, message: string} Set when --allow-shared-ext-failure quarantines this extension */ + protected ?array $skip_record = null; + /** * @param string $name Name of the php extension * @param string $type Type of the package, defaults to 'php-extension' @@ -198,6 +201,53 @@ public function isBuildWithPhp(): bool return $this->build_with_php; } + /** + * Quarantine this shared extension: drop it from every shared filter, delete the artefacts it + * may have left behind, and remember why so php::postInstall() can write it to the manifest. + * + * @param string $phase 'build' or 'load' + */ + public function markSharedSkipped(string $phase, \Throwable $e): void + { + $this->skip_record = ['phase' => $phase, 'exception' => $e::class, 'message' => $e->getMessage()]; + $this->setBuildShared(false); + $this->removeBuiltSharedObject(); + $this->setOutput('Shared extension SKIPPED', "{$phase} failure: {$e->getMessage()}"); + } + + /** + * @return null|array{phase: string, exception: string, message: string} + */ + public function getSharedSkipRecord(): ?array + { + return $this->skip_record; + } + + public function isSharedSkipped(): bool + { + return $this->skip_record !== null; + } + + /** + * Delete the .so (and its debug info) of a skipped shared extension so it can never be packaged. + */ + public function removeBuiltSharedObject(): void + { + // libtool's -release X gives $name-X.so as the real file, $name.so as a symlink to it + $release = preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m) ? "-{$m[1]}" : ''; + $name = $this->getExtensionName(); + foreach ([ + BUILD_MODULES_PATH . "/{$name}{$release}.so", + BUILD_MODULES_PATH . "/{$name}.so", + BUILD_ROOT_PATH . "/debug/{$name}{$release}.so.debug", + ] as $file) { + if (file_exists($file) || is_link($file)) { + @unlink($file); + logger()->warning("Removed artefact of skipped extension: {$file}"); + } + } + } + public function buildShared(): void { if ($this->hasStage('build')) { @@ -332,6 +382,30 @@ public function phpizeForUnix(array $env, PhpExtensionPackage $package): void shell()->cd($package->getSourceRoot())->setEnv($env)->exec(BUILD_BIN_PATH . '/phpize'); } + /** + * A phpize Makefile defines neither RE2C nor RE2C_FLAGS, so an extension whose sources ship + * only the .re file falls back to `$(RE2C) $(RE2C_FLAGS) -o out.c in.re` with both empty and + * dies with "-o: command not found". Release tarballs ship the generated .c, but php-src git + * checkouts and tag archives (every pre-release) do not — that is how ext/pdo breaks. Reuse + * whatever the main php-src configure resolved, so -g/cgoto stays consistent with the SAPIs. + * + * @return list + */ + public function re2cMakeVars(): array + { + $vars = ['RE2C' => 're2c', 'RE2C_FLAGS' => '--no-generation-date -W']; + $makefile = SOURCE_PATH . '/php-src/Makefile'; + if (is_file($makefile)) { + $content = (string) file_get_contents($makefile); + foreach (array_keys($vars) as $key) { + if (preg_match('/^' . $key . '\s*=\s*(.*)$/m', $content, $m) && trim($m[1]) !== '') { + $vars[$key] = trim($m[1]); + } + } + } + return array_map(static fn (string $k, string $v): string => $k . '=' . escapeshellarg($v), array_keys($vars), $vars); + } + /** * @internal */ @@ -354,11 +428,12 @@ public function configureForUnix(array $env, PhpExtensionPackage $package): void #[Stage] public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void { + $makeArgs = implode(' ', $package->re2cMakeVars()); shell()->cd($package->getSourceRoot()) ->setEnv($env) ->exec('make clean') - ->exec("make -j{$builder->concurrency}") - ->exec('make install') + ->exec("make -j{$builder->concurrency} {$makeArgs}") + ->exec("make install {$makeArgs}") // distclean after install: phpize build residues (.lo/.libs/config.status) would // poison a later static in-tree build sharing this source dir (php-src's make // clean uses find, which does not follow the ext symlink) @@ -443,7 +518,7 @@ public function getSharedExtensionLoadString(): string { $sharedExts = array_filter( $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), - fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() + fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() && !$ext->isSharedSkipped() ); if (empty($sharedExts)) { From 55323d2be95d91de4d4e39e735ea766260d958cd Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 8 Aug 2026 23:07:56 +0200 Subject: [PATCH 03/12] more fixes for 8.6 --- src/Package/Artifact/libaom.php | 17 ++++++++- src/Package/Target/php/unix.php | 65 +++++++++++++++++++++++++++++++++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/Package/Artifact/libaom.php b/src/Package/Artifact/libaom.php index ffdc7bb35..36becac9f 100644 --- a/src/Package/Artifact/libaom.php +++ b/src/Package/Artifact/libaom.php @@ -6,8 +6,9 @@ use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Exception\ValidationException; use StaticPHP\Runtime\SystemTarget; -use StaticPHP\Util\SourcePatcher; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\System\LinuxUtil; class libaom @@ -17,6 +18,18 @@ class libaom public function patch(string $target_path): void { spc_skip_if(SystemTarget::getTargetOS() !== 'Linux' || !LinuxUtil::isMuslDist(), 'Only for Linux Musl distros'); - SourcePatcher::patchFile('libaom_posix_implict.patch', $target_path); + + $cmakelists = $target_path . '/CMakeLists.txt'; + $content = FileSystem::readFile($cmakelists); + if (str_contains($content, '_POSIX_C_SOURCE')) { + return; + } + foreach (['if(ENABLE_APPS)', 'if(ENABLE_EXAMPLES)'] as $guard) { + if (str_contains($content, $guard)) { + FileSystem::replaceFileStr($cmakelists, $guard, $guard . "\n add_definitions(-D_POSIX_C_SOURCE=200112L)"); + return; + } + } + throw new ValidationException('libaom CMakeLists.txt has neither an ENABLE_APPS nor an ENABLE_EXAMPLES guard to patch'); } } diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index b48f0919a..3927c4bb3 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -387,6 +387,7 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa FileSystem::replaceFileLineContainsString(BUILD_BIN_PATH . '/php-config', 'extension_dir=', 'extension_dir="' . BUILD_MODULES_PATH . '"'); FileSystem::replaceFileStr(BUILD_LIB_PATH . '/php/build/phpize.m4', 'test "[$]$1" = "no" && $1=yes', '# test "[$]$1" = "no" && $1=yes'); + $this->installRemovedMacroCompatHeader(); } $allow_failure = (bool) $builder->getOption('allow-shared-ext-failure', false); @@ -815,6 +816,70 @@ private function processLibphpSoFile(string $libphpSo, PackageInstaller $install } } + /** + * PHP 8.6 dropped a batch of long-deprecated aliases + */ + private function installRemovedMacroCompatHeader(): void + { + if (php::getPHPVersionID() < 80600) { + return; + } + $header = BUILD_ROOT_PATH . '/include/php/main/php.h'; + if (!file_exists($header) || str_contains((string) file_get_contents($header), 'SPC_REMOVED_MACRO_COMPAT')) { + return; + } + FileSystem::writeFile($header, <<<'C' + + #ifndef SPC_REMOVED_MACRO_COMPAT + #define SPC_REMOVED_MACRO_COMPAT + #ifndef XtOffsetOf + # define XtOffsetOf(s_type, field) offsetof(s_type, field) + #endif + #ifndef ZVAL_IS_NULL + # define ZVAL_IS_NULL(z) (Z_TYPE_P(z) == IS_NULL) + #endif + #ifndef zval_dtor + # define zval_dtor(zvalue) zval_ptr_dtor_nogc(zvalue) + #endif + #ifndef zval_is_true + # define zval_is_true(op) zend_is_true(op) + #endif + #ifndef INI_INT + # define INI_INT(name) zend_ini_long((name), strlen(name), 0) + #endif + #ifndef INI_FLT + # define INI_FLT(name) zend_ini_double((name), strlen(name), 0) + #endif + #ifndef INI_STR + # define INI_STR(name) zend_ini_string_ex((name), strlen(name), 0, NULL) + #endif + #ifndef INI_BOOL + # define INI_BOOL(name) ((bool) INI_INT(name)) + #endif + #ifndef ZEND_PARSE_PARAMS_THROW + # define ZEND_PARSE_PARAMS_THROW 0 + #endif + #ifndef EMPTY_SWITCH_DEFAULT_CASE + # define EMPTY_SWITCH_DEFAULT_CASE() default: ZEND_UNREACHABLE(); break; + #endif + #ifndef zend_parse_parameters_throw + # define zend_parse_parameters_throw(num_args, ...) zend_parse_parameters(num_args, __VA_ARGS__) + #endif + #ifndef ZEND_WRONG_PARAM_COUNT + # define ZEND_WRONG_PARAM_COUNT() { zend_wrong_param_count(); return; } + #endif + #ifndef WRONG_PARAM_COUNT + # define WRONG_PARAM_COUNT ZEND_WRONG_PARAM_COUNT() + #endif + #ifndef OPENBASEDIR_CHECKPATH + # define OPENBASEDIR_CHECKPATH(filename) php_check_open_basedir(filename) + #endif + #endif + + C, FILE_APPEND); + logger()->info('Restored macros removed in PHP 8.6 for phpize builds'); + } + /** * Make environment variables for php make. * This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking. From aae7de113ab365986a272c9fb6c93b0209197d4b Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 10 Aug 2026 10:50:49 +0200 Subject: [PATCH 04/12] use configForResolvedBuild() for libpq configure vars Co-Authored-By: Claude Opus 5 (1M context) --- src/Package/Extension/pdo_pgsql.php | 7 ++++++- src/Package/Extension/pgsql.php | 16 +++++----------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/Package/Extension/pdo_pgsql.php b/src/Package/Extension/pdo_pgsql.php index 85ca1f5e1..eb9da7e72 100644 --- a/src/Package/Extension/pdo_pgsql.php +++ b/src/Package/Extension/pdo_pgsql.php @@ -10,6 +10,7 @@ use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; +use StaticPHP\Util\SPCConfigUtil; #[Extension('pdo_pgsql')] class pdo_pgsql extends PhpExtensionPackage @@ -19,7 +20,11 @@ class pdo_pgsql extends PhpExtensionPackage public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string { if (php::getPHPVersionID() >= 80400) { - return '--with-pdo-pgsql' . ($shared ? '=shared' : '') . pgsql::libpqConfigureVars($builder, $installer); + // These override pkg-config, so they must carry libpq itself too + $libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->configForResolvedBuild(['postgresql'], $installer)['libs']; + return '--with-pdo-pgsql' . ($shared ? '=shared' : '') . + ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libs . '"'; } return '--with-pdo-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); } diff --git a/src/Package/Extension/pgsql.php b/src/Package/Extension/pgsql.php index a064caaf3..5096c50d6 100644 --- a/src/Package/Extension/pgsql.php +++ b/src/Package/Extension/pgsql.php @@ -13,7 +13,6 @@ use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Package\TargetPackage; -use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; use StaticPHP\Util\SPCConfigUtil; @@ -25,20 +24,15 @@ class pgsql extends PhpExtensionPackage public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string { if (php::getPHPVersionID() >= 80400) { - return '--with-pgsql' . ($shared ? '=shared' : '') . self::libpqConfigureVars($builder, $installer); + // These override pkg-config, so they must carry libpq itself too + $libs = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->configForResolvedBuild(['postgresql'], $installer)['libs']; + return '--with-pgsql' . ($shared ? '=shared' : '') . + ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libs . '"'; } return '--with-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); } - /** These override pkg-config, so they must carry libpq itself too */ - public static function libpqConfigureVars(PackageBuilder $builder, PackageInstaller $installer): string - { - $closure = DependencyResolver::getResolvedPackageClosure(['postgresql'], array_keys($installer->getResolvedPackages())); - $libfiles = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true])->configWithResolvedPackages($closure)['libs']; - return ' PGSQL_CFLAGS=-I' . $builder->getIncludeDir() . - ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libfiles . '"'; - } - #[CustomPhpConfigureArg('Windows')] public function getWindowsConfigureArg(bool $shared, PackageBuilder $builder): string { From 23a943cd203b73ea0be664881561e903d92ec75e Mon Sep 17 00:00:00 2001 From: Marc Date: Mon, 10 Aug 2026 13:04:18 +0200 Subject: [PATCH 05/12] fix gd and intl on windows --- config/pkg/lib/libwebp.yml | 1 + src/Package/Artifact/php_src.php | 4 +- src/Package/Extension/intl.php | 7 +- src/Package/Target/php/windows.php | 3 +- src/globals/extra/gd_config_86.w32 | 106 +++++++++++++++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/globals/extra/gd_config_86.w32 diff --git a/config/pkg/lib/libwebp.yml b/config/pkg/lib/libwebp.yml index 24873b127..299b633c2 100644 --- a/config/pkg/lib/libwebp.yml +++ b/config/pkg/lib/libwebp.yml @@ -18,4 +18,5 @@ libwebp: - libwebp.lib - libwebpdecoder.lib - libwebpdemux.lib + - libwebpmux.lib - libsharpyuv.lib diff --git a/src/Package/Artifact/php_src.php b/src/Package/Artifact/php_src.php index b52ba7282..ab164574d 100644 --- a/src/Package/Artifact/php_src.php +++ b/src/Package/Artifact/php_src.php @@ -45,7 +45,9 @@ public function patchGDWin32(): void FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/ext/gd/libgd/gdft.c', '#ifndef MSWIN32', '#ifndef _WIN32'); } // custom config.w32, because official config.w32 is hard-coded many things - if ($ver_id >= 80500) { + if ($ver_id >= 80600) { + $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_86.w32'); + } elseif ($ver_id >= 80500) { $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_85.w32'); } elseif ($ver_id >= 80100) { $origin = file_get_contents(ROOT_DIR . '/src/globals/extra/gd_config_81.w32'); diff --git a/src/Package/Extension/intl.php b/src/Package/Extension/intl.php index 8637a0198..26331c388 100644 --- a/src/Package/Extension/intl.php +++ b/src/Package/Extension/intl.php @@ -31,10 +31,13 @@ public function forceLibcxxLocaleBeforePhpHeaders(): void public function patchBeforeBuildconfForWindows(PackageInstaller $installer): void { $php_src = $installer->getTargetPackage('php')->getSourceDir(); + // Match only the tail of the EXTENSION() call: the source list changes between PHP + // versions (8.6 added intl_icu_compat.c) and a missed replacement silently leaves the + // hardcoded true, which builds intl shared and drops it from the static binary. FileSystem::replaceFileStr( "{$php_src}/ext/intl/config.w32", - '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,' + 'intl_error.c ", true,', + 'intl_error.c ", PHP_INTL_SHARED,' ); // ICU 73+ headers (char16ptr.h etc.) unconditionally include which requires C++17. FileSystem::replaceFileStr( diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 7bfcd53eb..48ca985d3 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -553,7 +553,8 @@ public function patchBeforeBuildconfForWindows(TargetPackage $package): void $vc_matches = ['unknown', 'unknown']; } else { $vc_matches = match ($vc['major_version']) { - '18', // VS 2026 shares the VS2022 (v143) runtime conventions, so it reports as VS17. + // PHP >= 8.6 knows about VS 2026 (v144), so report it as VS18. + '18' => $this->getPHPVersionID() >= 80600 ? ['VS18', 'Visual C++ 2026'] : ['VS17', 'Visual C++ 2022'], '17' => ['VS17', 'Visual C++ 2022'], '16' => ['VS16', 'Visual C++ 2019'], default => ['unknown', 'unknown'], diff --git a/src/globals/extra/gd_config_86.w32 b/src/globals/extra/gd_config_86.w32 new file mode 100644 index 000000000..716965c35 --- /dev/null +++ b/src/globals/extra/gd_config_86.w32 @@ -0,0 +1,106 @@ +// vim:ft=javascript + +ARG_WITH("gd", "Bundled GD support", "yes"); + +if (PHP_GD != "no") { + // check for gd.h (required) + if (!CHECK_HEADER_ADD_INCLUDE("gd.h", "CFLAGS_GD", PHP_GD + ";ext\\gd\\libgd")) { + ERROR("gd not enabled; libraries and headers not found"); + } + + // zlib ext support (required) + if (!CHECK_LIB("zlib_a.lib;zlib.lib", "gd", PHP_GD)) { + ERROR("gd not enabled; zlib not enabled"); + } + + // libjpeg lib support + if (CHECK_LIB("libjpeg_a.lib;libjpeg.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("jpeglib.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include")) { + AC_DEFINE("HAVE_LIBJPEG", 1, "JPEG support"); + AC_DEFINE("HAVE_GD_JPG", 1, "JPEG support"); + } + + // libpng16 lib support + if (CHECK_LIB("libpng_a.lib;libpng.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("png.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\libpng16")) { + AC_DEFINE("HAVE_LIBPNG", 1, "PNG support"); + AC_DEFINE("HAVE_GD_PNG", 1, "PNG support"); + } + + // freetype lib support + if (CHECK_LIB("libfreetype_a.lib;libfreetype.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("ft2build.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\freetype2;" + PHP_PHP_BUILD + "\\include\\freetype")) { + AC_DEFINE("HAVE_LIBFREETYPE", 1, "FreeType support"); + AC_DEFINE("HAVE_GD_FREETYPE", 1, "FreeType support"); + } + + // xpm lib support + if (CHECK_LIB("libXpm_a.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("xpm.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\X11")) { + AC_DEFINE("HAVE_LIBXPM", 1, "XPM support"); + AC_DEFINE("HAVE_GD_XPM", 1, "XPM support"); + } + + // iconv lib support + if ((CHECK_LIB("libiconv_a.lib;libiconv.lib", "gd", PHP_GD) || CHECK_LIB("iconv_a.lib;iconv.lib", "gd", PHP_GD)) && + CHECK_HEADER_ADD_INCLUDE("iconv.h", "CFLAGS_GD", PHP_GD)) { + AC_DEFINE("HAVE_LIBICONV", 1, "Iconv support"); + } + + // libwebp lib support + // 8.6's bundled libgd uses the WebPAnimDecoder/WebPAnimEncoder APIs unconditionally, + // so demux and mux are required alongside the base library (see ext/gd/config.m4). + if ((CHECK_LIB("libwebp_a.lib", "gd", PHP_GD) || CHECK_LIB("libwebp.lib", "gd", PHP_GD)) && + (CHECK_LIB("libwebpdemux_a.lib", "gd", PHP_GD) || CHECK_LIB("libwebpdemux.lib", "gd", PHP_GD)) && + (CHECK_LIB("libwebpmux_a.lib", "gd", PHP_GD) || CHECK_LIB("libwebpmux.lib", "gd", PHP_GD)) && + CHECK_LIB("libsharpyuv.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("decode.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp") && + CHECK_HEADER_ADD_INCLUDE("demux.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp") && + CHECK_HEADER_ADD_INCLUDE("mux.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp") && + CHECK_HEADER_ADD_INCLUDE("encode.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\webp")) { + AC_DEFINE("HAVE_LIBWEBP", 1, "WebP support"); + AC_DEFINE("HAVE_GD_WEBP", 1, "WebP support"); + } + + // libavif lib support + if (CHECK_LIB("avif_a.lib", "gd", PHP_GD) && + CHECK_LIB("aom_a.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("avif.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\avif")) { + ADD_FLAG("CFLAGS_GD", "/D HAVE_LIBAVIF /D HAVE_GD_AVIF"); + } else if (CHECK_LIB("avif.lib", "gd", PHP_GD) && + CHECK_HEADER_ADD_INCLUDE("avif.h", "CFLAGS_GD", PHP_GD + ";" + PHP_PHP_BUILD + "\\include\\avif")) { + ADD_FLAG("CFLAGS_GD", "/D HAVE_LIBAVIF /D HAVE_GD_AVIF"); + } + + CHECK_LIB("User32.lib", "gd", PHP_GD); + CHECK_LIB("Gdi32.lib", "gd", PHP_GD); + + EXTENSION("gd", "gd.c", null, "-Iext/gd/libgd"); + ADD_SOURCES("ext/gd/libgd", "gd.c \ + gdcache.c gdfontg.c gdfontl.c gdfontmb.c gdfonts.c gdfontt.c \ + gdft.c gd_gd2.c gd_gd.c gd_gif_in.c gd_gif_out.c gdhelpers.c gd_io.c gd_io_dp.c \ + gd_io_file.c gd_io_ss.c gd_jpeg.c gdkanji.c gd_png.c gd_ss.c \ + gdtables.c gd_topal.c gd_wbmp.c gdxpm.c wbmp.c gd_xbm.c gd_security.c gd_transform.c \ + gd_filter.c gd_rotate.c gd_color_match.c gd_webp.c gd_avif.c \ + gd_crop.c gd_interpolation.c gd_matrix.c gd_bmp.c gd_tga.c \ + gd_metadata.c gd_qoi.c gd_jxl.c gd_color_map.c gd_heif.c gd_uhdr.c gd_tiff.c \ + gd_nnquant.c gd_color.c gd_readimage.c gd_filename.c gd_array.c gd_span_rle.c \ + gd_surface.c gd_version.c gd_compositor.c gd_gradient.c gd_path.c gd_path_arc.c \ + gd_path_dash.c gd_path_matrix.c gd_path_stroke.c gd_draw.c gd_draw_blend.c \ + gd_perceptual_diff.c", "gd"); + ADD_SOURCES("ext/gd/libgd/ftraster", "gd_ft_math.c gd_ft_raster.c gd_ft_stroker.c", "gd"); + + AC_DEFINE('HAVE_LIBGD', 1, 'GD support'); + AC_DEFINE('HAVE_GD_BUNDLED', 1, "Bundled GD"); + AC_DEFINE('HAVE_GD_BMP', 1, "BMP support"); + AC_DEFINE('HAVE_GD_TGA', 1, "TGA support"); + ADD_FLAG("CFLAGS_GD", " \ +/D PHP_GD_EXPORTS=1 \ +/D HAVE_GD_GET_INTERPOLATION \ + "); + if (ICC_TOOLSET) { + ADD_FLAG("LDFLAGS_GD", "/nodefaultlib:libcmt"); + } + + PHP_INSTALL_HEADERS("", "ext/gd ext/gd/libgd"); +} From 78a57e99c9ff85b886f5f8369cc890a5073efa74 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 10 Aug 2026 17:31:52 +0200 Subject: [PATCH 06/12] XtOffsetOf -> offsetof --- src/Package/Target/php.php | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index b168c801c..9113a8790 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -15,6 +15,7 @@ use StaticPHP\Attribute\Package\Stage; use StaticPHP\Attribute\Package\Target; use StaticPHP\Attribute\Package\Validate; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; @@ -455,6 +456,36 @@ public function syncExtensionSources(PackageInstaller $installer): void } } + #[BeforeStage('php', 'build')] + #[PatchDescription('Replace XtOffsetOf (removed in PHP 8.6) with offsetof in extension sources')] + public function patchExtensionXtOffsetOf(PackageInstaller $installer): void + { + if (self::getPHPVersionID() < 80600) { + return; + } + foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { + // extensions bundled with php-src are already 8.6-clean + if ($ext->getArtifact() === null) { + continue; + } + $build_dir = $ext->getBuildDir(); + if (!is_dir($build_dir)) { + continue; + } + foreach (FileSystem::scanDirFiles($build_dir) ?: [] as $file) { + if (!in_array(FileSystem::extname($file), ['c', 'cc', 'cpp', 'cxx', 'h', 'hpp'], true)) { + continue; + } + $content = FileSystem::readFile($file); + if (!str_contains($content, 'XtOffsetOf')) { + continue; + } + logger()->debug("Replacing XtOffsetOf with offsetof in {$file}"); + FileSystem::writeFile($file, str_replace('XtOffsetOf', 'offsetof', $content)); + } + } + } + #[Stage('postInstall')] public function postInstall(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { From a3a362cfd2f07c932fe8e8669e4fba7b21385422 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 10:32:45 +0200 Subject: [PATCH 07/12] php 8.6.0~beta1 compatibility --- config/env.ini | 2 +- src/Package/Target/php/unix.php | 5 +- src/Package/Target/php/windows.php | 3 +- src/StaticPHP/Artifact/ArtifactExtractor.php | 5 +- .../patch/php-src-patches/cli_checks_86.patch | 178 ++++++++++++++++++ 5 files changed, 189 insertions(+), 4 deletions(-) create mode 100644 src/globals/patch/php-src-patches/cli_checks_86.patch diff --git a/config/env.ini b/config/env.ini index cec799156..9d0ff86b9 100644 --- a/config/env.ini +++ b/config/env.ini @@ -120,7 +120,7 @@ SPC_CMD_PREFIX_PHP_CONFIGURE="./configure --prefix= --with-valgrind=no --disable ; embed type for php, static (libphp.a) or shared (libphp.so) SPC_CMD_VAR_PHP_EMBED_TYPE="static" ; EXTRA_CFLAGS for `configure` and `make` php -SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE -fvisibility=hidden -fvisibility-inlines-hidden ${SPC_DEFAULT_CFLAGS}" +SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE -fvisibility=hidden ${SPC_DEFAULT_CFLAGS}" ; EXTRA_CXXFLAGS for `configure` and `make` php SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE -fvisibility=hidden -fvisibility-inlines-hidden ${SPC_DEFAULT_CXXFLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 3927c4bb3..3401f577c 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -120,7 +120,10 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args[] = "--with-config-file-scan-dir={$option}"; } // perform enable cli options - $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; + // PHP >= 8.6 links the CLI objects into libphp for do_php_cli() + $cli = $installer->isPackageResolved('php-cli') + || ($version_id >= 80600 && $installer->isPackageResolved('php-embed')); + $args[] = $cli ? '--enable-cli' : '--disable-cli'; $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' . ($installer->isPackageResolved('libacl') ? ' --with-fpm-acl' : '') : '--disable-fpm'; diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 48ca985d3..2ab15b832 100644 --- a/src/Package/Target/php/windows.php +++ b/src/Package/Target/php/windows.php @@ -65,10 +65,11 @@ public function configureForWindows(TargetPackage $package, PackageInstaller $in "--with-extra-libs={$package->getLibDir()}", ]; // sapis - $cli = $installer->isPackageResolved('php-cli'); $cgi = $installer->isPackageResolved('php-cgi'); $micro = $installer->isPackageResolved('php-micro'); $embed = $installer->isPackageResolved('php-embed'); + // PHP >= 8.6 links the CLI objects into libphp for do_php_cli() + $cli = $installer->isPackageResolved('php-cli') || ($embed && self::getPHPVersionID() >= 80600); $args[] = $cli ? '--enable-cli=yes' : '--enable-cli=no'; $args[] = $cgi ? '--enable-cgi=yes' : '--enable-cgi=no'; $args[] = $micro ? '--enable-micro=yes' : '--enable-micro=no'; diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 2237e0bff..086e02455 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -175,7 +175,10 @@ protected function extractSource(Artifact $artifact): int } // Remove old directory if hash mismatch - if (is_dir($target_path)) { + if (FileSystem::isLink($target_path)) { + logger()->debug("Source [{$name}] is linked to a local directory, relinking..."); + FileSystem::removeLink($target_path); + } elseif (is_dir($target_path)) { logger()->notice("Source [{$name}] hash mismatch, re-extracting..."); FileSystem::removeDir($target_path); } diff --git a/src/globals/patch/php-src-patches/cli_checks_86.patch b/src/globals/patch/php-src-patches/cli_checks_86.patch new file mode 100644 index 000000000..e1518a0a3 --- /dev/null +++ b/src/globals/patch/php-src-patches/cli_checks_86.patch @@ -0,0 +1,178 @@ +diff --git a/TSRM/tsrm_win32.c b/TSRM/tsrm_win32.c +index 90317ab..52f7f5b 100644 +--- a/TSRM/tsrm_win32.c ++++ b/TSRM/tsrm_win32.c +@@ -533,7 +533,7 @@ TSRM_API FILE *popen_ex(const char *command, const char *type, const char *cwd, + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if (strcmp(sapi_module.name, "cli") != 0) { ++ if (strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + +diff --git a/ext/ffi/ffi.c b/ext/ffi/ffi.c +index c3c0832..4184e17 100644 +--- a/ext/ffi/ffi.c ++++ b/ext/ffi/ffi.c +@@ -5459,7 +5459,7 @@ ZEND_MINIT_FUNCTION(ffi) + { + REGISTER_INI_ENTRIES(); + +- FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0; ++ FFI_G(is_cli) = strcmp(sapi_module.name, "cli") == 0 || strcmp(sapi_module.name, "micro") == 0; + + zend_ffi_exception_ce = register_class_FFI_Exception(zend_ce_error); + +diff --git a/ext/opcache/ZendAccelerator.c b/ext/opcache/ZendAccelerator.c +index cf62765..e0d9510 100644 +--- a/ext/opcache/ZendAccelerator.c ++++ b/ext/opcache/ZendAccelerator.c +@@ -3066,6 +3066,7 @@ static void zps_startup_failure(const char *reason, const char *api_reason, int + static inline bool accel_sapi_is_cli(void) + { + return strcmp(sapi_module.name, "cli") == 0 ++ || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0; + } + +@@ -3413,6 +3414,7 @@ static int accel_startup(zend_extension *extension) + #ifdef HAVE_HUGE_CODE_PAGES + if (ZCG(accel_directives).huge_code_pages && + (strcmp(sapi_module.name, "cli") == 0 || ++ strcmp(sapi_module.name, "micro") == 0 || + strcmp(sapi_module.name, "cli-server") == 0 || + strcmp(sapi_module.name, "cgi-fcgi") == 0 || + strcmp(sapi_module.name, "fpm-fcgi") == 0)) { +@@ -5208,6 +5210,7 @@ static zend_result accel_finish_startup_preload_subprocess(pid_t *pid) + || !*ZCG(accel_directives).preload_user) { + + bool sapi_requires_preload_user = !(strcmp(sapi_module.name, "cli") == 0 ++ || strcmp(sapi_module.name, "micro") == 0 + || strcmp(sapi_module.name, "phpdbg") == 0); + + if (!sapi_requires_preload_user) { +diff --git a/ext/pdo_sqlite/pdo_sqlite.c b/ext/pdo_sqlite/pdo_sqlite.c +index 2da9329..9331c96 100644 +--- a/ext/pdo_sqlite/pdo_sqlite.c ++++ b/ext/pdo_sqlite/pdo_sqlite.c +@@ -92,6 +92,7 @@ PHP_METHOD(Pdo_Sqlite, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { + zend_throw_exception_ex(php_pdo_get_exception(), 0, "Not supported in multithreaded Web servers"); +diff --git a/ext/readline/readline_cli.c b/ext/readline/readline_cli.c +index 4ffe4df..6a1990b 100644 +--- a/ext/readline/readline_cli.c ++++ b/ext/readline/readline_cli.c +@@ -730,7 +730,7 @@ typedef cli_shell_callbacks_t *(__cdecl *get_cli_shell_callbacks)(void); + get_cli_shell_callbacks get_callbacks; \ + HMODULE hMod = GetModuleHandle("php.exe"); \ + (cb) = NULL; \ +- if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3)) { \ ++ if (strlen(sapi_module.name) >= 3 && 0 == strncmp("cli", sapi_module.name, 3) || 0 == strcmp("micro", sapi_module.name)) { \ + get_callbacks = (get_cli_shell_callbacks)GetProcAddress(hMod, "php_cli_get_shell_callbacks"); \ + if (get_callbacks) { \ + (cb) = get_callbacks(); \ +diff --git a/ext/sqlite3/sqlite3.c b/ext/sqlite3/sqlite3.c +index 4591892..526c092 100644 +--- a/ext/sqlite3/sqlite3.c ++++ b/ext/sqlite3/sqlite3.c +@@ -395,6 +395,7 @@ PHP_METHOD(SQLite3, loadExtension) + #ifdef ZTS + if ((strncmp(sapi_module.name, "cgi", 3) != 0) && + (strcmp(sapi_module.name, "cli") != 0) && ++ (strcmp(sapi_module.name, "micro") != 0) && + (strncmp(sapi_module.name, "embed", 5) != 0) + ) { php_sqlite3_error(db_obj, 0, "Not supported in multithreaded Web servers"); + RETURN_FALSE; +diff --git a/ext/standard/php_fopen_wrapper.c b/ext/standard/php_fopen_wrapper.c +index cca9445..4b12822 100644 +--- a/ext/standard/php_fopen_wrapper.c ++++ b/ext/standard/php_fopen_wrapper.c +@@ -270,7 +270,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + "URL file-access is disabled in the server configuration"); + return NULL; + } +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_in = 0; + fd = STDIN_FILENO; + if (cli_in) { +@@ -286,7 +286,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stdout")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_out = 0; + fd = STDOUT_FILENO; + if (cli_out++) { +@@ -302,7 +302,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + pipe_requested = 1; + #endif + } else if (!strcasecmp(path, "stderr")) { +- if (!strcmp(sapi_module.name, "cli")) { ++ if (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "micro")) { + static int cli_err = 0; + fd = STDERR_FILENO; + if (cli_err++) { +@@ -323,7 +323,7 @@ static php_stream * php_stream_url_wrap_php(php_stream_wrapper *wrapper, const c + zend_long fildes_ori; + int dtablesize; + +- if (strcmp(sapi_module.name, "cli")) { ++ if (strcmp(sapi_module.name, "cli") && strcmp(sapi_module.name, "micro")) { + php_stream_wrapper_warn(wrapper, context, options, + Disabled, + "Direct access to file descriptors is only available from command-line PHP"); +diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c +index beeb157..9d71f39 100644 +--- a/ext/standard/proc_open.c ++++ b/ext/standard/proc_open.c +@@ -1348,7 +1348,7 @@ PHP_FUNCTION(proc_open) + } + + dwCreateFlags = NORMAL_PRIORITY_CLASS; +- if(strcmp(sapi_module.name, "cli") != 0) { ++ if(strcmp(sapi_module.name, "cli") != 0 && strcmp(sapi_module.name, "micro") != 0) { + dwCreateFlags |= CREATE_NO_WINDOW; + } + if (create_process_group) { +diff --git a/main/main.c b/main/main.c +index 0539220..b2263c8 100644 +--- a/main/main.c ++++ b/main/main.c +@@ -549,7 +549,7 @@ static PHP_INI_DISP(display_errors_mode) + mode = php_get_display_errors_mode(temporary_value); + + /* Display 'On' for other SAPIs instead of STDOUT or STDERR */ +- cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")); ++ cgi_or_cli = (!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")); + + switch (mode) { + case PHP_DISPLAY_ERRORS_STDERR: +@@ -1433,7 +1433,7 @@ static ZEND_COLD void php_error_cb(int orig_type, zend_string *error_filename, c + } + } else { + /* Write CLI/CGI errors to stderr if display_errors = "stderr" */ +- if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg")) && ++ if ((!strcmp(sapi_module.name, "cli") || !strcmp(sapi_module.name, "cgi") || !strcmp(sapi_module.name, "phpdbg") || !strcmp(sapi_module.name, "micro")) && + PG(display_errors) == PHP_DISPLAY_ERRORS_STDERR + ) { + fprintf(stderr, "%s: ", error_type_str); +diff --git a/win32/console.c b/win32/console.c +index 44b614e..9953dcf 100644 +--- a/win32/console.c ++++ b/win32/console.c +@@ -109,6 +109,6 @@ PHP_WINUTIL_API BOOL php_win32_console_is_own(void) + + PHP_WINUTIL_API BOOL php_win32_console_is_cli_sapi(void) + {/*{{{*/ +- return strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1); ++ return (strlen(sapi_module.name) >= sizeof("cli") - 1 && !strncmp(sapi_module.name, "cli", sizeof("cli") - 1)) || 0 == strcmp(sapi_module.name, "micro"); + }/*}}}*/ + From bb92c7572ae0fabaa63ea14c4d0e3ff9d20ed47e Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 13:48:51 +0200 Subject: [PATCH 08/12] use existing patch instead --- src/Package/Artifact/libaom.php | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Package/Artifact/libaom.php b/src/Package/Artifact/libaom.php index 36becac9f..ffdc7bb35 100644 --- a/src/Package/Artifact/libaom.php +++ b/src/Package/Artifact/libaom.php @@ -6,9 +6,8 @@ use StaticPHP\Attribute\Artifact\AfterSourceExtract; use StaticPHP\Attribute\PatchDescription; -use StaticPHP\Exception\ValidationException; use StaticPHP\Runtime\SystemTarget; -use StaticPHP\Util\FileSystem; +use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\System\LinuxUtil; class libaom @@ -18,18 +17,6 @@ class libaom public function patch(string $target_path): void { spc_skip_if(SystemTarget::getTargetOS() !== 'Linux' || !LinuxUtil::isMuslDist(), 'Only for Linux Musl distros'); - - $cmakelists = $target_path . '/CMakeLists.txt'; - $content = FileSystem::readFile($cmakelists); - if (str_contains($content, '_POSIX_C_SOURCE')) { - return; - } - foreach (['if(ENABLE_APPS)', 'if(ENABLE_EXAMPLES)'] as $guard) { - if (str_contains($content, $guard)) { - FileSystem::replaceFileStr($cmakelists, $guard, $guard . "\n add_definitions(-D_POSIX_C_SOURCE=200112L)"); - return; - } - } - throw new ValidationException('libaom CMakeLists.txt has neither an ENABLE_APPS nor an ENABLE_EXAMPLES guard to patch'); + SourcePatcher::patchFile('libaom_posix_implict.patch', $target_path); } } From 05e1f1dd2b3d026c0d54087fdfaf91fc35226c89 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 14:38:08 +0200 Subject: [PATCH 09/12] remove test-extensions.php --- phpstan.neon | 1 - src/globals/test-extensions.php | 235 -------------------------------- 2 files changed, 236 deletions(-) delete mode 100644 src/globals/test-extensions.php diff --git a/phpstan.neon b/phpstan.neon index 46efc171a..98957d900 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -19,4 +19,3 @@ parameters: excludePaths: analyseAndScan: - ./src/globals/ext-tests/ - - ./src/globals/test-extensions.php diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php deleted file mode 100644 index bab801301..000000000 --- a/src/globals/test-extensions.php +++ /dev/null @@ -1,235 +0,0 @@ - 'openssl,zstd,clickhouse', - 'Windows' => 'intl', -}; - -// If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). -$shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => '', - 'Darwin' => '', - 'Windows' => '', -}; - -// If you want to test lib-suggests for all extensions and libraries, set it to true. -$with_suggested_libs = true; - -// If you want to test extra libs for extensions, add them below (comma separated, example `libwebp,libavif`). Unnecessary, when $with_suggested_libs is true. -$with_libs = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'krb5', - 'Windows' => '', -}; - -// Please change your test base combination. We recommend testing with `common`. -// You can use `common`, `bulk`, `minimal` or `none`. -// note: combination is only available for *nix platform. Windows must use `none` combination -$base_combination = match (PHP_OS_FAMILY) { - 'Linux', 'Darwin' => 'minimal', - 'Windows' => 'none', -}; - -// -------------------------- code area, do not modify -------------------------- - -/** - * get combination for tests, do not modify it if not necessary. - */ -function _getCombination(string $type = 'common'): string -{ - return match ($type) { - 'common' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,gd,gmp,iconv,xml,mbstring,mbregex,' . - 'mysqlnd,openssl,pcntl,pdo,pdo_mysql,pdo_sqlite,phar,posix,redis,session,simplexml,soap,sockets,' . - 'sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip', - 'bulk' => 'apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,event,exif,fileinfo,filter,ftp,gd,gmp,iconv,imagick,imap,' . - 'intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pdo_pgsql,pdo_sqlite,pgsql,phar,' . - 'posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,swoole,sysvmsg,sysvsem,' . - 'sysvshm,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib', - 'minimal' => 'pcntl,posix,mbstring,tokenizer,phar', - default => '', // none - }; -} - -if (!isset($argv[1])) { - exit("Please use 'extensions', 'cmd', 'os', 'php' or 'libs' as output type"); -} - -$trim_value = "\r\n \t,"; - -$final_extensions = trim(trim($extensions, $trim_value) . ',' . _getCombination($base_combination), $trim_value); -$download_extensions = trim($final_extensions . ',' . $shared_extensions, $trim_value); -$final_libs = trim($with_libs, $trim_value); - -if (PHP_OS_FAMILY === 'Windows') { - $final_extensions_cmd = '"' . $final_extensions . '"'; -} else { - $final_extensions_cmd = $final_extensions; -} - -function quote2(string $param): string -{ - global $argv; - if (str_starts_with($argv[2], 'windows-')) { - return '"' . $param . '"'; - } - return $param; -} - -// generate download command -if ($argv[1] === 'download_cmd') { - $down_cmd = 'download '; - $down_cmd .= '--for-extensions=' . quote2($download_extensions) . ' '; - $down_cmd .= '--for-libs=' . quote2($final_libs) . ' '; - $down_cmd .= '--with-php=' . quote2($argv[3]) . ' '; - $down_cmd .= '--ignore-cache-sources=php-src '; - $down_cmd .= '--debug '; - $down_cmd .= '--retry=5 '; - $down_cmd .= '--shallow-clone '; - $down_cmd .= $prefer_pre_built ? '--prefer-pre-built ' : ''; -} - -if ($argv[1] === 'doctor_cmd') { - $doctor_cmd = 'doctor --auto-fix --debug'; -} -if ($argv[1] === 'install_upx_cmd') { - $install_upx_cmd = 'install-pkg upx --debug'; -} - -$prefix = match ($argv[2] ?? null) { - 'windows-latest', 'windows-2022', 'windows-2019', 'windows-2025' => 'powershell.exe -file .\bin\spc.ps1 ', - 'ubuntu-latest' => 'bin/spc-alpine-docker ', - 'ubuntu-24.04', 'ubuntu-24.04-arm' => './bin/spc ', - 'ubuntu-22.04', 'ubuntu-22.04-arm' => 'bin/spc-gnu-docker ', - default => 'bin/spc ', -}; - -// shared_extension build -if ($shared_extensions) { - switch ($argv[2] ?? null) { - case 'ubuntu-22.04': - case 'ubuntu-22.04-arm': - case 'macos-15': - case 'macos-15-intel': - $shared_cmd = ' --build-shared=' . quote2($shared_extensions) . ' '; - break; - case 'ubuntu-24.04': - case 'ubuntu-24.04-arm': - break; - default: - $shared_cmd = ''; - break; - } -} else { - $shared_cmd = ''; -} - -// generate build command -if ($argv[1] === 'build_cmd' || $argv[1] === 'build_embed_cmd') { - $build_cmd = 'build '; - $build_cmd .= quote2($final_extensions) . ' '; - $build_cmd .= $shared_cmd; - $build_cmd .= $with_suggested_libs ? '--with-suggested-libs ' : ''; - $build_cmd .= $zts ? '--enable-zts ' : ''; - $build_cmd .= $no_strip ? '--no-strip ' : ''; - $build_cmd .= $upx ? '--with-upx-pack ' : ''; - $build_cmd .= $final_libs === '' ? '' : ('--with-libs=' . quote2($final_libs) . ' '); - $build_cmd .= str_starts_with($argv[2], 'windows-') ? '' : '--build-fpm '; - $build_cmd .= '--debug '; -} - -echo match ($argv[1]) { - 'os' => json_encode($test_os), - 'php' => json_encode($test_php_version), - 'extensions' => $final_extensions, - 'libs' => $final_libs, - 'libs_cmd' => ($final_libs === '' ? '' : (' --with-libs=' . $final_libs)), - 'cmd' => $final_extensions_cmd . ($final_libs === '' ? '' : (' --with-libs=' . $final_libs)), - 'zts' => $zts ? '--enable-zts' : '', - 'no_strip' => $no_strip ? '--no-strip' : '', - 'upx' => $upx ? '--with-upx-pack' : '', - 'prefer_pre_built' => $prefer_pre_built ? '--prefer-pre-built' : '', - 'download_cmd' => $down_cmd, - 'install_upx_cmd' => $install_upx_cmd, - 'doctor_cmd' => $doctor_cmd, - 'build_cmd' => $build_cmd, - 'build_embed_cmd' => $build_cmd, - default => '', -}; - -switch ($argv[1] ?? null) { - case 'download_cmd': - passthru($prefix . $down_cmd, $retcode); - break; - case 'build_cmd': - passthru($prefix . $build_cmd . ' --build-cli --build-micro --build-cgi', $retcode); - break; - case 'build_embed_cmd': - if ($frankenphp) { - passthru("{$prefix}install-pkg go-xcaddy --debug", $retcode); - if ($retcode !== 0) { - break; - } - } - passthru($prefix . $build_cmd . (str_starts_with($argv[2], 'windows-') ? ' --build-cli' : (' --build-embed' . ($frankenphp ? ' --build-frankenphp' : ''))), $retcode); - break; - case 'doctor_cmd': - passthru($prefix . $doctor_cmd, $retcode); - break; - case 'install_upx_cmd': - passthru($prefix . $install_upx_cmd, $retcode); - break; - default: - $retcode = 0; - break; -} - -exit($retcode); From 179aa98fa8cc3f1e2b19ac267c120bbf58887682 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 14:49:15 +0200 Subject: [PATCH 10/12] separate PhpTag from PhpRelease --- .../Artifact/Downloader/Type/PhpRelease.php | 78 ++++--------------- .../Artifact/Downloader/Type/PhpTag.php | 74 ++++++++++++++++++ 2 files changed, 88 insertions(+), 64 deletions(-) create mode 100644 src/StaticPHP/Artifact/Downloader/Type/PhpTag.php diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index 949c040ac..f20866f5e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -10,8 +10,6 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpdateInterface { - use GitHubTokenSetupTrait; - public const string DEFAULT_PHP_DOMAIN = 'https://www.php.net'; public const string API_URL = '/releases/index.php?json&version={version}'; @@ -22,10 +20,6 @@ class PhpRelease implements DownloadTypeInterface, ValidatorInterface, CheckUpda public const string GIT_REV = 'master'; - public const string GITHUB_TAGS_API = 'https://api.github.com/repos/php/php-src/git/matching-refs/tags/{prefix}'; - - public const string GITHUB_ARCHIVE_URL = 'https://github.com/php/php-src/archive/refs/tags/{tag}.tar.gz'; - private ?string $sha256 = ''; public function download(string $name, array $config, ArtifactDownloader $downloader): DownloadResult @@ -36,7 +30,12 @@ public function download(string $name, array $config, ArtifactDownloader $downlo $this->sha256 = null; return (new Git())->download($name, ['url' => self::GIT_URL, 'rev' => self::GIT_REV], $downloader); } - ['version' => $version, 'url' => $url, 'filename' => $filename, 'sha256' => $this->sha256] = $this->resolveRelease($name, $config, $downloader); + // php.net does not publish pre-releases: fall back to the php-src git tag archive + if (($info = $this->fetchPhpReleaseInfo($phpver, $config, $downloader)) === null) { + $this->sha256 = null; + return (new PhpTag())->download($name, ['version' => $phpver, 'extract' => $config['extract'] ?? null], $downloader); + } + ['version' => $version, 'url' => $url, 'filename' => $filename, 'sha256' => $this->sha256] = $this->resolveRelease($info, $config); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); @@ -72,7 +71,11 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A // git version: delegate to Git checkUpdate with master branch return (new Git())->checkUpdate($name, ['url' => 'https://github.com/php/php-src.git', 'rev' => 'master'], $old_version, $downloader); } - $new_version = $this->resolveRelease($name, $config, $downloader)['version']; + if (($info = $this->fetchPhpReleaseInfo($phpver, $config, $downloader)) === null) { + // pre-release: delegate to the php-src git tag check + return (new PhpTag())->checkUpdate($name, ['version' => $phpver], $old_version, $downloader); + } + $new_version = $info['version']; return new CheckUpdateResult( old: $old_version, new: $new_version, @@ -80,15 +83,9 @@ public function checkUpdate(string $name, array $config, ?string $old_version, A ); } - /** @return array{version: string, url: string, filename: string, sha256: null|string} */ - protected function resolveRelease(string $name, array $config, ArtifactDownloader $downloader): array + /** @return array{version: string, url: string, filename: string, sha256: string} */ + protected function resolveRelease(array $info, array $config): array { - $phpver = $downloader->getOption('with-php', '8.5'); - $info = $this->fetchPhpReleaseInfo($name, $config, $downloader); - if ($info === null) { - return $this->resolvePrereleaseFromGitTags($phpver, $downloader); - } - $version = $info['version']; $filename = null; $sha256 = ''; @@ -107,56 +104,9 @@ protected function resolveRelease(string $name, array $config, ArtifactDownloade return ['version' => $version, 'url' => $url, 'filename' => $filename, 'sha256' => $sha256]; } - /** @return array{version: string, url: string, filename: string, sha256: null|string} */ - protected function resolvePrereleaseFromGitTags(string $phpver, ArtifactDownloader $downloader): array - { - $is_branch = preg_match('/^\d+\.\d+$/', $phpver) === 1; - $prefix = $is_branch ? "php-{$phpver}." : "php-{$phpver}"; - $url = str_replace('{prefix}', $prefix, self::GITHUB_TAGS_API); - logger()->debug("PHP version {$phpver} is not published on php.net, looking it up in php-src tags from {$url}"); - - $data = default_shell()->executeCurl($url, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry()); - if ($data === false) { - throw new DownloaderException("Failed to fetch php-src git tags for PHP version {$phpver}"); - } - $data = json_decode($data, true); - if (!is_array($data)) { - throw new DownloaderException("Invalid php-src git tag list received for PHP version {$phpver}"); - } - - $pattern = '/^php-(' . preg_quote($phpver, '/') . ($is_branch ? '\.\d+' : '') . '(?:(?:alpha|beta|RC)\d+)?)$/'; - $versions = []; - foreach ($data as $ref) { - $tag = substr((string) ($ref['ref'] ?? ''), strlen('refs/tags/')); - if (preg_match($pattern, $tag, $match) === 1) { - $versions[] = $match[1]; - } - } - if (empty($versions)) { - throw new DownloaderException("PHP version {$phpver} is not available on php.net nor tagged in php-src."); - } - usort($versions, version_compare(...)); - $version = end($versions); - - logger()->notice("PHP {$version} is a pre-release, downloading its source archive from php-src git tag."); - return [ - 'version' => $version, - 'url' => str_replace('{tag}', "php-{$version}", self::GITHUB_ARCHIVE_URL), - 'filename' => "php-{$version}.tar.gz", - 'sha256' => null, - ]; - } - /** @return null|array null when php.net does not publish this version (yet) */ - protected function fetchPhpReleaseInfo(string $name, array $config, ArtifactDownloader $downloader): ?array + protected function fetchPhpReleaseInfo(string $phpver, array $config, ArtifactDownloader $downloader): ?array { - $phpver = $downloader->getOption('with-php', '8.5'); - // Handle 'git' version to clone from php-src repository - if ($phpver === 'git') { - // cannot fetch release info for git version, return empty info to skip validation - throw new DownloaderException("Cannot fetch PHP release info for 'git' version."); - } - $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; $url .= self::API_URL; $url = str_replace('{version}', $phpver, $url); diff --git a/src/StaticPHP/Artifact/Downloader/Type/PhpTag.php b/src/StaticPHP/Artifact/Downloader/Type/PhpTag.php new file mode 100644 index 000000000..860f67558 --- /dev/null +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpTag.php @@ -0,0 +1,74 @@ +resolveLatestTag($config['version'], $downloader->getRetry()); + $url = str_replace('{version}', $version, self::ARCHIVE_URL); + logger()->notice("PHP {$version} is a pre-release, downloading its source archive from {$url}"); + $filename = "php-{$version}.tar.gz"; + default_shell()->executeCurlDownload($url, DOWNLOAD_PATH . "/{$filename}", retries: $downloader->getRetry()); + return DownloadResult::archive($filename, config: $config, extract: $config['extract'] ?? null, version: $version, downloader: static::class); + } + + public function checkUpdate(string $name, array $config, ?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $new_version = $this->resolveLatestTag($config['version'], $downloader->getRetry()); + return new CheckUpdateResult( + old: $old_version, + new: $new_version, + needUpdate: $old_version === null || $new_version !== $old_version, + ); + } + + /** + * Resolve the highest php-src git tag matching the requested version. + * Accepts a branch ('8.6', picks the latest 8.6.x tag) or an exact version ('8.6.0RC1'). + */ + protected function resolveLatestTag(string $phpver, int $retries = 0): string + { + $is_branch = preg_match('/^\d+\.\d+$/', $phpver) === 1; + $prefix = $is_branch ? "php-{$phpver}." : "php-{$phpver}"; + $url = str_replace('{prefix}', $prefix, self::TAGS_API); + logger()->debug("PHP version {$phpver} is not published on php.net, looking it up in php-src tags from {$url}"); + + $data = default_shell()->executeCurl($url, headers: $this->getGitHubTokenHeaders(), retries: $retries); + if ($data === false) { + throw new DownloaderException("Failed to fetch php-src git tags for PHP version {$phpver}"); + } + $data = json_decode($data, true); + if (!is_array($data)) { + throw new DownloaderException("Invalid php-src git tag list received for PHP version {$phpver}"); + } + + $pattern = '/^php-(' . preg_quote($phpver, '/') . ($is_branch ? '\.\d+' : '') . '(?:(?:alpha|beta|RC)\d+)?)$/'; + $versions = []; + foreach ($data as $ref) { + $tag = substr((string) ($ref['ref'] ?? ''), strlen('refs/tags/')); + if (preg_match($pattern, $tag, $match) === 1) { + $versions[] = $match[1]; + } + } + if (empty($versions)) { + throw new DownloaderException("PHP version {$phpver} is not available on php.net nor tagged in php-src."); + } + usort($versions, version_compare(...)); + return end($versions); + } +} From cea2bcdd9c55d4addb5e8922538521b3c92c8163 Mon Sep 17 00:00:00 2001 From: henderkes Date: Wed, 12 Aug 2026 15:57:22 +0200 Subject: [PATCH 11/12] remove skipping shared extension failures --- docs/deps-craft-yml.md | 3 -- src/Package/Extension/swoole.php | 23 +------- src/Package/Target/php.php | 39 +------------- src/Package/Target/php/unix.php | 39 ++------------ src/StaticPHP/Package/PhpExtensionPackage.php | 52 +------------------ 5 files changed, 8 insertions(+), 148 deletions(-) diff --git a/docs/deps-craft-yml.md b/docs/deps-craft-yml.md index c9bc041fe..1716d9f37 100644 --- a/docs/deps-craft-yml.md +++ b/docs/deps-craft-yml.md @@ -26,9 +26,6 @@ build-options: enable-zts: false # Disable smoke test, or for specific SAPIs comma-separated (default: false) no-smoke-test: false - # Do not abort when a shared extension fails to build or to load: skip it, drop its .so, and - # record it in buildroot/skipped-shared-extensions.json (default: false) - allow-shared-ext-failure: false # PHP configuration options (same as --with-config-file-path) with-config-file-path: "" # PHP configuration options (same as --with-config-file-scan-dir) diff --git a/src/Package/Extension/swoole.php b/src/Package/Extension/swoole.php index deddee9c8..c21b132b3 100644 --- a/src/Package/Extension/swoole.php +++ b/src/Package/Extension/swoole.php @@ -18,9 +18,7 @@ use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\FileSystem; -use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\SPCConfigUtil; -use ZM\Logger\ConsoleColor; #[Extension('swoole')] class swoole extends PhpExtensionPackage @@ -121,26 +119,7 @@ public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, Packa } #[AfterStage('php', [php::class, 'smokeTestCliForUnix'], 'ext-swoole-hook-mysql')] - public function mysqlTest(PackageInstaller $installer, PackageBuilder $builder): void - { - // allow-shared-ext-failure already quarantined swoole and dropped its .so; there is nothing left to check - if ($this->isSharedSkipped() || $installer->getPhpExtensionPackage('swoole-hook-mysql')?->isSharedSkipped()) { - return; - } - - try { - $this->assertHooksEnabled($installer); - } catch (ValidationException $e) { - // only shared, separately-built extensions are skippable; static and build-with-php ones stay fatal - if (!$builder->getOption('allow-shared-ext-failure', false) || !$this->isBuildShared() || $this->isBuildWithPhp()) { - throw $e; - } - $this->markSharedSkipped('load', $e); - InteractiveTerm::error('SKIPPING shared extension ' . ConsoleColor::red($this->getExtensionName()) . ' — hook check failed (allow-shared-ext-failure): ' . $e->getMessage()); - } - } - - private function assertHooksEnabled(PackageInstaller $installer): void + public function mysqlTest(PackageInstaller $installer): void { [$ret, $out] = shell()->execWithResult(BUILD_ROOT_PATH . '/bin/php -n' . $this->getSharedExtensionLoadString() . ' --ri "swoole"', false); $out = implode('', $out); diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 9113a8790..310f8a9cf 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -162,7 +162,6 @@ public function init(TargetPackage $package): void // embed build options if ($package->getName() === 'php' || $package->getName() === 'php-embed') { $package->addBuildOption('build-shared', 'D', InputOption::VALUE_REQUIRED, 'Shared extensions to build, comma separated', ''); - $package->addBuildOption('allow-shared-ext-failure', null, null, 'Do not abort when a shared extension fails to build or to load; skip it, drop its .so, and record it in buildroot/skipped-shared-extensions.json'); $package->addBuildOption('maintainer-skip-build', null, null, '(maintainer only) skip embed build if exists'); } @@ -399,7 +398,6 @@ public function beforeBuild(PackageBuilder $builder, Package $package): void // clean old modules that may conflict with the new php build FileSystem::removeDir(BUILD_MODULES_PATH); - FileSystem::removeFileIfExists(BUILD_ROOT_PATH . '/skipped-shared-extensions.json'); } /** @@ -487,7 +485,7 @@ public function patchExtensionXtOffsetOf(PackageInstaller $installer): void } #[Stage('postInstall')] - public function postInstall(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void + public function postInstall(TargetPackage $package, PackageInstaller $installer): void { if ($package->getName() === 'frankenphp') { if (SystemTarget::getTargetOS() === 'Windows') { @@ -517,41 +515,6 @@ public function postInstall(TargetPackage $package, PackageInstaller $installer, InteractiveTerm::finish('PHP smoke tests passed'); } } - $this->writeSkippedSharedExtensionsManifest($package, $installer, $builder); - } - - /** - * Record the shared extensions quarantined by --allow-shared-ext-failure. - * Written unconditionally (even with an empty list) so consumers can tell - * "nothing was skipped" apart from "spc is too old to report". - */ - private function writeSkippedSharedExtensionsManifest(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void - { - $skipped = []; - foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { - if (($record = $ext->getSharedSkipRecord()) === null) { - continue; - } - $skipped[] = [ - 'package' => $ext->getName(), - 'extension' => $ext->getExtensionName(), - 'phase' => $record['phase'], - 'exception' => $record['exception'], - 'message' => $record['message'], - ]; - } - - FileSystem::writeFile(BUILD_ROOT_PATH . '/skipped-shared-extensions.json', json_encode([ - 'schema' => 1, - 'generated_at' => date('c'), - 'php_version_id' => self::getPHPVersionID(return_null_if_failed: true), - 'allow_shared_ext_failure' => (bool) $builder->getOption('allow-shared-ext-failure', false), - 'skipped' => $skipped, - ], JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); - - if ($skipped !== []) { - $package->setOutput('Skipped shared extensions', implode(', ', array_column($skipped, 'extension'))); - } } private function makeStaticExtensionString(PackageInstaller $installer): string diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 3401f577c..f5c0e7b64 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -366,7 +366,7 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta } #[Stage] - public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void + public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterface $toolchain): void { // collect shared extensions /** @var PhpExtensionPackage[] $shared_extensions */ @@ -393,27 +393,11 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa $this->installRemovedMacroCompatHeader(); } - $allow_failure = (bool) $builder->getOption('allow-shared-ext-failure', false); - if ($allow_failure) { - logger()->warning('allow-shared-ext-failure is ON: shared extension build/load failures will be skipped, not fatal.'); - } - try { logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - InteractiveTerm::setMessage('Building shared extension: ' . ConsoleColor::yellow($extension->getName())); - try { - $extension->buildShared(); - } catch (\Throwable $e) { - InteractiveTerm::error('Building shared extension failed: ' . ConsoleColor::red($extension->getName())); - if (!$allow_failure) { - throw $e; - } - $extension->markSharedSkipped('build', $e); - InteractiveTerm::error('SKIPPING shared extension ' . ConsoleColor::red($extension->getName()) . ' (allow-shared-ext-failure): ' . $e->getMessage()); - continue; - } - InteractiveTerm::success('Built shared extension: ' . ConsoleColor::green($extension->getName()), true); + InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); + $extension->buildShared(); } } finally { // restore php-config @@ -537,7 +521,7 @@ public function patchUnixEmbedScripts(): void } #[Stage] - public function smokeTestCliForUnix(PackageInstaller $installer, PackageBuilder $builder): void + public function smokeTestCliForUnix(PackageInstaller $installer): void { InteractiveTerm::setMessage('Running basic php-cli smoke test'); [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); @@ -546,23 +530,10 @@ public function smokeTestCliForUnix(PackageInstaller $installer, PackageBuilder throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); } - $allow_failure = (bool) $builder->getOption('allow-shared-ext-failure', false); $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); foreach ($exts as $ext) { - if ($ext->isSharedSkipped()) { - continue; - } InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); - try { - $ext->runSmokeTestCliUnix(); - } catch (\Throwable $e) { - // only shared, separately-built extensions are skippable; static and build-with-php ones stay fatal - if (!$allow_failure || !$ext->isBuildShared() || $ext->isBuildWithPhp()) { - throw $e; - } - $ext->markSharedSkipped('load', $e); - InteractiveTerm::error('SKIPPING shared extension ' . ConsoleColor::red($ext->getExtensionName()) . ' — failed to load (allow-shared-ext-failure): ' . $e->getMessage()); - } + $ext->runSmokeTestCliUnix(); } } diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index aa93b2292..cac0000fe 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -31,9 +31,6 @@ class PhpExtensionPackage extends Package protected bool $build_with_php = false; - /** @var null|array{phase: string, exception: string, message: string} Set when --allow-shared-ext-failure quarantines this extension */ - protected ?array $skip_record = null; - /** * @param string $name Name of the php extension * @param string $type Type of the package, defaults to 'php-extension' @@ -201,53 +198,6 @@ public function isBuildWithPhp(): bool return $this->build_with_php; } - /** - * Quarantine this shared extension: drop it from every shared filter, delete the artefacts it - * may have left behind, and remember why so php::postInstall() can write it to the manifest. - * - * @param string $phase 'build' or 'load' - */ - public function markSharedSkipped(string $phase, \Throwable $e): void - { - $this->skip_record = ['phase' => $phase, 'exception' => $e::class, 'message' => $e->getMessage()]; - $this->setBuildShared(false); - $this->removeBuiltSharedObject(); - $this->setOutput('Shared extension SKIPPED', "{$phase} failure: {$e->getMessage()}"); - } - - /** - * @return null|array{phase: string, exception: string, message: string} - */ - public function getSharedSkipRecord(): ?array - { - return $this->skip_record; - } - - public function isSharedSkipped(): bool - { - return $this->skip_record !== null; - } - - /** - * Delete the .so (and its debug info) of a skipped shared extension so it can never be packaged. - */ - public function removeBuiltSharedObject(): void - { - // libtool's -release X gives $name-X.so as the real file, $name.so as a symlink to it - $release = preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m) ? "-{$m[1]}" : ''; - $name = $this->getExtensionName(); - foreach ([ - BUILD_MODULES_PATH . "/{$name}{$release}.so", - BUILD_MODULES_PATH . "/{$name}.so", - BUILD_ROOT_PATH . "/debug/{$name}{$release}.so.debug", - ] as $file) { - if (file_exists($file) || is_link($file)) { - @unlink($file); - logger()->warning("Removed artefact of skipped extension: {$file}"); - } - } - } - public function buildShared(): void { if ($this->hasStage('build')) { @@ -518,7 +468,7 @@ public function getSharedExtensionLoadString(): string { $sharedExts = array_filter( $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), - fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() && !$ext->isSharedSkipped() + fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() ); if (empty($sharedExts)) { From 8512e4365fc5bdeebd479ceb339ea9911a578fe6 Mon Sep 17 00:00:00 2001 From: Jerry Ma Date: Wed, 12 Aug 2026 22:50:43 +0800 Subject: [PATCH 12/12] Update src/Package/Target/php/unix.php --- src/Package/Target/php/unix.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index f5c0e7b64..9ab5bd69d 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -43,7 +43,6 @@ public function patchBeforeBuildconf(TargetPackage $package): void // patch libc detection for musl and musl-toolchain $musl = SystemTarget::getTargetOS() === 'Linux' && SystemTarget::getLibc() === 'musl'; - FileSystem::backupFile(SOURCE_PATH . '/php-src/configure.ac'); foreach (['configure.ac', 'build/php.m4'] as $libc_probe_file) { FileSystem::replaceFileStr( SOURCE_PATH . "/php-src/{$libc_probe_file}",