mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-03-17 20:34:51 +08:00
Refactor FrankenPHP build and smoke test processes for Unix
This commit is contained in:
parent
a57b48fda6
commit
3238c44745
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 '<?php echo \"<h1>Hello, World!</h1>\";' | " . 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
|
||||
? '<?php echo "[micro-test-start][micro-test-end]";'
|
||||
: $this->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 = "<?php\n\necho '[micro-test-start]' . PHP_EOL;\n";
|
||||
foreach ($installer->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-<release>.so if -release is set in LDFLAGS.
|
||||
*/
|
||||
|
||||
@ -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 <?php / declare, replaces newlines and special shell characters.
|
||||
*/
|
||||
private static function escapeInlineTest(string $code): string
|
||||
{
|
||||
return str_replace(
|
||||
['<?php', 'declare(strict_types=1);', "\n", '"', '$', '!'],
|
||||
['', '', '', '\"', '\$', '"\'!\'"'],
|
||||
$code
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user