isLibsOnly()) { $libraries = array_keys($support_lib_list); $sorted_libraries = DependencyUtil::getLibs($libraries); } // add lib object for builder foreach ($sorted_libraries as $library) { if (!in_array(Config::getLib($library, 'type', 'lib'), ['lib', 'package'])) { continue; } // 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}."); } $lib = new ($support_lib_list[$library])($this); $this->addLib($lib); } // calculate and check dependencies foreach ($this->libs as $lib) { $lib->calcDependency(); } $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'); if (PHP_OS_FAMILY === 'Darwin') { shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); } elseif (PHP_OS_FAMILY === 'Linux') { shell() ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}") ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); } else { 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 { logger()->debug('Deploying binary from ' . $src . ' to ' . $dst); // 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. */ protected function sanityCheck(int $build_target): void { // sanity check for php-cli if (($build_target & BUILD_TARGET_CLI) === BUILD_TARGET_CLI) { logger()->info('running cli sanity check'); [$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'); } foreach ($this->getExts() as $ext) { logger()->debug('testing ext: ' . $ext->getName()); $ext->runCliCheckUnix(); } } // 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}]" ); } } } } // 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 'Hello, World!\";' | " . 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'); } } // sanity check for embed if (($build_target & BUILD_TARGET_EMBED) === BUILD_TARGET_EMBED) { 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']}"; 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') { $ext_path = 'DYLD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$DYLD_LIBRARY_PATH '; } else { $ext_path = 'LD_LIBRARY_PATH=' . BUILD_LIB_PATH . ':$LD_LIBRARY_PATH '; } FileSystem::removeFileIfExists(BUILD_LIB_PATH . '/libphp.a'); } else { $ext_path = ''; $suffix = PHP_OS_FAMILY === 'Darwin' ? 'dylib' : 'so'; 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; } } $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'); 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' ); } } // 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' ); } $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 sanity check: ret[' . $ret . ']. out[' . implode('', $output) . ']', validation_module: 'FrankenPHP sanity check' ); } } } /** * Deploy binaries that produces executable SAPI */ protected function deploySAPIBinary(int $type): string { $src = match ($type) { 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', 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}"), }; $dst = BUILD_BIN_PATH . '/' . basename($src); return $this->deployBinary($src, $dst); } /** * 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); } foreach ($this->getLibs() as $lib) { if ($lib->patchPhpConfig()) { logger()->debug("Library {$lib->getName()} patched php-config"); } } } /** * Process the --with-frankenphp-app option * Creates app.tar and app.checksum in source/frankenphp directory */ protected function processFrankenphpApp(): void { $frankenphpSourceDir = SOURCE_PATH . '/frankenphp'; SourceManager::initSource(['frankenphp'], ['frankenphp']); $frankenphpAppPath = $this->getOption('with-frankenphp-app'); if ($frankenphpAppPath) { if (!is_dir($frankenphpAppPath)) { throw new WrongUsageException("The path provided to --with-frankenphp-app is not a valid directory: {$frankenphpAppPath}"); } $appTarPath = $frankenphpSourceDir . '/app.tar'; logger()->info("Creating app.tar from {$frankenphpAppPath}"); shell()->exec('tar -cf ' . escapeshellarg($appTarPath) . ' -C ' . escapeshellarg($frankenphpAppPath) . ' .'); $checksum = hash_file('md5', $appTarPath); file_put_contents($frankenphpSourceDir . '/app_checksum.txt', $checksum); } else { FileSystem::removeFileIfExists($frankenphpSourceDir . '/app.tar'); FileSystem::removeFileIfExists($frankenphpSourceDir . '/app_checksum.txt'); file_put_contents($frankenphpSourceDir . '/app.tar', ''); file_put_contents($frankenphpSourceDir . '/app_checksum.txt', ''); } } protected function getFrankenPHPVersion(): string { $goModPath = SOURCE_PATH . '/frankenphp/caddy/go.mod'; if (!file_exists($goModPath)) { throw new SPCInternalException("FrankenPHP caddy/go.mod file not found at {$goModPath}, why did we not download FrankenPHP?"); } $content = file_get_contents($goModPath); if (preg_match('/github\.com\/dunglas\/frankenphp\s+v?(\d+\.\d+\.\d+)/', $content, $matches)) { return $matches[1]; } throw new SPCInternalException('Could not find FrankenPHP version in caddy/go.mod'); } protected function buildFrankenphp(): void { GlobalEnvManager::addPathIfNotExists(GoXcaddy::getPath()); $this->processFrankenphpApp(); $nobrotli = $this->getLib('brotli') === null ? ',nobrotli' : ''; $nowatcher = $this->getLib('watcher') === null ? ',nowatcher' : ''; $xcaddyModules = getenv('SPC_CMD_VAR_FRANKENPHP_XCADDY_MODULES'); $frankenphpSourceDir = SOURCE_PATH . '/frankenphp'; $xcaddyModules = preg_replace('#--with github.com/dunglas/frankenphp(=\S+)?#', '', $xcaddyModules); $xcaddyModules = preg_replace('#--with github.com/dunglas/frankenphp/caddy(=\S+)?#', '', $xcaddyModules); $xcaddyModules = "--with github.com/dunglas/frankenphp={$frankenphpSourceDir} " . "--with github.com/dunglas/frankenphp/caddy={$frankenphpSourceDir}/caddy {$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); } $frankenPhpVersion = $this->getFrankenPHPVersion(); $libphpVersion = $this->getPHPVersion(); $dynamic_exports = ''; if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared') { $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{$dynamic_exports} {$this->arch_ld_flags}'"; $muslTags = ''; $staticFlags = ''; if (SPCTarget::isStatic()) { $extLdFlags = "-extldflags '-static-pie -Wl,-z,stack-size=0x80000{$dynamic_exports} {$this->arch_ld_flags}'"; $muslTags = 'static_build,'; $staticFlags = '-static-pie'; } $config = (new SPCConfigUtil($this))->config($this->ext_list, $this->lib_list); $cflags = "{$this->arch_c_flags} {$config['cflags']} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS') . ' -DFRANKENPHP_VERSION=' . $frankenPhpVersion; $libs = $config['libs']; // Go's gcc driver doesn't automatically link against -lgcov or -lrt. Ugly, but necessary fix. if ((str_contains((string) getenv('SPC_DEFAULT_C_FLAGS'), '-fprofile') || str_contains((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), '-fprofile')) && ToolchainManager::getToolchainClass() === GccNativeToolchain::class) { $cflags .= ' -Wno-error=missing-profile'; $libs .= ' -lgcov'; } $env = [...[ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => clean_spaces($cflags), 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . $debugFlags . '-X \'github.com/caddyserver/caddy/v2.CustomVersion=FrankenPHP ' . "v{$frankenPhpVersion} PHP {$libphpVersion} Caddy'\\\" " . "-tags={$muslTags}nobadger,nomysql,nopgx{$nobrotli}{$nowatcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ], ...GoXcaddy::getEnvironment()]; shell()->cd(BUILD_BIN_PATH) ->setEnv($env) ->exec("xcaddy build --output frankenphp {$xcaddyModules}"); $this->deploySAPIBinary(BUILD_TARGET_FRANKENPHP); } /** * 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; } } }