diff --git a/src/SPC/builder/freebsd/BSDBuilder.php b/src/SPC/builder/freebsd/BSDBuilder.php index 985de5d2..0c1d1ee4 100644 --- a/src/SPC/builder/freebsd/BSDBuilder.php +++ b/src/SPC/builder/freebsd/BSDBuilder.php @@ -118,6 +118,11 @@ class BSDBuilder extends UnixBuilderBase } $this->buildEmbed(); } + $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); + if (!empty($shared_extensions)) { + logger()->info('Building shared extensions ...'); + $this->buildSharedExts(); + } if ($enableFrankenphp) { logger()->info('building frankenphp'); $this->buildFrankenphp(); diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 373e1b21..3d8a30f7 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -142,6 +142,12 @@ class LinuxBuilder extends UnixBuilderBase } $this->buildEmbed(); } + // build dynamic extensions if needed, must happen before building FrankenPHP to make sure we export all necessary, undefined symbols + $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); + if (!empty($shared_extensions)) { + logger()->info('Building shared extensions ...'); + $this->buildSharedExts(); + } if ($enableFrankenphp) { logger()->info('building frankenphp'); $this->buildFrankenphp(); @@ -312,6 +318,8 @@ 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)) { diff --git a/src/SPC/builder/linux/library/liburing.php b/src/SPC/builder/linux/library/liburing.php index 668659ec..82249cf8 100644 --- a/src/SPC/builder/linux/library/liburing.php +++ b/src/SPC/builder/linux/library/liburing.php @@ -51,8 +51,7 @@ class liburing extends LinuxLibraryBase $use_libc ? '--use-libc' : '', ) ->configure() - ->make('library', with_clean: false) - ->exec("rm -rf {$this->getLibDir()}/liburing*.so*"); + ->make('library', 'install ENABLE_SHARED=0', with_clean: false); $this->patchPkgconfPrefix(); } diff --git a/src/SPC/builder/macos/MacOSBuilder.php b/src/SPC/builder/macos/MacOSBuilder.php index 00c1da8c..66e9a399 100644 --- a/src/SPC/builder/macos/MacOSBuilder.php +++ b/src/SPC/builder/macos/MacOSBuilder.php @@ -156,6 +156,11 @@ class MacOSBuilder extends UnixBuilderBase } $this->buildEmbed(); } + $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); + if (!empty($shared_extensions)) { + logger()->info('Building shared extensions ...'); + $this->buildSharedExts(); + } if ($enableFrankenphp) { logger()->info('building frankenphp'); $this->buildFrankenphp(); @@ -247,6 +252,8 @@ class MacOSBuilder 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'); } $this->patchPhpScripts(); } diff --git a/src/SPC/builder/traits/UnixSystemUtilTrait.php b/src/SPC/builder/traits/UnixSystemUtilTrait.php index a64c2d94..b1ef9db4 100644 --- a/src/SPC/builder/traits/UnixSystemUtilTrait.php +++ b/src/SPC/builder/traits/UnixSystemUtilTrait.php @@ -4,15 +4,92 @@ declare(strict_types=1); namespace SPC\builder\traits; -/** - * Unix 系统的工具函数 Trait,适用于 Linux、macOS - */ +use SPC\exception\ExecutionException; +use SPC\exception\SPCInternalException; +use SPC\exception\WrongUsageException; +use SPC\toolchain\ToolchainManager; +use SPC\toolchain\ZigToolchain; +use SPC\util\SPCTarget; + trait UnixSystemUtilTrait { /** - * @param string $name 命令名称 - * @param array $paths 寻找的目标路径(如果不传入,则使用环境变量 PATH) - * @return null|string 找到了返回命令路径,找不到返回 null + * Export static library dynamic symbols to a .dynsym file. + * It will export to "/path/to/libxxx.a.dynsym". + * + * @param string $lib_file Static library file path (e.g. /path/to/libxxx.a) + */ + public static function exportDynamicSymbols(string $lib_file): void + { + // check + if (!is_file($lib_file)) { + throw new WrongUsageException("The lib archive file {$lib_file} does not exist, please build it first."); + } + // shell out + $cmd = 'nm -g --defined-only -P ' . escapeshellarg($lib_file); + $result = shell()->execWithResult($cmd); + if ($result[0] !== 0) { + throw new ExecutionException($cmd, 'Failed to get defined symbols from ' . $lib_file); + } + // parse shell output and filter + $defined = []; + foreach ($result[1] as $line) { + $line = trim($line); + if ($line === '' || str_ends_with($line, '.o:') || str_ends_with($line, '.o]:')) { + continue; + } + $name = strtok($line, " \t"); + if (!$name) { + continue; + } + $name = preg_replace('/@.*$/', '', $name); + if ($name !== '' && $name !== false) { + $defined[] = $name; + } + } + $defined = array_unique($defined); + sort($defined); + // export + if (SPCTarget::getTargetOS() === 'Linux') { + file_put_contents("{$lib_file}.dynsym", "{\n" . implode("\n", array_map(fn ($x) => " {$x};", $defined)) . "};\n"); + } else { + file_put_contents("{$lib_file}.dynsym", implode("\n", $defined) . "\n"); + } + } + + /** + * Get linker flag to export dynamic symbols from a static library. + * + * @param string $lib_file Static library file path (e.g. /path/to/libxxx.a) + * @return null|string Linker flag to export dynamic symbols, null if no .dynsym file found + */ + public static function getDynamicExportedSymbols(string $lib_file): ?string + { + $symbol_file = "{$lib_file}.dynsym"; + if (!is_file($symbol_file)) { + self::exportDynamicSymbols($lib_file); + } + if (!is_file($symbol_file)) { + throw new SPCInternalException("The symbol file {$symbol_file} does not exist, please check if nm command is available."); + } + // https://github.com/ziglang/zig/issues/24662 + if (ToolchainManager::getToolchainClass() === ZigToolchain::class) { + return '-Wl,--export-dynamic'; + } + // macOS + if (SPCTarget::getTargetOS() !== 'Linux') { + return "-Wl,-exported_symbols_list,{$symbol_file}"; + } + return "-Wl,--dynamic-list={$symbol_file}"; + } + + /** + * Find a command in given paths or system PATH. + * If $name is an absolute path, check if it exists. + * + * @param string $name Command name or absolute path + * @param array $paths Paths to search, if empty, use system PATH + * @return null|string Absolute path of the command if found, null otherwise */ public static function findCommand(string $name, array $paths = []): ?string { @@ -31,6 +108,8 @@ trait UnixSystemUtilTrait } /** + * Make environment variable string for shell command. + * * @param array $vars Variables, like: ["CFLAGS" => "-Ixxx"] * @return string like: CFLAGS="-Ixxx" */ diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index c09a9e1b..9e96c7a2 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace SPC\builder\unix; use SPC\builder\BuilderBase; +use SPC\builder\linux\SystemUtil as LinuxSystemUtil; use SPC\exception\SPCInternalException; use SPC\exception\ValidationException; use SPC\exception\WrongUsageException; @@ -137,6 +138,7 @@ abstract class UnixBuilderBase extends BuilderBase if (SPCTarget::isStatic()) { $lens .= ' -static'; } + $dynamic_exports = ''; // if someone changed to EMBED_TYPE=shared, we need to add LD_LIBRARY_PATH if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { if (PHP_OS_FAMILY === 'Darwin') { @@ -151,8 +153,13 @@ abstract class UnixBuilderBase extends BuilderBase foreach (glob(BUILD_LIB_PATH . "/libphp*.{$suffix}") as $file) { 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; + } } - [$ret, $out] = shell()->cd($sample_file_path)->execWithResult(getenv('CC') . ' -o embed embed.c ' . $lens); + $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), @@ -267,15 +274,20 @@ abstract class UnixBuilderBase extends BuilderBase ), true); $frankenPhpVersion = $releaseInfo['tag_name']; $libphpVersion = $this->getPHPVersion(); + $dynamic_exports = ''; if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { - $libphpVersion = preg_replace('/\.\d$/', '', $libphpVersion); + $libphpVersion = preg_replace('/\.\d+$/', '', $libphpVersion); + } else { + if ($dynamicSymbolsArgument = LinuxSystemUtil::getDynamicExportedSymbols(BUILD_LIB_PATH . '/libphp.a')) { + $dynamic_exports = ' ' . $dynamicSymbolsArgument; + } } $debugFlags = $this->getOption('no-strip') ? '-w -s ' : ''; - $extLdFlags = "-extldflags '-pie'"; + $extLdFlags = "-extldflags '-pie{$dynamic_exports}'"; $muslTags = ''; $staticFlags = ''; if (SPCTarget::isStatic()) { - $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000'"; + $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports}'"; $muslTags = 'static_build,'; $staticFlags = '-static-pie'; } diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index fb14e47c..58b6059b 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -211,12 +211,6 @@ class BuildPHPCommand extends BuildCommand // start to build $builder->buildPHP($rule); - // build dynamic extensions if needed - if (!empty($shared_extensions)) { - logger()->info('Building shared extensions ...'); - $builder->buildSharedExts(); - } - $builder->testPHP($rule); // compile stopwatch :P diff --git a/src/globals/test-extensions.php b/src/globals/test-extensions.php index 939a25ff..764cfc7d 100644 --- a/src/globals/test-extensions.php +++ b/src/globals/test-extensions.php @@ -13,9 +13,9 @@ declare(strict_types=1); // test php version (8.1 ~ 8.4 available, multiple for matrix) $test_php_version = [ - '8.1', - '8.2', - '8.3', + // '8.1', + // '8.2', + // '8.3', '8.4', // '8.5', // 'git', @@ -28,9 +28,9 @@ $test_os = [ 'macos-15', // bin/spc for arm64 'ubuntu-latest', // bin/spc-alpine-docker for x86_64 'ubuntu-22.04', // bin/spc-gnu-docker for x86_64 - 'ubuntu-24.04', // bin/spc for x86_64 + // 'ubuntu-24.04', // bin/spc for x86_64 'ubuntu-22.04-arm', // bin/spc-gnu-docker for arm64 - 'ubuntu-24.04-arm', // bin/spc for arm64 + // 'ubuntu-24.04-arm', // bin/spc for arm64 // 'windows-latest', // .\bin\spc.ps1 ]; @@ -50,13 +50,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' => 'swoole,swoole-hook-mysql,swoole-hook-pgsql,swoole-hook-sqlite,swoole-hook-odbc,apcu,bcmath,bz2,calendar,ctype,curl,dba,dom,event,exif,fileinfo,filter,ftp,gd,gmp,iconv,imagick,intl,mbregex,mbstring,mysqli,mysqlnd,opcache,openssl,pcntl,pdo,pdo_mysql,pgsql,phar,posix,protobuf,readline,redis,session,shmop,simplexml,soap,sockets,sodium,sqlite3,swoole,sysvmsg,sysvsem,sysvshm,tokenizer,xml,xmlreader,xmlwriter,xsl,zip,zlib', + 'Linux', 'Darwin' => 'bcmath', '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', }; // If you want to test shared extensions, add them below (comma separated, example `bcmath,openssl`). $shared_extensions = match (PHP_OS_FAMILY) { - 'Linux' => '', + 'Linux' => 'zip', 'Darwin' => '', 'Windows' => '', };