From eff46209da4fa27c5106021598772ab4bcd7e9f6 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 27 Apr 2026 19:34:45 +0700 Subject: [PATCH 1/9] add --pgi and --pgo to facilitate PGO builds --- src/SPC/builder/Extension.php | 15 -- src/SPC/builder/linux/LinuxBuilder.php | 66 +++++---- src/SPC/builder/unix/UnixBuilderBase.php | 3 +- src/SPC/command/BuildPHPCommand.php | 17 ++- src/SPC/store/pkg/Zig.php | 158 ++++++++++++++++++-- src/SPC/store/scripts/zig-cc.sh | 18 +++ src/SPC/toolchain/ZigToolchain.php | 21 --- src/SPC/util/PgoManager.php | 179 +++++++++++++++++++++++ 8 files changed, 401 insertions(+), 76 deletions(-) create mode 100644 src/SPC/util/PgoManager.php diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index 63dc31d04..b405612c0 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -187,14 +187,6 @@ public function patchBeforeWindowsConfigure(): bool */ public function patchBeforeMake(): bool { - if (SPCTarget::getTargetOS() === 'Linux' && $this->isBuildShared() && ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS'))) { - FileSystem::replaceFileRegex( - SOURCE_PATH . '/php-src/Makefile', - "/^(shared_objects_{$this->getName()}\\s*=.*)$/m", - "$1 {$objs}", - ); - return true; - } return false; } @@ -244,13 +236,6 @@ public function patchBeforeSharedMake(): bool ); } - if ($objs = getenv('SPC_EXTRA_RUNTIME_OBJECTS')) { - FileSystem::replaceFileRegex( - $this->source_dir . '/Makefile', - "/^(shared_objects_{$this->getName()}\\s*=.*)$/m", - "$1 {$objs}", - ); - } return true; } diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index ed26d64f3..3963c74e3 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -14,6 +14,7 @@ use SPC\toolchain\ToolchainManager; use SPC\toolchain\ZigToolchain; use SPC\util\GlobalEnvManager; +use SPC\util\PgoManager; use SPC\util\SPCConfigUtil; use SPC\util\SPCTarget; @@ -132,32 +133,36 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $this->cleanMake(); - if ($enableCli) { - logger()->info('building cli'); - $this->buildCli(); - } - if ($enableFpm) { - logger()->info('building fpm'); - $this->buildFpm(); - } - if ($enableCgi) { - logger()->info('building cgi'); - $this->buildCgi(); - } - if ($enableMicro) { - logger()->info('building micro'); - $this->buildMicro(); - } - if ($enableEmbed) { - logger()->info('building embed'); - if ($enableMicro) { - FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); + $pgo = PgoManager::active(); + $needsClean = false; + $sapiBuilds = [ + ['cli', $enableCli, true, fn () => $this->buildCli()], + ['fpm', $enableFpm, true, fn () => $this->buildFpm()], + ['cgi', $enableCgi, true, fn () => $this->buildCgi()], + ['micro', $enableMicro, true, fn () => $this->buildMicro()], + ['embed', $enableEmbed, true, function () use ($enableMicro): void { + if ($enableMicro) { + FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); + } + $this->buildEmbed(); + }], + // frankenphp doesn't rebuild php-src; xcaddy links against the deployed libphp.so + ['frankenphp', $enableFrankenphp, false, fn () => $this->buildFrankenphp()], + ]; + + foreach ($sapiBuilds as [$sapi, $enabled, $rebuildsPhpSrc, $build]) { + if (!$enabled) { + continue; } - $this->buildEmbed(); - } - if ($enableFrankenphp) { - logger()->info('building frankenphp'); - $this->buildFrankenphp(); + if ($pgo) { + if ($needsClean && $rebuildsPhpSrc) { + $this->cleanMake(); + } + $pgo->applyForSapi($sapi); + $needsClean = $needsClean || $rebuildsPhpSrc; + } + logger()->info('building ' . $sapi); + $build(); } $shared_extensions = array_map('trim', array_filter(explode(',', $this->getOption('build-shared')))); if (!empty($shared_extensions)) { @@ -327,11 +332,18 @@ 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; + $extra_ldflags = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'); + if (getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'shared' + && !str_contains($extra_ldflags, '-avoid-version') + && !preg_match('/-release\s+\S+/', $extra_ldflags)) { + $extra_ldflags = trim($extra_ldflags . ' -avoid-version -module'); + } + $extra_ldflags_program = trim("-L{$lib} {$static} -pie " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM')); 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", + 'EXTRA_LDFLAGS' => $extra_ldflags, + 'EXTRA_LDFLAGS_PROGRAM' => $extra_ldflags_program, ]); } diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index fd16656ce..8891d8023 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -450,10 +450,11 @@ protected function buildFrankenphp(): void $cflags .= ' -Wno-error=missing-profile'; $libs .= ' -lgcov'; } + $extraLdProgram = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM'); $env = [...[ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => clean_spaces($cflags), - 'CGO_LDFLAGS' => "{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs}", + 'CGO_LDFLAGS' => trim("{$this->arch_ld_flags} {$staticFlags} {$config['ldflags']} {$libs} {$extraLdProgram}"), 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index ad884b3ba..69772efe0 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -12,6 +12,7 @@ use SPC\util\DependencyUtil; use SPC\util\GlobalEnvManager; use SPC\util\LicenseDumper; +use SPC\util\PgoManager; use SPC\util\SPCTarget; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -49,6 +50,8 @@ public function configure(): void $this->addOption('with-micro-logo', null, InputOption::VALUE_REQUIRED, 'Use custom .ico for micro.sfx (windows only)'); $this->addOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); $this->addOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); + $this->addOption('pgi', null, null, 'Build instrumented binaries (-fprofile-generate). Run them to collect .profraw files, then re-run with --pgo.'); + $this->addOption('pgo', null, null, 'Build optimised binaries (-fprofile-use) from .profraw collected by a previous --pgi run.'); } public function handle(): int @@ -210,9 +213,19 @@ public function handle(): int // clean old modules that may conflict with the new php build FileSystem::removeDir(BUILD_MODULES_PATH); - // start to build - $builder->buildPHP($rule); + $pgi = (bool) $this->getOption('pgi'); + $pgo = (bool) $this->getOption('pgo'); + if ($pgi && $pgo) { + $this->output->writeln('--pgi and --pgo are mutually exclusive'); + return static::FAILURE; + } + if ($pgi) { + (new PgoManager())->setupInstrument($rule); + } elseif ($pgo) { + (new PgoManager())->setupUse($rule); + } + $builder->buildPHP($rule); $builder->testPHP($rule); // compile stopwatch :P diff --git a/src/SPC/store/pkg/Zig.php b/src/SPC/store/pkg/Zig.php index c2a81c0da..5beba5bde 100644 --- a/src/SPC/store/pkg/Zig.php +++ b/src/SPC/store/pkg/Zig.php @@ -116,18 +116,17 @@ public function extract(string $name): void break; } } - if ($all_exist) { - return; - } - - $lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true); - $source_type = $lock[$name]['source_type']; - $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); - $extract = "{$pkgroot}/zig"; + if (!$all_exist) { + $lock = json_decode(FileSystem::readFile(LockFile::LOCK_FILE), true); + $source_type = $lock[$name]['source_type']; + $filename = DOWNLOAD_PATH . '/' . ($lock[$name]['filename'] ?? $lock[$name]['dirname']); + $extract = "{$pkgroot}/zig"; - FileSystem::extractPackage($name, $source_type, $filename, $extract); + FileSystem::extractPackage($name, $source_type, $filename, $extract); - $this->createZigCcScript($zig_bin_dir); + $this->createZigCcScript($zig_bin_dir); + } + $this->buildClangRuntimeBits($zig_bin_dir); } public static function getEnvironment(): array @@ -140,6 +139,145 @@ public static function getPath(): ?string return PKG_ROOT_PATH . '/zig'; } + /** + * Build the bits of clang's runtime that zig 0.16 doesn't ship: the + * profile runtime (so -fprofile-generate actually emits .profraw) and + * crtbegin.o/crtend.o (so shared libraries get __dso_handle and the + * __cxa_finalize atexit hook). + * + * Build from 2mb compiler-rt-.src tar + * to avoid downloading 2gb full prebuilt tarball. + */ + private function buildClangRuntimeBits(string $zig_bin_dir): void + { + if (PHP_OS_FAMILY !== 'Linux') { + return; + } + $libDir = "{$zig_bin_dir}/lib"; + $profileLib = "{$libDir}/libclang_rt.profile.a"; + $crtBegin = "{$libDir}/clang_rt.crtbegin.o"; + $crtEnd = "{$libDir}/clang_rt.crtend.o"; + if (file_exists($profileLib) && file_exists($crtBegin) && file_exists($crtEnd)) { + return; + } + + $zig = "{$zig_bin_dir}/zig"; + $verLine = trim((string)shell_exec(escapeshellarg($zig) . ' cc --version 2>/dev/null')); + if (!preg_match('/clang version (\d+\.\d+\.\d+)/', $verLine, $m)) { + logger()->warning('[zig] could not detect bundled clang version; skipping runtime bit build (--pgo + shared libs without __dso_handle)'); + return; + } + $llvmVersion = $m[1]; + logger()->info("Building clang runtime bits for LLVM {$llvmVersion} (zig's bundled clang)"); + + $srcRoot = $this->fetchCompilerRtSource($llvmVersion); + if ($srcRoot === null) { + return; + } + + f_mkdir($libDir, recursive: true); + if (!file_exists($profileLib)) { + $this->buildProfileRuntime($zig, $srcRoot, $profileLib); + } + if (!file_exists($crtBegin) || !file_exists($crtEnd)) { + $this->buildCrtObjects($zig, $srcRoot, $crtBegin, $crtEnd); + } + FileSystem::removeDir($srcRoot); + } + + private function fetchCompilerRtSource(string $llvmVersion): ?string + { + $pkgName = "compiler-rt-{$llvmVersion}"; + $tarball = "compiler-rt-{$llvmVersion}.src.tar.xz"; + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}"; + try { + Downloader::downloadPackage($pkgName, [ + 'type' => 'url', + 'url' => $url, + 'filename' => $tarball, + ]); + } + catch (\Throwable $e) { + logger()->warning("[zig] failed to download {$tarball}: {$e->getMessage()}"); + return null; + } + $srcRoot = PKG_ROOT_PATH . "/compiler-rt-src-{$llvmVersion}"; + FileSystem::removeDir($srcRoot); + FileSystem::extractPackage($pkgName, SPC_SOURCE_ARCHIVE, DOWNLOAD_PATH . '/' . $tarball, $srcRoot); + return $srcRoot; + } + + private function buildProfileRuntime(string $zig, string $srcRoot, string $libPath): void + { + $profileSrc = "{$srcRoot}/lib/profile"; + $profileInc = "{$srcRoot}/include"; + if (!is_dir($profileSrc)) { + logger()->warning("[zig] profile src dir missing at {$profileSrc} — --pgo will not work"); + return; + } + $sources = array_merge( + glob("{$profileSrc}/*.c") ?: [], + glob("{$profileSrc}/*.cpp") ?: [] + ); + $skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap']; + $sources = array_filter($sources, function ($f) use ($skip) { + foreach ($skip as $s) { + if (str_contains($f, $s)) { + return false; + } + } + return true; + }); + + $objDir = "{$srcRoot}/obj-profile"; + f_mkdir($objDir, recursive: true); + $cflags = '-c -O2 -fPIC -fvisibility=hidden ' . + '-I' . escapeshellarg($profileInc) . ' ' . + '-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1'; + $objs = []; + foreach ($sources as $src) { + $obj = $objDir . '/' . pathinfo($src, PATHINFO_FILENAME) . '.o'; + $cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($obj) . ' ' . escapeshellarg($src) . ' 2>&1'; + if (!$this->runZigCmd($cmd, $obj, "failed to compile {$src}")) { + return; + } + $objs[] = $obj; + } + $arCmd = escapeshellarg($zig) . ' ar rcs ' . escapeshellarg($libPath) . ' ' . implode(' ', array_map('escapeshellarg', $objs)) . ' 2>&1'; + if (!$this->runZigCmd($arCmd, $libPath, 'zig ar failed')) { + return; + } + logger()->info('[zig] libclang_rt.profile.a installed (' . filesize($libPath) . ' bytes)'); + } + + private function buildCrtObjects(string $zig, string $srcRoot, string $crtBegin, string $crtEnd): void + { + $beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c"; + $endSrc = "{$srcRoot}/lib/builtins/crtend.c"; + if (!is_file($beginSrc) || !is_file($endSrc)) { + logger()->error("[zig] crtbegin/crtend source missing under {$srcRoot}/lib/builtins — shared libs will lack __dso_handle"); + return; + } + $cflags = '-c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY'; + foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) { + $cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($dst) . ' ' . escapeshellarg($src) . ' 2>&1'; + if (!$this->runZigCmd($cmd, $dst, "failed to compile {$src}")) { + return; + } + } + logger()->info('[zig] clang_rt.crtbegin.o + clang_rt.crtend.o installed (' . filesize($crtBegin) . ' + ' . filesize($crtEnd) . ' bytes)'); + } + + private function runZigCmd(string $cmd, string $dst, string $errPrefix): bool + { + exec($cmd, $out, $rc); + if ($rc !== 0 || !is_file($dst)) { + logger()->warning("[zig] {$errPrefix}: " . implode("\n", $out)); + return false; + } + return true; + } + private function createZigCcScript(string $bin_dir): void { $script_path = __DIR__ . '/../scripts/zig-cc.sh'; diff --git a/src/SPC/store/scripts/zig-cc.sh b/src/SPC/store/scripts/zig-cc.sh index 6b340bc1d..c2b93e8f7 100644 --- a/src/SPC/store/scripts/zig-cc.sh +++ b/src/SPC/store/scripts/zig-cc.sh @@ -39,6 +39,24 @@ while [[ $# -gt 0 ]]; do esac done +IS_LINK=1 +NEED_PROFILE_RT=0 # https://codeberg.org/ziglang/zig/issues/32066 +NEED_CRT=0 # https://codeberg.org/ziglang/zig/issues/32064 +for _a in "${PARSED_ARGS[@]}"; do + case "$_a" in + -c|-S|-E|-M|-MM) IS_LINK=0 ;; + -fprofile-generate*|-fprofile-instr-generate*) NEED_PROFILE_RT=1 ;; + -shared) NEED_CRT=1 ;; + esac +done +[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* ]] && NEED_PROFILE_RT=1 +if [[ $IS_LINK -eq 1 && $NEED_PROFILE_RT -eq 1 && -f "$SCRIPT_DIR/lib/libclang_rt.profile.a" ]]; then + PARSED_ARGS+=("$SCRIPT_DIR/lib/libclang_rt.profile.a" "-Wl,-u,__llvm_profile_runtime") +fi +if [[ $IS_LINK -eq 1 && $NEED_CRT -eq 1 && -f "$SCRIPT_DIR/lib/clang_rt.crtbegin.o" && -f "$SCRIPT_DIR/lib/clang_rt.crtend.o" ]]; then + PARSED_ARGS+=("$SCRIPT_DIR/lib/clang_rt.crtbegin.o" "$SCRIPT_DIR/lib/clang_rt.crtend.o") +fi + [[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET="" if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then diff --git a/src/SPC/toolchain/ZigToolchain.php b/src/SPC/toolchain/ZigToolchain.php index 1b7cc70dc..8d121db53 100644 --- a/src/SPC/toolchain/ZigToolchain.php +++ b/src/SPC/toolchain/ZigToolchain.php @@ -17,27 +17,6 @@ public function initEnv(): void GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_CXX=zig-c++'); GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_AR=zig-ar'); GlobalEnvManager::putenv('SPC_LINUX_DEFAULT_LD=zig-ld.lld'); - - // Generate additional objects needed for zig toolchain - $paths = ['/usr/lib/gcc', '/usr/local/lib/gcc']; - $objects = ['crtbeginS.o', 'crtendS.o']; - $found = []; - - foreach ($objects as $obj) { - $located = null; - foreach ($paths as $base) { - $output = shell_exec("find {$base} -name {$obj} 2>/dev/null | grep -v '/32/' | head -n 1"); - $line = trim((string) $output); - if ($line !== '') { - $located = $line; - break; - } - } - if ($located) { - $found[] = $located; - } - } - GlobalEnvManager::putenv('SPC_EXTRA_RUNTIME_OBJECTS=' . implode(' ', $found)); } public function afterInit(): void diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php new file mode 100644 index 000000000..57e2b3e96 --- /dev/null +++ b/src/SPC/util/PgoManager.php @@ -0,0 +1,179 @@ + BUILD_TARGET_CLI, + 'micro' => BUILD_TARGET_MICRO, + 'cgi' => BUILD_TARGET_CGI, + 'fpm' => BUILD_TARGET_FPM, + 'embed' => BUILD_TARGET_EMBED, + 'frankenphp' => BUILD_TARGET_FRANKENPHP, + ]; + + public const MODE_INSTRUMENT = 'instrument'; + + public const MODE_USE = 'use'; + + private string $profileRoot; + + private string $mode; + + private static ?self $active = null; + + public function __construct() + { + $this->profileRoot = BUILD_ROOT_PATH . '/pgo-data'; + } + + public static function active(): ?self + { + return self::$active; + } + + /** Setup --pgi: build with -fprofile-generate=. */ + public function setupInstrument(int $rule): void + { + $this->validateRule($rule); + FileSystem::removeDir($this->profileRoot); + f_mkdir($this->profileRoot, recursive: true); + foreach ($this->trainableIn($rule) as $sapi) { + f_mkdir($this->rawDir($sapi), recursive: true); + } + $this->mode = self::MODE_INSTRUMENT; + self::$active = $this; + $this->applyForSapi($this->trainableIn($rule)[0]); + logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '//'); + } + + /** Setup --pgo: merge collected .profraw, then build with -fprofile-use=. */ + public function setupUse(int $rule): void + { + $this->validateRule($rule); + if (trim((string) shell_exec('command -v llvm-profdata 2>/dev/null')) === '') { + throw new WrongUsageException('--pgo: llvm-profdata not on PATH'); + } + foreach ($this->trainableIn($rule) as $sapi) { + $this->mergeSapi($sapi); + } + $this->mode = self::MODE_USE; + self::$active = $this; + $this->applyForSapi($this->trainableIn($rule)[0]); + } + + /** + * Set EXTRA_CFLAGS / EXTRA_LDFLAGS_PROGRAM for the SAPI about to be built. + * Non-trainable SAPIs (e.g. frankenphp's Go side) are left untouched. + */ + public function applyForSapi(string $sapi): void + { + $sapi = $this->resolveSapi($sapi); + if (!isset(self::TRAINABLE[$sapi])) { + return; + } + $flags = $this->mode === self::MODE_INSTRUMENT + ? '-fprofile-generate=' . $this->rawDir($sapi) . ' -fprofile-continuous -mllvm -disable-vp' + : '-fprofile-use=' . $this->profDataFile($sapi) . ' -Wno-error=profile-instr-unprofiled -Wno-error=profile-instr-out-of-date -Wno-backend-plugin'; + $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $flags); + $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldOnly($flags)); + logger()->info("pgo {$this->mode} ({$sapi})"); + } + + /** + * In static-embed mode libphp.a is linked into frankenphp, and the linker + * resolves all `__llvm_profile_filename` references to a single path — + * the embed SAPI's per-TU `-fprofile-generate=…` setting is silently + * dropped. Compile libphp.a with frankenphp's path so all counter writes + * agree on one file, and read libphp.a's PGO from frankenphp.profdata. + */ + private function resolveSapi(string $sapi): string + { + if ($sapi === 'embed' && getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { + return 'frankenphp'; + } + return $sapi; + } + + private function validateRule(int $rule): void + { + if (empty($this->trainableIn($rule))) { + throw new WrongUsageException('--pgi/--pgo: no trainable SAPI in build rule (need one of: ' . implode(', ', array_keys(self::TRAINABLE)) . ')'); + } + } + + private function mergeSapi(string $sapi): void + { + $raws = glob($this->rawDir($sapi) . '/*.profraw') ?: []; + if (empty($raws)) { + throw new WrongUsageException("--pgo: no .profraw for {$sapi}; run --pgi, exercise the binary, then re-run --pgo"); + } + $out = $this->profDataFile($sapi); + $argv = implode(' ', array_map('escapeshellarg', $raws)); + shell()->exec('llvm-profdata merge --failure-mode=warn -output=' . escapeshellarg($out) . ' ' . $argv); + if (!is_file($out) || filesize($out) === 0) { + throw new WrongUsageException("--pgo: empty merge output for {$sapi}"); + } + logger()->info("pgo merged {$sapi}: " . filesize($out) . ' bytes'); + } + + private function rawDir(string $sapi): string + { + return $this->profileRoot . '/' . $sapi; + } + + private function profDataFile(string $sapi): string + { + return $this->profileRoot . '/' . $sapi . '.profdata'; + } + + /** @return list */ + private function trainableIn(int $rule): array + { + $out = []; + foreach (self::TRAINABLE as $sapi => $mask) { + if (($rule & $mask) !== $mask) { + continue; + } + $resolved = $this->resolveSapi($sapi); + if (!in_array($resolved, $out, true)) { + $out[] = $resolved; + } + } + return $out; + } + + /** Strip the previous PGO flags from $var and append the new ones. */ + private function setFlag(string $var, string $append): void + { + $cur = (string) getenv($var); + $cur = preg_replace('/\s*-fprofile-(generate|use)=\S+/', '', $cur) ?? $cur; + $cur = str_replace([' -fprofile-continuous', ' -mllvm -disable-vp'], '', $cur); + $cur = preg_replace('/\s*-Wno-error=profile-instr-unprofiled\s+-Wno-error=profile-instr-out-of-date\s+-Wno-backend-plugin/', '', $cur) ?? $cur; + f_putenv($var . '=' . trim($cur . ' ' . $append)); + } + + /** Linker only takes -fprofile-{generate,use}; strip the codegen-only -mllvm and warning flags. */ + private function ldOnly(string $flags): string + { + return preg_replace(['/\s*-mllvm\s+\S+/', '/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/'], '', $flags) ?? $flags; + } +} From b536d0c694f3fd3babb1ee44fffbd40de581dc22 Mon Sep 17 00:00:00 2001 From: henderkes Date: Mon, 27 Apr 2026 19:35:47 +0700 Subject: [PATCH 2/9] lint --- src/SPC/builder/Extension.php | 1 - src/SPC/builder/linux/LinuxBuilder.php | 10 +++++----- src/SPC/store/pkg/Zig.php | 5 ++--- src/SPC/util/PgoManager.php | 18 +++++++++--------- 4 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/SPC/builder/Extension.php b/src/SPC/builder/Extension.php index b405612c0..6b56f4e4d 100644 --- a/src/SPC/builder/Extension.php +++ b/src/SPC/builder/Extension.php @@ -15,7 +15,6 @@ use SPC\toolchain\ZigToolchain; use SPC\util\GlobalEnvManager; use SPC\util\SPCConfigUtil; -use SPC\util\SPCTarget; class Extension { diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 3963c74e3..770366ff2 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -136,11 +136,11 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $pgo = PgoManager::active(); $needsClean = false; $sapiBuilds = [ - ['cli', $enableCli, true, fn () => $this->buildCli()], - ['fpm', $enableFpm, true, fn () => $this->buildFpm()], - ['cgi', $enableCgi, true, fn () => $this->buildCgi()], - ['micro', $enableMicro, true, fn () => $this->buildMicro()], - ['embed', $enableEmbed, true, function () use ($enableMicro): void { + ['cli', $enableCli, true, fn () => $this->buildCli()], + ['fpm', $enableFpm, true, fn () => $this->buildFpm()], + ['cgi', $enableCgi, true, fn () => $this->buildCgi()], + ['micro', $enableMicro, true, fn () => $this->buildMicro()], + ['embed', $enableEmbed, true, function () use ($enableMicro): void { if ($enableMicro) { FileSystem::replaceFileStr(SOURCE_PATH . '/php-src/Makefile', 'OVERALL_TARGET =', 'OVERALL_TARGET = libphp.la'); } diff --git a/src/SPC/store/pkg/Zig.php b/src/SPC/store/pkg/Zig.php index 5beba5bde..ffe46657c 100644 --- a/src/SPC/store/pkg/Zig.php +++ b/src/SPC/store/pkg/Zig.php @@ -162,7 +162,7 @@ private function buildClangRuntimeBits(string $zig_bin_dir): void } $zig = "{$zig_bin_dir}/zig"; - $verLine = trim((string)shell_exec(escapeshellarg($zig) . ' cc --version 2>/dev/null')); + $verLine = trim((string) shell_exec(escapeshellarg($zig) . ' cc --version 2>/dev/null')); if (!preg_match('/clang version (\d+\.\d+\.\d+)/', $verLine, $m)) { logger()->warning('[zig] could not detect bundled clang version; skipping runtime bit build (--pgo + shared libs without __dso_handle)'); return; @@ -196,8 +196,7 @@ private function fetchCompilerRtSource(string $llvmVersion): ?string 'url' => $url, 'filename' => $tarball, ]); - } - catch (\Throwable $e) { + } catch (\Throwable $e) { logger()->warning("[zig] failed to download {$tarball}: {$e->getMessage()}"); return null; } diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php index 57e2b3e96..91cee8e2b 100644 --- a/src/SPC/util/PgoManager.php +++ b/src/SPC/util/PgoManager.php @@ -14,6 +14,10 @@ */ class PgoManager { + public const MODE_INSTRUMENT = 'instrument'; + + public const MODE_USE = 'use'; + /** * SAPIs whose clang-compiled output can be PGO'd. frankenphp is included * because its cgo glue is C compiled by zig — the Go side it wraps is @@ -22,18 +26,14 @@ class PgoManager * frankenphp (because the cgo glue runs too). */ private const TRAINABLE = [ - 'cli' => BUILD_TARGET_CLI, - 'micro' => BUILD_TARGET_MICRO, - 'cgi' => BUILD_TARGET_CGI, - 'fpm' => BUILD_TARGET_FPM, - 'embed' => BUILD_TARGET_EMBED, + 'cli' => BUILD_TARGET_CLI, + 'micro' => BUILD_TARGET_MICRO, + 'cgi' => BUILD_TARGET_CGI, + 'fpm' => BUILD_TARGET_FPM, + 'embed' => BUILD_TARGET_EMBED, 'frankenphp' => BUILD_TARGET_FRANKENPHP, ]; - public const MODE_INSTRUMENT = 'instrument'; - - public const MODE_USE = 'use'; - private string $profileRoot; private string $mode; From b5dca48acf7afb1ad607022d69899191de5f05a3 Mon Sep 17 00:00:00 2001 From: henderkes Date: Tue, 28 Apr 2026 22:24:13 +0700 Subject: [PATCH 3/9] exit handlers patch instead of continuous --- src/SPC/builder/unix/UnixBuilderBase.php | 2 +- src/SPC/exception/ValidationException.php | 4 +- src/SPC/util/LicenseDumper.php | 2 +- src/SPC/util/PgoManager.php | 117 ++++++++++++++---- .../patch/spc_pgo_flush_frankenphp.patch | 15 +++ .../patch/spc_pgo_flush_php_main.patch | 15 +++ 6 files changed, 125 insertions(+), 30 deletions(-) create mode 100644 src/globals/patch/spc_pgo_flush_frankenphp.patch create mode 100644 src/globals/patch/spc_pgo_flush_php_main.patch diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 8891d8023..2f543bb78 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -145,7 +145,7 @@ public function deployBinary(string $src, string $dst, bool $executable = true): throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); } - if (!$this->getOption('no-strip')) { + if (!$this->getOption('no-strip') && !$this->getOption('pgi')) { // extract debug info $this->extractDebugInfo($dst); // extra strip diff --git a/src/SPC/exception/ValidationException.php b/src/SPC/exception/ValidationException.php index 3d51ad48a..3af44303b 100644 --- a/src/SPC/exception/ValidationException.php +++ b/src/SPC/exception/ValidationException.php @@ -14,9 +14,9 @@ */ class ValidationException extends SPCException { - private array|string|null $validation_module = null; + private null|array|string $validation_module = null; - public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, array|string|null $validation_module = null) + public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, null|array|string $validation_module = null) { parent::__construct($message, $code, $previous); diff --git a/src/SPC/util/LicenseDumper.php b/src/SPC/util/LicenseDumper.php index 6a40a73a1..8ba17c91a 100644 --- a/src/SPC/util/LicenseDumper.php +++ b/src/SPC/util/LicenseDumper.php @@ -117,7 +117,7 @@ private function getSourceLicenses(string $source_name): iterable /** * Loads a source license file from the specified path. */ - private function loadSourceFile(string $source_name, int $index, array|string|null $in_path, ?string $custom_base_path = null): string + private function loadSourceFile(string $source_name, int $index, null|array|string $in_path, ?string $custom_base_path = null): string { if (is_null($in_path)) { throw new SPCInternalException("source [{$source_name}] license file is not set, please check config/source.json"); diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php index 91cee8e2b..854e687ee 100644 --- a/src/SPC/util/PgoManager.php +++ b/src/SPC/util/PgoManager.php @@ -6,11 +6,11 @@ use SPC\exception\WrongUsageException; use SPC\store\FileSystem; +use SPC\store\SourcePatcher; /** * Two-call PGO driver: --pgi instruments, --pgo uses the .profraw the user - * collected by running the instrumented binaries. PgoManager only sets the - * compiler flags; it does not run any workload itself. + * collected by running the instrumented binaries. */ class PgoManager { @@ -18,13 +18,6 @@ class PgoManager public const MODE_USE = 'use'; - /** - * SAPIs whose clang-compiled output can be PGO'd. frankenphp is included - * because its cgo glue is C compiled by zig — the Go side it wraps is - * not clang-PGO'd here. libphp.so is the embed SAPI; running frankenphp - * produces profile data for embed (because it loads libphp.so) AND for - * frankenphp (because the cgo glue runs too). - */ private const TRAINABLE = [ 'cli' => BUILD_TARGET_CLI, 'micro' => BUILD_TARGET_MICRO, @@ -34,6 +27,15 @@ class PgoManager 'frankenphp' => BUILD_TARGET_FRANKENPHP, ]; + /** + * Applied during --pgi only: explicit __llvm_profile_write_file() at + * shutdown, since Go/frankenphp exits skip libc atexit. + */ + private const SHUTDOWN_PATCHES = [ + 'php-src' => 'spc_pgo_flush_php_main.patch', + 'frankenphp' => 'spc_pgo_flush_frankenphp.patch', + ]; + private string $profileRoot; private string $mode; @@ -61,6 +63,7 @@ public function setupInstrument(int $rule): void } $this->mode = self::MODE_INSTRUMENT; self::$active = $this; + $this->applyShutdownPatches(); $this->applyForSapi($this->trainableIn($rule)[0]); logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '//'); } @@ -80,30 +83,33 @@ public function setupUse(int $rule): void $this->applyForSapi($this->trainableIn($rule)[0]); } - /** - * Set EXTRA_CFLAGS / EXTRA_LDFLAGS_PROGRAM for the SAPI about to be built. - * Non-trainable SAPIs (e.g. frankenphp's Go side) are left untouched. - */ public function applyForSapi(string $sapi): void { $sapi = $this->resolveSapi($sapi); if (!isset(self::TRAINABLE[$sapi])) { return; } + if ($this->mode === self::MODE_USE && !is_file($this->profDataFile($sapi))) { + logger()->warning("pgo --pgo: no profdata for {$sapi}, building without PGO for this sapi"); + $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', ''); + $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', ''); + return; + } $flags = $this->mode === self::MODE_INSTRUMENT - ? '-fprofile-generate=' . $this->rawDir($sapi) . ' -fprofile-continuous -mllvm -disable-vp' - : '-fprofile-use=' . $this->profDataFile($sapi) . ' -Wno-error=profile-instr-unprofiled -Wno-error=profile-instr-out-of-date -Wno-backend-plugin'; + ? '-fprofile-generate=' . $this->rawDir($sapi) + : '-fprofile-use=' . $this->profDataFile($sapi) + . ' -Wno-error=profile-instr-unprofiled' + . ' -Wno-error=profile-instr-out-of-date' + . ' -Wno-backend-plugin'; $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $flags); - $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldOnly($flags)); + $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldOnly($flags, $sapi)); logger()->info("pgo {$this->mode} ({$sapi})"); } /** - * In static-embed mode libphp.a is linked into frankenphp, and the linker - * resolves all `__llvm_profile_filename` references to a single path — - * the embed SAPI's per-TU `-fprofile-generate=…` setting is silently - * dropped. Compile libphp.a with frankenphp's path so all counter writes - * agree on one file, and read libphp.a's PGO from frankenphp.profdata. + * Static-embed mode links libphp.a into frankenphp; both end up in one + * binary so must share one profdata. Shared-embed mode keeps libphp.so + * standalone — embed and frankenphp keep separate profiles. */ private function resolveSapi(string $sapi): string { @@ -124,6 +130,10 @@ private function mergeSapi(string $sapi): void { $raws = glob($this->rawDir($sapi) . '/*.profraw') ?: []; if (empty($raws)) { + if ($sapi === 'frankenphp') { + logger()->warning('pgo --pgo: no .profraw for frankenphp (cgo glue PGO will be skipped); run --pgi, exercise frankenphp longer, then re-run --pgo to include it'); + return; + } throw new WrongUsageException("--pgo: no .profraw for {$sapi}; run --pgi, exercise the binary, then re-run --pgo"); } $out = $this->profDataFile($sapi); @@ -166,14 +176,69 @@ private function setFlag(string $var, string $append): void { $cur = (string) getenv($var); $cur = preg_replace('/\s*-fprofile-(generate|use)=\S+/', '', $cur) ?? $cur; - $cur = str_replace([' -fprofile-continuous', ' -mllvm -disable-vp'], '', $cur); - $cur = preg_replace('/\s*-Wno-error=profile-instr-unprofiled\s+-Wno-error=profile-instr-out-of-date\s+-Wno-backend-plugin/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-Wno-error=profile-instr-\S+/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-Wno-backend-plugin/', '', $cur) ?? $cur; f_putenv($var . '=' . trim($cur . ' ' . $append)); } - /** Linker only takes -fprofile-{generate,use}; strip the codegen-only -mllvm and warning flags. */ - private function ldOnly(string $flags): string + /** + * Linker flags: cli wants -fprofile-use= at link too (LTO does its + * profile-driven inlining/reordering at link time). Strip -Wno-error + * flags (linker doesn't accept them). + */ + private function ldOnly(string $flags, string $sapi = ''): string + { + $patterns = ['/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/']; + if ($sapi === 'frankenphp') { + $patterns[] = '/\s*-fprofile-use=\S+/'; + } + return trim(preg_replace($patterns, '', $flags) ?? $flags); + } + + /** --pgi patch: inject __llvm_profile_write_file() flush handler to php and frankenphp sources. */ + private function applyShutdownPatches(): void { - return preg_replace(['/\s*-mllvm\s+\S+/', '/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/'], '', $flags) ?? $flags; + $applied = []; + foreach (self::SHUTDOWN_PATCHES as $dir => $patch) { + $cwd = SOURCE_PATH . '/' . $dir; + if (!is_dir($cwd)) { + continue; + } + if (!SourcePatcher::patchFile($patch, $cwd)) { + throw new WrongUsageException("--pgi: patch {$patch} failed to apply in {$cwd}"); + } + $applied[] = ['cwd' => $cwd, 'patch' => $patch]; + logger()->info("pgo --pgi: applied {$patch}"); + } + if ($applied === []) { + return; + } + register_shutdown_function(static function () use ($applied): void { + foreach ($applied as $entry) { + $cwd = $entry['cwd']; + $patch = $entry['patch']; + if (!is_dir($cwd)) { + continue; + } + $patch_file = ROOT_DIR . "/src/globals/patch/{$patch}"; + if (!is_file($patch_file)) { + continue; + } + $args = ' -p1 -s -R -F0 '; + exec('cd ' . escapeshellarg($cwd) . ' && patch --dry-run' . $args + . ' < ' . escapeshellarg($patch_file) . ' >/dev/null 2>&1', $_, $detect_status); + if ($detect_status !== 0) { + logger()->info("pgo --pgi: {$patch} already clean, skipping revert"); + continue; + } + exec('cd ' . escapeshellarg($cwd) . ' && patch' . $args + . ' < ' . escapeshellarg($patch_file), $out, $apply_status); + if ($apply_status === 0) { + logger()->info("pgo --pgi: reverted {$patch}"); + } else { + logger()->warning("pgo --pgi: failed to revert {$patch} (status {$apply_status}): " . implode("\n", $out)); + } + } + }); } } diff --git a/src/globals/patch/spc_pgo_flush_frankenphp.patch b/src/globals/patch/spc_pgo_flush_frankenphp.patch new file mode 100644 index 000000000..7c58fafff --- /dev/null +++ b/src/globals/patch/spc_pgo_flush_frankenphp.patch @@ -0,0 +1,15 @@ +--- a/frankenphp.c ++++ b/frankenphp.c +@@ -1254,6 +1254,12 @@ + + go_frankenphp_shutdown_main_thread(); + ++ /* spc-pgo: explicit profile flush so the cgo-instrumented frankenphp ++ * still writes .profraw on Go-runtime exit (which bypasses libc atexit). ++ * Weak symbol → no-op in non-PGO builds. */ ++ { extern int __llvm_profile_write_file(void) __attribute__((weak)); ++ if (__llvm_profile_write_file) __llvm_profile_write_file(); } ++ + return NULL; + } + diff --git a/src/globals/patch/spc_pgo_flush_php_main.patch b/src/globals/patch/spc_pgo_flush_php_main.patch new file mode 100644 index 000000000..56bb2862c --- /dev/null +++ b/src/globals/patch/spc_pgo_flush_php_main.patch @@ -0,0 +1,15 @@ +--- a/main/main.c ++++ b/main/main.c +@@ -2563,6 +2563,12 @@ + #endif + + zend_observer_shutdown(); ++ ++ /* spc-pgo: explicit profile flush so embed/frankenphp callers that exit ++ * via SYS_exit_group (skipping libc atexit) still get .profraw written. ++ * Weak symbol → no-op in non-PGO builds. */ ++ { extern int __llvm_profile_write_file(void) __attribute__((weak)); ++ if (__llvm_profile_write_file) __llvm_profile_write_file(); } + } + /* }}} */ + From d774fda15de291dff18cd6125f414060f0c2dadf Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 30 Apr 2026 18:50:16 +0700 Subject: [PATCH 4/9] add cs profiling as optional second step profiling pass --- src/SPC/builder/linux/LinuxBuilder.php | 1 + src/SPC/builder/unix/UnixBuilderBase.php | 2 +- src/SPC/command/BuildPHPCommand.php | 8 ++- src/SPC/store/scripts/zig-cc.sh | 4 +- src/SPC/util/PgoManager.php | 71 +++++++++++++++++-- .../patch/spc_pgo_flush_php_main.patch | 5 +- 6 files changed, 75 insertions(+), 16 deletions(-) diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index 770366ff2..d15816fa8 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -130,6 +130,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $this->emitPatchPoint('before-php-make'); SourcePatcher::patchBeforeMake($this); + PgoManager::patchBeforeMake($this); $this->cleanMake(); diff --git a/src/SPC/builder/unix/UnixBuilderBase.php b/src/SPC/builder/unix/UnixBuilderBase.php index 2f543bb78..37127f24a 100644 --- a/src/SPC/builder/unix/UnixBuilderBase.php +++ b/src/SPC/builder/unix/UnixBuilderBase.php @@ -145,7 +145,7 @@ public function deployBinary(string $src, string $dst, bool $executable = true): throw new SPCInternalException("Deploy failed. Cannot find file after copy: {$dst}"); } - if (!$this->getOption('no-strip') && !$this->getOption('pgi')) { + if (!$this->getOption('no-strip') && !$this->getOption('pgi') && !$this->getOption('cs-pgi')) { // extract debug info $this->extractDebugInfo($dst); // extra strip diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 69772efe0..9c10b1027 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -51,6 +51,7 @@ public function configure(): void $this->addOption('enable-micro-win32', null, null, 'Enable win32 mode for phpmicro (Windows only)'); $this->addOption('with-frankenphp-app', null, InputOption::VALUE_REQUIRED, 'Path to a folder to be embedded in FrankenPHP'); $this->addOption('pgi', null, null, 'Build instrumented binaries (-fprofile-generate). Run them to collect .profraw files, then re-run with --pgo.'); + $this->addOption('cs-pgi', null, null, 'Build cs-instrumented binaries (-fprofile-use= -fcs-profile-generate). Requires a prior --pgi+--pgo cycle.'); $this->addOption('pgo', null, null, 'Build optimised binaries (-fprofile-use) from .profraw collected by a previous --pgi run.'); } @@ -215,13 +216,16 @@ public function handle(): int FileSystem::removeDir(BUILD_MODULES_PATH); $pgi = (bool) $this->getOption('pgi'); + $csPgi = (bool) $this->getOption('cs-pgi'); $pgo = (bool) $this->getOption('pgo'); - if ($pgi && $pgo) { - $this->output->writeln('--pgi and --pgo are mutually exclusive'); + if (((int) $pgi + (int) $csPgi + (int) $pgo) > 1) { + $this->output->writeln('--pgi, --cs-pgi, and --pgo are mutually exclusive'); return static::FAILURE; } if ($pgi) { (new PgoManager())->setupInstrument($rule); + } elseif ($csPgi) { + (new PgoManager())->setupCsInstrument($rule); } elseif ($pgo) { (new PgoManager())->setupUse($rule); } diff --git a/src/SPC/store/scripts/zig-cc.sh b/src/SPC/store/scripts/zig-cc.sh index c2b93e8f7..586c828c3 100644 --- a/src/SPC/store/scripts/zig-cc.sh +++ b/src/SPC/store/scripts/zig-cc.sh @@ -45,11 +45,11 @@ NEED_CRT=0 # https://codeberg.org/ziglang/zig/issues/32064 for _a in "${PARSED_ARGS[@]}"; do case "$_a" in -c|-S|-E|-M|-MM) IS_LINK=0 ;; - -fprofile-generate*|-fprofile-instr-generate*) NEED_PROFILE_RT=1 ;; + -fprofile-generate*|-fprofile-instr-generate*|-fcs-profile-generate*) NEED_PROFILE_RT=1 ;; -shared) NEED_CRT=1 ;; esac done -[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* ]] && NEED_PROFILE_RT=1 +[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* || "$SPC_COMPILER_EXTRA" == *-fcs-profile-generate* ]] && NEED_PROFILE_RT=1 if [[ $IS_LINK -eq 1 && $NEED_PROFILE_RT -eq 1 && -f "$SCRIPT_DIR/lib/libclang_rt.profile.a" ]]; then PARSED_ARGS+=("$SCRIPT_DIR/lib/libclang_rt.profile.a" "-Wl,-u,__llvm_profile_runtime") fi diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php index 854e687ee..efb75c0d2 100644 --- a/src/SPC/util/PgoManager.php +++ b/src/SPC/util/PgoManager.php @@ -4,6 +4,7 @@ namespace SPC\util; +use SPC\builder\BuilderBase; use SPC\exception\WrongUsageException; use SPC\store\FileSystem; use SPC\store\SourcePatcher; @@ -16,6 +17,8 @@ class PgoManager { public const MODE_INSTRUMENT = 'instrument'; + public const MODE_CS_INSTRUMENT = 'cs-instrument'; + public const MODE_USE = 'use'; private const TRAINABLE = [ @@ -68,6 +71,23 @@ public function setupInstrument(int $rule): void logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '//'); } + /** Setup --cs-pgi: build with -fprofile-use= -fcs-profile-generate=. Requires existing .profdata. */ + public function setupCsInstrument(int $rule): void + { + $this->validateRule($rule); + foreach ($this->trainableIn($rule) as $sapi) { + if (!is_file($this->profDataFile($sapi))) { + throw new WrongUsageException("--cs-pgi: missing {$sapi}.profdata; run --pgi + --pgo first"); + } + f_mkdir($this->csRawDir($sapi), recursive: true); + } + $this->mode = self::MODE_CS_INSTRUMENT; + self::$active = $this; + $this->applyShutdownPatches(); + $this->applyForSapi($this->trainableIn($rule)[0]); + logger()->info('pgo --cs-pgi: cs-instrumented build, cs-profraw under ' . $this->profileRoot . '/cs-/'); + } + /** Setup --pgo: merge collected .profraw, then build with -fprofile-use=. */ public function setupUse(int $rule): void { @@ -83,6 +103,29 @@ public function setupUse(int $rule): void $this->applyForSapi($this->trainableIn($rule)[0]); } + /** Patches php-src/libtool to passthrough -fcs-profile-* flags (otherwise dropped during shared lib link). */ + public static function patchBeforeMake(BuilderBase $builder): void + { + if (!$builder->getOption('cs-pgi')) { + return; + } + $libtool = SOURCE_PATH . '/php-src/libtool'; + if (!is_file($libtool)) { + return; + } + $contents = file_get_contents($libtool); + if (str_contains($contents, '-fcs-profile-*')) { + return; + } + $patched = str_replace('-fprofile-*|-F*', '-fprofile-*|-fcs-profile-*|-F*', $contents); + if ($patched === $contents) { + logger()->warning('pgo --cs-pgi: could not patch libtool for -fcs-profile-* passthrough'); + return; + } + file_put_contents($libtool, $patched); + logger()->info('pgo --cs-pgi: patched libtool for -fcs-profile-* passthrough'); + } + public function applyForSapi(string $sapi): void { $sapi = $this->resolveSapi($sapi); @@ -95,12 +138,18 @@ public function applyForSapi(string $sapi): void $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', ''); return; } - $flags = $this->mode === self::MODE_INSTRUMENT - ? '-fprofile-generate=' . $this->rawDir($sapi) - : '-fprofile-use=' . $this->profDataFile($sapi) + $flags = match ($this->mode) { + self::MODE_INSTRUMENT => '-fprofile-generate=' . $this->rawDir($sapi), + self::MODE_CS_INSTRUMENT => '-fprofile-use=' . $this->profDataFile($sapi) + . ' -fcs-profile-generate=' . $this->csRawDir($sapi) + . ' -Wno-error=profile-instr-unprofiled' + . ' -Wno-error=profile-instr-out-of-date' + . ' -Wno-backend-plugin', + default => '-fprofile-use=' . $this->profDataFile($sapi) . ' -Wno-error=profile-instr-unprofiled' . ' -Wno-error=profile-instr-out-of-date' - . ' -Wno-backend-plugin'; + . ' -Wno-backend-plugin', + }; $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $flags); $this->setFlag('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldOnly($flags, $sapi)); logger()->info("pgo {$this->mode} ({$sapi})"); @@ -129,7 +178,8 @@ private function validateRule(int $rule): void private function mergeSapi(string $sapi): void { $raws = glob($this->rawDir($sapi) . '/*.profraw') ?: []; - if (empty($raws)) { + $csRaws = glob($this->csRawDir($sapi) . '/*.profraw') ?: []; + if (empty($raws) && empty($csRaws)) { if ($sapi === 'frankenphp') { logger()->warning('pgo --pgo: no .profraw for frankenphp (cgo glue PGO will be skipped); run --pgi, exercise frankenphp longer, then re-run --pgo to include it'); return; @@ -137,7 +187,8 @@ private function mergeSapi(string $sapi): void throw new WrongUsageException("--pgo: no .profraw for {$sapi}; run --pgi, exercise the binary, then re-run --pgo"); } $out = $this->profDataFile($sapi); - $argv = implode(' ', array_map('escapeshellarg', $raws)); + $inputs = array_merge($raws, $csRaws); + $argv = implode(' ', array_map('escapeshellarg', $inputs)); shell()->exec('llvm-profdata merge --failure-mode=warn -output=' . escapeshellarg($out) . ' ' . $argv); if (!is_file($out) || filesize($out) === 0) { throw new WrongUsageException("--pgo: empty merge output for {$sapi}"); @@ -150,6 +201,11 @@ private function rawDir(string $sapi): string return $this->profileRoot . '/' . $sapi; } + private function csRawDir(string $sapi): string + { + return $this->profileRoot . '/cs-' . $sapi; + } + private function profDataFile(string $sapi): string { return $this->profileRoot . '/' . $sapi . '.profdata'; @@ -175,7 +231,7 @@ private function trainableIn(int $rule): array private function setFlag(string $var, string $append): void { $cur = (string) getenv($var); - $cur = preg_replace('/\s*-fprofile-(generate|use)=\S+/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-f(cs-)?profile-(generate|use)=\S+/', '', $cur) ?? $cur; $cur = preg_replace('/\s*-Wno-error=profile-instr-\S+/', '', $cur) ?? $cur; $cur = preg_replace('/\s*-Wno-backend-plugin/', '', $cur) ?? $cur; f_putenv($var . '=' . trim($cur . ' ' . $append)); @@ -191,6 +247,7 @@ private function ldOnly(string $flags, string $sapi = ''): string $patterns = ['/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/']; if ($sapi === 'frankenphp') { $patterns[] = '/\s*-fprofile-use=\S+/'; + $patterns[] = '/\s*-fcs-profile-generate=\S+/'; } return trim(preg_replace($patterns, '', $flags) ?? $flags); } diff --git a/src/globals/patch/spc_pgo_flush_php_main.patch b/src/globals/patch/spc_pgo_flush_php_main.patch index 56bb2862c..6bf905bf0 100644 --- a/src/globals/patch/spc_pgo_flush_php_main.patch +++ b/src/globals/patch/spc_pgo_flush_php_main.patch @@ -1,13 +1,10 @@ --- a/main/main.c +++ b/main/main.c -@@ -2563,6 +2563,12 @@ +@@ -2563,6 +2563,9 @@ #endif zend_observer_shutdown(); + -+ /* spc-pgo: explicit profile flush so embed/frankenphp callers that exit -+ * via SYS_exit_group (skipping libc atexit) still get .profraw written. -+ * Weak symbol → no-op in non-PGO builds. */ + { extern int __llvm_profile_write_file(void) __attribute__((weak)); + if (__llvm_profile_write_file) __llvm_profile_write_file(); } } From d19930a8e23e3fcc6c5da7715a12f6492f4554b1 Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 30 Apr 2026 19:33:00 +0700 Subject: [PATCH 5/9] unrelated cs-fix --- src/SPC/exception/ValidationException.php | 4 ++-- src/SPC/util/LicenseDumper.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/SPC/exception/ValidationException.php b/src/SPC/exception/ValidationException.php index 3af44303b..3d51ad48a 100644 --- a/src/SPC/exception/ValidationException.php +++ b/src/SPC/exception/ValidationException.php @@ -14,9 +14,9 @@ */ class ValidationException extends SPCException { - private null|array|string $validation_module = null; + private array|string|null $validation_module = null; - public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, null|array|string $validation_module = null) + public function __construct(string $message = '', int $code = 0, ?\Throwable $previous = null, array|string|null $validation_module = null) { parent::__construct($message, $code, $previous); diff --git a/src/SPC/util/LicenseDumper.php b/src/SPC/util/LicenseDumper.php index 8ba17c91a..6a40a73a1 100644 --- a/src/SPC/util/LicenseDumper.php +++ b/src/SPC/util/LicenseDumper.php @@ -117,7 +117,7 @@ private function getSourceLicenses(string $source_name): iterable /** * Loads a source license file from the specified path. */ - private function loadSourceFile(string $source_name, int $index, null|array|string $in_path, ?string $custom_base_path = null): string + private function loadSourceFile(string $source_name, int $index, array|string|null $in_path, ?string $custom_base_path = null): string { if (is_null($in_path)) { throw new SPCInternalException("source [{$source_name}] license file is not set, please check config/source.json"); From 814d3cba58367978d494b996eb3005a79d4c91ea Mon Sep 17 00:00:00 2001 From: henderkes Date: Thu, 30 Apr 2026 19:46:07 +0700 Subject: [PATCH 6/9] move pgo from new to static creation method --- src/SPC/builder/linux/LinuxBuilder.php | 6 +- src/SPC/command/BuildPHPCommand.php | 15 ---- src/SPC/util/PgoManager.php | 112 ++++++++++++++----------- 3 files changed, 66 insertions(+), 67 deletions(-) diff --git a/src/SPC/builder/linux/LinuxBuilder.php b/src/SPC/builder/linux/LinuxBuilder.php index d15816fa8..74702b1f3 100644 --- a/src/SPC/builder/linux/LinuxBuilder.php +++ b/src/SPC/builder/linux/LinuxBuilder.php @@ -23,6 +23,8 @@ class LinuxBuilder extends UnixBuilderBase /** @var bool Micro patch phar flag */ private bool $phar_patched = false; + private ?PgoManager $pgo = null; + public function __construct(array $options = []) { $this->options = $options; @@ -49,6 +51,8 @@ public function __construct(array $options = []) */ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void { + $this->pgo = PgoManager::fromBuilder($this, $build_target); + $cflags = $this->arch_c_flags; f_putenv('CFLAGS=' . $cflags); @@ -134,7 +138,7 @@ public function buildPHP(int $build_target = BUILD_TARGET_NONE): void $this->cleanMake(); - $pgo = PgoManager::active(); + $pgo = $this->pgo; $needsClean = false; $sapiBuilds = [ ['cli', $enableCli, true, fn () => $this->buildCli()], diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 9c10b1027..8ae537d0a 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -12,7 +12,6 @@ use SPC\util\DependencyUtil; use SPC\util\GlobalEnvManager; use SPC\util\LicenseDumper; -use SPC\util\PgoManager; use SPC\util\SPCTarget; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -215,20 +214,6 @@ public function handle(): int // clean old modules that may conflict with the new php build FileSystem::removeDir(BUILD_MODULES_PATH); - $pgi = (bool) $this->getOption('pgi'); - $csPgi = (bool) $this->getOption('cs-pgi'); - $pgo = (bool) $this->getOption('pgo'); - if (((int) $pgi + (int) $csPgi + (int) $pgo) > 1) { - $this->output->writeln('--pgi, --cs-pgi, and --pgo are mutually exclusive'); - return static::FAILURE; - } - if ($pgi) { - (new PgoManager())->setupInstrument($rule); - } elseif ($csPgi) { - (new PgoManager())->setupCsInstrument($rule); - } elseif ($pgo) { - (new PgoManager())->setupUse($rule); - } $builder->buildPHP($rule); $builder->testPHP($rule); diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php index efb75c0d2..3401798c9 100644 --- a/src/SPC/util/PgoManager.php +++ b/src/SPC/util/PgoManager.php @@ -43,64 +43,29 @@ class PgoManager private string $mode; - private static ?self $active = null; - - public function __construct() + private function __construct() { $this->profileRoot = BUILD_ROOT_PATH . '/pgo-data'; } - public static function active(): ?self - { - return self::$active; - } - - /** Setup --pgi: build with -fprofile-generate=. */ - public function setupInstrument(int $rule): void - { - $this->validateRule($rule); - FileSystem::removeDir($this->profileRoot); - f_mkdir($this->profileRoot, recursive: true); - foreach ($this->trainableIn($rule) as $sapi) { - f_mkdir($this->rawDir($sapi), recursive: true); - } - $this->mode = self::MODE_INSTRUMENT; - self::$active = $this; - $this->applyShutdownPatches(); - $this->applyForSapi($this->trainableIn($rule)[0]); - logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '//'); - } - - /** Setup --cs-pgi: build with -fprofile-use= -fcs-profile-generate=. Requires existing .profdata. */ - public function setupCsInstrument(int $rule): void - { - $this->validateRule($rule); - foreach ($this->trainableIn($rule) as $sapi) { - if (!is_file($this->profDataFile($sapi))) { - throw new WrongUsageException("--cs-pgi: missing {$sapi}.profdata; run --pgi + --pgo first"); - } - f_mkdir($this->csRawDir($sapi), recursive: true); - } - $this->mode = self::MODE_CS_INSTRUMENT; - self::$active = $this; - $this->applyShutdownPatches(); - $this->applyForSapi($this->trainableIn($rule)[0]); - logger()->info('pgo --cs-pgi: cs-instrumented build, cs-profraw under ' . $this->profileRoot . '/cs-/'); - } - - /** Setup --pgo: merge collected .profraw, then build with -fprofile-use=. */ - public function setupUse(int $rule): void + /** Build a PgoManager for the active --pgi/--cs-pgi/--pgo option, or null if none set. */ + public static function fromBuilder(BuilderBase $builder, int $rule): ?self { - $this->validateRule($rule); - if (trim((string) shell_exec('command -v llvm-profdata 2>/dev/null')) === '') { - throw new WrongUsageException('--pgo: llvm-profdata not on PATH'); + $modes = array_filter(['pgi', 'cs-pgi', 'pgo'], fn ($m) => (bool) $builder->getOption($m)); + if (count($modes) > 1) { + throw new WrongUsageException('--pgi, --cs-pgi, and --pgo are mutually exclusive'); } - foreach ($this->trainableIn($rule) as $sapi) { - $this->mergeSapi($sapi); + $mode = array_values($modes)[0] ?? null; + if ($mode === null) { + return null; } - $this->mode = self::MODE_USE; - self::$active = $this; - $this->applyForSapi($this->trainableIn($rule)[0]); + $instance = new self(); + match ($mode) { + 'pgi' => $instance->setupInstrument($rule), + 'cs-pgi' => $instance->setupCsInstrument($rule), + 'pgo' => $instance->setupUse($rule), + }; + return $instance; } /** Patches php-src/libtool to passthrough -fcs-profile-* flags (otherwise dropped during shared lib link). */ @@ -155,6 +120,51 @@ public function applyForSapi(string $sapi): void logger()->info("pgo {$this->mode} ({$sapi})"); } + /** Setup --pgi: build with -fprofile-generate=. */ + private function setupInstrument(int $rule): void + { + $this->validateRule($rule); + FileSystem::removeDir($this->profileRoot); + f_mkdir($this->profileRoot, recursive: true); + foreach ($this->trainableIn($rule) as $sapi) { + f_mkdir($this->rawDir($sapi), recursive: true); + } + $this->mode = self::MODE_INSTRUMENT; + $this->applyShutdownPatches(); + $this->applyForSapi($this->trainableIn($rule)[0]); + logger()->info('pgo --pgi: instrumented build, profraw will land under ' . $this->profileRoot . '//'); + } + + /** Setup --cs-pgi: build with -fprofile-use= -fcs-profile-generate=. Requires existing .profdata. */ + private function setupCsInstrument(int $rule): void + { + $this->validateRule($rule); + foreach ($this->trainableIn($rule) as $sapi) { + if (!is_file($this->profDataFile($sapi))) { + throw new WrongUsageException("--cs-pgi: missing {$sapi}.profdata; run --pgi + --pgo first"); + } + f_mkdir($this->csRawDir($sapi), recursive: true); + } + $this->mode = self::MODE_CS_INSTRUMENT; + $this->applyShutdownPatches(); + $this->applyForSapi($this->trainableIn($rule)[0]); + logger()->info('pgo --cs-pgi: cs-instrumented build, cs-profraw under ' . $this->profileRoot . '/cs-/'); + } + + /** Setup --pgo: merge collected .profraw, then build with -fprofile-use=. */ + private function setupUse(int $rule): void + { + $this->validateRule($rule); + if (trim((string) shell_exec('command -v llvm-profdata 2>/dev/null')) === '') { + throw new WrongUsageException('--pgo: llvm-profdata not on PATH'); + } + foreach ($this->trainableIn($rule) as $sapi) { + $this->mergeSapi($sapi); + } + $this->mode = self::MODE_USE; + $this->applyForSapi($this->trainableIn($rule)[0]); + } + /** * Static-embed mode links libphp.a into frankenphp; both end up in one * binary so must share one profdata. Shared-embed mode keeps libphp.so From b72b734bbe763a22a0c8ed50ecb3dbd80c556936 Mon Sep 17 00:00:00 2001 From: henderkes Date: Fri, 1 May 2026 21:10:27 +0700 Subject: [PATCH 7/9] profile atomic --- src/SPC/util/PgoManager.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/SPC/util/PgoManager.php b/src/SPC/util/PgoManager.php index 3401798c9..d1564c886 100644 --- a/src/SPC/util/PgoManager.php +++ b/src/SPC/util/PgoManager.php @@ -104,9 +104,11 @@ public function applyForSapi(string $sapi): void return; } $flags = match ($this->mode) { - self::MODE_INSTRUMENT => '-fprofile-generate=' . $this->rawDir($sapi), + self::MODE_INSTRUMENT => '-fprofile-generate=' . $this->rawDir($sapi) + . ' -fprofile-update=atomic', self::MODE_CS_INSTRUMENT => '-fprofile-use=' . $this->profDataFile($sapi) . ' -fcs-profile-generate=' . $this->csRawDir($sapi) + . ' -fprofile-update=atomic' . ' -Wno-error=profile-instr-unprofiled' . ' -Wno-error=profile-instr-out-of-date' . ' -Wno-backend-plugin', From 20030096d6a41f8b73a62ff8ce182d470e53223c Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 2 May 2026 20:04:02 +0700 Subject: [PATCH 8/9] pass proper optimization flags to a few libs that ignored them --- src/SPC/builder/linux/library/icu.php | 6 ++++-- src/SPC/builder/linux/library/openssl.php | 8 +++++++- src/SPC/builder/unix/library/bzip2.php | 3 ++- src/SPC/command/CraftCommand.php | 8 ++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/SPC/builder/linux/library/icu.php b/src/SPC/builder/linux/library/icu.php index 7b8bcf8e9..3a41cb135 100644 --- a/src/SPC/builder/linux/library/icu.php +++ b/src/SPC/builder/linux/library/icu.php @@ -15,9 +15,11 @@ class icu extends LinuxLibraryBase protected function build(): void { + $userCxxFlags = trim((string) getenv('SPC_DEFAULT_CXX_FLAGS')); + $userLdFlags = trim((string) getenv('SPC_DEFAULT_LD_FLAGS')); $cppflags = 'CPPFLAGS="-DU_CHARSET_IS_UTF8=1 -DU_USING_ICU_NAMESPACE=1 -DU_STATIC_IMPLEMENTATION=1 -DPIC -fPIC"'; - $cxxflags = 'CXXFLAGS="-std=c++17 -DPIC -fPIC -fno-ident"'; - $ldflags = SPCTarget::isStatic() ? 'LDFLAGS="-static"' : ''; + $cxxflags = "CXXFLAGS=\"-std=c++17 -DPIC -fPIC -fno-ident {$userCxxFlags}\""; + $ldflags = SPCTarget::isStatic() ? "LDFLAGS=\"-static {$userLdFlags}\"" : "LDFLAGS=\"{$userLdFlags}\""; shell()->cd($this->source_dir . '/source')->initializeEnv($this) ->exec( "{$cppflags} {$cxxflags} {$ldflags} " . diff --git a/src/SPC/builder/linux/library/openssl.php b/src/SPC/builder/linux/library/openssl.php index bfc3936ba..c4efdfdf4 100644 --- a/src/SPC/builder/linux/library/openssl.php +++ b/src/SPC/builder/linux/library/openssl.php @@ -57,6 +57,11 @@ public function build(): void $openssl_dir ??= '/etc/ssl'; $ex_lib = trim($ex_lib); + // OpenSSL's Configure ignores env CFLAGS for its target template; pass our flags as extra args after the target. + $userCFlags = trim((string) getenv('SPC_DEFAULT_C_FLAGS')); + $userLdFlags = trim((string) getenv('SPC_DEFAULT_LD_FLAGS')); + $userExtraFlags = trim($userCFlags . ' ' . $userLdFlags); + shell()->cd($this->source_dir)->initializeEnv($this) ->exec( "{$env} ./Configure no-shared {$extra} " . @@ -67,7 +72,8 @@ public function build(): void 'enable-pie ' . 'no-legacy ' . 'no-tests ' . - "linux-{$arch}" + "linux-{$arch} " . + $userExtraFlags ) ->exec('make clean') ->exec("make -j{$this->builder->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") diff --git a/src/SPC/builder/unix/library/bzip2.php b/src/SPC/builder/unix/library/bzip2.php index 32d87270c..4ed3b46a2 100644 --- a/src/SPC/builder/unix/library/bzip2.php +++ b/src/SPC/builder/unix/library/bzip2.php @@ -10,7 +10,8 @@ trait bzip2 { public function patchBeforeBuild(): bool { - FileSystem::replaceFileStr($this->source_dir . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); + $extra = trim((string) getenv('SPC_DEFAULT_C_FLAGS')); + FileSystem::replaceFileStr($this->source_dir . '/Makefile', 'CFLAGS=-Wall', "CFLAGS=-Wall {$extra}"); return true; } diff --git a/src/SPC/command/CraftCommand.php b/src/SPC/command/CraftCommand.php index 6c40133d1..7a4765acf 100644 --- a/src/SPC/command/CraftCommand.php +++ b/src/SPC/command/CraftCommand.php @@ -19,6 +19,9 @@ class CraftCommand extends BuildCommand public function configure(): void { $this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml'); + $this->addOption('pgi', null, null, 'Forward --pgi to the inner build (instrumented binaries).'); + $this->addOption('cs-pgi', null, null, 'Forward --cs-pgi to the inner build (cs-instrumented binaries).'); + $this->addOption('pgo', null, null, 'Forward --pgo to the inner build (use collected profile data).'); } public function handle(): int @@ -105,6 +108,11 @@ public function handle(): int if ($craft['craft-options']['build']) { $args = [$static_extensions, "--with-libs={$libs}", "--build-shared={$shared_extensions}", ...array_map(fn ($x) => "--build-{$x}", $craft['sapi'])]; $this->optionsToArguments($craft['build-options'], $args); + foreach (['pgi', 'cs-pgi', 'pgo'] as $pgoFlag) { + if ($this->getOption($pgoFlag)) { + $args[] = "--{$pgoFlag}"; + } + } $retcode = $this->runCommand('build', ...$args); if ($retcode !== 0) { $this->output->writeln('craft build failed'); From 7dfa4c5c3bee7151c0a7199fa5d7b79e24b92d90 Mon Sep 17 00:00:00 2001 From: henderkes Date: Sat, 2 May 2026 20:19:31 +0700 Subject: [PATCH 9/9] only run micro patches when building it --- src/SPC/builder/BuilderBase.php | 6 ++++-- src/SPC/command/BuildPHPCommand.php | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/SPC/builder/BuilderBase.php b/src/SPC/builder/BuilderBase.php index fdba936d7..3a6c39337 100644 --- a/src/SPC/builder/BuilderBase.php +++ b/src/SPC/builder/BuilderBase.php @@ -143,7 +143,7 @@ public function setLibsOnly(bool $status = true): void * * @internal */ - public function proveExts(array $static_extensions, array $shared_extensions = [], bool $skip_check_deps = false, bool $skip_extract = false): void + public function proveExts(array $static_extensions, array $shared_extensions = [], bool $skip_check_deps = false, bool $skip_extract = false, int $build_target = BUILD_TARGET_NONE): void { // judge ext foreach ($static_extensions as $ext) { @@ -171,7 +171,9 @@ public function proveExts(array $static_extensions, array $shared_extensions = [ SourceManager::initSource(exts: [...$static_extensions, ...$shared_extensions]); $this->emitPatchPoint('after-exts-extract'); // patch micro - SourcePatcher::patchMicro(); + if (($build_target & BUILD_TARGET_MICRO) === BUILD_TARGET_MICRO) { + SourcePatcher::patchMicro(); + } } foreach ([...$static_extensions, ...$shared_extensions] as $extension) { diff --git a/src/SPC/command/BuildPHPCommand.php b/src/SPC/command/BuildPHPCommand.php index 8ae537d0a..58be9087e 100644 --- a/src/SPC/command/BuildPHPCommand.php +++ b/src/SPC/command/BuildPHPCommand.php @@ -180,7 +180,7 @@ public function handle(): int // compile libraries $builder->proveLibs($libraries); // check extensions - $builder->proveExts($static_extensions, $shared_extensions); + $builder->proveExts($static_extensions, $shared_extensions, build_target: $rule); // validate libs and extensions $builder->validateLibsAndExts();