static-php-cli/src/SPC/builder/unix/UnixBuilderBase.php

401 lines
19 KiB
PHP
Raw Normal View History

2023-03-18 17:32:21 +08:00
<?php
declare(strict_types=1);
2024-01-10 21:08:25 +08:00
namespace SPC\builder\unix;
2023-03-18 17:32:21 +08:00
2024-01-10 21:08:25 +08:00
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;
use SPC\exception\WrongUsageException;
2024-01-10 21:08:25 +08:00
use SPC\store\Config;
use SPC\store\FileSystem;
use SPC\store\pkg\GoXcaddy;
2025-08-25 18:44:03 +07:00
use SPC\toolchain\GccNativeToolchain;
use SPC\toolchain\ToolchainManager;
2024-01-10 21:08:25 +08:00
use SPC\util\DependencyUtil;
2025-06-26 17:23:37 +07:00
use SPC\util\GlobalEnvManager;
2024-12-10 23:08:01 +08:00
use SPC\util\SPCConfigUtil;
use SPC\util\SPCTarget;
2023-03-18 17:32:21 +08:00
2024-01-10 21:08:25 +08:00
abstract class UnixBuilderBase extends BuilderBase
2023-03-18 17:32:21 +08:00
{
/** @var string cflags */
2023-03-18 17:32:21 +08:00
public string $arch_c_flags;
/** @var string C++ flags */
2023-03-18 17:32:21 +08:00
public string $arch_cxx_flags;
/** @var string LD flags */
public string $arch_ld_flags;
public function proveLibs(array $sorted_libraries): void
2024-01-10 21:08:25 +08:00
{
// search all supported libs
$support_lib_list = [];
$classes = FileSystem::getClassesPsr4(
ROOT_DIR . '/src/SPC/builder/' . osfamily2dir() . '/library',
'SPC\builder\\' . osfamily2dir() . '\library'
2024-01-10 21:08:25 +08:00
);
foreach ($classes as $class) {
if (defined($class . '::NAME') && $class::NAME !== 'unknown' && Config::getLib($class::NAME) !== null) {
$support_lib_list[$class::NAME] = $class;
}
}
// if no libs specified, compile all supported libs
if ($sorted_libraries === [] && $this->isLibsOnly()) {
$libraries = array_keys($support_lib_list);
$sorted_libraries = DependencyUtil::getLibs($libraries);
2024-01-10 21:08:25 +08:00
}
// add lib object for builder
foreach ($sorted_libraries as $library) {
if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) {
continue;
}
2024-01-10 21:08:25 +08:00
// if some libs are not supported (but in config "lib.json", throw exception)
if (!isset($support_lib_list[$library])) {
$os = match (PHP_OS_FAMILY) {
'Linux' => 'Linux',
'Darwin' => 'macOS',
'Windows' => 'Windows',
'BSD' => 'FreeBSD',
default => PHP_OS_FAMILY,
};
throw new WrongUsageException("library [{$library}] is in the lib.json list but not supported to build on {$os}.");
2024-01-10 21:08:25 +08:00
}
$lib = new ($support_lib_list[$library])($this);
$this->addLib($lib);
}
// calculate and check dependencies
foreach ($this->libs as $lib) {
$lib->calcDependency();
}
2024-12-10 23:08:01 +08:00
$this->lib_list = $sorted_libraries;
2024-01-10 21:08:25 +08:00
}
2023-03-18 17:32:21 +08:00
/**
* Sanity check after build complete.
2023-03-18 17:32:21 +08:00
*/
protected function sanityCheck(int $build_target): void
2023-03-18 17:32:21 +08:00
{
2023-04-23 20:31:58 +08:00
// sanity check for php-cli
if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) {
2023-04-23 20:31:58 +08:00
logger()->info('running cli sanity check');
2025-05-21 18:35:48 +07:00
[$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 sanity check. code: {$ret}, output: {$raw_output}", validation_module: 'php-cli sanity check');
2023-03-18 17:32:21 +08:00
}
2025-05-21 17:57:53 +07:00
foreach ($this->getExts() as $ext) {
logger()->debug('testing ext: ' . $ext->getName());
2024-01-10 21:08:25 +08:00
$ext->runCliCheckUnix();
2023-03-18 17:32:21 +08:00
}
}
2023-04-23 20:31:58 +08:00
// sanity check for phpmicro
if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) {
$test_task = $this->getMicroTestTasks();
foreach ($test_task as $task_name => $task) {
$test_file = SOURCE_PATH . '/' . $task_name . '.exe';
if (file_exists($test_file)) {
@unlink($test_file);
}
file_put_contents($test_file, file_get_contents(SOURCE_PATH . '/php-src/sapi/micro/micro.sfx') . $task['content']);
chmod($test_file, 0755);
[$ret, $out] = shell()->execWithResult($test_file);
foreach ($task['conditions'] as $condition => $closure) {
if (!$closure($ret, $out)) {
$raw_out = trim(implode('', $out));
throw new ValidationException(
"failure info: {$condition}, code: {$ret}, output: {$raw_out}",
validation_module: "phpmicro sanity check item [{$task_name}]"
);
}
2024-01-29 10:04:21 +08:00
}
2023-03-18 17:32:21 +08:00
}
}
2024-12-10 23:08:01 +08:00
2025-09-04 14:05:00 +08:00
// sanity check for php-cgi
if (($build_target & BUILD_TARGET_CGI) === BUILD_TARGET_CGI) {
logger()->info('running cgi sanity check');
[$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 sanity check. code: {$ret}, output: {$raw_output}", validation_module: 'php-cgi sanity check');
}
}
2024-12-10 23:08:01 +08:00
// sanity check for embed
2025-06-12 00:41:33 +08:00
if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) {
2024-12-10 23:08:01 +08:00
logger()->info('running embed sanity check');
$sample_file_path = SOURCE_PATH . '/embed-test';
if (!is_dir($sample_file_path)) {
@mkdir($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');
$util = new SPCConfigUtil($this);
$config = $util->config($this->ext_list, $this->lib_list, $this->getOption('with-suggested-exts'), $this->getOption('with-suggested-libs'));
$lens = "{$config['cflags']} {$config['ldflags']} {$config['libs']}";
2025-06-29 22:49:48 +08:00
if (SPCTarget::isStatic()) {
2024-12-10 23:08:01 +08:00
$lens .= ' -static';
}
$dynamic_exports = '';
2025-07-01 13:02:59 +07:00
// if someone changed to EMBED_TYPE=shared, we need to add LD_LIBRARY_PATH
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
2025-07-25 16:24:22 +07:00
if (PHP_OS_FAMILY === 'Darwin') {
$ext_path = 'DYLD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$DYLD_LIBRARY_PATH ';
2025-09-04 17:45:13 +07:00
} else {
2025-07-25 16:24:22 +07:00
$ext_path = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$LD_LIBRARY_PATH ';
}
2025-07-01 13:02:59 +07:00
FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a');
2025-09-04 17:45:13 +07:00
} else {
2025-07-01 13:02:59 +07:00
$ext_path = '';
2025-07-25 16:26:02 +07:00
$suffix = PHP_OS_FAMILY === 'Darwin' ? 'dylib' : 'so';
foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) {
2025-07-01 13:02:59 +07:00
unlink($file);
}
// calling linux system util in other unix OS is okay
if ($dynamic_exports = LinuxSystemUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) {
$dynamic_exports = ' ' . $dynamic_exports;
2025-08-29 15:14:54 +07:00
}
2025-07-01 13:02:59 +07:00
}
$cc = getenv('CC');
[$ret, $out] = shell()->cd($sample_file_path)->execWithResult("{$cc} -o embed embed.c {$lens} {$dynamic_exports}");
if ($ret !== 0) {
throw new ValidationException(
'embed failed sanity check: build failed. Error message: ' . implode("\n", $out),
validation_module: 'static libphp.a sanity check'
);
}
[$ret, $output] = shell()->cd($sample_file_path)->execWithResult($ext_path . './embed');
2024-12-10 23:08:01 +08:00
if ($ret !== 0 || trim(implode('', $output)) !== 'hello') {
throw new ValidationException(
'embed failed sanity check: run failed. Error message: ' . implode("\n", $output),
validation_module: 'static libphp.a sanity check'
);
2024-12-10 23:08:01 +08:00
}
}
// sanity check for frankenphp
if (($build_target & BUILD_TARGET_FRANKENPHP) === BUILD_TARGET_FRANKENPHP) {
logger()->info('running frankenphp sanity check');
$frankenphp = BUILD_BIN_PATH . '/frankenphp';
if (!file_exists($frankenphp)) {
throw new ValidationException(
"FrankenPHP binary not found: {$frankenphp}",
validation_module: 'FrankenPHP sanity check'
);
}
2025-07-22 14:46:41 +08:00
$prefix = PHP_OS_FAMILY === 'Darwin' ? 'DYLD_' : 'LD_';
[$ret, $output] = shell()
2025-07-22 14:46:41 +08:00
->setEnv(["{$prefix}LIBRARY_PATH" => BUILD_LIB_PATH])
->execWithResult("{$frankenphp} version");
if ($ret !== 0 || !str_contains(implode('', $output), 'FrankenPHP')) {
throw new ValidationException(
'FrankenPHP failed sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']',
validation_module: 'FrankenPHP sanity check'
);
}
}
2023-03-18 17:32:21 +08:00
}
/**
* 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
*/
protected function deployBinary(int $type): void
{
FileSystem::createDir(BUILD_BIN_PATH);
$copy_files = [];
$src = match ($type) {
2023-04-23 20:31:58 +08:00
BUILD_TARGET_CLI => SOURCE_PATH . '/php-src/sapi/cli/php',
BUILD_TARGET_MICRO => SOURCE_PATH . '/php-src/sapi/micro/micro.sfx',
BUILD_TARGET_FPM => SOURCE_PATH . '/php-src/sapi/fpm/php-fpm',
2025-09-04 14:05:00 +08:00
BUILD_TARGET_CGI => SOURCE_PATH . '/php-src/sapi/cgi/php-cgi',
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
2025-11-01 00:49:50 +08:00
->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}");
}
2025-11-01 00:49:50 +08:00
// ignore copy to self
if (realpath($file) !== realpath(BUILD_BIN_PATH . '/' . basename($file))) {
shell()->exec('cp ' . escapeshellarg($file) . ' ' . escapeshellarg(BUILD_BIN_PATH . '/'));
}
}
}
/**
* Run php clean
*/
protected function cleanMake(): void
{
logger()->info('cleaning up php-src build files');
shell()->cd(SOURCE_PATH . '/php-src')->exec('make clean');
}
/**
* Patch phpize and php-config if needed
*/
protected function patchPhpScripts(): 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#');
}
// 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);
}
2025-07-10 12:59:27 +08:00
foreach ($this->getLibs() as $lib) {
if ($lib->patchPhpConfig()) {
logger()->debug("Library {$lib->getName()} patched php-config");
}
}
}
2025-06-18 10:48:09 +07:00
protected function buildFrankenphp(): void
{
2025-09-09 12:10:06 +07:00
GlobalEnvManager::addPathIfNotExists(GoXcaddy::getPath());
2025-06-18 10:48:09 +07:00
$nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : '';
$nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : '';
$xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES');
// make it possible to build from a different frankenphp directory!
2025-06-19 12:23:33 +07:00
if (!str_contains($xcaddyModules, '--with github.com/dunglas/frankenphp')) {
$xcaddyModules = '--with github.com/dunglas/frankenphp ' . $xcaddyModules;
}
if ($this->getLib('brotli') === null && str_contains($xcaddyModules, '--with github.com/dunglas/caddy-cbrotli')) {
logger()->warning('caddy-cbrotli module is enabled, but brotli library is not built. Disabling caddy-cbrotli.');
$xcaddyModules = str_replace('--with github.com/dunglas/caddy-cbrotli', '', $xcaddyModules);
}
[, $out] = shell()->execWithResult('go list -m github.com/dunglas/frankenphp@latest');
$frankenPhpVersion = str_replace('github.com/dunglas/frankenphp v', '', $out[0]);
2025-06-18 15:50:55 +07:00
$libphpVersion = $this->getPHPVersion();
$dynamic_exports = '';
if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') {
$libphpVersion = preg_replace('/\.\d+$/', '', $libphpVersion);
2025-09-04 17:45:13 +07:00
} else {
if ($dynamicSymbolsArgument = LinuxSystemUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) {
2025-08-29 16:36:43 +07:00
$dynamic_exports = ' ' . $dynamicSymbolsArgument;
}
}
$debugFlags = $this->getOption('no-strip') ? '-w -s ' : '';
2025-09-19 15:29:49 +02:00
$extLdFlags = "-extldflags '-pie{$dynamic_exports} {$this->arch_ld_flags}'";
2025-06-19 11:59:41 +07:00
$muslTags = '';
2025-07-01 13:02:59 +07:00
$staticFlags = '';
2025-06-29 22:49:48 +08:00
if (SPCTarget::isStatic()) {
2025-09-19 15:29:49 +02:00
$extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$this->arch_ld_flags}'";
2025-06-19 11:59:41 +07:00
$muslTags = 'static_build,';
2025-07-29 13:34:01 +07:00
$staticFlags = '-static-pie';
2025-06-19 11:59:41 +07:00
}
2025-07-24 22:01:32 +07:00
$config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list);
2025-11-03 20:39:26 +01:00
$cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -DFRANKENPHP_VERSION=' . $frankenPhpVersion;
2025-08-25 18:44:03 +07:00
$libs = $config['libs'];
2025-08-27 08:31:48 +07:00
// Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix.
2025-09-04 17:45:13 +07:00
if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') ||
str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) &&
2025-08-25 18:44:03 +07:00
ToolchainManager::getToolchainClass() === GccNativeToolchain::class) {
$cflags .= ' -Wno-error=missing-profile';
2025-08-25 18:44:03 +07:00
$libs .= ' -lgcov';
}
2025-09-09 12:10:06 +07:00
$env = [...[
2025-06-18 10:48:09 +07:00
'CGO_ENABLED' => '1',
'CGO_CFLAGS' => clean_spaces($cflags),
2025-08-25 19:31:15 +07:00
'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}",
2025-06-18 15:50:55 +07:00
'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' .
2025-06-19 11:59:41 +07:00
'-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . $debugFlags .
2025-06-18 15:50:55 +07:00
'-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' .
2025-09-10 23:35:17 +07:00
"v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " .
2025-06-19 11:59:41 +07:00
"-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}",
'LD_LIBRARY_PATH' => BUILD_LIB_PATH,
2025-09-09 12:10:06 +07:00
], ...GoXcaddy::getEnvironment()];
2025-06-18 10:48:09 +07:00
shell()->cd(BUILD_BIN_PATH)
->setEnv($env)
->exec("xcaddy build --output frankenphp {$xcaddyModules}");
2025-06-27 22:48:15 +07:00
$this->deployBinary(BUILD_TARGET_FRANKENPHP);
2025-06-18 10:48:09 +07:00
}
/**
* Seek php-src/config.log when building PHP, add it to exception.
*/
protected function seekPhpSrcLogFileOnException(callable $callback): void
{
try {
$callback();
} catch (SPCException $e) {
if (file_exists(SOURCE_PATH . '/php-src/config.log')) {
$e->addExtraLogFile('php-src config.log', 'php-src.config.log');
copy(SOURCE_PATH . '/php-src/config.log', SPC_LOGS_DIR . '/php-src.config.log');
}
throw $e;
}
}
2023-03-18 17:32:21 +08:00
}