Use separated deploy functions

This commit is contained in:
crazywhalecc 2025-11-06 16:24:59 +08:00
parent f5d93d2f54
commit f09c18e78f
No known key found for this signature in database
GPG Key ID: 1F4BDD59391F2680
7 changed files with 201 additions and 115 deletions

View File

@ -448,6 +448,13 @@ class Extension
->exec('make clean')
->exec('make -j' . $this->builder->concurrency)
->exec('make install');
// process *.so file
$soFile = BUILD_MODULES_PATH . '/' . $this->getName() . '.so';
if (!file_exists($soFile)) {
throw new ValidationException("extension {$this->getName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getName()} build");
}
$this->builder->deployBinary($soFile, $soFile, false);
}
/**

View File

@ -153,7 +153,7 @@ class BSDBuilder extends UnixBuilderBase
if (!$this->getOption('no-strip', false)) {
$shell->exec('strip sapi/cli/php');
}
$this->deployBinary(BUILD_TARGET_CLI);
$this->deploySAPIBinary(BUILD_TARGET_CLI);
}
/**
@ -184,7 +184,7 @@ class BSDBuilder extends UnixBuilderBase
if (!$this->getOption('no-strip', false)) {
shell()->cd(SOURCE_PATH . '/php-src/sapi/micro')->exec('strip --strip-unneeded micro.sfx');
}
$this->deployBinary(BUILD_TARGET_MICRO);
$this->deploySAPIBinary(BUILD_TARGET_MICRO);
if ($this->phar_patched) {
SourcePatcher::unpatchMicroPhar();
@ -206,7 +206,7 @@ class BSDBuilder extends UnixBuilderBase
if (!$this->getOption('no-strip', false)) {
$shell->exec('strip sapi/fpm/php-fpm');
}
$this->deployBinary(BUILD_TARGET_FPM);
$this->deploySAPIBinary(BUILD_TARGET_FPM);
}
/**

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace SPC\builder\linux;
use SPC\builder\unix\UnixBuilderBase;
use SPC\exception\PatchException;
use SPC\exception\WrongUsageException;
use SPC\store\Config;
use SPC\store\FileSystem;
@ -193,7 +194,7 @@ class LinuxBuilder extends UnixBuilderBase
SourcePatcher::patchFile('musl_static_readline.patch', SOURCE_PATH . '/php-src', true);
}
$this->deployBinary(BUILD_TARGET_CLI);
$this->deploySAPIBinary(BUILD_TARGET_CLI);
}
protected function buildCgi(): void
@ -204,7 +205,7 @@ class LinuxBuilder extends UnixBuilderBase
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("make {$concurrency} {$vars} cgi");
$this->deployBinary(BUILD_TARGET_CGI);
$this->deploySAPIBinary(BUILD_TARGET_CGI);
}
/**
@ -233,7 +234,11 @@ class LinuxBuilder extends UnixBuilderBase
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("make {$concurrency} {$vars} micro");
$this->deployBinary(BUILD_TARGET_MICRO);
// deploy micro.sfx
$dst = $this->deploySAPIBinary(BUILD_TARGET_MICRO);
// patch after UPX-ed micro.sfx
$this->processUpxedMicroSfx($dst);
} finally {
if ($this->phar_patched) {
SourcePatcher::unpatchMicroPhar();
@ -252,7 +257,7 @@ class LinuxBuilder extends UnixBuilderBase
->exec('sed -i "s|//lib|/lib|g" Makefile')
->exec("make {$concurrency} {$vars} fpm");
$this->deployBinary(BUILD_TARGET_FPM);
$this->deploySAPIBinary(BUILD_TARGET_FPM);
}
/**
@ -272,10 +277,48 @@ class LinuxBuilder extends UnixBuilderBase
->exec('sed -i "s|^EXTENSION_DIR = .*|EXTENSION_DIR = /' . basename(BUILD_MODULES_PATH) . '|" Makefile')
->exec("make {$concurrency} INSTALL_ROOT=" . BUILD_ROOT_PATH . " {$vars} install-sapi {$install_modules} install-build install-headers install-programs");
// process libphp.so for shared embed
$libphpSo = BUILD_LIB_PATH . '/libphp.so';
if (file_exists($libphpSo)) {
// deploy libphp.so
$this->deployBinary($libphpSo, $libphpSo, false);
// post actions: rename libphp.so to libphp-<release>.so if -release is set in LDFLAGS
$this->processLibphpSoFile($libphpSo);
}
// process libphp.a for static embed
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') {
$AR = getenv('AR') ?: 'ar';
f_passthru("{$AR} -t " . BUILD_LIB_PATH . "/libphp.a | grep '\\.a$' | xargs -n1 {$AR} d " . BUILD_LIB_PATH . '/libphp.a');
// export dynamic symbols
SystemUtil::exportDynamicSymbols(BUILD_LIB_PATH . '/libphp.a');
}
// patch embed php scripts
$this->patchPhpScripts();
}
/**
* Return extra variables for php make command.
*/
private function getMakeExtraVars(): array
{
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$static = SPCTarget::isStatic() ? '-all-static' : '';
$lib = BUILD_LIB_PATH;
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LIBS' => $config['libs'],
'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie",
]);
}
private function processLibphpSoFile(string $libphpSo): void
{
$ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: '';
$libDir = BUILD_LIB_PATH;
$modulesDir = BUILD_MODULES_PATH;
$libphpSo = "{$libDir}/libphp.so";
$realLibName = 'libphp.so';
$cwd = getcwd();
@ -337,33 +380,29 @@ class LinuxBuilder extends UnixBuilderBase
}
}
}
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') {
$AR = getenv('AR') ?: 'ar';
f_passthru("{$AR} -t " . BUILD_LIB_PATH . "/libphp.a | grep '\\.a$' | xargs -n1 {$AR} d " . BUILD_LIB_PATH . '/libphp.a');
// export dynamic symbols
SystemUtil::exportDynamicSymbols(BUILD_LIB_PATH . '/libphp.a');
}
if (!$this->getOption('no-strip', false) && file_exists(BUILD_LIB_PATH . '/' . $realLibName)) {
shell()->cd(BUILD_LIB_PATH)->exec("strip --strip-unneeded {$realLibName}");
}
$this->patchPhpScripts();
}
/**
* Return extra variables for php make command.
* 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
*/
private function getMakeExtraVars(): array
private function processUpxedMicroSfx(string $dst): void
{
$config = (new SPCConfigUtil($this, ['libs_only_deps' => true, 'absolute_libs' => true]))->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$static = SPCTarget::isStatic() ? '-all-static' : '';
$lib = BUILD_LIB_PATH;
return array_filter([
'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'),
'EXTRA_LIBS' => $config['libs'],
'EXTRA_LDFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'),
'EXTRA_LDFLAGS_PROGRAM' => "-L{$lib} {$static} -pie",
]);
if ($this->getOption('with-upx-pack') && version_compare($this->getMicroVersion(), '0.2.0') >= 0) {
// 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');
}
$offset = hexdec($offset);
// remove upx extra wastes
file_put_contents($dst, substr(file_get_contents($dst), 0, $offset));
}
}
}

