diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index c21ae590..ff92ed6a 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Package\Target; +use Package\Target\php\frankenphp; use Package\Target\php\unix; use Package\Target\php\windows; use StaticPHP\Attribute\Package\BeforeStage; @@ -42,6 +43,7 @@ class php extends TargetPackage { use unix; use windows; + use frankenphp; /** @var string[] Supported major PHP versions */ public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; @@ -119,6 +121,7 @@ class php extends TargetPackage $package->addBuildOption('with-config-file-scan-dir', null, InputOption::VALUE_REQUIRED, 'Set the directory to scan for .ini files after reading php.ini', PHP_OS_FAMILY === 'Windows' ? null : '/usr/local/etc/php/conf.d'); $package->addBuildOption('with-hardcoded-ini', 'I', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED, 'Patch PHP source code, inject hardcoded INI'); $package->addBuildOption('enable-zts', null, null, 'Enable thread safe support'); + $package->addBuildOption('no-smoke-test', null, InputOption::VALUE_OPTIONAL, 'Disable smoke test for specific SAPIs, or all if no value provided', false); // phpmicro build options if ($package->getName() === 'php' || $package->getName() === 'php-micro') { @@ -198,6 +201,11 @@ class php extends TargetPackage $installer->addBuildPackage('php-embed'); } + // frankenphp depends on embed SAPI (libphp.a) + if ($package->getName() === 'frankenphp') { + $installer->addBuildPackage('php-embed'); + } + return [...$extensions_pkg, ...$additional_packages]; } @@ -209,7 +217,7 @@ class php extends TargetPackage if (!$package->getBuildOption('enable-zts')) { throw new WrongUsageException('FrankenPHP SAPI requires ZTS enabled PHP, build with `--enable-zts`!'); } - // frankenphp doesn't support windows, BSD is currently not supported by static-php-cli + // frankenphp doesn't support windows, BSD is currently not supported by StaticPHP if (!in_array(PHP_OS_FAMILY, ['Linux', 'Darwin'])) { throw new WrongUsageException('FrankenPHP SAPI is only available on Linux and macOS!'); } @@ -272,10 +280,10 @@ class php extends TargetPackage // Patch StaticPHP version // detect patch (remove this when 8.3 deprecated) $file = FileSystem::readFile("{$package->getSourceDir()}/main/main.c"); - if (!str_contains($file, 'static-php-cli.version')) { + if (!str_contains($file, 'StaticPHP.version')) { $version = SPC_VERSION; - logger()->debug('Inserting static-php-cli.version to php-src'); - $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"static-php-cli.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); + logger()->debug('Inserting StaticPHP.version to php-src'); + $file = str_replace('PHP_INI_BEGIN()', "PHP_INI_BEGIN()\n\tPHP_INI_ENTRY(\"StaticPHP.version\",\t\"{$version}\",\tPHP_INI_ALL,\tNULL)", $file); FileSystem::writeFile("{$package->getSourceDir()}/main/main.c", $file); } diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 889f90b3..d8324574 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -7,6 +7,7 @@ namespace Package\Target\php; use Package\Target\php; use StaticPHP\Attribute\Package\Stage; use StaticPHP\Exception\SPCInternalException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,7 +23,7 @@ use ZM\Logger\ConsoleColor; trait frankenphp { #[Stage] - public function buildFrankenphpUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void + public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $installer, ToolchainInterface $toolchain, PackageBuilder $builder): void { if (getenv('GOROOT') === false) { throw new SPCInternalException('go-xcaddy is not initialized properly. GOROOT is not set.'); @@ -89,6 +90,7 @@ trait frankenphp 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . + '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenphp_version} PHP {$libphp_version} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", @@ -103,6 +105,29 @@ trait frankenphp $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); } + #[Stage] + public function smokeTestFrankenphpForUnix(): void + { + InteractiveTerm::setMessage('Running FrankenPHP smoke test'); + $frankenphp = BUILD_BIN_PATH . '/frankenphp'; + if (!file_exists($frankenphp)) { + throw new ValidationException( + "FrankenPHP binary not found: {$frankenphp}", + validation_module: 'FrankenPHP smoke test' + ); + } + $prefix = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_' : 'LD_'; + [$ret, $output] = shell() + ->setEnv(["{$prefix}LIBRARY_PATH" => BUILD_LIB_PATH]) + ->execWithResult("{$frankenphp} version"); + if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) { + throw new ValidationException( + 'FrankenPHP failed smoke test: ret[' . $ret . ']. out[' . implode('', $output) . ']', + validation_module: 'FrankenPHP smoke test' + ); + } + } + /** * Process the --with-frankenphp-app option * Creates app.tar and app.checksum in source/frankenphp directory diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 13c89780..bb64271e 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -12,6 +12,7 @@ use StaticPHP\Attribute\PatchDescription; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\PatchException; use StaticPHP\Exception\SPCException; +use StaticPHP\Exception\ValidationException; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; @@ -22,6 +23,7 @@ use StaticPHP\Toolchain\Interface\ToolchainInterface; use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; use StaticPHP\Util\InteractiveTerm; +use StaticPHP\Util\SourcePatcher; use StaticPHP\Util\SPCConfigUtil; use StaticPHP\Util\System\UnixUtil; use StaticPHP\Util\V2CompatLayer; @@ -29,8 +31,6 @@ use ZM\Logger\ConsoleColor; trait unix { - use frankenphp; - #[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')] #[PatchDescription('Patch configure.ac for musl and musl-toolchain')] #[PatchDescription('Let php m4 tools use static pkg-config')] @@ -49,47 +49,11 @@ trait unix FileSystem::replaceFileStr("{$package->getSourceDir()}/build/php.m4", 'PKG_CHECK_MODULES(', 'PKG_CHECK_MODULES_STATIC('); } - #[BeforeStage('php', [php::class, 'makeForUnix'], 'php')] - #[PatchDescription('Patch TSRM for musl TLS symbol visibility issue')] - #[PatchDescription('Patch ext/standard/info.c for configure command info')] - public function patchTSRMBeforeUnixMake(ToolchainInterface $toolchain): void - { - if (!$toolchain->isStatic() && SystemTarget::getLibc() === 'musl') { - // we need to patch the symbol to global visibility, otherwise extensions with `initial-exec` TLS model will fail to load - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } else { - FileSystem::replaceFileStr( - SOURCE_PATH . '/php-src/TSRM/TSRM.h', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS __attribute__((visibility("default"))) void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - '#define TSRMLS_MAIN_CACHE_DEFINE() TSRM_TLS void *TSRMLS_CACHE TSRM_TLS_MODEL_ATTR = NULL;', - ); - } - - if (str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), '-release')) { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef CONFIGURE_COMMAND', - '#ifdef NO_CONFIGURE_COMMAND', - ); - } else { - FileSystem::replaceFileLineContainsString( - SOURCE_PATH . '/php-src/ext/standard/info.c', - '#ifdef NO_CONFIGURE_COMMAND', - '#ifdef CONFIGURE_COMMAND', - ); - } - } - #[Stage] public function buildconfForUnix(TargetPackage $package): void { InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('./buildconf')); V2CompatLayer::emitPatchPoint('before-php-buildconf'); - // run ./buildconf shell()->cd($package->getSourceDir())->exec(getenv('SPC_CMD_PREFIX_PHP_BUILDCONF')); } @@ -102,6 +66,13 @@ trait unix $args = []; $version_id = self::getPHPVersionID(); + + // disable undefined behavior sanitizer when opcache JIT is enabled (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && !$package->getBuildOption('disable-opcache-jit', false)) { + if ($version_id >= 80500 || $installer->isPackageResolved('ext-opcache')) { + f_putenv('SPC_COMPILER_EXTRA=-fno-sanitize=undefined'); + } + } // PHP JSON extension is built-in since PHP 8.0 if ($version_id < 80000) { $args[] = '--enable-json'; @@ -122,7 +93,9 @@ trait unix } // perform enable cli options $args[] = $installer->isPackageResolved('php-cli') ? '--enable-cli' : '--disable-cli'; - $args[] = $installer->isPackageResolved('php-fpm') ? '--enable-fpm' : '--disable-fpm'; + $args[] = $installer->isPackageResolved('php-fpm') + ? '--enable-fpm' . ($installer->isPackageResolved('libacl') ? ' --with-fpm-acl' : '') + : '--disable-fpm'; $args[] = $installer->isPackageResolved('php-micro') ? match (SystemTarget::getTargetOS()) { 'Linux' => '--enable-micro=all-static', default => '--enable-micro', @@ -151,23 +124,18 @@ trait unix logger()->info('cleaning up php-src build files'); shell()->cd($package->getSourceDir())->exec('make clean'); - // cli if ($installer->isPackageResolved('php-cli')) { $package->runStage([self::class, 'makeCliForUnix']); } - // cgi if ($installer->isPackageResolved('php-cgi')) { $package->runStage([self::class, 'makeCgiForUnix']); } - // fpm if ($installer->isPackageResolved('php-fpm')) { $package->runStage([self::class, 'makeFpmForUnix']); } - // micro if ($installer->isPackageResolved('php-micro')) { $package->runStage([self::class, 'makeMicroForUnix']); } - // embed if ($installer->isPackageResolved('php-embed')) { $package->runStage([self::class, 'makeEmbedForUnix']); } @@ -180,6 +148,9 @@ trait unix $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cli"); @@ -195,6 +166,9 @@ trait unix $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} cgi"); @@ -210,6 +184,9 @@ trait unix $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); $makeArgs = $this->makeVarsToArgs($vars); + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } shell()->cd($package->getSourceDir()) ->setEnv($vars) ->exec("make -j{$concurrency} {$makeArgs} fpm"); @@ -219,44 +196,49 @@ trait unix } #[Stage] - #[PatchDescription('Patch micro.sfx after UPX compression')] + #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); - // apply --with-micro-fake-cli option - $vars = $this->makeVars($installer); - $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - - $dst = BUILD_BIN_PATH . '/micro.sfx'; - $builder->deployBinary("{$package->getSourceDir()}/sapi/micro/micro.sfx", $dst); - - /* - * Patch micro.sfx after UPX compression. - * micro needs special section handling in LinuxBuilder. - * The micro.sfx does not support UPX directly, but we can remove UPX - * info segment to adapt. - * This will also make micro.sfx with upx-packed more like a malware fore antivirus - */ - if ($package->getBuildOption('with-upx-pack') && SystemTarget::getTargetOS() === 'Linux') { - // strip first - // cut binary with readelf - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \$1, \$2, \$3, \$4, \$6, \$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + $phar_patched = false; + try { + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); - } + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); + // apply --with-micro-fake-cli option + $vars = $this->makeVars($installer); + $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $makeArgs = $this->makeVarsToArgs($vars); + // build + if (SystemTarget::getTargetOS() === 'Linux') { + shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); + } + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - $package->setOutput('Binary path for micro SAPI', BUILD_BIN_PATH . '/micro.sfx'); + $dst = BUILD_BIN_PATH . '/micro.sfx'; + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); + // patch after UPX-ed micro.sfx (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { + // cut binary with readelf to remove UPX extra segment + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); + } + $package->setOutput('Binary path for micro SAPI', $dst); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); + } + } } #[Stage] @@ -285,18 +267,13 @@ trait unix // process libphp.so for shared embed $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; - $libphp_so_dst = $libphp_so; if (file_exists($libphp_so)) { // rename libphp.so if -release is set if (SystemTarget::getTargetOS() === 'Linux') { - // deploy libphp.so - preg_match('/-release\s+(\S*)/', getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $matches); - if (!empty($matches[1])) { - $libphp_so_dst = str_replace('.so', '-' . $matches[1] . '.so', $libphp_so); - } + $this->processLibphpSoFile($libphp_so, $installer); } // deploy - $builder->deployBinary($libphp_so, $libphp_so_dst, false); + $builder->deployBinary($libphp_so, $libphp_so, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -368,16 +345,68 @@ trait unix } } + #[Stage] + public function smokeTestForUnix(PackageBuilder $builder, TargetPackage $package, PackageInstaller $installer): void + { + // analyse --no-smoke-test option + $no_smoke_test = $builder->getOption('no-smoke-test'); + // validate option + $option = match ($no_smoke_test) { + false => false, // default value, run all smoke tests + null => 'all', // --no-smoke-test without value, skip all smoke tests + default => parse_comma_list($no_smoke_test), // --no-smoke-test=cli,fpm, skip specified smoke tests + }; + $valid_tests = ['cli', 'cgi', 'micro', 'micro-exts', 'embed', 'frankenphp']; + // compat: --without-micro-ext-test is equivalent to --no-smoke-test=micro-exts + if ($builder->getOption('without-micro-ext-test', false)) { + $valid_tests = array_diff($valid_tests, ['micro-exts']); + } + if (is_array($option)) { + /* + 1. if option is not in valid tests, throw WrongUsageException + 2. if all passed options are valid, remove them from $valid_tests, and run the remaining tests + */ + foreach ($option as $test) { + if (!in_array($test, $valid_tests, true)) { + throw new WrongUsageException("Invalid value for --no-smoke-test: {$test}. Valid values are: " . implode(', ', $valid_tests)); + } + $valid_tests = array_diff($valid_tests, [$test]); + } + } elseif ($option === 'all') { + $valid_tests = []; + } + // run cli tests + if (in_array('cli', $valid_tests, true) && $installer->isPackageResolved('php-cli')) { + $package->runStage([$this, 'smokeTestCliForUnix']); + } + // run cgi tests + if (in_array('cgi', $valid_tests, true) && $installer->isPackageResolved('php-cgi')) { + $package->runStage([$this, 'smokeTestCgiForUnix']); + } + // run micro tests + if (in_array('micro', $valid_tests, true) && $installer->isPackageResolved('php-micro')) { + $skipExtTest = !in_array('micro-exts', $valid_tests, true); + $package->runStage([$this, 'smokeTestMicroForUnix'], ['skipExtTest' => $skipExtTest]); + } + // run embed tests + if (in_array('embed', $valid_tests, true) && $installer->isPackageResolved('php-embed')) { + $package->runStage([$this, 'smokeTestEmbedForUnix']); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function build(TargetPackage $package): void { - // virtual target, do nothing - if (in_array($package->getName(), ['php-cli', 'php-fpm', 'php-cgi', 'php-micro', 'php-embed'], true)) { + // frankenphp is not a php sapi, it's a standalone Go binary that depends on libphp.a (embed) + if ($package->getName() === 'frankenphp') { + /* @var php $this */ + $package->runStage([$this, 'buildFrankenphpForUnix']); + $package->runStage([$this, 'smokeTestFrankenphpForUnix']); return; } - if ($package->getName() === 'frankenphp') { - $package->runStage([$this, 'buildFrankenphpUnix']); + // virtual target, do nothing + if ($package->getName() !== 'php') { return; } @@ -386,6 +415,7 @@ trait unix $package->runStage([$this, 'makeForUnix']); $package->runStage([$this, 'unixBuildSharedExt']); + $package->runStage([$this, 'smokeTestForUnix']); } /** @@ -415,6 +445,132 @@ trait unix } } + #[Stage] + public function smokeTestCliForUnix(PackageInstaller $installer): void + { + InteractiveTerm::setMessage('Running basic php-cli smoke test'); + [$ret, $output] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n -r "echo \"hello\";"'); + $raw_output = implode('', $output); + if ($ret !== 0 || trim($raw_output) !== 'hello') { + throw new ValidationException("cli failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli smoke test'); + } + + $exts = $installer->getResolvedPackages(PhpExtensionPackage::class); + foreach ($exts as $ext) { + InteractiveTerm::setMessage('Running php-cli smoke test for ' . ConsoleColor::yellow($ext->getExtensionName()) . ' extension'); + $ext->runSmokeTestCliUnix(); + } + } + + #[Stage] + public function smokeTestCgiForUnix(): void + { + InteractiveTerm::setMessage('Running basic php-cgi smoke test'); + [$ret, $output] = shell()->execWithResult("echo 'Hello, World!\";' | " . BUILD_BIN_PATH . '/php-cgi -n'); + $raw_output = implode('', $output); + if ($ret !== 0 || !str_contains($raw_output, 'Hello, World!') || !str_contains($raw_output, 'text/html')) { + throw new ValidationException("cgi failed smoke test. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi smoke test'); + } + } + + #[Stage] + public function smokeTestMicroForUnix(PackageInstaller $installer, bool $skipExtTest = false): void + { + $micro_sfx = BUILD_BIN_PATH . '/micro.sfx'; + + // micro_ext_test + InteractiveTerm::setMessage('Running php-micro ext smoke test'); + $content = $skipExtTest + ? 'generateMicroExtTests($installer); + $test_file = SOURCE_PATH . '/micro_ext_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + $raw_out = trim(implode('', $out)); + if ($ret !== 0 || !str_starts_with($raw_out, '[micro-test-start]') || !str_ends_with($raw_out, '[micro-test-end]')) { + throw new ValidationException( + "micro_ext_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_ext_test]' + ); + } + + // micro_zend_bug_test + InteractiveTerm::setMessage('Running php-micro zend bug smoke test'); + $content = file_get_contents(ROOT_DIR . '/src/globals/common-tests/micro_zend_mm_heap_corrupted.txt'); + $test_file = SOURCE_PATH . '/micro_zend_bug_test.exe'; + if (file_exists($test_file)) { + @unlink($test_file); + } + file_put_contents($test_file, file_get_contents($micro_sfx) . $content); + chmod($test_file, 0755); + [$ret, $out] = shell()->execWithResult($test_file); + if ($ret !== 0) { + $raw_out = trim(implode('', $out)); + throw new ValidationException( + "micro_zend_bug_test failed. code: {$ret}, output: {$raw_out}", + validation_module: 'phpmicro sanity check item [micro_zend_bug_test]' + ); + } + } + + #[Stage] + public function smokeTestEmbedForUnix(PackageInstaller $installer, ToolchainInterface $toolchain): void + { + $sample_file_path = SOURCE_PATH . '/embed-test'; + FileSystem::createDir($sample_file_path); + // copy embed test files + copy(ROOT_DIR . '/src/globals/common-tests/embed.c', $sample_file_path . '/embed.c'); + copy(ROOT_DIR . '/src/globals/common-tests/embed.php', $sample_file_path . '/embed.php'); + + $config = new SPCConfigUtil()->config(array_map(fn ($x) => $x->getName(), $installer->getResolvedPackages())); + $lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}"; + if ($toolchain->isStatic()) { + $lens .= ' -static'; + } + + $dynamic_exports = ''; + $envVars = []; + $embedType = 'static'; + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { + $embedType = 'shared'; + $libPathKey = SystemTarget::getTargetOS() === 'Darwin' ? 'DYLD_LIBRARY_PATH' : 'LD_LIBRARY_PATH'; + $envVars[$libPathKey] = BUILD_LIB_PATH . (($existing = getenv($libPathKey)) ? ':' . $existing : ''); + FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a'); + } else { + $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; + foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) { + unlink($file); + } + // calling getDynamicExportedSymbols on non-Linux is okay + if ($dynamic_exports = UnixUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { + $dynamic_exports = ' ' . $dynamic_exports; + } + } + + $cc = getenv('CC'); + InteractiveTerm::setMessage('Running php-embed build smoke test'); + [$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens}{$dynamic_exports}"); + if ($ret !== 0) { + throw new ValidationException( + 'embed failed to build. Error message: ' . implode("\n", $out), + validation_module: $embedType . ' libphp embed build smoke test' + ); + } + + InteractiveTerm::setMessage('Running php-embed run smoke test'); + [$ret, $output] = shell()->cd($sample_file_path)->setEnv($envVars)->execWithResult('./embed'); + if ($ret !== 0 || trim(implode('', $output)) !== 'hello') { + throw new ValidationException( + 'embed failed to run. Error message: ' . implode("\n", $output), + validation_module: $embedType . ' libphp embed run smoke test' + ); + } + } + /** * Seek php-src/config.log when building PHP, add it to exception. */ @@ -431,6 +587,26 @@ trait unix } } + /** + * Generate micro extension test php code. + */ + private function generateMicroExtTests(PackageInstaller $installer): string + { + $php = "getResolvedPackages(PhpExtensionPackage::class) as $ext) { + if (!$ext->isBuildStatic()) { + continue; + } + $ext_name = $ext->getDistName(); + if (!empty($ext_name)) { + $php .= "echo 'Running micro with {$ext_name} test' . PHP_EOL;\n"; + $php .= "assert(extension_loaded('{$ext_name}'));\n\n"; + } + } + $php .= "echo '[micro-test-end]';\n"; + return $php; + } + /** * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. */ diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index 3f2f18cf..29dd2942 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -79,7 +79,7 @@ class PhpExtensionPackage extends Package return ApplicationContext::invoke($callback, ['shared' => $shared, static::class => $this, Package::class => $this]); } $escapedPath = str_replace("'", '', escapeshellarg(BUILD_ROOT_PATH)) !== BUILD_ROOT_PATH || str_contains(BUILD_ROOT_PATH, ' ') ? escapeshellarg(BUILD_ROOT_PATH) : BUILD_ROOT_PATH; - $name = str_replace('_', '-', $this->getName()); + $name = str_replace('_', '-', $this->getExtensionName()); $ext_config = PackageConfig::get($name, 'php-extension', []); $arg_type = match (SystemTarget::getTargetOS()) { @@ -146,6 +146,54 @@ class PhpExtensionPackage extends Package } } + /** + * Get the dist name used for `--ri` check in smoke test. + * Reads from config `dist-name` field, defaults to extension name. + */ + public function getDistName(): string + { + return $this->extension_config['dist-name'] ?? $this->getExtensionName(); + } + + /** + * Run smoke test for the extension on Unix CLI. + * Override this method in a subclass。 + */ + public function runSmokeTestCliUnix(): void + { + if (($this->extension_config['smoke-test'] ?? true) === false) { + return; + } + + $distName = $this->getDistName(); + // empty dist-name → no --ri check (e.g. password_argon2) + if ($distName === '') { + return; + } + + $sharedExtensions = $this->getSharedExtensionLoadString(); + [$ret] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' --ri "' . $distName . '"', false); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed compile check: php-cli returned {$ret}", + validation_module: 'Extension ' . $this->getName() . ' sanity check' + ); + } + + $test_file = ROOT_DIR . '/src/globals/ext-tests/' . $this->getExtensionName() . '.php'; + if (file_exists($test_file)) { + // Trim additional content & escape special characters to allow inline usage + $test = self::escapeInlineTest(file_get_contents($test_file)); + [$ret, $out] = shell()->execWithResult(BUILD_BIN_PATH . '/php -n' . $sharedExtensions . ' -r "' . trim($test) . '"'); + if ($ret !== 0) { + throw new ValidationException( + "extension {$this->getName()} failed sanity check. Code: {$ret}, output: " . implode("\n", $out), + validation_module: 'Extension ' . $this->getName() . ' function check' + ); + } + } + } + /** * Get shared extension build environment variables for Unix. * @@ -284,4 +332,45 @@ class PhpExtensionPackage extends Package } return [trim($staticLibString), trim($sharedLibString)]; } + + /** + * Builds the `-d extension_dir=... -d extension=...` string for all resolved shared extensions. + * Used in CLI smoke test to load shared extension dependencies at runtime. + */ + private function getSharedExtensionLoadString(): string + { + $sharedExts = array_filter( + $this->getInstaller()->getResolvedPackages(PhpExtensionPackage::class), + fn (PhpExtensionPackage $ext) => $ext->isBuildShared() && !$ext->isBuildWithPhp() + ); + + if (empty($sharedExts)) { + return ''; + } + + $ret = ' -d "extension_dir=' . BUILD_MODULES_PATH . '"'; + foreach ($sharedExts as $ext) { + $extConfig = PackageConfig::get($ext->getName(), 'php-extension', []); + if ($extConfig['zend-extension'] ?? false) { + $ret .= ' -d "zend_extension=' . $ext->getExtensionName() . '"'; + } else { + $ret .= ' -d "extension=' . $ext->getExtensionName() . '"'; + } + } + + return $ret; + } + + /** + * Escape PHP test file content for inline `-r` usage. + * Strips