2025-12-11 13:50:36 +08:00
< ? php
declare ( strict_types = 1 );
namespace Package\Target\php ;
2026-02-10 22:35:25 +08:00
use Package\Target\php ;
2025-12-11 13:50:36 +08:00
use StaticPHP\Attribute\Package\BeforeStage ;
use StaticPHP\Attribute\Package\BuildFor ;
use StaticPHP\Attribute\Package\Stage ;
use StaticPHP\Attribute\PatchDescription ;
use StaticPHP\DI\ApplicationContext ;
2026-02-10 22:35:25 +08:00
use StaticPHP\Exception\PatchException ;
2025-12-11 13:50:36 +08:00
use StaticPHP\Exception\SPCException ;
2026-02-26 15:46:18 +08:00
use StaticPHP\Exception\ValidationException ;
2025-12-11 13:50:36 +08:00
use StaticPHP\Exception\WrongUsageException ;
use StaticPHP\Package\PackageBuilder ;
use StaticPHP\Package\PackageInstaller ;
use StaticPHP\Package\PhpExtensionPackage ;
use StaticPHP\Package\TargetPackage ;
use StaticPHP\Runtime\SystemTarget ;
use StaticPHP\Toolchain\Interface\ToolchainInterface ;
use StaticPHP\Util\DirDiff ;
use StaticPHP\Util\FileSystem ;
use StaticPHP\Util\InteractiveTerm ;
2026-02-26 15:46:18 +08:00
use StaticPHP\Util\SourcePatcher ;
2025-12-11 13:50:36 +08:00
use StaticPHP\Util\SPCConfigUtil ;
use StaticPHP\Util\System\UnixUtil ;
use StaticPHP\Util\V2CompatLayer ;
use ZM\Logger\ConsoleColor ;
trait unix
{
#[BeforeStage('php', [self::class, 'buildconfForUnix'], 'php')]
#[PatchDescription('Patch configure.ac for musl and musl-toolchain')]
#[PatchDescription('Let php m4 tools use static pkg-config')]
public function patchBeforeBuildconf ( TargetPackage $package ) : void
{
// patch configure.ac 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' )
);
// let php m4 tools use static pkg-config
FileSystem :: replaceFileStr ( " { $package -> getSourceDir () } /build/php.m4 " , 'PKG_CHECK_MODULES(' , 'PKG_CHECK_MODULES_STATIC(' );
}
#[Stage]
public function buildconfForUnix ( TargetPackage $package ) : void
{
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( './buildconf' ));
V2CompatLayer :: emitPatchPoint ( 'before-php-buildconf' );
shell () -> cd ( $package -> getSourceDir ()) -> exec ( getenv ( 'SPC_CMD_PREFIX_PHP_BUILDCONF' ));
}
#[Stage]
public function configureForUnix ( TargetPackage $package , PackageInstaller $installer ) : void
{
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( './configure' ));
V2CompatLayer :: emitPatchPoint ( 'before-php-configure' );
$cmd = getenv ( 'SPC_CMD_PREFIX_PHP_CONFIGURE' );
$args = [];
$version_id = self :: getPHPVersionID ();
2026-02-26 15:46:18 +08:00
// 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' );
}
}
2025-12-11 13:50:36 +08:00
// PHP JSON extension is built-in since PHP 8.0
if ( $version_id < 80000 ) {
$args [] = '--enable-json' ;
}
// zts
if ( $package -> getBuildOption ( 'enable-zts' , false )) {
$args [] = '--enable-zts --disable-zend-signals' ;
if ( $version_id >= 80100 && SystemTarget :: getTargetOS () === 'Linux' ) {
$args [] = '--enable-zend-max-execution-timers' ;
}
}
// config-file-path and config-file-scan-dir
if ( $option = $package -> getBuildOption ( 'with-config-file-path' , false )) {
$args [] = " --with-config-file-path= { $option } " ;
}
if ( $option = $package -> getBuildOption ( 'with-config-file-scan-dir' , false )) {
$args [] = " --with-config-file-scan-dir= { $option } " ;
}
// perform enable cli options
$args [] = $installer -> isPackageResolved ( 'php-cli' ) ? '--enable-cli' : '--disable-cli' ;
2026-02-26 15:46:18 +08:00
$args [] = $installer -> isPackageResolved ( 'php-fpm' )
? '--enable-fpm' . ( $installer -> isPackageResolved ( 'libacl' ) ? ' --with-fpm-acl' : '' )
: '--disable-fpm' ;
2025-12-11 13:50:36 +08:00
$args [] = $installer -> isPackageResolved ( 'php-micro' ) ? match ( SystemTarget :: getTargetOS ()) {
'Linux' => '--enable-micro=all-static' ,
default => '--enable-micro' ,
} : null ;
$args [] = $installer -> isPackageResolved ( 'php-cgi' ) ? '--enable-cgi' : '--disable-cgi' ;
$embed_type = getenv ( 'SPC_CMD_VAR_PHP_EMBED_TYPE' ) ? : 'static' ;
$args [] = $installer -> isPackageResolved ( 'php-embed' ) ? " --enable-embed= { $embed_type } " : '--disable-embed' ;
$args [] = getenv ( 'SPC_EXTRA_PHP_VARS' ) ? : null ;
$args = implode ( ' ' , array_filter ( $args ));
$static_extension_str = $this -> makeStaticExtensionString ( $installer );
// run ./configure with args
$this -> seekPhpSrcLogFileOnException ( fn () => shell () -> cd ( $package -> getSourceDir ()) -> setEnv ([
'CFLAGS' => getenv ( 'SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS' ),
'CPPFLAGS' => " -I { $package -> getIncludeDir () } " ,
'LDFLAGS' => " -L { $package -> getLibDir () } " . getenv ( 'SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS' ),
]) -> exec ( " { $cmd } { $args } { $static_extension_str } " ), $package -> getSourceDir ());
}
#[Stage]
public function makeForUnix ( TargetPackage $package , PackageInstaller $installer ) : void
{
V2CompatLayer :: emitPatchPoint ( 'before-php-make' );
logger () -> info ( 'cleaning up php-src build files' );
shell () -> cd ( $package -> getSourceDir ()) -> exec ( 'make clean' );
if ( $installer -> isPackageResolved ( 'php-cli' )) {
$package -> runStage ([ self :: class , 'makeCliForUnix' ]);
}
if ( $installer -> isPackageResolved ( 'php-cgi' )) {
$package -> runStage ([ self :: class , 'makeCgiForUnix' ]);
}
if ( $installer -> isPackageResolved ( 'php-fpm' )) {
$package -> runStage ([ self :: class , 'makeFpmForUnix' ]);
}
if ( $installer -> isPackageResolved ( 'php-micro' )) {
$package -> runStage ([ self :: class , 'makeMicroForUnix' ]);
}
if ( $installer -> isPackageResolved ( 'php-embed' )) {
$package -> runStage ([ self :: class , 'makeEmbedForUnix' ]);
}
}
#[Stage]
public function makeCliForUnix ( TargetPackage $package , PackageInstaller $installer , PackageBuilder $builder ) : void
{
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( 'make cli' ));
$concurrency = $builder -> concurrency ;
2026-02-04 15:28:10 +08:00
$vars = $this -> makeVars ( $installer );
$makeArgs = $this -> makeVarsToArgs ( $vars );
2026-02-26 15:46:18 +08:00
if ( SystemTarget :: getTargetOS () === 'Linux' ) {
shell () -> cd ( $package -> getSourceDir ()) -> exec ( 'sed -i "s|//lib|/lib|g" Makefile' );
}
2025-12-11 13:50:36 +08:00
shell () -> cd ( $package -> getSourceDir ())
2026-02-04 15:28:10 +08:00
-> setEnv ( $vars )
-> exec ( " make -j { $concurrency } { $makeArgs } cli " );
2025-12-11 13:50:36 +08:00
$builder -> deployBinary ( " { $package -> getSourceDir () } /sapi/cli/php " , BUILD_BIN_PATH . '/php' );
$package -> setOutput ( 'Binary path for cli SAPI' , BUILD_BIN_PATH . '/php' );
}
#[Stage]
public function makeCgiForUnix ( TargetPackage $package , PackageInstaller $installer , PackageBuilder $builder ) : void
{
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( 'make cgi' ));
$concurrency = $builder -> concurrency ;
2026-02-04 15:28:10 +08:00
$vars = $this -> makeVars ( $installer );
$makeArgs = $this -> makeVarsToArgs ( $vars );
2026-02-26 15:46:18 +08:00
if ( SystemTarget :: getTargetOS () === 'Linux' ) {
shell () -> cd ( $package -> getSourceDir ()) -> exec ( 'sed -i "s|//lib|/lib|g" Makefile' );
}
2025-12-11 13:50:36 +08:00
shell () -> cd ( $package -> getSourceDir ())
2026-02-04 15:28:10 +08:00
-> setEnv ( $vars )
-> exec ( " make -j { $concurrency } { $makeArgs } cgi " );
2025-12-11 13:50:36 +08:00
$builder -> deployBinary ( " { $package -> getSourceDir () } /sapi/cgi/php-cgi " , BUILD_BIN_PATH . '/php-cgi' );
$package -> setOutput ( 'Binary path for cgi SAPI' , BUILD_BIN_PATH . '/php-cgi' );
}
#[Stage]
public function makeFpmForUnix ( TargetPackage $package , PackageInstaller $installer , PackageBuilder $builder ) : void
{
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( 'make fpm' ));
$concurrency = $builder -> concurrency ;
2026-02-04 15:28:10 +08:00
$vars = $this -> makeVars ( $installer );
$makeArgs = $this -> makeVarsToArgs ( $vars );
2026-02-26 15:46:18 +08:00
if ( SystemTarget :: getTargetOS () === 'Linux' ) {
shell () -> cd ( $package -> getSourceDir ()) -> exec ( 'sed -i "s|//lib|/lib|g" Makefile' );
}
2025-12-11 13:50:36 +08:00
shell () -> cd ( $package -> getSourceDir ())
2026-02-04 15:28:10 +08:00
-> setEnv ( $vars )
-> exec ( " make -j { $concurrency } { $makeArgs } fpm " );
2025-12-11 13:50:36 +08:00
$builder -> deployBinary ( " { $package -> getSourceDir () } /sapi/fpm/php-fpm " , BUILD_BIN_PATH . '/php-fpm' );
$package -> setOutput ( 'Binary path for fpm SAPI' , BUILD_BIN_PATH . '/php-fpm' );
}
#[Stage]
2026-02-26 15:46:18 +08:00
#[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')]
2025-12-11 13:50:36 +08:00
public function makeMicroForUnix ( TargetPackage $package , PackageInstaller $installer , PackageBuilder $builder ) : void
{
2026-02-26 15:46:18 +08:00
$phar_patched = false ;
try {
if ( $installer -> isPackageResolved ( 'ext-phar' )) {
$phar_patched = true ;
SourcePatcher :: patchMicroPhar ( self :: getPHPVersionID ());
}
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( 'make micro' ));
// apply --with-micro-fake-cli option
$vars = $this -> makeVars ( $installer );
$vars [ 'EXTRA_CFLAGS' ] .= $package -> getBuildOption ( 'with-micro-fake-cli' , false ) ? ' -DPHP_MICRO_FAKE_CLI' : '' ;
$makeArgs = $this -> makeVarsToArgs ( $vars );
// build
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 " );
$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 ();
2025-12-11 13:50:36 +08:00
}
}
}
#[Stage]
public function makeEmbedForUnix ( TargetPackage $package , PackageInstaller $installer , PackageBuilder $builder ) : void
{
InteractiveTerm :: setMessage ( 'Building php: ' . ConsoleColor :: yellow ( 'make embed' ));
$shared_exts = array_filter (
$installer -> getResolvedPackages (),
static fn ( $x ) => $x instanceof PhpExtensionPackage && $x -> isBuildShared () && $x -> isBuildWithPhp ()
);
$install_modules = $shared_exts ? 'install-modules' : '' ;
// detect changes in module path
$diff = new DirDiff ( BUILD_MODULES_PATH , true );
$root = BUILD_ROOT_PATH ;
$sed_prefix = SystemTarget :: getTargetOS () === 'Darwin' ? 'sed -i ""' : 'sed -i' ;
shell () -> cd ( $package -> getSourceDir ())
-> setEnv ( $this -> makeVars ( $installer ))
-> exec ( " { $sed_prefix } \" s|^EXTENSION_DIR = .*|EXTENSION_DIR = / " . basename ( BUILD_MODULES_PATH ) . '|" Makefile' )
-> exec ( " make -j { $builder -> concurrency } INSTALL_ROOT= { $root } install-sapi { $install_modules } install-build install-headers install-programs " );
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared -------------
// process libphp.so for shared embed
$suffix = SystemTarget :: getTargetOS () === 'Darwin' ? 'dylib' : 'so' ;
$libphp_so = " { $package -> getLibDir () } /libphp. { $suffix } " ;
if ( file_exists ( $libphp_so )) {
// rename libphp.so if -release is set
if ( SystemTarget :: getTargetOS () === 'Linux' ) {
2026-02-26 15:46:18 +08:00
$this -> processLibphpSoFile ( $libphp_so , $installer );
2025-12-11 13:50:36 +08:00
}
// deploy
2026-02-26 15:46:18 +08:00
$builder -> deployBinary ( $libphp_so , $libphp_so , false );
2025-12-11 13:50:36 +08:00
$package -> setOutput ( 'Library path for embed SAPI' , $libphp_so );
}
// process shared extensions that built-with-php
$increment_files = $diff -> getChangedFiles ();
$files = [];
foreach ( $increment_files as $increment_file ) {
$builder -> deployBinary ( $increment_file , $increment_file , false );
$files [] = basename ( $increment_file );
}
if ( ! empty ( $files )) {
$package -> setOutput ( 'Built shared extensions' , implode ( ', ' , $files ));
}
// ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static -------------
// process libphp.a for static embed
if ( ! file_exists ( " { $package -> getLibDir () } /libphp.a " )) {
return ;
}
$ar = getenv ( 'AR' ) ? : 'ar' ;
$libphp_a = " { $package -> getLibDir () } /libphp.a " ;
shell () -> exec ( " { $ar } -t { $libphp_a } | grep ' \\ .a $ ' | xargs -n1 { $ar } d { $libphp_a } " );
UnixUtil :: exportDynamicSymbols ( $libphp_a );
// deploy embed php scripts
2026-02-06 16:34:51 +08:00
$package -> runStage ([ $this , 'patchUnixEmbedScripts' ]);
2025-12-11 13:50:36 +08:00
}
#[Stage]
public function unixBuildSharedExt ( PackageInstaller $installer , ToolchainInterface $toolchain ) : void
{
// collect shared extensions
/** @var PhpExtensionPackage[] $shared_extensions */
$shared_extensions = array_filter (
$installer -> getResolvedPackages ( PhpExtensionPackage :: class ),
fn ( $x ) => $x -> isBuildShared () && ! $x -> isBuildWithPhp ()
);
if ( ! empty ( $shared_extensions )) {
if ( $toolchain -> isStatic ()) {
throw new WrongUsageException (
" You're building against musl libc statically (the default on Linux), but you're trying to build shared extensions. \n " .
'Static musl libc does not implement `dlopen`, so your php binary is not able to load shared extensions.' . " \n " .
'Either use SPC_LIBC=glibc to link against glibc on a glibc OS, or use SPC_TARGET="native-native-musl -dynamic" to link against musl libc dynamically using `zig cc`.'
);
}
FileSystem :: createDir ( BUILD_MODULES_PATH );
// backup
FileSystem :: backupFile ( BUILD_BIN_PATH . '/php-config' );
FileSystem :: backupFile ( BUILD_LIB_PATH . '/php/build/phpize.m4' );
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' );
}
try {
logger () -> debug ( 'Building shared extensions...' );
foreach ( $shared_extensions as $extension ) {
InteractiveTerm :: setMessage ( 'Building shared PHP extension: ' . ConsoleColor :: yellow ( $extension -> getName ()));
$extension -> buildShared ();
}
} finally {
// restore php-config
if ( ! empty ( $shared_extensions )) {
FileSystem :: restoreBackupFile ( BUILD_BIN_PATH . '/php-config' );
FileSystem :: restoreBackupFile ( BUILD_LIB_PATH . '/php/build/phpize.m4' );
}
}
}
2026-02-26 15:46:18 +08:00
#[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' ]);
}
}
2025-12-11 13:50:36 +08:00
#[BuildFor('Darwin')]
#[BuildFor('Linux')]
public function build ( TargetPackage $package ) : void
{
2026-02-26 15:46:18 +08:00
// 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' ]);
2026-02-10 22:35:25 +08:00
return ;
}
2026-02-26 15:46:18 +08:00
// virtual target, do nothing
if ( $package -> getName () !== 'php' ) {
2025-12-11 13:50:36 +08:00
return ;
}
$package -> runStage ([ $this , 'buildconfForUnix' ]);
$package -> runStage ([ $this , 'configureForUnix' ]);
$package -> runStage ([ $this , 'makeForUnix' ]);
$package -> runStage ([ $this , 'unixBuildSharedExt' ]);
2026-02-26 15:46:18 +08:00
$package -> runStage ([ $this , 'smokeTestForUnix' ]);
2025-12-11 13:50:36 +08:00
}
/**
* Patch phpize and php - config if needed
*/
#[Stage]
public function patchUnixEmbedScripts () : void
{
// patch phpize
if ( file_exists ( BUILD_BIN_PATH . '/phpize' )) {
logger () -> debug ( 'Patching phpize prefix' );
FileSystem :: replaceFileStr ( BUILD_BIN_PATH . '/phpize' , " prefix='' " , " prefix=' " . BUILD_ROOT_PATH . " ' " );
FileSystem :: replaceFileStr ( BUILD_BIN_PATH . '/phpize' , 's##' , 's#/usr/local#' );
$this -> setOutput ( 'phpize script path for embed SAPI' , BUILD_BIN_PATH . '/phpize' );
}
// patch php-config
if ( file_exists ( BUILD_BIN_PATH . '/php-config' )) {
logger () -> debug ( 'Patching php-config prefix and libs order' );
$php_config_str = FileSystem :: readFile ( BUILD_BIN_PATH . '/php-config' );
$php_config_str = str_replace ( 'prefix=""' , 'prefix="' . BUILD_ROOT_PATH . '"' , $php_config_str );
// move mimalloc to the beginning of libs
$php_config_str = preg_replace ( '/(libs=")(.*?)\s*(' . preg_quote ( BUILD_LIB_PATH , '/' ) . '\/mimalloc\.o)\s*(.*?)"/' , '$1$3 $2 $4"' , $php_config_str );
// move lstdc++ to the end of libs
$php_config_str = preg_replace ( '/(libs=")(.*?)\s*(-lstdc\+\+)\s*(.*?)"/' , '$1$2 $4 $3"' , $php_config_str );
FileSystem :: writeFile ( BUILD_BIN_PATH . '/php-config' , $php_config_str );
$this -> setOutput ( 'php-config script path for embed SAPI' , BUILD_BIN_PATH . '/php-config' );
}
}
2026-02-26 15:46:18 +08:00
#[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'
);
}
}
2025-12-11 13:50:36 +08:00
/**
* Seek php - src / config . log when building PHP , add it to exception .
*/
protected function seekPhpSrcLogFileOnException ( callable $callback , string $source_dir ) : void
{
try {
$callback ();
} catch ( SPCException $e ) {
if ( file_exists ( " { $source_dir } /config.log " )) {
$e -> addExtraLogFile ( 'php-src config.log' , 'php-src.config.log' );
copy ( " { $source_dir } /config.log " , SPC_LOGS_DIR . '/php-src.config.log' );
}
throw $e ;
}
}
2026-02-26 15:46:18 +08:00
/**
* Generate micro extension test php code .
*/
private function generateMicroExtTests ( PackageInstaller $installer ) : string
{
$php = " <?php \n \n echo '[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 ;
}
2025-12-11 13:50:36 +08:00
/**
* Rename libphp . so to libphp -< release >. so if - release is set in LDFLAGS .
*/
private function processLibphpSoFile ( string $libphpSo , PackageInstaller $installer ) : void
{
$ldflags = getenv ( 'SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS' ) ? : '' ;
$libDir = BUILD_LIB_PATH ;
$modulesDir = BUILD_MODULES_PATH ;
$realLibName = 'libphp.so' ;
$cwd = getcwd ();
if ( preg_match ( '/-release\s+(\S+)/' , $ldflags , $matches )) {
$release = $matches [ 1 ];
$realLibName = " libphp- { $release } .so " ;
$libphpRelease = " { $libDir } / { $realLibName } " ;
if ( ! file_exists ( $libphpRelease ) && file_exists ( $libphpSo )) {
rename ( $libphpSo , $libphpRelease );
}
if ( file_exists ( $libphpRelease )) {
chdir ( $libDir );
if ( file_exists ( $libphpSo )) {
unlink ( $libphpSo );
}
symlink ( $realLibName , 'libphp.so' );
shell () -> exec ( sprintf (
'patchelf --set-soname %s %s' ,
escapeshellarg ( $realLibName ),
escapeshellarg ( $libphpRelease )
));
}
if ( is_dir ( $modulesDir )) {
chdir ( $modulesDir );
foreach ( $installer -> getResolvedPackages ( PhpExtensionPackage :: class ) as $ext ) {
if ( ! $ext -> isBuildShared ()) {
continue ;
}
$name = $ext -> getName ();
$versioned = " { $name } - { $release } .so " ;
$unversioned = " { $name } .so " ;
$src = " { $modulesDir } / { $versioned } " ;
$dst = " { $modulesDir } / { $unversioned } " ;
if ( is_file ( $src )) {
rename ( $src , $dst );
shell () -> exec ( sprintf (
'patchelf --set-soname %s %s' ,
escapeshellarg ( $unversioned ),
escapeshellarg ( $dst )
));
}
}
}
chdir ( $cwd );
}
$target = " { $libDir } / { $realLibName } " ;
if ( file_exists ( $target )) {
[, $output ] = shell () -> execWithResult ( 'readelf -d ' . escapeshellarg ( $target ));
$output = implode ( " \n " , $output );
if ( preg_match ( '/SONAME.*\[(.+)]/' , $output , $sonameMatch )) {
$currentSoname = $sonameMatch [ 1 ];
if ( $currentSoname !== basename ( $target )) {
shell () -> exec ( sprintf (
'patchelf --set-soname %s %s' ,
escapeshellarg ( basename ( $target )),
escapeshellarg ( $target )
));
}
}
}
}
/**
* Make environment variables for php make .
* This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking .
*/
private function makeVars ( PackageInstaller $installer ) : array
{
2026-02-03 10:59:16 +08:00
$config = new SPCConfigUtil ([ 'libs_only_deps' => true ]) -> config ( array_map ( fn ( $x ) => $x -> getName (), $installer -> getResolvedPackages ()));
2025-12-11 13:50:36 +08:00
$static = ApplicationContext :: get ( ToolchainInterface :: class ) -> isStatic () ? '-all-static' : '' ;
$pie = SystemTarget :: getTargetOS () === 'Linux' ? '-pie' : '' ;
2026-02-04 15:28:10 +08:00
// Append SPC_EXTRA_LIBS to libs for dynamic linking support (e.g., X11)
$extra_libs = getenv ( 'SPC_EXTRA_LIBS' ) ? : '' ;
$libs = trim ( $config [ 'libs' ] . ' ' . $extra_libs );
2025-12-11 13:50:36 +08:00
return array_filter ([
'EXTRA_CFLAGS' => getenv ( 'SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS' ),
'EXTRA_LDFLAGS_PROGRAM' => getenv ( 'SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS' ) . " { $config [ 'ldflags' ] } { $static } { $pie } " ,
'EXTRA_LDFLAGS' => $config [ 'ldflags' ],
2026-02-04 15:28:10 +08:00
'EXTRA_LIBS' => $libs ,
2025-12-11 13:50:36 +08:00
]);
}
2026-02-04 15:28:10 +08:00
/**
* Convert make variables array to command line argument string .
* This is needed because make command line arguments have higher priority than environment variables .
*/
private function makeVarsToArgs ( array $vars ) : string
{
$args = [];
foreach ( $vars as $key => $value ) {
if ( trim ( $value ) !== '' ) {
$args [] = $key . '=' . escapeshellarg ( $value );
}
}
return implode ( ' ' , $args );
}
2025-12-11 13:50:36 +08:00
}