View File

@ -189,7 +189,7 @@ class MacOSBuilder extends UnixBuilderBase
$shell = shell()->cd(SOURCE_PATH . '/php-src');
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} cli");
$this->deployBinary(BUILD_TARGET_CLI);
$this->deploySAPIBinary(BUILD_TARGET_CLI);
}
protected function buildCgi(): void
@ -199,7 +199,7 @@ class MacOSBuilder extends UnixBuilderBase
$shell = shell()->cd(SOURCE_PATH . '/php-src');
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} cgi");
$this->deployBinary(BUILD_TARGET_CGI);
$this->deploySAPIBinary(BUILD_TARGET_CGI);
}
/**
@ -229,7 +229,7 @@ class MacOSBuilder extends UnixBuilderBase
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} micro");
$this->deployBinary(BUILD_TARGET_MICRO);
$this->deploySAPIBinary(BUILD_TARGET_MICRO);
} finally {
if ($this->phar_patched) {
SourcePatcher::unpatchMicroPhar();
@ -247,7 +247,7 @@ class MacOSBuilder extends UnixBuilderBase
$shell = shell()->cd(SOURCE_PATH . '/php-src');
$concurrency = getenv('SPC_CONCURRENCY') ? '-j' . getenv('SPC_CONCURRENCY') : '';
$shell->exec("make {$concurrency} {$vars} fpm");
$this->deployBinary(BUILD_TARGET_FPM);
$this->deploySAPIBinary(BUILD_TARGET_FPM);
}
/**

View File

@ -6,7 +6,6 @@ namespace SPC\builder\unix;
use SPC\builder\BuilderBase;
use SPC\builder\linux\SystemUtil as LinuxSystemUtil;
use SPC\exception\PatchException;
use SPC\exception\SPCException;
use SPC\exception\SPCInternalException;
use SPC\exception\ValidationException;
@ -79,6 +78,82 @@ abstract class UnixBuilderBase extends BuilderBase
$this->lib_list = $sorted_libraries;
}
/**
* Strip unneeded symbols from binary file.
*/
public function stripBinary(string $binary_path): void
{
shell()->exec(match (PHP_OS_FAMILY) {
'Darwin' => "strip -S {$binary_path}",
'Linux' => "strip --strip-unneeded {$binary_path}",
default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'),
});
}
/**
* Extract debug information from binary file.
*
* @param string $binary_path the path to the binary file, including executables, shared libraries, etc
*/
public function extractDebugInfo(string $binary_path): string
{
$target_dir = BUILD_ROOT_PATH . '/debug';
FileSystem::createDir($target_dir);
$basename = basename($binary_path);
$debug_file = "{$target_dir}/{$basename}" . (PHP_OS_FAMILY === 'Darwin' ? '.dwarf' : '.debug');
shell()->exec(match (PHP_OS_FAMILY) {
'Darwin' => "dsymutil -f {$binary_path} -o {$debug_file}",
'Linux' => "objcopy ---only-keep-debug {$binary_path} {$debug_file}",
default => throw new SPCInternalException('extractDebugInfo is only supported on Linux and macOS'),
});
return $debug_file;
}
/**
* Deploy the binary file from src to dst.
*/
public function deployBinary(string $src, string $dst, bool $executable = true): string
{
// UPX for linux
$upx_option = (bool) $this->getOption('with-upx-pack', false);
// file must exists
if (!file_exists($src)) {
throw new SPCInternalException("Deploy failed. Cannot find file: {$src}");
}
// dst dir must exists
FileSystem::createDir(dirname($dst));
// ignore copy to self
if (realpath($src) !== realpath($dst)) {
shell()->exec('cp ' . escapeshellarg($src) . ' ' . escapeshellarg($dst));
}
// file exist
if (!file_exists($dst)) {
throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}");
}
// extract debug info
$this->extractDebugInfo($dst);
// strip
if (!$this->getOption('no-strip', false)) {
$this->stripBinary($dst);
}
// Compress binary with UPX if needed (only for Linux)
if ($upx_option && PHP_OS_FAMILY === 'Linux' && $executable) {
if ($this->getOption('no-strip', false)) {
logger()->warning('UPX compression is not recommended when --no-strip is enabled.');
}
logger()->info("Compressing {$dst} with UPX");
shell()->exec(getenv('UPX_EXEC') . " --best {$dst}");
}
return $dst;
}
/**
* Sanity check after build complete.
*/
@ -209,14 +284,10 @@ abstract class UnixBuilderBase extends BuilderBase
}
/**
* Deploy the binary file to the build bin path.
*
* @param int $type Type integer, one of BUILD_TARGET_CLI, BUILD_TARGET_MICRO, BUILD_TARGET_FPM, BUILD_TARGET_CGI, BUILD_TARGET_FRANKENPHP
* Deploy binaries that produces executable SAPI
*/
protected function deployBinary(int $type): void
protected function deploySAPIBinary(int $type): string
{
FileSystem::createDir(BUILD_BIN_PATH);
$copy_files = [];
$src = match ($type) {
BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php',
BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx',
@ -225,60 +296,8 @@ abstract class UnixBuilderBase extends BuilderBase
BUILD_TARGET_FRANKENPHP => BUILD_BIN_PATH . '/frankenphp',
default => throw new SPCInternalException("Deployment does not accept type {$type}"),
};
$no_strip_option = (bool) $this->getOption('no-strip', false);
$upx_option = (bool) $this->getOption('with-upx-pack', false);
// Generate debug symbols if needed
$copy_files[] = $src;
if (!$no_strip_option && PHP_OS_FAMILY === 'Darwin') {
shell()
->exec("dsymutil -f {$src}") // generate .dwarf file
->exec("strip -S {$src}"); // strip unneeded symbols
$copy_files[] = "{$src}.dwarf";
} elseif (!$no_strip_option && PHP_OS_FAMILY === 'Linux') {
shell()
->exec("objcopy --only-keep-debug {$src} {$src}.debug") // extract debug symbols
->exec("objcopy --add-gnu-debuglink={$src}.debug {$src}") // link debug symbols
->exec("strip --strip-unneeded {$src}"); // strip unneeded symbols
$copy_files[] = "{$src}.debug";
}
// Compress binary with UPX if needed (only for Linux)
if ($upx_option && PHP_OS_FAMILY === 'Linux') {
if ($no_strip_option) {
logger()->warning('UPX compression is not recommended when --no-strip is enabled.');
}
logger()->info("Compressing {$src} with UPX");
shell()->exec(getenv('UPX_EXEC') . " --best {$src}");
// 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 ($type === BUILD_TARGET_MICRO && version_compare($this->getMicroVersion(), '0.2.0') >= 0) {
// strip first
// cut binary with readelf
[$ret, $out] = shell()->execWithResult("readelf -l {$src} | 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($src, substr(file_get_contents($src), 0, $offset));
}
}
// Copy files
foreach ($copy_files as $file) {
if (!file_exists($file)) {
throw new SPCInternalException("Deploy failed. Cannot find file: {$file}");
}
// ignore copy to self
if (realpath($file) !== realpath(BUILD_BIN_PATH . '/' . basename($file))) {
shell()->exec('cp ' . escapeshellarg($file) . ' ' . escapeshellarg(BUILD_BIN_PATH . '/'));
}
}
$dst = BUILD_BIN_PATH . '/' . basename($src);
return $this->deployBinary($src, $dst);
}
/**
@ -379,7 +398,7 @@ abstract class UnixBuilderBase extends BuilderBase
->setEnv($env)
->exec("xcaddy build --output frankenphp {$xcaddyModules}");
$this->deployBinary(BUILD_TARGET_FRANKENPHP);
$this->deploySAPIBinary(BUILD_TARGET_FRANKENPHP);
}
/**

View File

@ -161,7 +161,7 @@ class WindowsBuilder extends BuilderBase
cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cli_wrapper.bat --task-args php.exe");
$this->deployBinary(BUILD_TARGET_CLI);
$this->deploySAPIBinary(BUILD_TARGET_CLI);
}
public function buildCgi(): void
@ -186,7 +186,7 @@ class WindowsBuilder extends BuilderBase
cmd()->cd(SOURCE_PATH . '\php-src')->exec("{$this->sdk_prefix} nmake_cgi_wrapper.bat --task-args php-cgi.exe");
$this->deployBinary(BUILD_TARGET_CGI);
$this->deploySAPIBinary(BUILD_TARGET_CGI);
}
public function buildEmbed(): void
@ -243,7 +243,7 @@ class WindowsBuilder extends BuilderBase
}
}
$this->deployBinary(BUILD_TARGET_MICRO);
$this->deploySAPIBinary(BUILD_TARGET_MICRO);
}
public function proveLibs(array $sorted_libraries): void
@ -357,8 +357,18 @@ class WindowsBuilder extends BuilderBase
*
* @param int $type Deploy type
*/
public function deployBinary(int $type): void
public function deploySAPIBinary(int $type): void
{
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
$debug_dir = BUILD_ROOT_PATH . '\debug';
$bin_path = BUILD_BIN_PATH;
// create dirs
FileSystem::createDir($debug_dir);
FileSystem::createDir($bin_path);
// determine source path for different SAPI
$rel_type = 'Release'; // TODO: Debug build support
$ts = $this->zts ? '_TS' : '';
$src = match ($type) {
@ -368,21 +378,32 @@ class WindowsBuilder extends BuilderBase
default => throw new SPCInternalException("Deployment does not accept type {$type}"),
};
$src = "{$src[0]}\\{$src[1]}";
$dst = BUILD_BIN_PATH . '\\' . basename($src);
// file must exists
if (!file_exists($src)) {
throw new SPCInternalException("Deploy failed. Cannot find file: {$src}");
}
// dst dir must exists
FileSystem::createDir(dirname($dst));
// copy file
if (realpath($src) !== realpath($dst)) {
cmd()->exec('copy ' . escapeshellarg($src) . ' ' . escapeshellarg($dst));
}
// extract debug info in buildroot/debug
if ($this->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) {
cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg($debug_dir));
}
// with-upx-pack for cli and micro
if ($this->getOption('with-upx-pack', false)) {
if ($type === BUILD_TARGET_CLI || $type === BUILD_TARGET_CGI || ($type === BUILD_TARGET_MICRO && version_compare($this->getMicroVersion(), '0.2.0') >= 0)) {
cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg("{$src[0]}\\{$src[1]}"));
cmd()->exec(getenv('UPX_EXEC') . ' --best ' . escapeshellarg($dst));
}
}
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' file');
FileSystem::createDir(BUILD_BIN_PATH);
cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[1]}") . ' ' . escapeshellarg(BUILD_BIN_PATH . '\\'));
if ($this->getOption('no-strip', false) && file_exists("{$src[0]}\\{$src[2]}")) {
logger()->info('Deploying ' . $this->getBuildTypeName($type) . ' debug symbols');
cmd()->exec('copy ' . escapeshellarg("{$src[0]}\\{$src[2]}") . ' ' . escapeshellarg(BUILD_BIN_PATH . '\\'));
}
}
/**

View File

@ -31,7 +31,7 @@ $test_os = [
'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64
'ubuntu-24.04-arm', // bin/spc for arm64
// 'windows-2022', // .\bin\spc.ps1
// 'windows-2025',
'windows-2025',
];
// whether enable thread safe
@ -51,13 +51,13 @@ $prefer_pre_built = false;
// If you want to test your added extensions and libs, add below (comma separated, example `bcmath,openssl`).
$extensions = match (PHP_OS_FAMILY) {
'Linux', 'Darwin' => 'rdkafka',
'Windows' => 'bcmath,bz2,calendar,ctype,curl,dom,exif,fileinfo,filter,ftp,iconv,xml,mbstring,mbregex,mysqlnd,openssl,pdo,pdo_mysql,pdo_sqlite,phar,session,simplexml,soap,sockets,sqlite3,tokenizer,xmlwriter,xmlreader,zlib,zip',
'Windows' => 'bcmath,bz2,calendar,ctype,zlib,zip',
};
// If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`).
$shared_extensions = match (PHP_OS_FAMILY) {
'Linux' => '',
'Darwin' => '',
'Linux' => 'snmp',
'Darwin' => 'snmp',
'Windows' => '',
};