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/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/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/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/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/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/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..26331c388 100644 --- a/src/Package/Extension/intl.php +++ b/src/Package/Extension/intl.php @@ -11,19 +11,33 @@ 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 { $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/Extension/pdo_pgsql.php b/src/Package/Extension/pdo_pgsql.php new file mode 100644 index 000000000..eb9da7e72 --- /dev/null +++ b/src/Package/Extension/pdo_pgsql.php @@ -0,0 +1,31 @@ += 80400) { + // 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 5b7f50a06..5096c50d6 100644 --- a/src/Package/Extension/pgsql.php +++ b/src/Package/Extension/pgsql.php @@ -24,12 +24,11 @@ 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); + // 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() . ' ' . $libfiles . '"'; + ' PGSQL_LIBS="-L' . $builder->getLibDir() . ' ' . $libs . '"'; } return '--with-pgsql=' . ($shared ? 'shared,' : '') . $builder->getBuildRootPath(); } @@ -67,7 +66,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/Extension/swoole.php b/src/Package/Extension/swoole.php index 20257a226..c21b132b3 100644 --- a/src/Package/Extension/swoole.php +++ b/src/Package/Extension/swoole.php @@ -71,7 +71,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'; diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index 37e59feab..310f8a9cf 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; @@ -50,7 +51,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 @@ -453,6 +454,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): void { diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index e306647f5..9ab5bd69d 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -41,14 +41,15 @@ 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('); @@ -118,7 +119,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'; @@ -134,6 +138,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 +152,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')] @@ -380,6 +389,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(); } try { @@ -779,6 +789,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. diff --git a/src/Package/Target/php/windows.php b/src/Package/Target/php/windows.php index 7bfcd53eb..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'; @@ -553,7 +554,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/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/StaticPHP/Artifact/Downloader/Type/PhpRelease.php b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php index 8368c409f..f20866f5e 100644 --- a/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php +++ b/src/StaticPHP/Artifact/Downloader/Type/PhpRelease.php @@ -30,20 +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); } - $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}"); + // 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); } - $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($info, $config); logger()->debug("Downloading PHP release {$version} from {$url}"); $path = DOWNLOAD_PATH . "/{$filename}"; default_shell()->executeCurlDownload($url, $path, retries: $downloader->getRetry()); @@ -79,7 +71,10 @@ 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); + 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, @@ -88,15 +83,30 @@ 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: string} */ + protected function resolveRelease(array $info, array $config): 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."); + $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 null|array null when php.net does not publish this version (yet) */ + protected function fetchPhpReleaseInfo(string $phpver, array $config, ArtifactDownloader $downloader): ?array + { $url = $config['domain'] ?? self::DEFAULT_PHP_DOMAIN; $url .= self::API_URL; $url = str_replace('{version}', $phpver, $url); @@ -108,9 +118,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/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); + } +} 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/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/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 0c40daa96..cac0000fe 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -332,6 +332,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 +378,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) 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/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"); +} 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"); + }/*}}}*/ + diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php deleted file mode 100644 index 2e6e807e4..000000000 --- a/src/globals/test-extensions.php +++ /dev/null @@ -1,234 +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);