diff --git a/config/env.ini b/config/env.ini index acad25231..65fad0775 100644 --- a/config/env.ini +++ b/config/env.ini @@ -100,9 +100,10 @@ SPC_TARGET=${GNU_ARCH}-linux-musl CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} AR=${SPC_DEFAULT_AR} +RANLIB=${SPC_DEFAULT_RANLIB} LD=${SPC_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build -SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections" +SPC_DEFAULT_CFLAGS="-fPIC -O3 -pipe -fno-plt -fno-semantic-interposition -fstack-clash-protection -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections -Wno-unused-command-line-argument" SPC_DEFAULT_CXXFLAGS="${SPC_DEFAULT_CFLAGS}" SPC_DEFAULT_LDFLAGS="-Wl,-z,relro -Wl,--as-needed -Wl,-z,now -Wl,-z,noexecstack -Wl,--gc-sections" ; upx executable path @@ -125,6 +126,8 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fno-ident -fPIE SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fPIE -fvisibility=hidden -fvisibility-inlines-hidden ${SPC_DEFAULT_CXXFLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.so SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" +; EXTRA_LDFLAGS_PROGRAM for `make` php; appended only to SAPI executable links (cli/fpm/cgi/micro/embed). Used by PGO to inject -fprofile-use= without polluting libphp.{a,so}. +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM="" ; optional, path to openssl conf. This affects where openssl will look for the default CA. ; default on Debian/Alpine: /etc/ssl, default on RHEL: /etc/pki/tls @@ -140,6 +143,7 @@ SPC_USE_LLVM=system CC=${SPC_DEFAULT_CC} CXX=${SPC_DEFAULT_CXX} AR=${SPC_DEFAULT_AR} +RANLIB=${SPC_DEFAULT_RANLIB} LD=${SPC_DEFAULT_LD} ; default compiler flags, used in CMake toolchain file, openssl and pkg-config build SPC_DEFAULT_CFLAGS="--target=${MAC_ARCH}-apple-darwin -O3 -fno-plt -fno-omit-frame-pointer -mno-omit-leaf-frame-pointer -ffunction-sections -fdata-sections" @@ -163,5 +167,7 @@ SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS="-g -fstack-protector-strong -fpic -fpie -fvis SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS="-g -fstack-protector-strong -fno-ident -fpie -fvisibility=hidden -fvisibility-inlines-hidden -Werror=unknown-warning-option ${SPC_DEFAULT_CXXFLAGS}" ; EXTRA_LDFLAGS for `make` php, can use -release to set a soname for libphp.dylib SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS="" +; EXTRA_LDFLAGS_PROGRAM for `make` php; appended only to SAPI executable links (cli/fpm/cgi/micro/embed). Used by PGO to inject -fprofile-use= without polluting libphp.{a,dylib}. +SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM="" ; minimum compatible macOS version (LLVM vars, availability not guaranteed) MACOSX_DEPLOYMENT_TARGET=12.0 diff --git a/config/pkg/ext/builtin-extensions.yml b/config/pkg/ext/builtin-extensions.yml index 750745cba..c97b32dd2 100644 --- a/config/pkg/ext/builtin-extensions.yml +++ b/config/pkg/ext/builtin-extensions.yml @@ -186,6 +186,7 @@ ext-password-argon2: type: php-extension depends: - libargon2 + suggests: - ext-openssl php-extension: os: diff --git a/config/pkg/lib/gettext-win.yml b/config/pkg/lib/gettext-win.yml index 142383077..286f5218b 100644 --- a/config/pkg/lib/gettext-win.yml +++ b/config/pkg/lib/gettext-win.yml @@ -4,6 +4,6 @@ gettext-win: source: type: git url: 'https://github.com/winlibs/gettext.git' - rev: master + rev: '0.18' static-libs@windows: - libintl_a.lib diff --git a/config/pkg/target/llvm-compiler-rt.yml b/config/pkg/target/llvm-compiler-rt.yml new file mode 100644 index 000000000..b7c7da74d --- /dev/null +++ b/config/pkg/target/llvm-compiler-rt.yml @@ -0,0 +1,6 @@ +llvm-compiler-rt: + type: target + artifact: + binary: custom + depends: + - zig diff --git a/config/pkg/target/llvm-tools.yml b/config/pkg/target/llvm-tools.yml new file mode 100644 index 000000000..c43721ac6 --- /dev/null +++ b/config/pkg/target/llvm-tools.yml @@ -0,0 +1,6 @@ +llvm-tools: + type: target + artifact: + binary: custom + depends: + - zig diff --git a/src/Package/Artifact/go_win.php b/src/Package/Artifact/go_win.php index e06e615ab..7a87d2db2 100644 --- a/src/Package/Artifact/go_win.php +++ b/src/Package/Artifact/go_win.php @@ -20,8 +20,6 @@ class go_win ])] public function downBinary(ArtifactDownloader $downloader): DownloadResult { - $pkgroot = PKG_ROOT_PATH; - // get version [$version] = explode("\n", default_shell()->executeCurl('https://go.dev/VERSION?m=text', retries: $downloader->getRetry()) ?: ''); if ($version === '') { @@ -52,7 +50,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult throw new DownloaderException("Hash mismatch for downloaded go-win binary. Expected {$hash}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-win", verified: true, version: $version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: '{pkg_root_path}/go-win', verified: true, version: $version); } #[CustomBinaryCheckUpdate('go-win', ['windows-x86_64'])] diff --git a/src/Package/Artifact/go_xcaddy.php b/src/Package/Artifact/go_xcaddy.php index 51ccfb87b..08bf02d32 100644 --- a/src/Package/Artifact/go_xcaddy.php +++ b/src/Package/Artifact/go_xcaddy.php @@ -25,7 +25,6 @@ class go_xcaddy ])] public function downBinary(ArtifactDownloader $downloader): DownloadResult { - $pkgroot = PKG_ROOT_PATH; $name = SystemTarget::getCurrentPlatformString(); $arch = match (explode('-', $name)[1]) { 'x86_64' => 'amd64', @@ -64,7 +63,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult if ($file_hash !== $hash) { throw new DownloaderException("Hash mismatch for downloaded go-xcaddy binary. Expected {$hash}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: "{$pkgroot}/go-xcaddy", verified: true, version: $version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $version], extract: '{pkg_root_path}/go-xcaddy', verified: true, version: $version); } #[CustomBinaryCheckUpdate('go-xcaddy', [ @@ -109,7 +108,7 @@ public function afterExtract(string $target_path): void 'GOROOT' => "{$target_path}", 'GOBIN' => "{$target_path}/bin", 'GOPATH' => "{$target_path}/go", - ])->exec('CC=cc go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest'); + ])->exec('CGO_ENABLED=0 go install github.com/caddyserver/xcaddy/cmd/xcaddy@master'); GlobalEnvManager::addPathIfNotExists("{$target_path}/bin"); } } diff --git a/src/Package/Artifact/llvm_compiler_rt.php b/src/Package/Artifact/llvm_compiler_rt.php new file mode 100644 index 000000000..974b9d57f --- /dev/null +++ b/src/Package/Artifact/llvm_compiler_rt.php @@ -0,0 +1,141 @@ +detectZigLlvmVersion() + ?? throw new DownloaderException('llvm-compiler-rt: could not detect bundled clang version from zig cc --version'); + $tarball = "compiler-rt-{$llvmVersion}.src.tar.xz"; + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}"; + $tarballPath = DOWNLOAD_PATH . '/' . $tarball; + default_shell()->executeCurlDownload($url, $tarballPath, retries: $downloader->getRetry()); + return DownloadResult::archive($tarball, ['url' => $url, 'version' => $llvmVersion], extract: '{source_path}/llvm-compiler-rt', verified: false, version: $llvmVersion); + } + + #[CustomBinaryCheckUpdate('llvm-compiler-rt', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $llvmVersion = $this->detectZigLlvmVersion() + ?? throw new DownloaderException('llvm-compiler-rt: could not detect bundled clang version from zig cc --version'); + return new CheckUpdateResult( + old: $old_version, + new: $llvmVersion, + needUpdate: $old_version === null || $llvmVersion !== $old_version, + ); + } + + #[AfterBinaryExtract('llvm-compiler-rt', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function postExtract(string $target_path): void + { + $this->buildForTriple($target_path); + } + + public function buildForTriple(?string $sourceDir = null, ?string $triple = null): void + { + $sourceDir ??= SOURCE_PATH . '/llvm-compiler-rt'; + $triple ??= SystemTarget::getCanonicalTriple(); + $libDir = PKG_ROOT_PATH . '/zig/lib/' . $triple; + if ($this->isBuilt($libDir)) { + return; + } + if (!is_dir($sourceDir) || !is_dir("{$sourceDir}/lib/profile")) { + throw new BuildFailureException("llvm-compiler-rt: missing source at {$sourceDir} (extraction layout changed?)"); + } + f_mkdir($libDir, recursive: true); + $profileLib = "{$libDir}/libclang_rt.profile.a"; + $crtBegin = "{$libDir}/clang_rt.crtbegin.o"; + $crtEnd = "{$libDir}/clang_rt.crtend.o"; + if (!file_exists($profileLib)) { + $this->buildProfileRuntime($sourceDir, $profileLib, $triple); + } + if (!file_exists($crtBegin) || !file_exists($crtEnd)) { + $this->buildCrtObjects($sourceDir, $crtBegin, $crtEnd, $triple); + } + } + + public function isBuilt(string $libDir): bool + { + return file_exists("{$libDir}/libclang_rt.profile.a") + && file_exists("{$libDir}/clang_rt.crtbegin.o") + && file_exists("{$libDir}/clang_rt.crtend.o"); + } + + private function detectZigLlvmVersion(): ?string + { + [$rc, $out] = shell()->execWithResult('zig cc --version', false); + if ($rc !== 0) { + return null; + } + return preg_match('/clang version (\d+\.\d+\.\d+)/', implode("\n", $out), $m) ? $m[1] : null; + } + + private function buildProfileRuntime(string $srcRoot, string $libPath, string $triple): void + { + $profileSrc = "{$srcRoot}/lib/profile"; + $profileInc = "{$srcRoot}/include"; + // Skip OS-specific sources we can't satisfy without their SDKs. + $skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap']; + $sources = array_filter( + array_merge(glob("{$profileSrc}/*.c") ?: [], glob("{$profileSrc}/*.cpp") ?: []), + fn ($f) => !array_any($skip, fn ($s) => str_contains($f, $s)), + ); + + $objDir = "{$srcRoot}/obj-profile-{$triple}"; + f_mkdir($objDir, recursive: true); + $cflags = "-target {$triple} -c -O2 -fPIC -fvisibility=hidden " + . '-I' . escapeshellarg($profileInc) . ' ' + . '-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1'; + $srcArgs = implode(' ', array_map('escapeshellarg', $sources)); + shell()->cd($objDir)->exec("zig cc {$cflags} {$srcArgs}"); + shell()->cd($objDir)->exec('zig ar rcs ' . escapeshellarg($libPath) . ' *.o'); + } + + private function buildCrtObjects(string $srcRoot, string $crtBegin, string $crtEnd, string $triple): void + { + $beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c"; + $endSrc = "{$srcRoot}/lib/builtins/crtend.c"; + if (!is_file($beginSrc) || !is_file($endSrc)) { + throw new BuildFailureException("llvm-compiler-rt: crtbegin/crtend source missing under {$srcRoot}/lib/builtins"); + } + $cflags = "-target {$triple} -c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY"; + foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) { + shell()->exec("zig cc {$cflags} -o " . escapeshellarg($dst) . ' ' . escapeshellarg($src)); + } + } +} diff --git a/src/Package/Artifact/llvm_tools.php b/src/Package/Artifact/llvm_tools.php new file mode 100644 index 000000000..5dd26214e --- /dev/null +++ b/src/Package/Artifact/llvm_tools.php @@ -0,0 +1,147 @@ +detectLlvmVersion() + ?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first'); + $tarball = "llvm-project-{$llvmVersion}.src.tar.xz"; + $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}"; + $tarballPath = DOWNLOAD_PATH . '/' . $tarball; + default_shell()->executeCurlDownload($url, $tarballPath, retries: $downloader->getRetry()); + return DownloadResult::archive($tarball, ['url' => $url, 'version' => $llvmVersion], extract: '{source_path}/llvm-tools', verified: false, version: $llvmVersion); + } + + #[CustomBinaryCheckUpdate('llvm-tools', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $llvmVersion = $this->detectLlvmVersion() + ?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first'); + return new CheckUpdateResult( + old: $old_version, + new: $llvmVersion, + needUpdate: $old_version === null || $llvmVersion !== $old_version, + ); + } + + #[AfterBinaryExtract('llvm-tools', [ + 'linux-x86_64', + 'linux-aarch64', + 'macos-x86_64', + 'macos-aarch64', + ])] + public function postExtract(string $target_path): void + { + $this->buildForHost($target_path); + } + + public function buildForHost(?string $sourceRoot = null): void + { + $sourceRoot ??= SOURCE_PATH . '/llvm-tools'; + $binDir = PKG_ROOT_PATH . '/llvm-tools/bin'; + if ($this->allBuilt($binDir)) { + return; + } + $llvmDir = "{$sourceRoot}/llvm"; + if (!is_dir($llvmDir)) { + throw new BuildFailureException("llvm-tools: missing source at {$llvmDir} (extraction layout changed?)"); + } + $buildDir = "{$sourceRoot}/build"; + $installDir = PKG_ROOT_PATH . '/llvm-tools'; + f_mkdir($buildDir, recursive: true); + f_mkdir($binDir, recursive: true); + + $cmakeArgs = implode(' ', array_map('escapeshellarg', [ + '-S', $llvmDir, + '-B', $buildDir, + '-DCMAKE_BUILD_TYPE=Release', + '-DLLVM_ENABLE_PROJECTS=', + '-DLLVM_TARGETS_TO_BUILD=', + '-DLLVM_INCLUDE_BENCHMARKS=OFF', + '-DLLVM_INCLUDE_TESTS=OFF', + '-DLLVM_INCLUDE_EXAMPLES=OFF', + '-DLLVM_INCLUDE_DOCS=OFF', + '-DLLVM_ENABLE_ZLIB=OFF', + '-DLLVM_ENABLE_ZSTD=OFF', + '-DLLVM_ENABLE_LIBXML2=OFF', + '-DLLVM_ENABLE_TERMINFO=OFF', + '-DLLVM_ENABLE_LIBEDIT=OFF', + '-DLLVM_ENABLE_LIBPFM=OFF', + '-DLLVM_BUILD_LLVM_DYLIB=OFF', + '-DLLVM_LINK_LLVM_DYLIB=OFF', + '-DBUILD_SHARED_LIBS=OFF', + '-DCMAKE_C_COMPILER=' . PKG_ROOT_PATH . '/zig/zig-cc', + '-DCMAKE_CXX_COMPILER=' . PKG_ROOT_PATH . '/zig/zig-c++', + '-DCMAKE_INSTALL_PREFIX=' . $installDir, + ])); + $jobs = ApplicationContext::get(PackageBuilder::class)->concurrency; + $targetArgs = implode(' ', array_map(fn ($t) => '--target ' . escapeshellarg($t), self::TOOLS)); + + shell() + ->setEnv(['SPC_TARGET' => GNU_ARCH . '-linux-musl']) + ->exec('cmake ' . $cmakeArgs) + ->exec('cmake --build ' . escapeshellarg($buildDir) . ' ' . $targetArgs . " -j {$jobs}"); + + foreach (self::TOOLS as $t) { + $built = "{$buildDir}/bin/{$t}"; + if (!is_file($built)) { + throw new BuildFailureException("llvm-tools: missing build output {$built}"); + } + copy($built, "{$binDir}/{$t}"); + chmod("{$binDir}/{$t}", 0755); + } + } + + public function allBuilt(string $binDir): bool + { + foreach (self::TOOLS as $t) { + $p = "{$binDir}/{$t}"; + if (!is_file($p) || !is_executable($p)) { + return false; + } + } + return true; + } + + private function detectLlvmVersion(): ?string + { + $zig = PKG_ROOT_PATH . '/zig/zig'; + if (!is_file($zig)) { + return null; + } + [$rc, $out] = shell()->execWithResult(escapeshellarg($zig) . ' cc --version', false); + if ($rc !== 0) { + return null; + } + return preg_match('/clang version (\d+\.\d+\.\d+)/', implode("\n", $out), $m) ? $m[1] : null; + } +} diff --git a/src/Package/Artifact/rust.php b/src/Package/Artifact/rust.php index e5c9f5259..cc9bb1757 100644 --- a/src/Package/Artifact/rust.php +++ b/src/Package/Artifact/rust.php @@ -46,7 +46,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult $download_url = "https://static.rust-lang.org/dist/rust-{$latest_version}-{$arch}-unknown-linux-{$distro}.tar.xz"; $path = DOWNLOAD_PATH . DIRECTORY_SEPARATOR . basename($download_url); default_shell()->executeCurlDownload($download_url, $path, retries: $downloader->getRetry()); - return DownloadResult::archive(basename($path), ['url' => $download_url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/rust-install', verified: false, version: $latest_version); + return DownloadResult::archive(basename($path), ['url' => $download_url, 'version' => $latest_version], extract: '{pkg_root_path}/rust-install', verified: false, version: $latest_version); } #[CustomBinaryCheckUpdate('rust', [ diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 95520aa4b..04ae0b1cc 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -61,7 +61,7 @@ public function downBinary(ArtifactDownloader $downloader): DownloadResult if ($file_hash !== $sha256) { throw new DownloaderException("Hash mismatch for downloaded Zig binary. Expected {$sha256}, got {$file_hash}"); } - return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: PKG_ROOT_PATH . '/zig', verified: true, version: $latest_version); + return DownloadResult::archive(basename($path), ['url' => $url, 'version' => $latest_version], extract: '{pkg_root_path}/zig', verified: true, version: $latest_version); } #[CustomBinaryCheckUpdate('zig', [ @@ -110,26 +110,24 @@ public function postExtractZig(string $target_path): void break; } } - if ($all_exist) { - return; - } - - $script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh'; - $script_content = file_get_contents($script_path); + if (!$all_exist) { + $script_path = ROOT_DIR . '/src/globals/scripts/zig-cc.sh'; + $script_content = file_get_contents($script_path); - file_put_contents("{$target_path}/zig-cc", $script_content); - chmod("{$target_path}/zig-cc", 0755); + file_put_contents("{$target_path}/zig-cc", $script_content); + chmod("{$target_path}/zig-cc", 0755); - $script_content = str_replace('zig cc', 'zig c++', $script_content); - file_put_contents("{$target_path}/zig-c++", $script_content); - file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@"); - file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@"); - file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@"); - file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@"); - chmod("{$target_path}/zig-c++", 0755); - chmod("{$target_path}/zig-ar", 0755); - chmod("{$target_path}/zig-ld.lld", 0755); - chmod("{$target_path}/zig-ranlib", 0755); - chmod("{$target_path}/zig-objcopy", 0755); + $script_content = str_replace('zig cc', 'zig c++', $script_content); + file_put_contents("{$target_path}/zig-c++", $script_content); + file_put_contents("{$target_path}/zig-ar", "#!/usr/bin/env bash\nexec zig ar $@"); + file_put_contents("{$target_path}/zig-ld.lld", "#!/usr/bin/env bash\nexec zig ld.lld $@"); + file_put_contents("{$target_path}/zig-ranlib", "#!/usr/bin/env bash\nexec zig ranlib $@"); + file_put_contents("{$target_path}/zig-objcopy", "#!/usr/bin/env bash\nexec zig objcopy $@"); + chmod("{$target_path}/zig-c++", 0755); + chmod("{$target_path}/zig-ar", 0755); + chmod("{$target_path}/zig-ld.lld", 0755); + chmod("{$target_path}/zig-ranlib", 0755); + chmod("{$target_path}/zig-objcopy", 0755); + } } } diff --git a/src/Package/Extension/opcache.php b/src/Package/Extension/opcache.php index 07758de26..699e124d0 100644 --- a/src/Package/Extension/opcache.php +++ b/src/Package/Extension/opcache.php @@ -72,6 +72,10 @@ public function getUnixConfigureArg(bool $shared, PackageBuilder $builder): stri ) { $opcache_jit = ' --disable-opcache-jit'; } - return '--enable-opcache' . ($shared ? '=shared' : '') . $opcache_jit; + // PHP 8.5+ has opcache built-in + if ($phpVersionID < 80500) { + return '--enable-opcache' . ($shared ? '=shared' : '') . $opcache_jit; + } + return trim($opcache_jit); } } diff --git a/src/Package/Extension/password_argon2.php b/src/Package/Extension/password_argon2.php index 77122405d..efef3d059 100644 --- a/src/Package/Extension/password_argon2.php +++ b/src/Package/Extension/password_argon2.php @@ -27,7 +27,7 @@ public function runSmokeTestCliUnix(): void #[CustomPhpConfigureArg('Darwin')] public function getConfigureArg(PackageInstaller $installer, PackageBuilder $builder): string { - if ($installer->getLibraryPackage('openssl') !== null) { + if ($installer->getPhpExtensionPackage('openssl')?->isBuildStatic() || $this->isBuildShared()) { if (php::getPHPVersionID() >= 80500 || (php::getPHPVersionID() >= 80400 && !$builder->getOption('enable-zts'))) { return '--without-password-argon2'; // use --with-openssl-argon2 in openssl extension instead } diff --git a/src/Package/Extension/pgsql.php b/src/Package/Extension/pgsql.php index 6e2b8f0b5..b5b22a001 100644 --- a/src/Package/Extension/pgsql.php +++ b/src/Package/Extension/pgsql.php @@ -20,7 +20,7 @@ class pgsql extends PhpExtensionPackage public function getUnixConfigureArg(bool $shared, PackageBuilder $builder, PackageInstaller $installer): string { if (php::getPHPVersionID() >= 80400) { - $libfiles = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true])->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), $builder->getOption('with-suggests'))['libs']; + $libfiles = new SPCConfigUtil(['libs_only_deps' => true, 'absolute_libs' => true])->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()))['libs']; $libfiles = str_replace("{$builder->getLibDir()}/lib", '-l', $libfiles); $libfiles = str_replace('.a', '', $libfiles); return '--with-pgsql' . ($shared ? '=shared' : '') . diff --git a/src/Package/Extension/spx.php b/src/Package/Extension/spx.php index bb230ec94..96a8c75da 100644 --- a/src/Package/Extension/spx.php +++ b/src/Package/Extension/spx.php @@ -6,14 +6,27 @@ use Package\Target\php; use StaticPHP\Attribute\Package\BeforeStage; +use StaticPHP\Attribute\Package\CustomPhpConfigureArg; use StaticPHP\Attribute\Package\Extension; use StaticPHP\Attribute\PatchDescription; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Util\FileSystem; #[Extension('spx')] class spx extends PhpExtensionPackage { + #[CustomPhpConfigureArg('Linux')] + #[CustomPhpConfigureArg('Darwin')] + public function getUnixConfigureArg(bool $shared, PackageInstaller $installer): string + { + $arg = '--enable-SPX' . ($shared ? '=shared' : ''); + if ($installer->getLibraryPackage('zlib') !== null) { + $arg .= ' --with-zlib-dir=' . BUILD_ROOT_PATH; + } + return $arg; + } + #[BeforeStage('php', [php::class, 'buildconfForUnix'], 'ext-spx')] #[PatchDescription('Fix spx extension compile error when building as static')] public function patchBeforeBuildconf(): bool diff --git a/src/Package/Library/bzip2.php b/src/Package/Library/bzip2.php index 90fcce7c6..7f99c22b4 100644 --- a/src/Package/Library/bzip2.php +++ b/src/Package/Library/bzip2.php @@ -17,7 +17,9 @@ class bzip2 #[PatchBeforeBuild] public function patchBeforeBuild(LibraryPackage $lib): void { - FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', 'CFLAGS=-fPIC -Wall'); + // Makefile pins -O2 -fPIC; inject SPC_DEFAULT_CFLAGS + $extra = deduplicate_flags(trim((string) getenv('SPC_DEFAULT_CFLAGS')) . ' -fPIC -Wall'); + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS=-Wall', "CFLAGS={$extra}"); } #[BuildFor('Windows')] diff --git a/src/Package/Library/fastlz.php b/src/Package/Library/fastlz.php index f9dd010dc..7c3792cd2 100644 --- a/src/Package/Library/fastlz.php +++ b/src/Package/Library/fastlz.php @@ -18,9 +18,11 @@ public function build(LibraryPackage $lib): void { $cc = getenv('CC') ?: 'cc'; $ar = getenv('AR') ?: 'ar'; + $extra = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + $extra = $extra !== '' ? $extra . ' -fPIC' : '-O3 -fPIC'; shell()->cd($lib->getSourceDir())->initializeEnv($lib) - ->exec("{$cc} -c -O3 -fPIC fastlz.c -o fastlz.o") + ->exec("{$cc} -c {$extra} fastlz.c -o fastlz.o") ->exec("{$ar} rcs libfastlz.a fastlz.o"); // Copy header file diff --git a/src/Package/Library/icu.php b/src/Package/Library/icu.php index 7364e94ab..bab245081 100644 --- a/src/Package/Library/icu.php +++ b/src/Package/Library/icu.php @@ -24,9 +24,12 @@ public function beforePack(LibraryPackage $lib): void #[BuildFor('Linux')] public function buildLinux(LibraryPackage $lib, ToolchainInterface $toolchain, PackageBuilder $builder): void { + // runConfigureICU bakes CXXFLAGS/LDFLAGS, apply user flags too + $userCxxFlags = trim((string) getenv('SPC_DEFAULT_CXXFLAGS')); + $userLdFlags = trim((string) getenv('SPC_DEFAULT_LDFLAGS')); $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 = $toolchain->isStatic() ? 'LDFLAGS="-static"' : ''; + $cxxflags = "CXXFLAGS=\"-std=c++17 -DPIC -fPIC -fno-ident {$userCxxFlags}\""; + $ldflags = $toolchain->isStatic() ? "LDFLAGS=\"-static {$userLdFlags}\"" : "LDFLAGS=\"{$userLdFlags}\""; shell()->cd($lib->getSourceDir() . '/source')->initializeEnv($lib) ->exec( "{$cppflags} {$cxxflags} {$ldflags} " . diff --git a/src/Package/Library/jbig.php b/src/Package/Library/jbig.php index 1cfe60b74..94527f457 100644 --- a/src/Package/Library/jbig.php +++ b/src/Package/Library/jbig.php @@ -17,7 +17,9 @@ class jbig #[PatchBeforeBuild] public function patchBeforeBuild(LibraryPackage $lib): void { - FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', 'CFLAGS = -O2 -W -Wno-unused-result -fPIC'); + $extra = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + $cflags = ($extra !== '' ? $extra : '-O2') . ' -W -Wno-unused-result -fPIC'; + FileSystem::replaceFileStr($lib->getSourceDir() . '/Makefile', 'CFLAGS = -O2 -W -Wno-unused-result', "CFLAGS = {$cflags}"); } #[BuildFor('Darwin')] diff --git a/src/Package/Library/krb5.php b/src/Package/Library/krb5.php index 1370afccb..428c6d9b3 100644 --- a/src/Package/Library/krb5.php +++ b/src/Package/Library/krb5.php @@ -27,7 +27,7 @@ public function build(LibraryPackage $lib, PackageInstaller $installer, Toolchai $resolved = array_keys($installer->getResolvedPackages()); $spc = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); - $config = $spc->getPackageDepsConfig($lib->getName(), $resolved, include_suggests: true); + $config = $spc->getPackageDepsConfig($lib->getName(), $resolved); $extraEnv = [ 'CFLAGS' => '-fcommon', 'LIBS' => $config['libs'], diff --git a/src/Package/Library/liblz4.php b/src/Package/Library/liblz4.php index 5dfbc9956..1be56fe03 100644 --- a/src/Package/Library/liblz4.php +++ b/src/Package/Library/liblz4.php @@ -17,10 +17,17 @@ class liblz4 { #[PatchBeforeBuild] - #[PatchDescription('Fix Makefile install target for static liblz4')] + #[PatchDescription('Compile lib sources individually so -flto -c with multiple inputs works under zig-cc/clang')] public function patchBeforeBuild(LibraryPackage $lib): void { - FileSystem::replaceFileStr($lib->getSourceDir() . '/programs/Makefile', 'install: lz4', "install: lz4\n\ninstallewfwef: lz4"); + // `-flto -c` with multiple input files only writes a .o for the + // first source — the others are silently dropped, leaving liblz4.a with a + // single object. Compile each source individually so all .o files exist. + FileSystem::replaceFileStr( + $lib->getSourceDir() . '/lib/Makefile', + "liblz4.a: \$(SRCFILES)\nifeq (\$(BUILD_STATIC),yes) # can be disabled on command line\n\t@echo compiling static library\n\t\$(COMPILE.c) \$^\n\t\$(AR) rcs \$@ *.o\nendif", + "liblz4.a: \$(SRCFILES:.c=.o)\nifeq (\$(BUILD_STATIC),yes) # can be disabled on command line\n\t@echo compiling static library\n\t\$(AR) rcs \$@ \$^\nendif" + ); } #[BuildFor('Windows')] diff --git a/src/Package/Library/ncurses.php b/src/Package/Library/ncurses.php index dd591a6f9..e27cefcd4 100644 --- a/src/Package/Library/ncurses.php +++ b/src/Package/Library/ncurses.php @@ -6,6 +6,8 @@ use StaticPHP\Attribute\Package\BuildFor; use StaticPHP\Attribute\Package\Library; +use StaticPHP\Attribute\Package\PatchBeforeBuild; +use StaticPHP\Attribute\PatchDescription; use StaticPHP\Package\LibraryPackage; use StaticPHP\Runtime\Executor\UnixAutoconfExecutor; use StaticPHP\Toolchain\Interface\ToolchainInterface; @@ -16,6 +18,24 @@ #[Library('ncursesw')] class ncurses { + #[PatchBeforeBuild] + #[PatchDescription('Filter clang/zig "N warning(s) generated." line out of MKlib_gen.sh preprocessor pipe')] + public function patchBeforeBuild(LibraryPackage $lib): void + { + // MKlib_gen.sh feeds the C preprocessor's stdout through a sed/awk + // pipeline into lib_gen.c. zig-cc/clang emits "N warning(s) generated." + // on stdout (not stderr), and that line ends up as invalid C in the + // generated source. Filter it out of the pipe before sed sees it. + $mklibGen = $lib->getSourceDir() . '/ncurses/base/MKlib_gen.sh'; + if (is_file($mklibGen) && !str_contains((string) file_get_contents($mklibGen), "| grep -v ' generated")) { + FileSystem::replaceFileStr( + $mklibGen, + '$preprocessor $TMP 2>/dev/null \\', + "\$preprocessor \$TMP 2>/dev/null \\\n| grep -v ' generated\\.\$' \\", + ); + } + } + #[BuildFor('Darwin')] #[BuildFor('Linux')] public function build(LibraryPackage $package, ToolchainInterface $toolchain): void diff --git a/src/Package/Library/openssl.php b/src/Package/Library/openssl.php index 69297198d..e0493ea5d 100644 --- a/src/Package/Library/openssl.php +++ b/src/Package/Library/openssl.php @@ -111,6 +111,12 @@ public function build(LibraryPackage $lib): void $openssl_dir ??= LinuxUtil::getOSRelease()['dist'] === 'redhat' ? '/etc/pki/tls' : '/etc/ssl'; $ex_lib = trim($ex_lib); + // anything we want included (PGO -fprofile-*, LTO, custom hardening) + // has to be appended on the command line *after* the target name. + $userCFlags = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + $userLdFlags = trim((string) getenv('SPC_DEFAULT_LDFLAGS')); + $userExtra = trim($userCFlags . ' ' . $userLdFlags); + shell()->cd($lib->getSourceDir())->initializeEnv($lib) ->exec( "{$env} ./Configure no-shared zlib " . @@ -121,7 +127,8 @@ public function build(LibraryPackage $lib): void 'enable-pie ' . 'no-legacy ' . 'no-tests ' . - "linux-{$arch}" + "linux-{$arch} " . + $userExtra ) ->exec('make clean') ->exec("make -j{$lib->getBuilder()->concurrency} CNF_EX_LIBS=\"{$ex_lib}\"") diff --git a/src/Package/Library/postgresql.php b/src/Package/Library/postgresql.php index 45c48577c..b449e859e 100644 --- a/src/Package/Library/postgresql.php +++ b/src/Package/Library/postgresql.php @@ -58,7 +58,7 @@ public function patchBeforeBuild(): bool public function buildUnix(PackageInstaller $installer, PackageBuilder $builder): void { $spc_config = new SPCConfigUtil(['no_php' => true, 'libs_only_deps' => true]); - $config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages()), include_suggests: $builder->getOption('with-suggests', false)); + $config = $spc_config->getPackageDepsConfig('postgresql', array_keys($installer->getResolvedPackages())); $env_vars = [ 'CFLAGS' => $config['cflags'] . ' -std=c17', diff --git a/src/Package/Library/qdbm.php b/src/Package/Library/qdbm.php index 358846fb6..c7785683a 100644 --- a/src/Package/Library/qdbm.php +++ b/src/Package/Library/qdbm.php @@ -20,6 +20,11 @@ public function buildUnix(LibraryPackage $lib): void { $ac = UnixAutoconfExecutor::create($lib)->configure(); FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/MYLIBS = libqdbm.a.*/m', 'MYLIBS = libqdbm.a'); + // Makefile pins -O3, replace with SPC_DEFAULT_CFLAGS + $extra = trim((string) getenv('SPC_DEFAULT_CFLAGS')); + if ($extra !== '') { + FileSystem::replaceFileRegex($lib->getSourceDir() . '/Makefile', '/^CFLAGS = .*$/m', "CFLAGS = -Wall {$extra}"); + } $ac->make(SystemTarget::getTargetOS() === 'Darwin' ? 'mac' : ''); $lib->patchPkgconfPrefix(['qdbm.pc']); } diff --git a/src/Package/Library/unixodbc.php b/src/Package/Library/unixodbc.php index e482e68be..62149b1f7 100644 --- a/src/Package/Library/unixodbc.php +++ b/src/Package/Library/unixodbc.php @@ -27,7 +27,20 @@ public function buildUnix(): void 'Linux' => '/etc', default => throw new WrongUsageException("Unsupported OS: {$os}"), }; - UnixAutoconfExecutor::create($this) + + // unixodbc bundles libltdl; libltdl is incompatible with -flto + // (https://bugs.gentoo.org/532672). + $stripLto = static fn (string $s): string => clean_spaces((string) preg_replace('/(^|\s)-flto(=\S+)?(?=\s|$)/', ' ', $s)); + $cflags = $stripLto((string) getenv('SPC_DEFAULT_CFLAGS')); + $cxxflags = $stripLto((string) getenv('SPC_DEFAULT_CXXFLAGS')); + $ldflags = $stripLto((string) getenv('SPC_DEFAULT_LDFLAGS')); + + $make = UnixAutoconfExecutor::create($this) + ->setEnv([ + 'CFLAGS' => $cflags, + 'CXXFLAGS' => $cxxflags, + 'LDFLAGS' => $ldflags, + ]) ->configure( '--disable-debug', '--disable-dependency-tracking', @@ -35,8 +48,15 @@ public function buildUnix(): void '--with-included-ltdl', "--sysconfdir={$sysconf_selector}", '--enable-gui=no', - ) - ->make(); + ); + + // The exe/ subdirectory builds odbcinst/iusql/etc, turn it into a no-op + file_put_contents( + "{$this->getSourceDir()}/exe/Makefile", + ".PHONY: all install clean check distclean install-strip\nall install clean check distclean install-strip:\n\t@true\n", + ); + + $make->make(); $this->patchPkgconfPrefix(['odbc.pc', 'odbccr.pc', 'odbcinst.pc']); $this->patchLaDependencyPrefix(); } diff --git a/src/Package/Library/watcher.php b/src/Package/Library/watcher.php index 56f93d931..34f8eb9aa 100644 --- a/src/Package/Library/watcher.php +++ b/src/Package/Library/watcher.php @@ -20,9 +20,8 @@ public function build(): void if (stripos($cflags, '-fpic') === false) { $cflags .= ' -fPIC'; } - $ldflags = $this->getLibExtraLdFlags() ? ' ' . $this->getLibExtraLdFlags() : ''; shell()->cd("{$this->getSourceDir()}/watcher-c") - ->exec(getenv('CXX') . " -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra {$cflags}{$ldflags}") + ->exec(getenv('CXX') . " -c -o libwatcher-c.o ./src/watcher-c.cpp -I ./include -I ../include -std=c++17 -Wall -Wextra {$cflags}") ->exec(getenv('AR') . ' rcs libwatcher-c.a libwatcher-c.o'); copy("{$this->getSourceDir()}/watcher-c/libwatcher-c.a", "{$this->getLibDir()}/libwatcher-c.a"); diff --git a/src/Package/Target/curl.php b/src/Package/Target/curl.php index 43a2904b7..6085feaf4 100644 --- a/src/Package/Target/curl.php +++ b/src/Package/Target/curl.php @@ -39,7 +39,7 @@ public function buildWin(LibraryPackage $lib): void ->optionalPackage('zstd', ...cmake_boolean_args('CURL_ZSTD')) ->optionalPackage('brotli', ...cmake_boolean_args('CURL_BROTLI')) ->addConfigureArgs( - '-DBUILD_CURL_EXE=OFF', + '-DBUILD_CURL_EXE=ON', '-DZSTD_LIBRARY=zstd_static.lib', '-DBUILD_TESTING=OFF', '-DBUILD_EXAMPLES=OFF', @@ -81,6 +81,7 @@ public function build(LibraryPackage $lib): void ->addConfigureArgs( '-DBUILD_CURL_EXE=ON', '-DBUILD_LIBCURL_DOCS=OFF', + '-DOPENSSL_ROOT_DIR=' . BUILD_ROOT_PATH, ) ->build(); diff --git a/src/Package/Target/php.php b/src/Package/Target/php.php index a4ae5c56a..caf0eb18c 100644 --- a/src/Package/Target/php.php +++ b/src/Package/Target/php.php @@ -5,6 +5,7 @@ namespace Package\Target; use Package\Target\php\frankenphp; +use Package\Target\php\pgo; use Package\Target\php\unix; use Package\Target\php\windows; use StaticPHP\Artifact\ArtifactCache; @@ -48,6 +49,7 @@ class php extends TargetPackage use unix; use windows; use frankenphp; + use pgo; /** @var string[] Supported major PHP versions */ public const array SUPPORTED_MAJOR_VERSIONS = ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5']; @@ -268,12 +270,14 @@ public function validate(Package $package): void } } // linux does not support loading shared libraries when target is pure static - $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; - if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') { - throw new WrongUsageException( - 'Linux does not support loading shared libraries when linking libc statically. ' . - 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' - ); + if ($package->getName() === 'php-embed') { + $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; + if (SystemTarget::getTargetOS() === 'Linux' && ApplicationContext::get(ToolchainInterface::class)->isStatic() && $embed_type === 'shared') { + throw new WrongUsageException( + 'Linux does not support loading shared libraries when linking libc statically. ' . + 'Change SPC_CMD_VAR_PHP_EMBED_TYPE to static.' + ); + } } } diff --git a/src/Package/Target/php/frankenphp.php b/src/Package/Target/php/frankenphp.php index 4c4286234..663f495ef 100644 --- a/src/Package/Target/php/frankenphp.php +++ b/src/Package/Target/php/frankenphp.php @@ -88,10 +88,13 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller $libs .= ' -lgcov'; } + $extraLdProgram = clean_spaces((string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM')); $env = [ 'CGO_ENABLED' => '1', 'CGO_CFLAGS' => clean_spaces($cflags), - 'CGO_LDFLAGS' => "{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs}", + 'CGO_LDFLAGS' => clean_spaces("{$package->getLibExtraLdFlags()} {$staticFlags} {$config['ldflags']} {$libs} {$extraLdProgram}"), + // cgo strips flags not on its safe allowlist; widen it + 'CGO_LDFLAGS_ALLOW' => '-Wl,-z,.*|-Wl,--.*|-flto.*|-fprofile-.*', 'XCADDY_GO_BUILD_FLAGS' => '-buildmode=pie ' . '-ldflags \"-linkmode=external ' . $extLdFlags . ' ' . '-X \'github.com/caddyserver/caddy/v2/modules/caddyhttp.ServerHeader=FrankenPHP Caddy\' ' . @@ -101,10 +104,12 @@ public function buildFrankenphpForUnix(TargetPackage $package, PackageInstaller "-tags={$muslTags}nobadger,nomysql,nopgx{$no_brotli}{$no_watcher}", 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; + $pgo = file_exists("{$source_dir}/caddy/frankenphp/default.pgo") ? "--pgo {$source_dir}/caddy/frankenphp/default.pgo " : ''; InteractiveTerm::setMessage('Building frankenphp: ' . ConsoleColor::yellow('building with xcaddy')); shell()->cd(BUILD_LIB_PATH) ->setEnv($env) - ->exec("xcaddy build --output frankenphp {$xcaddy_modules}"); + ->exec('go clean -cache') // fix stale include evaluation + ->exec("xcaddy build --output frankenphp {$pgo}{$xcaddy_modules}"); $builder->deployBinary(BUILD_LIB_PATH . '/frankenphp', BUILD_BIN_PATH . '/frankenphp'); $package->setOutput('Binary path for FrankenPHP SAPI', BUILD_BIN_PATH . '/frankenphp'); diff --git a/src/Package/Target/php/pgo.php b/src/Package/Target/php/pgo.php new file mode 100644 index 000000000..40be88e6a --- /dev/null +++ b/src/Package/Target/php/pgo.php @@ -0,0 +1,127 @@ +isInstrument() && !$pgo->isCsInstrument()) { + return; + } + foreach (PgoContext::SHUTDOWN_PATCHES as $dir => $patch) { + $cwd = SOURCE_PATH . '/' . $dir; + if (!is_dir($cwd)) { + continue; + } + if (!SourcePatcher::patchFile($patch, $cwd)) { + throw new WrongUsageException("PGO --phase=instrument: patch {$patch} failed to apply in {$cwd}"); + } + logger()->info("PGO --phase=instrument: applied {$patch}"); + } + } + + #[ConditionalOn(PgoContext::class)] + #[AfterStage('php', [self::class, 'configureForUnix'], 'php')] + #[PatchDescription('Patch libtool to passthrough -fcs-profile-* for context-sensitive PGO')] + public function pgoPatchLibtoolForCsInstrument(PgoContext $pgo): void + { + if (!$pgo->isCsInstrument()) { + return; + } + $libtool = SOURCE_PATH . '/php-src/libtool'; + if (!is_file($libtool)) { + return; + } + $contents = (string) 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 --phase=cs-instrument: could not patch libtool for -fcs-profile-* passthrough'); + return; + } + file_put_contents($libtool, $patched); + logger()->info('PGO --phase=cs-instrument: patched libtool for -fcs-profile-* passthrough'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'configureForUnix'], 'php')] + public function pgoApplyConfigureFlags(PgoContext $pgo): void + { + $sapis = $pgo->trainableSapis(); + if ($sapis === []) { + return; + } + $pgo->applyEnvFor($sapis[0]); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeCliForUnix'], 'php')] + public function pgoBeforeMakeCli(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'cli'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeCgiForUnix'], 'php')] + public function pgoBeforeMakeCgi(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'cgi'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeFpmForUnix'], 'php')] + public function pgoBeforeMakeFpm(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'fpm'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeMicroForUnix'], 'php')] + public function pgoBeforeMakeMicro(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'micro'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'makeEmbedForUnix'], 'php')] + public function pgoBeforeMakeEmbed(PgoContext $pgo, TargetPackage $package): void + { + $this->pgoBeforeSapiMake($pgo, $package, 'embed'); + } + + #[ConditionalOn(PgoContext::class)] + #[BeforeStage('php', [self::class, 'buildFrankenphpForUnix'], 'php')] + public function pgoBeforeBuildFrankenphp(PgoContext $pgo): void + { + $pgo->applyEnvFor('frankenphp'); + logger()->info("PGO {$pgo->mode}: applying flags for frankenphp"); + } + + private function pgoBeforeSapiMake(PgoContext $pgo, TargetPackage $package, string $sapi): void + { + $resolved = $pgo->resolveSapi($sapi); + if (!in_array($resolved, $pgo->trainableSapis(), true)) { + return; + } + shell()->cd($package->getSourceDir())->exec('make clean'); + $pgo->applyEnvFor($sapi); + logger()->info("PGO {$pgo->mode}: applying flags for {$sapi}"); + } +} diff --git a/src/Package/Target/php/unix.php b/src/Package/Target/php/unix.php index 7d4d00631..805813eac 100644 --- a/src/Package/Target/php/unix.php +++ b/src/Package/Target/php/unix.php @@ -20,6 +20,7 @@ use StaticPHP\Package\TargetPackage; use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\ToolchainInterface; +use StaticPHP\Toolchain\ToolchainManager; use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\DirDiff; use StaticPHP\Util\FileSystem; @@ -59,7 +60,7 @@ public function patchBeforeBuildconf(TargetPackage $package): void } if (self::getPHPVersionID() >= 80300 && self::getPHPVersionID() < 80400) { - SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', $this->getSourceDir()); + SourcePatcher::patchFile('spc_fix_avx512_cache_before_80400.patch', SOURCE_PATH . '/php-src'); } } @@ -92,12 +93,10 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args = []; $version_id = self::getPHPVersionID(); - // disable undefined behavior sanitizer when opcache JIT is enabled (Linux only) - if (SystemTarget::getTargetOS() === 'Linux' && !$package->getBuildOption('disable-opcache-jit', false)) { - if ($version_id >= 80500 || $installer->isPackageResolved('ext-opcache')) { - $compiler_extra = getenv('SPC_COMPILER_EXTRA') ?: ''; - GlobalEnvManager::putenv('SPC_COMPILER_EXTRA=' . trim($compiler_extra . ' -fno-sanitize=undefined')); - } + // disable undefined behavior sanitizer for zig, trips up on lua minijit and opcache-jit + if (SystemTarget::getTargetOS() === 'Linux' && ToolchainManager::getToolchainClass() === ZigToolchain::class) { + $compiler_extra = getenv('SPC_COMPILER_EXTRA') ?: ''; + GlobalEnvManager::putenv('SPC_COMPILER_EXTRA=' . trim($compiler_extra . ' -fno-sanitize=undefined')); } // PHP JSON extension is built-in since PHP 8.0 if ($version_id < 80000) { @@ -129,6 +128,10 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $args[] = $installer->isPackageResolved('php-cgi') ? '--enable-cgi' : '--disable-cgi'; $embed_type = getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') ?: 'static'; $args[] = $installer->isPackageResolved('php-embed') ? "--enable-embed={$embed_type}" : '--disable-embed'; + // Cross-compile: pass --host so configure picks the correct fiber asm file and host_cpu logic + if ($host_triple = SystemTarget::getAutoconfHostTriple()) { + $args[] = "--host={$host_triple}"; + } $args[] = getenv('SPC_EXTRA_PHP_VARS') ?: null; $args = implode(' ', array_filter($args)); @@ -141,7 +144,7 @@ public function configureForUnix(TargetPackage $package, PackageInstaller $insta $this->seekPhpSrcLogFileOnException(fn () => shell()->cd($package->getSourceDir())->setEnv([ 'CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'CPPFLAGS' => "-I{$package->getIncludeDir()}", - 'LDFLAGS' => "-L{$package->getLibDir()} " . getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), + 'LDFLAGS' => "-L{$package->getLibDir()}", 'LIBS' => $vars['EXTRA_LIBS'] ?? '', ])->exec("{$cmd} {$args} {$static_extension_str}"), $package->getSourceDir()); } @@ -168,7 +171,7 @@ public function beforeMakeUnix(ToolchainInterface $toolchain): void #[BeforeStage('php', [self::class, 'makeForUnix'], 'php')] #[PatchDescription('Patch Makefile to fix //lib path for Linux builds')] - #[PatchDescription('Patch BUILD_CC to use system cc instead of zig-cc (prevents minilua crash)')] + #[PatchDescription('Under CI: patch BUILD_CC to system cc — zig-cc-built minilua segfaults there for reasons we cannot reproduce locally')] public function tryPatchMakefileUnix(TargetPackage $package, ToolchainInterface $toolchain): void { if (SystemTarget::getTargetOS() !== 'Linux') { @@ -178,7 +181,8 @@ public function tryPatchMakefileUnix(TargetPackage $package, ToolchainInterface // replace //lib with /lib in Makefile shell()->cd($package->getSourceDir())->exec('sed -i "s|//lib|/lib|g" Makefile'); - if ($toolchain instanceof ZigToolchain) { + // CI escape hatch: in CI, zig-cc-built minilua segfaults + if ($toolchain instanceof ZigToolchain && getenv('CI')) { $makefile = "{$package->getSourceDir()}/Makefile"; FileSystem::replaceFileRegex($makefile, '/^BUILD_CC\s*=\s*zig-cc\s*$/m', 'BUILD_CC = cc'); } @@ -229,6 +233,7 @@ public function makeForUnix(TargetPackage $package, PackageInstaller $installer) #[Stage] public function makeCliForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cli')); $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); @@ -239,11 +244,13 @@ public function makeCliForUnix(TargetPackage $package, PackageInstaller $install $builder->deployBinary("{$package->getSourceDir()}/sapi/cli/php", BUILD_BIN_PATH . '/php'); $package->setOutput('Binary path for cli SAPI', BUILD_BIN_PATH . '/php'); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-cli'), true, $start); } #[Stage] public function makeCgiForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make cgi')); $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); @@ -254,11 +261,13 @@ public function makeCgiForUnix(TargetPackage $package, PackageInstaller $install $builder->deployBinary("{$package->getSourceDir()}/sapi/cgi/php-cgi", BUILD_BIN_PATH . '/php-cgi'); $package->setOutput('Binary path for cgi SAPI', BUILD_BIN_PATH . '/php-cgi'); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-cgi'), true, $start); } #[Stage] public function makeFpmForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make fpm')); $concurrency = $builder->concurrency; $vars = $this->makeVars($installer); @@ -269,43 +278,58 @@ public function makeFpmForUnix(TargetPackage $package, PackageInstaller $install $builder->deployBinary("{$package->getSourceDir()}/sapi/fpm/php-fpm", BUILD_BIN_PATH . '/php-fpm'); $package->setOutput('Binary path for fpm SAPI', BUILD_BIN_PATH . '/php-fpm'); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-fpm'), true, $start); } #[Stage] #[PatchDescription('Patch phar extension for micro SAPI to support compressed phar')] public function makeMicroForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { - InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); - // apply --with-micro-fake-cli option - $vars = $this->makeVars($installer); - $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; - $makeArgs = $this->makeVarsToArgs($vars); - // build - shell()->cd($package->getSourceDir()) - ->setEnv($vars) - ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); - - $dst = BUILD_BIN_PATH . '/micro.sfx'; - $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); - // patch after UPX-ed micro.sfx (Linux only) - if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { - // cut binary with readelf to remove UPX extra segment - [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); - $out[1] = explode(' ', $out[1]); - $offset = $out[1][0]; - if ($ret !== 0 || !str_starts_with($offset, '0x')) { - throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + $start = microtime(true); + $phar_patched = false; + try { + if ($installer->isPackageResolved('ext-phar')) { + $phar_patched = true; + SourcePatcher::patchMicroPhar(self::getPHPVersionID()); + } + InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make micro')); + // apply --with-micro-fake-cli option + $vars = $this->makeVars($installer); + $vars['EXTRA_CFLAGS'] .= $package->getBuildOption('with-micro-fake-cli', false) ? ' -DPHP_MICRO_FAKE_CLI' : ''; + $makeArgs = $this->makeVarsToArgs($vars); + // build + shell()->cd($package->getSourceDir()) + ->setEnv($vars) + ->exec("make -j{$builder->concurrency} {$makeArgs} micro"); + + $dst = BUILD_BIN_PATH . '/micro.sfx'; + $builder->deployBinary($package->getSourceDir() . '/sapi/micro/micro.sfx', $dst); + // patch after UPX-ed micro.sfx (Linux only) + if (SystemTarget::getTargetOS() === 'Linux' && $builder->getOption('with-upx-pack')) { + // cut binary with readelf to remove UPX extra segment + [$ret, $out] = shell()->execWithResult("readelf -l {$dst} | awk '/LOAD|GNU_STACK/ {getline; print \\$1, \\$2, \\$3, \\$4, \\$6, \\$7}'"); + $out[1] = explode(' ', $out[1]); + $offset = $out[1][0]; + if ($ret !== 0 || !str_starts_with($offset, '0x')) { + throw new PatchException('phpmicro UPX patcher', 'Cannot find offset in readelf output'); + } + $offset = hexdec($offset); + // remove upx extra wastes + file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); + } + $package->setOutput('Binary path for micro SAPI', $dst); + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-micro'), true, $start); + } finally { + if ($phar_patched) { + SourcePatcher::unpatchMicroPhar(); } - $offset = hexdec($offset); - // remove upx extra wastes - file_put_contents($dst, substr(file_get_contents($dst), 0, $offset)); } - $package->setOutput('Binary path for micro SAPI', $dst); } #[Stage] public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $installer, PackageBuilder $builder): void { + $start = microtime(true); InteractiveTerm::setMessage('Building php: ' . ConsoleColor::yellow('make embed')); $shared_exts = array_filter( $installer->getResolvedPackages(), @@ -319,22 +343,43 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $root = BUILD_ROOT_PATH; $sed_prefix = SystemTarget::getTargetOS() === 'Darwin' ? 'sed -i ""' : 'sed -i'; + $vars = $this->makeVars($installer); + $makeArgs = $this->makeVarsToArgs($vars); shell()->cd($package->getSourceDir()) - ->setEnv($this->makeVars($installer)) + ->setEnv($vars) ->exec("{$sed_prefix} \"s|^EXTENSION_DIR = .*|EXTENSION_DIR = /" . basename(BUILD_MODULES_PATH) . '|" Makefile') - ->exec("make -j{$builder->concurrency} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); + ->exec("make -j{$builder->concurrency} {$makeArgs} INSTALL_ROOT={$root} install-sapi {$install_modules} install-build install-headers install-programs"); + + // install-modules deref'd libtool's `$ext.so → $ext-X.so` symlink for each built-with-php ext; restore them. + $release = null; + if (preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m)) { + $release = $m[1]; + foreach ($shared_exts as $ext) { + $name = $ext->getExtensionName(); + $u = BUILD_MODULES_PATH . "/{$name}.so"; + $v = BUILD_MODULES_PATH . "/{$name}-{$release}.so"; + if (file_exists($v) && file_exists($u) && !is_link($u)) { + unlink($u); + symlink(basename($v), $u); + } + } + } // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=shared ------------- - // process libphp.so for shared embed + // INSTALL_IT for embed copies through libtool's symlink, leaving only unversioned libphp.{so,dylib} — rename and symlink back so shared exts can `-lphp`. (static libphp.a is never versioned, even with -release.) $suffix = SystemTarget::getTargetOS() === 'Darwin' ? 'dylib' : 'so'; $libphp_so = "{$package->getLibDir()}/libphp.{$suffix}"; if (file_exists($libphp_so)) { - // rename libphp.so if -release is set - if (SystemTarget::getTargetOS() === 'Linux') { - $this->processLibphpSoFile($libphp_so, $installer); + if ($release !== null) { + $versioned = "{$package->getLibDir()}/libphp-{$release}.{$suffix}"; + if (file_exists($versioned)) { + @unlink($versioned); + } + rename($libphp_so, $versioned); + symlink(basename($versioned), $libphp_so); + $libphp_so = $versioned; } - // deploy $builder->deployBinary($libphp_so, $libphp_so, false); $package->setOutput('Library path for embed SAPI', $libphp_so); } @@ -343,6 +388,9 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $increment_files = $diff->getChangedFiles(); $files = []; foreach ($increment_files as $increment_file) { + if (is_link($increment_file) || !file_exists($increment_file)) { + continue; + } $builder->deployBinary($increment_file, $increment_file, false); $files[] = basename($increment_file); } @@ -350,6 +398,11 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta $package->setOutput('Built shared extensions', implode(', ', $files)); } + // phpize needs prefix patched whether libphp is .a or .so + $package->runStage([$this, 'patchUnixEmbedScripts']); + + InteractiveTerm::success('Built SAPI: ' . ConsoleColor::green('php-embed'), true, $start); + // ------------- SPC_CMD_VAR_PHP_EMBED_TYPE=static ------------- // process libphp.a for static embed (only when present) @@ -359,9 +412,6 @@ public function makeEmbedForUnix(TargetPackage $package, PackageInstaller $insta shell()->exec("{$ar} -t {$libphp_a} | grep '\\.a$' | xargs -n1 {$ar} d {$libphp_a}"); UnixUtil::exportDynamicSymbols($libphp_a); } - - // deploy embed php scripts - $package->runStage([$this, 'patchUnixEmbedScripts']); } #[Stage] @@ -394,8 +444,15 @@ public function unixBuildSharedExt(PackageInstaller $installer, ToolchainInterfa try { logger()->debug('Building shared extensions...'); foreach ($shared_extensions as $extension) { - InteractiveTerm::setMessage('Building shared PHP extension: ' . ConsoleColor::yellow($extension->getName())); - $extension->buildShared(); + $ext_start = microtime(true); + InteractiveTerm::setMessage('Building shared extension: ' . ConsoleColor::yellow($extension->getName())); + try { + $extension->buildShared(); + } catch (\Throwable $e) { + InteractiveTerm::error('Building shared extension failed: ' . ConsoleColor::red($extension->getName())); + throw $e; + } + InteractiveTerm::success('Built shared extension: ' . ConsoleColor::green($extension->getName()), true, $ext_start); } } finally { // restore php-config @@ -680,77 +737,6 @@ protected function generateMicroExtTests(PackageInstaller $installer): string return $php; } - /** - * Rename libphp.so to libphp-.so if -release is set in LDFLAGS. - */ - private function processLibphpSoFile(string $libphpSo, PackageInstaller $installer): void - { - $ldflags = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') ?: ''; - $libDir = BUILD_LIB_PATH; - $modulesDir = BUILD_MODULES_PATH; - $realLibName = 'libphp.so'; - $cwd = getcwd(); - - if (preg_match('/-release\s+(\S+)/', $ldflags, $matches)) { - $release = $matches[1]; - $realLibName = "libphp-{$release}.so"; - $libphpRelease = "{$libDir}/{$realLibName}"; - if (!file_exists($libphpRelease) && file_exists($libphpSo)) { - rename($libphpSo, $libphpRelease); - } - if (file_exists($libphpRelease)) { - chdir($libDir); - if (file_exists($libphpSo)) { - unlink($libphpSo); - } - symlink($realLibName, 'libphp.so'); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($realLibName), - escapeshellarg($libphpRelease) - )); - } - if (is_dir($modulesDir)) { - chdir($modulesDir); - foreach ($installer->getResolvedPackages(PhpExtensionPackage::class) as $ext) { - if (!$ext->isBuildShared()) { - continue; - } - $name = $ext->getName(); - $versioned = "{$name}-{$release}.so"; - $unversioned = "{$name}.so"; - $src = "{$modulesDir}/{$versioned}"; - $dst = "{$modulesDir}/{$unversioned}"; - if (is_file($src)) { - rename($src, $dst); - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg($unversioned), - escapeshellarg($dst) - )); - } - } - } - chdir($cwd); - } - - $target = "{$libDir}/{$realLibName}"; - if (file_exists($target)) { - [, $output] = shell()->execWithResult('readelf -d ' . escapeshellarg($target)); - $output = implode("\n", $output); - if (preg_match('/SONAME.*\[(.+)]/', $output, $sonameMatch)) { - $currentSoname = $sonameMatch[1]; - if ($currentSoname !== basename($target)) { - shell()->exec(sprintf( - 'patchelf --set-soname %s %s', - escapeshellarg(basename($target)), - escapeshellarg($target) - )); - } - } - } - } - /** * Make environment variables for php make. * This will call SPCConfigUtil to generate proper LDFLAGS and LIBS for static linking. @@ -760,16 +746,28 @@ private function makeVars(PackageInstaller $installer): array $config = new SPCConfigUtil(['libs_only_deps' => true])->config($installer->getAvailableResolvedPackageNames()); $static = ApplicationContext::get(ToolchainInterface::class)->isStatic() ? '-all-static' : ''; $pie = SystemTarget::getTargetOS() === 'Linux' ? '-pie' : ''; + $lib = BUILD_LIB_PATH; // Append SPC_EXTRA_LIBS to libs for dynamic linking support (e.g., X11) $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; $libs = trim($config['libs'] . ' ' . $extra_libs); + // libtool input (libphp.la). `make EXTRA_LDFLAGS=…` cmdline overrides fully replace the Makefile value, so re-include $config['ldflags'] for -L paths. + $extra_ldflags = clean_spaces($config['ldflags'] . ' ' . 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_env = getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM') ?: ''; + $extra_ldflags_program = clean_spaces("-L{$lib} {$static} {$pie} {$extra_ldflags_program_env}"); + return array_filter([ 'EXTRA_CFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS'), 'EXTRA_CXXFLAGS' => getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_CXXFLAGS'), - 'EXTRA_LDFLAGS_PROGRAM' => deduplicate_flags(getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS') . " {$config['ldflags']} {$static} {$pie}"), - 'EXTRA_LDFLAGS' => $config['ldflags'], + 'EXTRA_LDFLAGS' => $extra_ldflags, + 'EXTRA_LDFLAGS_PROGRAM' => $extra_ldflags_program, 'EXTRA_LIBS' => $libs, ]); } diff --git a/src/StaticPHP/Artifact/Artifact.php b/src/StaticPHP/Artifact/Artifact.php index e241d4fe8..8e2166d1a 100644 --- a/src/StaticPHP/Artifact/Artifact.php +++ b/src/StaticPHP/Artifact/Artifact.php @@ -285,15 +285,19 @@ public function getDownloadConfig(string $type): mixed * Get source extraction directory. * * Rules: - * 1. If extract is not specified: SOURCE_PATH/{artifact_name} - * 2. If extract is relative path: SOURCE_PATH/{value} - * 3. If extract is absolute path: {value} - * 4. If extract is array (dict): handled by extractor (selective extraction) + * 1. If cache_type is 'local': use the absolute dirname recorded at download time (no symlink/copy). + * 2. If extract is not specified: SOURCE_PATH/{artifact_name} + * 3. If extract is relative path: SOURCE_PATH/{value} + * 4. If extract is absolute path: {value} + * 5. If extract is array (dict): handled by extractor (selective extraction) */ public function getSourceDir(): string { // Prefer cache extract path, fall back to config $cache_info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo($this->name); + if (($cache_info['cache_type'] ?? null) === 'local' && isset($cache_info['dirname'])) { + return FileSystem::convertPath($cache_info['dirname']); + } $extract = is_string($cache_info['extract'] ?? null) ? $cache_info['extract'] : ($this->config['source']['extract'] ?? null); diff --git a/src/StaticPHP/Artifact/ArtifactDownloader.php b/src/StaticPHP/Artifact/ArtifactDownloader.php index f52036c64..83e80f319 100644 --- a/src/StaticPHP/Artifact/ArtifactDownloader.php +++ b/src/StaticPHP/Artifact/ArtifactDownloader.php @@ -317,7 +317,10 @@ public function download(): void if (!is_dir(DOWNLOAD_PATH)) { FileSystem::createDir(DOWNLOAD_PATH); } - logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $this->artifacts)) . " with concurrency {$this->parallel} ..."); + $pending = array_values(array_filter($this->artifacts, fn ($a) => $this->generateQueue($a) !== [])); + if ($pending !== []) { + logger()->info('Downloading' . implode(', ', array_map(fn ($x) => " '{$x->getName()}'", $pending)) . " with concurrency {$this->parallel} ..."); + } // Download artifacts parallelly if ($this->parallel > 1) { $this->downloadWithConcurrency(); @@ -731,6 +734,16 @@ private function generateQueue(Artifact $artifact): array $binary_downloaded = $artifact->isBinaryDownloaded(compare_hash: true); $source_downloaded = $artifact->isSourceDownloaded(compare_hash: true); + if ($source_downloaded && $artifact->getName() === 'php-src' && ($requested = $this->getOption('with-php'))) { + $info = ApplicationContext::get(ArtifactCache::class)->getSourceInfo('php-src'); + $cv = $info['version'] ?? null; + $ct = $info['cache_type'] ?? null; + $matches = $requested === 'git' ? $ct === 'git' : ($cv !== null && $ct !== 'git' && ($cv === $requested || str_starts_with($cv, $requested . '.'))); + if (!$matches) { + $source_downloaded = false; + } + } + $item_source = ['display' => 'source', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source')]; $item_source_mirror = ['display' => 'source (mirror)', 'lock' => 'source', 'config' => $artifact->getDownloadConfig('source-mirror')]; diff --git a/src/StaticPHP/Artifact/ArtifactExtractor.php b/src/StaticPHP/Artifact/ArtifactExtractor.php index 987ec554e..2a9862461 100644 --- a/src/StaticPHP/Artifact/ArtifactExtractor.php +++ b/src/StaticPHP/Artifact/ArtifactExtractor.php @@ -136,6 +136,12 @@ protected function extractSource(Artifact $artifact): int throw new WrongUsageException("Artifact source [{$name}] not downloaded, please download it first!"); } + // Local (--custom-local): source lives in place at $cache_info['dirname']. + if (($cache_info['cache_type'] ?? null) === 'local') { + $artifact->emitAfterSourceExtract($artifact->getSourceDir()); + return SPC_STATUS_ALREADY_EXTRACTED; + } + $source_file = $this->cache->getCacheFullPath($cache_info); $target_path = $artifact->getSourceDir(); @@ -171,8 +177,12 @@ protected function extractSource(Artifact $artifact): int return SPC_STATUS_ALREADY_EXTRACTED; } - // Remove old directory if hash mismatch - if (is_dir($target_path)) { + // Remove old directory if hash mismatch. + // Guard: a symlink at $target_path (left over from older local-source handling) must be + // unlinked directly — never recurse into the link target, that would wipe the user's tree. + if (is_link($target_path)) { + @unlink($target_path); + } elseif (is_dir($target_path)) { logger()->notice("Source [{$name}] hash mismatch, re-extracting..."); FileSystem::removeDir($target_path); } diff --git a/src/StaticPHP/Command/CraftCommand.php b/src/StaticPHP/Command/CraftCommand.php index f390e4335..e187ad73e 100644 --- a/src/StaticPHP/Command/CraftCommand.php +++ b/src/StaticPHP/Command/CraftCommand.php @@ -9,7 +9,10 @@ use StaticPHP\Exception\ValidationException; use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; +use StaticPHP\Registry\PackageLoader; +use StaticPHP\Util\DependencyResolver; use StaticPHP\Util\FileSystem; +use StaticPHP\Util\Pgo\PgoContext; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Yaml\Exception\ParseException; @@ -21,6 +24,8 @@ class CraftCommand extends BaseCommand public function configure(): void { $this->addArgument('craft', null, 'Path to craft.yml file', WORKING_DIR . '/craft.yml'); + $this->addOption('libs-only', null, null, 'Build only the libraries needed by the configured extensions (skip PHP and SAPI build).'); + PgoContext::registerOptions($this); } public function handle(): int @@ -39,6 +44,9 @@ public function handle(): int // apply env array_walk($craft['extra-env'], fn ($v, $k) => f_putenv("{$k}={$v}")); + // stash craft for doctor checks that depend on what's being built (e.g. frankenphp → go-xcaddy) + ApplicationContext::set('craft', $craft); + // run doctor if ($craft['craft-options']['doctor']) { $doctor = new Doctor($this->output, FIX_POLICY_AUTOFIX); @@ -83,23 +91,67 @@ public function handle(): int FileSystem::resetDir(SOURCE_PATH); } + $pgo = $this->getOption('libs-only') ? null : PgoContext::tryFromInput($this->input, $craft['sapi'], $build_options); + $starttime = microtime(true); // run installer $installer = new PackageInstaller($build_options); ApplicationContext::get(PackageBuilder::class)->setArgument('extensions', implode(',', $craft['extensions'])); - $installer->addBuildPackage('php'); + + if ($this->getOption('libs-only')) { + $with_suggests = (bool) ($craft['build-options']['with-suggests'] ?? false); + $libs = $this->resolveLibsForExtensions($craft, $with_suggests); + if ($libs === []) { + $this->output->writeln('No libraries needed for the configured extensions; nothing to do.'); + return static::SUCCESS; + } + foreach ($libs as $lib) { + $installer->addBuildPackage($lib); + } + } else { + $installer->addBuildPackage('php'); + } $installer->run(true); $usedtime = round(microtime(true) - $starttime, 1); + $tag = $pgo !== null ? " (PGO {$pgo->mode})" : ''; $this->output->writeln("\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"); - $this->output->writeln("✔ BUILD SUCCESSFUL ({$usedtime} s)"); + $this->output->writeln("✔ BUILD SUCCESSFUL{$tag} ({$usedtime} s)"); $this->output->writeln("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"); + if ($pgo !== null && $pgo->isInstrument()) { + $this->output->writeln("Next: exercise the instrumented binary, then re-run craft with --pgo to consume {$pgo->profileRoot}."); + } + $installer->printBuildPackageOutputs(); return static::SUCCESS; } + /** @return list library package names transitively required by the configured extensions */ + private function resolveLibsForExtensions(array $craft, bool $include_suggests): array + { + $exts = array_merge($craft['extensions'], $craft['shared-extensions'] ?? []); + $ext_pkgs = array_map(fn ($x) => "ext-{$x}", $exts); + $extra = $craft['packages'] ?? []; + + $resolved = DependencyResolver::resolve( + array_merge($ext_pkgs, $extra), + include_suggests: $include_suggests, + ); + + $libs = []; + foreach ($resolved as $pkg_name) { + if (str_starts_with($pkg_name, 'ext-') || !PackageLoader::hasPackage($pkg_name)) { + continue; + } + if (PackageLoader::getPackage($pkg_name)->getType() === 'library') { + $libs[] = $pkg_name; + } + } + return $libs; + } + /** * Validate and parse craft.yml file to array. * diff --git a/src/StaticPHP/Command/ExtractCommand.php b/src/StaticPHP/Command/ExtractCommand.php index e2e79bdf0..8be8bf4b2 100644 --- a/src/StaticPHP/Command/ExtractCommand.php +++ b/src/StaticPHP/Command/ExtractCommand.php @@ -10,6 +10,7 @@ use StaticPHP\Registry\ArtifactLoader; use StaticPHP\Registry\PackageLoader; use StaticPHP\Util\DependencyResolver; +use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\InteractiveTerm; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Input\InputArgument; @@ -34,6 +35,7 @@ public function configure(): void public function handle(): int { + GlobalEnvManager::afterInit(); $cache = ApplicationContext::get(ArtifactCache::class); $extractor = new ArtifactExtractor($cache); $force_source = (bool) $this->getOption('source-only'); diff --git a/src/StaticPHP/DI/ApplicationContext.php b/src/StaticPHP/DI/ApplicationContext.php index 73ff9f2d4..2eca635c5 100644 --- a/src/StaticPHP/DI/ApplicationContext.php +++ b/src/StaticPHP/DI/ApplicationContext.php @@ -98,6 +98,25 @@ public static function has(string $id): bool return self::getContainer()->has($id); } + /** + * Resolve $id, returning null if it can't be constructed. + * PHP-DI's has() returns true for any autowirable class even when get() + * would throw on missing scalar args — for "is this resolvable right now" + * semantics use this. + * + * @template T + * @param class-string $id + * @return null|T + */ + public static function tryGet(string $id): mixed + { + try { + return self::getContainer()->get($id); + } catch (\Throwable) { + return null; + } + } + /** * Set a service in the container. * Use sparingly - prefer configuration-based definitions. diff --git a/src/StaticPHP/Doctor/Item/GoXcaddyCheck.php b/src/StaticPHP/Doctor/Item/GoXcaddyCheck.php new file mode 100644 index 000000000..112687e52 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/GoXcaddyCheck.php @@ -0,0 +1,44 @@ +addInstallPackage('go-xcaddy')->isPackageInstalled('go-xcaddy')) { + return CheckResult::fail('go-xcaddy is not installed', 'install-go-xcaddy'); + } + return CheckResult::ok(PKG_ROOT_PATH . '/go-xcaddy/bin/xcaddy'); + } + + #[FixItem('install-go-xcaddy')] + public function installGoXcaddy(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('go-xcaddy'); + $installer->run(true); + return $installer->isPackageInstalled('go-xcaddy'); + } +} diff --git a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php index df3b5241c..1fe7ed00d 100644 --- a/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php +++ b/src/StaticPHP/Doctor/Item/LinuxMuslCheck.php @@ -73,13 +73,20 @@ public function fixMusl(): bool $prefix = 'sudo '; logger()->warning('Current user is not root, using sudo for running command'); } + $sysEnv = ['CC' => 'gcc', 'CXX' => 'g++', 'AR' => 'ar', 'LD' => 'ld', 'RANLIB' => 'ranlib']; + $envFlags = ''; + foreach ($sysEnv as $k => $v) { + $envFlags .= "{$k}={$v} "; + } + $envFlags = rtrim($envFlags); $shell = shell()->cd(SOURCE_PATH . '/musl-wrapper') - ->exec('CC=gcc CXX=g++ AR=ar LD=ld ./configure --disable-gcc-wrapper') - ->exec('CC=gcc CXX=g++ AR=ar LD=ld make -j'); + ->setEnv($sysEnv) + ->exec('./configure --disable-gcc-wrapper') + ->exec('make -j'); if ($prefix !== '') { - f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + f_passthru('cd ' . SOURCE_PATH . "/musl-wrapper && {$envFlags} {$prefix}make install"); } else { - $shell->exec("CC=gcc CXX=g++ AR=ar LD=ld {$prefix}make install"); + $shell->exec("{$prefix}make install"); } return true; } diff --git a/src/StaticPHP/Doctor/Item/LlvmCompilerRtCheck.php b/src/StaticPHP/Doctor/Item/LlvmCompilerRtCheck.php new file mode 100644 index 000000000..368e02675 --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LlvmCompilerRtCheck.php @@ -0,0 +1,47 @@ +isBuilt($libDir)) { + return CheckResult::ok($libDir); + } + return CheckResult::fail('llvm-compiler-rt is not built for ' . SystemTarget::getCanonicalTriple(), 'build-llvm-compiler-rt'); + } + + #[FixItem('build-llvm-compiler-rt')] + public function fixLlvmCompilerRt(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('llvm-compiler-rt'); + $installer->run(true); + new llvm_compiler_rt()->buildForTriple(); + $libDir = PKG_ROOT_PATH . '/zig/lib/' . SystemTarget::getCanonicalTriple(); + return new llvm_compiler_rt()->isBuilt($libDir); + } +} diff --git a/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php b/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php new file mode 100644 index 000000000..cc14af94e --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php @@ -0,0 +1,45 @@ +allBuilt($binDir)) { + return CheckResult::ok($binDir); + } + return CheckResult::fail('llvm-tools are not built', 'build-llvm-tools'); + } + + #[FixItem('build-llvm-tools')] + public function fixLlvmTools(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('llvm-tools'); + $installer->run(true); + new llvm_tools()->buildForHost(); + return new llvm_tools()->allBuilt(PKG_ROOT_PATH . '/llvm-tools/bin'); + } +} diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index baa6d4cbc..ec140fd75 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -26,7 +26,7 @@ public static function optionalCheck(): bool public function checkZig(): CheckResult { if (new PackageInstaller()->addInstallPackage('zig')->isPackageInstalled('zig')) { - return CheckResult::ok(); + return CheckResult::ok(PKG_ROOT_PATH . '/zig/zig'); } return CheckResult::fail('zig is not installed', 'install-zig'); } diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 582fa474d..ff7ac2c15 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -11,6 +11,8 @@ use StaticPHP\Exception\WrongUsageException; use StaticPHP\Runtime\Shell\Shell; use StaticPHP\Runtime\SystemTarget; +use StaticPHP\Toolchain\Interface\ToolchainInterface; +use StaticPHP\Toolchain\ZigToolchain; use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalPathTrait; use StaticPHP\Util\InteractiveTerm; @@ -178,14 +180,15 @@ public function extractDebugInfo(string $binary_path): string if (SystemTarget::getTargetOS() === 'Darwin') { shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}"); } elseif (SystemTarget::getTargetOS() === 'Linux') { + $objcopy = getenv('OBJCOPY') ?: 'objcopy'; if ($eu_strip = LinuxUtil::findCommand('eu-strip')) { shell() ->exec("{$eu_strip} -f {$debug_file} {$binary_path}") - ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + ->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}"); } else { shell() - ->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}") - ->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}"); + ->exec("{$objcopy} --only-keep-debug {$binary_path} {$debug_file}") + ->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}"); } } else { logger()->debug('extractDebugInfo is only supported on Linux and macOS'); @@ -199,9 +202,12 @@ public function extractDebugInfo(string $binary_path): string */ public function stripBinary(string $binary_path): void { + $strip = ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain + ? PKG_ROOT_PATH . '/llvm-tools/bin/llvm-strip' + : 'strip'; shell()->exec(match (SystemTarget::getTargetOS()) { - 'Darwin' => "strip -S {$binary_path}", - 'Linux' => "strip --strip-unneeded {$binary_path}", + 'Darwin' => "{$strip} -S {$binary_path}", + 'Linux' => "{$strip} --strip-unneeded {$binary_path}", 'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'), }); diff --git a/src/StaticPHP/Package/PackageInstaller.php b/src/StaticPHP/Package/PackageInstaller.php index 9cd0a1067..8510a94bd 100644 --- a/src/StaticPHP/Package/PackageInstaller.php +++ b/src/StaticPHP/Package/PackageInstaller.php @@ -154,6 +154,8 @@ public function run(bool $disable_delay_msg = false): void $this->resolvePackages(); } + $this->reconcilePhpSrcVersion(); + if ($this->interactive && !$disable_delay_msg) { // show install or build options in terminal with beautiful output $this->printInstallerInfo(); @@ -215,7 +217,7 @@ public function run(bool $disable_delay_msg = false): void if (!$is_to_build && $should_use_binary) { // install binary if ($this->interactive) { - InteractiveTerm::indicateProgress('Installing package: ' . ConsoleColor::yellow($package->getName())); + InteractiveTerm::indicateProgress('Installing ' . $this->kindLabel($package) . ': ' . ConsoleColor::yellow($package->getName())); } try { // Start tracking for binary installation @@ -227,17 +229,17 @@ public function run(bool $disable_delay_msg = false): void // Stop tracking on error $this->tracker?->stopTracking(); if ($this->interactive) { - InteractiveTerm::finish('Installing binary package failed: ' . ConsoleColor::red($package->getName()), false); + InteractiveTerm::finish('Installing ' . $this->kindLabel($package) . ' failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } if ($this->interactive) { - InteractiveTerm::finish('Installed binary package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); + InteractiveTerm::finish('Installed ' . $this->kindLabel($package) . ': ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_INSTALLED ? ' (already installed, skipped)' : '')); } } elseif ($is_to_build && $has_build_stage || $has_source && $has_build_stage) { if ($this->interactive) { - InteractiveTerm::indicateProgress('Building package: ' . ConsoleColor::yellow($package->getName())); + InteractiveTerm::indicateProgress('Building ' . $this->kindLabel($package) . ': ' . ConsoleColor::yellow($package->getName())); } try { // Start tracking for build @@ -260,13 +262,13 @@ public function run(bool $disable_delay_msg = false): void // Stop tracking on error $this->tracker?->stopTracking(); if ($this->interactive) { - InteractiveTerm::finish('Building package failed: ' . ConsoleColor::red($package->getName()), false); + InteractiveTerm::finish('Building ' . $this->kindLabel($package) . ' failed: ' . ConsoleColor::red($package->getName()), false); echo PHP_EOL; } throw $e; } if ($this->interactive) { - InteractiveTerm::finish('Built package: ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); + InteractiveTerm::finish('Built ' . $this->kindLabel($package) . ': ' . ConsoleColor::green($package->getName()) . ($status === SPC_STATUS_ALREADY_BUILT ? ' (already built, skipped)' : '')); } } } @@ -571,6 +573,77 @@ public function getPhpExtensionPackage(string $package_or_ext_name): ?PhpExtensi return null; } + private function reconcilePhpSrcVersion(): void + { + $src_dir = SOURCE_PATH . '/php-src'; + $cache = ApplicationContext::get(ArtifactCache::class); + $requested = $this->options['dl-with-php'] ?? null; + if ($requested !== null && $requested !== '' && $requested !== 'git') { + $info = $cache->getSourceInfo('php-src') ?? []; + $cv = $info['version'] ?? null; + if (($info['cache_type'] ?? null) === 'git' || $cv === null + || ($cv !== $requested && !str_starts_with($cv, $requested . '.'))) { + $resolved = null; + $candidates = glob(DOWNLOAD_PATH . '/php-' . $requested . '.*.tar.xz') ?: []; + if ($candidates !== []) { + usort($candidates, 'strnatcmp'); + if (preg_match('/^php-([0-9.]+)\.tar\.xz$/', basename(end($candidates)), $vm)) { + $resolved = $vm[1]; + } + } elseif ($this->download) { + $j = @file_get_contents('https://www.php.net/releases/index.php?json&version=' . urlencode($requested)); + $rel = is_string($j) ? json_decode($j, true) : null; + $resolved = is_array($rel) ? ($rel['version'] ?? null) : null; + } else { + throw new WrongUsageException("Requested PHP '{$requested}' but no php-{$requested}.*.tar.xz in downloads/; drop --no-download or run 'bin/spc download php-src --with-php={$requested}' first."); + } + if ($resolved !== null) { + $cf = DOWNLOAD_PATH . '/.cache.json'; + $j = json_decode(@file_get_contents($cf) ?: '{}', true) ?: []; + $tarball = DOWNLOAD_PATH . "/php-{$resolved}.tar.xz"; + $j['php-src']['source'] = [ + 'lock_type' => 'source', 'cache_type' => 'archive', + 'filename' => "php-{$resolved}.tar.xz", + 'extract' => $info['extract'] ?? null, + 'hash' => is_file($tarball) ? sha1_file($tarball) : null, + 'time' => time(), 'version' => $resolved, + 'config' => $info['config'] ?? ['type' => 'php-release', 'domain' => 'https://www.php.net'], + 'downloader' => $info['downloader'] ?? \StaticPHP\Artifact\Downloader\Type\PhpRelease::class, + ]; + $j['php-src']['binary'] ??= []; + file_put_contents($cf, json_encode($j, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)); + ApplicationContext::set(ArtifactCache::class, $cache = new ArtifactCache()); + } + } + } + if (!is_dir($src_dir) || ($info = $cache->getSourceInfo('php-src')) === null) { + return; + } + if (($info['cache_type'] ?? null) === 'git') { + if (!is_dir($src_dir . '/.git')) { + FileSystem::removeDir($src_dir); + } + return; + } + $vh = $src_dir . '/main/php_version.h'; + if (is_file($vh) + && preg_match('/#define\s+PHP_VERSION\s+"([^"]+)"/', file_get_contents($vh), $m) + && $m[1] !== ($info['version'] ?? null) + ) { + FileSystem::removeDir($src_dir); + } + } + + private function kindLabel(Package $package): string + { + return match (true) { + $package instanceof PhpExtensionPackage => 'extension', + $package instanceof TargetPackage => 'target', + $package instanceof LibraryPackage => 'library', + default => 'package', + }; + } + /** * @param Package[] $packages */ @@ -688,7 +761,7 @@ private function handlePhpTargetPackage(TargetPackage $package): void if ($package->getBuildOption('build-all') || $package->getBuildOption('build-frankenphp')) { $frankenphp = PackageLoader::getPackage('frankenphp'); $this->install_packages[$frankenphp->getName()] = $frankenphp; - $this->build_packages[$package->getName()] = $package; + $this->build_packages[$frankenphp->getName()] = $frankenphp; $added = true; } $this->build_packages[$package->getName()] = $package; diff --git a/src/StaticPHP/Package/PhpExtensionPackage.php b/src/StaticPHP/Package/PhpExtensionPackage.php index fef88efd4..8fb63ece2 100644 --- a/src/StaticPHP/Package/PhpExtensionPackage.php +++ b/src/StaticPHP/Package/PhpExtensionPackage.php @@ -12,6 +12,7 @@ use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\ToolchainManager; use StaticPHP\Toolchain\ZigToolchain; +use StaticPHP\Util\FileSystem; use StaticPHP\Util\GlobalEnvManager; use StaticPHP\Util\SPCConfigUtil; @@ -278,10 +279,15 @@ public function getSharedExtensionEnv(): array [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); $preStatic = PHP_OS_FAMILY === 'Darwin' ? '' : '-Wl,--start-group '; $postStatic = PHP_OS_FAMILY === 'Darwin' ? '' : ' -Wl,--end-group '; + // -Wl,-Bsymbolic: bind zend_* refs to the .so's own copies, not via global lookup + $ldflags = (string) $config['ldflags']; + if (PHP_OS_FAMILY !== 'Darwin' && !str_contains($ldflags, '-Wl,-Bsymbolic')) { + $ldflags = clean_spaces($ldflags . ' -Wl,-Bsymbolic'); + } return [ 'CFLAGS' => $config['cflags'], 'CXXFLAGS' => $config['cflags'], - 'LDFLAGS' => $config['ldflags'], + 'LDFLAGS' => $ldflags, 'LIBS' => clean_spaces("{$preStatic} {$staticLibs} {$postStatic} {$sharedLibs}"), 'LD_LIBRARY_PATH' => BUILD_LIB_PATH, ]; @@ -303,6 +309,7 @@ public function phpizeForUnix(array $env, PhpExtensionPackage $package): void public function configureForUnix(array $env, PhpExtensionPackage $package): void { $phpvars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; + // CustomPhpConfigureArg keys are OS names ('Linux'/'Darwin'), not platform strings shell()->cd($package->getSourceDir()) ->setEnv($env) ->exec( @@ -318,11 +325,53 @@ public function configureForUnix(array $env, PhpExtensionPackage $package): void #[Stage] public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBuilder $builder): void { + // phpize Makefile's _SHARED_LIBADD line misses our static archives — splice them in + $package->patchSharedLibAdd(); + $extra_ldflags = (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'); + $makeArgs = $extra_ldflags !== '' ? 'EXTRA_LDFLAGS=' . escapeshellarg($extra_ldflags) : ''; shell()->cd($package->getSourceDir()) ->setEnv($env) ->exec('make clean') - ->exec("make -j{$builder->concurrency}") - ->exec('make install'); + ->exec("make -j{$builder->concurrency} {$makeArgs}") + ->exec("make install {$makeArgs}"); + + // install-modules deref'd libtool's `$ext.so → $ext-X.so` symlink into two regular files; restore the symlink. + if (preg_match('/-release\s+(\S+)/', $extra_ldflags, $m)) { + $name = $package->getExtensionName(); + $unversioned = BUILD_MODULES_PATH . "/{$name}.so"; + $versioned = BUILD_MODULES_PATH . "/{$name}-{$m[1]}.so"; + if (file_exists($versioned) && file_exists($unversioned) && !is_link($unversioned)) { + unlink($unversioned); + symlink(basename($versioned), $unversioned); + } + } + } + + public function patchSharedLibAdd(): void + { + $config = new SPCConfigUtil()->getExtensionConfig($this); + [$staticLibs, $sharedLibs] = $this->splitLibsIntoStaticAndShared($config['libs']); + $lstdcpp = str_contains($sharedLibs, '-l:libstdc++.a') + ? '-l:libstdc++.a' + : (str_contains($sharedLibs, '-lstdc++') ? '-lstdc++' : ''); + + $makefile = $this->getSourceDir() . '/Makefile'; + if (!is_file($makefile)) { + return; + } + $content = (string) file_get_contents($makefile); + if (!preg_match('/^(.*_SHARED_LIBADD\s*=\s*)(.*)$/m', $content, $m)) { + return; + } + $prefix = $m[1]; + $current = trim($m[2]); + $merged = clean_spaces("{$current} {$staticLibs} {$lstdcpp}"); + $merged = deduplicate_flags($merged); + FileSystem::replaceFileRegex( + $makefile, + '/^(.*_SHARED_LIBADD\s*=.*)$/m', + $prefix . $merged + ); } /** @@ -333,14 +382,31 @@ public function makeForUnix(array $env, PhpExtensionPackage $package, PackageBui */ public function buildSharedForUnix(PackageBuilder $builder): void { + // skip virtual addons (arg-type=none + display-name → owning ext); the parent ext built it + $argType = $this->extension_config['arg-type'] ?? null; + $displayName = $this->extension_config['display-name'] ?? null; + if ($argType === 'none' && $displayName !== null && $displayName !== $this->getExtensionName()) { + logger()->info("Skipping virtual extension [{$this->getName()}] — it's part of [{$displayName}]."); + return; + } + + if (!is_dir($this->getSourceDir())) { + throw new ValidationException( + "Extension source directory not found: {$this->getSourceDir()}", + validation_module: "Extension {$this->getName()} source" + ); + } + $env = $this->getSharedExtensionEnv(); $this->runStage([$this, 'phpizeForUnix'], ['env' => $env]); $this->runStage([$this, 'configureForUnix'], ['env' => $env]); $this->runStage([$this, 'makeForUnix'], ['env' => $env]); - // process *.so file - $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() . '.so'; + // libtool's -release X gives $name-X.so as the real file + $soFile = BUILD_MODULES_PATH . '/' . $this->getExtensionName() + . (preg_match('/-release\s+(\S+)/', (string) getenv('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS'), $m) ? "-{$m[1]}" : '') + . '.so'; if (!file_exists($soFile)) { throw new ValidationException("Extension {$this->getExtensionName()} build failed: {$soFile} not found", validation_module: "Extension {$this->getExtensionName()} build"); } diff --git a/src/StaticPHP/Registry/ArtifactLoader.php b/src/StaticPHP/Registry/ArtifactLoader.php index ed4ef1e7c..ecbeccfa5 100644 --- a/src/StaticPHP/Registry/ArtifactLoader.php +++ b/src/StaticPHP/Registry/ArtifactLoader.php @@ -36,6 +36,13 @@ public static function initArtifactInstances(): void public static function getArtifactInstance(string $artifact_name): ?Artifact { self::initArtifactInstances(); + if (!isset(self::$artifacts[$artifact_name])) { + // Artifact may have been registered after initArtifactInstances() ran (e.g., from a vendor registry) + $config = ArtifactConfig::get($artifact_name); + if ($config !== null) { + self::$artifacts[$artifact_name] = new Artifact($artifact_name, $config); + } + } return self::$artifacts[$artifact_name] ?? null; } diff --git a/src/StaticPHP/Registry/PackageLoader.php b/src/StaticPHP/Registry/PackageLoader.php index cf0301c04..4930ac7f9 100644 --- a/src/StaticPHP/Registry/PackageLoader.php +++ b/src/StaticPHP/Registry/PackageLoader.php @@ -367,15 +367,18 @@ public static function getAllAnnotations(): array public static function getBeforeStageCallbacks(string $package_name, string $stage): iterable { - // match condition + // match condition; '*' is a wildcard that fires for every package's stage $installer = ApplicationContext::get(PackageInstaller::class); - $stages = self::$before_stages[$package_name][$stage] ?? []; + $stages = array_merge( + self::$before_stages[$package_name][$stage] ?? [], + $package_name === '*' ? [] : (self::$before_stages['*'][$stage] ?? []), + ); foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) { if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } foreach ($conditionals as $class) { - if (!ApplicationContext::has($class)) { + if (ApplicationContext::tryGet($class) === null) { continue 2; } } @@ -385,16 +388,19 @@ public static function getBeforeStageCallbacks(string $package_name, string $sta public static function getAfterStageCallbacks(string $package_name, string $stage): array { - // match condition + // match condition; '*' is a wildcard that fires for every package's stage $installer = ApplicationContext::get(PackageInstaller::class); - $stages = self::$after_stages[$package_name][$stage] ?? []; + $stages = array_merge( + self::$after_stages[$package_name][$stage] ?? [], + $package_name === '*' ? [] : (self::$after_stages['*'][$stage] ?? []), + ); $result = []; foreach ($stages as [$callback, $only_when_package_resolved, $conditionals]) { if ($only_when_package_resolved !== null && !$installer->isPackageResolved($only_when_package_resolved)) { continue; } foreach ($conditionals as $class) { - if (!ApplicationContext::has($class)) { + if (ApplicationContext::tryGet($class) === null) { continue 2; } } @@ -425,6 +431,20 @@ public static function checkLoadedStageEvents(): void { foreach (['BeforeStage' => self::$before_stages, 'AfterStage' => self::$after_stages] as $event_name => $ev_all) { foreach ($ev_all as $package_name => $stages) { + // wildcard hooks fire for every package's stage; nothing to validate against + if ($package_name === '*') { + foreach ($stages as $stage_name => $before_events) { + foreach ($before_events as [$event_callable, $only_when_package_resolved, $conditionals]) { + if ($only_when_package_resolved !== null && !self::hasPackage($only_when_package_resolved)) { + throw new RegistryException("{$event_name} event for wildcard [*] stage [{$stage_name}] has unknown only_when_package_resolved package [{$only_when_package_resolved}]."); + } + if (!is_callable($event_callable)) { + throw new RegistryException("{$event_name} event for wildcard [*] stage [{$stage_name}] has invalid callable."); + } + } + } + continue; + } // check package exists if (!self::hasPackage($package_name)) { throw new RegistryException( diff --git a/src/StaticPHP/Registry/Registry.php b/src/StaticPHP/Registry/Registry.php index 8f206c300..2cd119ec1 100644 --- a/src/StaticPHP/Registry/Registry.php +++ b/src/StaticPHP/Registry/Registry.php @@ -89,14 +89,19 @@ public static function loadRegistry(string $registry_file, bool $auto_require = self::$current_registry_name = $registry_name; try { - // Load composer autoload if specified (for external registries with their own dependencies) + // resolve autoload manually — path-repo installs have no vendor/, FileSystem::fullpath would throw if (isset($data['autoload']) && is_string($data['autoload'])) { - $autoload_path = FileSystem::fullpath($data['autoload'], dirname($registry_file)); + $base = dirname($registry_file); + $autoload_path = FileSystem::isRelativePath($data['autoload']) + ? rtrim($base, '/') . DIRECTORY_SEPARATOR . $data['autoload'] + : $data['autoload']; if (file_exists($autoload_path)) { logger()->debug("Loading external autoload from: {$autoload_path}"); require_once $autoload_path; + } elseif (str_contains(rtrim(FileSystem::convertPath($base), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR, DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR)) { + logger()->debug("Registry autoload not present, relying on consumer autoloader: {$autoload_path}"); } else { - logger()->warning("Autoload file not found: {$autoload_path}"); + throw new RegistryException("Path does not exist: {$autoload_path}"); } } diff --git a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php index c59859cf4..b206fee19 100644 --- a/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixAutoconfExecutor.php @@ -11,6 +11,7 @@ use StaticPHP\Package\PackageBuilder; use StaticPHP\Package\PackageInstaller; use StaticPHP\Runtime\Shell\UnixShell; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Util\InteractiveTerm; use ZM\Logger\ConsoleColor; @@ -149,13 +150,17 @@ public function appendEnv(array $env): static */ private function getDefaultConfigureArgs(): array { - return [ + $args = [ '--disable-shared', '--enable-static', "--prefix={$this->package->getBuildRootPath()}", '--with-pic', '--enable-pic', ]; + if ($host_triple = SystemTarget::getAutoconfHostTriple()) { + $args[] = "--host={$host_triple}"; + } + return $args; } /** diff --git a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php index 82fa468ae..9fc008408 100644 --- a/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php +++ b/src/StaticPHP/Runtime/Executor/UnixCMakeExecutor.php @@ -302,9 +302,12 @@ private function makeCmakeToolchainFile(): string set(CMAKE_C_STANDARD_INCLUDE_DIRECTORIES "{$include}") set(CMAKE_CXX_STANDARD_INCLUDE_DIRECTORIES "{$include}") CMAKE; - // Whoops, linux may need CMAKE_AR sometimes + // pin AR/RANLIB so cmake uses zig-ar/zig-ranlib instead of system /usr/bin/ranlib (zig archives need it) if (PHP_OS_FAMILY === 'Linux') { - $toolchain .= "\nSET(CMAKE_AR \"ar\")"; + $ar = getenv('SPC_DEFAULT_AR') ?: getenv('AR') ?: 'ar'; + $ranlib = getenv('SPC_DEFAULT_RANLIB') ?: (getenv('RANLIB') ?: 'ranlib'); + $toolchain .= "\nSET(CMAKE_AR \"{$ar}\")"; + $toolchain .= "\nSET(CMAKE_RANLIB \"{$ranlib}\")"; } FileSystem::writeFile(SOURCE_PATH . '/toolchain.cmake', $toolchain); return $created = realpath(SOURCE_PATH . '/toolchain.cmake'); diff --git a/src/StaticPHP/Runtime/SystemTarget.php b/src/StaticPHP/Runtime/SystemTarget.php index 489f84384..b968dee81 100644 --- a/src/StaticPHP/Runtime/SystemTarget.php +++ b/src/StaticPHP/Runtime/SystemTarget.php @@ -127,4 +127,60 @@ public static function isUnix(): bool { return in_array(self::getTargetOS(), ['Linux', 'Darwin', 'BSD']); } + + /** + * Returns the canonical target triple (arch-os-abi) for per-target build + * artifacts. Always returns a non-null triple, falling back to a host-derived + * triple when SPC_TARGET is unset or names 'native'. + * Strips libc version suffix (-gnu.2.17 → -gnu) and trailing flags (' -dynamic'). + */ + public static function getCanonicalTriple(): string + { + $target = (string) getenv('SPC_TARGET'); + if ($target !== '' && !str_contains($target, 'native')) { + $cleaned = (string) preg_replace('/(-gnu|-musl)\.[\d.]+/', '$1', $target); + $cleaned = preg_split('/\s+/', trim($cleaned))[0] ?? ''; + if ($cleaned !== '') { + return $cleaned; + } + } + $arch = self::getTargetArch(); + return match (self::getTargetOS()) { + 'Linux' => $arch . '-linux-' . (self::getLibc() === 'musl' ? 'musl' : 'gnu'), + 'Darwin' => $arch . '-macos-none', + 'Windows' => $arch . '-windows-gnu', + default => $arch . '-unknown-unknown', + }; + } + + /** + * Returns a GNU host triple for autoconf --host= when SPC_TARGET names an + * architecture different from the build host (true cross-compile). + * Returns null for same-arch builds. + * Strips libc version suffix (-gnu.2.17 → -gnu) and trailing flags (e.g. ' -dynamic'). + */ + public static function getAutoconfHostTriple(): ?string + { + $target = (string) getenv('SPC_TARGET'); + if ($target === '' || str_contains($target, 'native')) { + return null; + } + $cleaned = preg_split('/\s+/', trim((string) preg_replace('/(-gnu|-musl)\.[\d.]+/', '$1', $target)))[0]; + if ($cleaned === '') { + return null; + } + // Only emit --host for true cross-arch builds; same-arch (incl. cross-libc) lets autoconf detect. + $target_arch_token = explode('-', $cleaned)[0]; + $arch_aliases = [ + 'x86_64' => ['x86_64', 'amd64'], + 'aarch64' => ['aarch64', 'arm64'], + 'arm' => ['arm', 'armv6', 'armv7', 'armhf', 'armel'], + 'i386' => ['i386', 'i486', 'i586', 'i686'], + ]; + $host_arch = GNU_ARCH; + if (array_any($arch_aliases, fn ($aliases) => in_array($target_arch_token, $aliases, true) && in_array($host_arch, $aliases, true))) { + return null; + } + return $cleaned; + } } diff --git a/src/StaticPHP/Toolchain/ClangBrewToolchain.php b/src/StaticPHP/Toolchain/ClangBrewToolchain.php index 5d8963ef4..232609548 100644 --- a/src/StaticPHP/Toolchain/ClangBrewToolchain.php +++ b/src/StaticPHP/Toolchain/ClangBrewToolchain.php @@ -15,6 +15,7 @@ public function initEnv(): void GlobalEnvManager::putenv("SPC_DEFAULT_CC={$homebrew_prefix}/opt/llvm/bin/clang"); GlobalEnvManager::putenv("SPC_DEFAULT_CXX={$homebrew_prefix}/opt/llvm/bin/clang++"); GlobalEnvManager::putenv("SPC_DEFAULT_AR={$homebrew_prefix}/opt/llvm/bin/llvm-ar"); + GlobalEnvManager::putenv("SPC_DEFAULT_RANLIB={$homebrew_prefix}/opt/llvm/bin/llvm-ranlib"); GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); GlobalEnvManager::addPathIfNotExists("{$homebrew_prefix}/opt/llvm/bin"); } diff --git a/src/StaticPHP/Toolchain/ClangNativeToolchain.php b/src/StaticPHP/Toolchain/ClangNativeToolchain.php index 27ae2b65b..7602c3ed5 100644 --- a/src/StaticPHP/Toolchain/ClangNativeToolchain.php +++ b/src/StaticPHP/Toolchain/ClangNativeToolchain.php @@ -21,6 +21,7 @@ public function initEnv(): void GlobalEnvManager::putenv('SPC_DEFAULT_CC=clang'); GlobalEnvManager::putenv('SPC_DEFAULT_CXX=clang++'); GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=ranlib'); GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); } diff --git a/src/StaticPHP/Toolchain/GccNativeToolchain.php b/src/StaticPHP/Toolchain/GccNativeToolchain.php index 7c339e69e..9aecb7e05 100644 --- a/src/StaticPHP/Toolchain/GccNativeToolchain.php +++ b/src/StaticPHP/Toolchain/GccNativeToolchain.php @@ -18,6 +18,7 @@ public function initEnv(): void GlobalEnvManager::putenv('SPC_DEFAULT_CC=gcc'); GlobalEnvManager::putenv('SPC_DEFAULT_CXX=g++'); GlobalEnvManager::putenv('SPC_DEFAULT_AR=ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=ranlib'); GlobalEnvManager::putenv('SPC_DEFAULT_LD=ld'); } diff --git a/src/StaticPHP/Toolchain/ZigToolchain.php b/src/StaticPHP/Toolchain/ZigToolchain.php index 36e42d040..83e8a4fe7 100644 --- a/src/StaticPHP/Toolchain/ZigToolchain.php +++ b/src/StaticPHP/Toolchain/ZigToolchain.php @@ -4,45 +4,38 @@ namespace StaticPHP\Toolchain; +use Package\Artifact\llvm_compiler_rt; +use StaticPHP\DI\ApplicationContext; +use StaticPHP\Package\PackageBuilder; +use StaticPHP\Package\PackageInstaller; +use StaticPHP\Runtime\SystemTarget; use StaticPHP\Toolchain\Interface\UnixToolchainInterface; use StaticPHP\Util\GlobalEnvManager; +use StaticPHP\Util\InteractiveTerm; use StaticPHP\Util\System\LinuxUtil; +use ZM\Logger\ConsoleColor; class ZigToolchain implements UnixToolchainInterface { + private static bool $afterInitDone = false; + public function initEnv(): void { // Set environment variables for zig toolchain GlobalEnvManager::putenv('SPC_DEFAULT_CC=zig-cc'); GlobalEnvManager::putenv('SPC_DEFAULT_CXX=zig-c++'); GlobalEnvManager::putenv('SPC_DEFAULT_AR=zig-ar'); + GlobalEnvManager::putenv('SPC_DEFAULT_RANLIB=zig-ranlib'); GlobalEnvManager::putenv('SPC_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)); + GlobalEnvManager::addPathIfNotExists($this->getPath()); } public function afterInit(): void { - GlobalEnvManager::addPathIfNotExists($this->getPath()); + if (self::$afterInitDone) { + return; + } + self::$afterInitDone = true; f_passthru('ulimit -n 2048'); // zig opens extra file descriptors, so when a lot of extensions are built statically, 1024 is not enough $cflags = getenv('SPC_DEFAULT_CFLAGS') ?: ''; $cxxflags = getenv('SPC_DEFAULT_CXXFLAGS') ?: ''; @@ -54,7 +47,8 @@ public function afterInit(): void GlobalEnvManager::putenv("SPC_DEFAULT_CXXFLAGS={$cxxflags}"); GlobalEnvManager::putenv("SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS={$extraCflags}"); GlobalEnvManager::putenv('RANLIB=zig-ranlib'); - GlobalEnvManager::putenv('OBJCOPY=zig-objcopy'); + GlobalEnvManager::putenv('SPC_COMPILER_RT_DIR=' . PKG_ROOT_PATH . '/zig/lib/' . SystemTarget::getCanonicalTriple()); + GlobalEnvManager::putenv('OBJCOPY=' . PKG_ROOT_PATH . '/llvm-tools/bin/llvm-objcopy'); $extra_libs = getenv('SPC_EXTRA_LIBS') ?: ''; if (!str_contains($extra_libs, '-lunwind')) { // Add unwind library if not already present @@ -70,6 +64,8 @@ public function afterInit(): void // zig-cc/clang treats strlcpy/strlcat as compiler builtins, so configure link tests pass (HAVE_STRLCPY=1) $extra_vars = getenv('SPC_EXTRA_PHP_VARS') ?: ''; GlobalEnvManager::putenv("SPC_EXTRA_PHP_VARS=ac_cv_func_strlcpy=no ac_cv_func_strlcat=no {$extra_vars}"); + + $this->ensureCompilerRt(); } public function getCompilerInfo(): ?string @@ -105,6 +101,48 @@ public function isStatic(): bool return false; } + private function ensureCompilerRt(): void + { + $rt = new llvm_compiler_rt(); + $triple = SystemTarget::getCanonicalTriple(); + $libDir = PKG_ROOT_PATH . '/zig/lib/' . $triple; + if ($rt->isBuilt($libDir)) { + return; + } + if (!is_dir(SOURCE_PATH . '/llvm-compiler-rt/lib/profile')) { + // Source not yet downloaded; install via nested PackageInstaller. The recursion guard + // on afterInit prevents the nested run from re-entering this method. Save the outer + // installer/builder in the container so executors keep seeing the outer one after. + // The PackageInstaller surfaces its own spinner for the install; AfterBinaryExtract + // builds for the current triple, so we're done after run(). + $outerInstaller = ApplicationContext::tryGet(PackageInstaller::class); + $outerBuilder = ApplicationContext::tryGet(PackageBuilder::class); + try { + new PackageInstaller() + ->addInstallPackage('llvm-compiler-rt') + ->run(true); + } finally { + if ($outerInstaller !== null) { + ApplicationContext::set(PackageInstaller::class, $outerInstaller); + } + if ($outerBuilder !== null) { + ApplicationContext::set(PackageBuilder::class, $outerBuilder); + } + } + return; + } + // Source already extracted from a previous run on a different triple; rebuild here with our + // own progress spinner since we're outside the PackageInstaller flow. + InteractiveTerm::indicateProgress('Building llvm-compiler-rt for ' . ConsoleColor::yellow($triple)); + try { + $rt->buildForTriple(); + } catch (\Throwable $e) { + InteractiveTerm::finish('Build llvm-compiler-rt for ' . ConsoleColor::red($triple) . ' failed', false); + throw $e; + } + InteractiveTerm::finish('Built llvm-compiler-rt for ' . ConsoleColor::green($triple)); + } + private function getPath(): string { return PKG_ROOT_PATH . '/zig'; diff --git a/src/StaticPHP/Util/FileSystem.php b/src/StaticPHP/Util/FileSystem.php index 0869bae2f..28a4facd7 100644 --- a/src/StaticPHP/Util/FileSystem.php +++ b/src/StaticPHP/Util/FileSystem.php @@ -244,6 +244,7 @@ public static function scanDirFiles(string $dir, bool $recursive = true, bool|st */ public static function getClassesPsr4(string $dir, string $base_namespace, mixed $rule = null, bool|string $return_path_value = false, bool $auto_require = false): array { + $base_namespace = rtrim($base_namespace, '\\'); $classes = []; $files = FileSystem::scanDirFiles($dir, true, true); if ($files === false) { diff --git a/src/StaticPHP/Util/InteractiveTerm.php b/src/StaticPHP/Util/InteractiveTerm.php index 0570f31c6..1842cce4c 100644 --- a/src/StaticPHP/Util/InteractiveTerm.php +++ b/src/StaticPHP/Util/InteractiveTerm.php @@ -5,6 +5,7 @@ namespace StaticPHP\Util; use StaticPHP\DI\ApplicationContext; +use Symfony\Component\Console\Helper\Helper; use Symfony\Component\Console\Helper\ProgressIndicator; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; @@ -27,14 +28,19 @@ public static function notice(string $message, bool $indent = false): void } } - public static function success(string $message, bool $indent = false): void + public static function success(string $message, bool $indent = false, ?float $start_time = null): void { $no_ansi = ApplicationContext::get(InputInterface::class)?->getOption('no-ansi') ?? false; $output = ApplicationContext::get(OutputInterface::class) ?? new ConsoleOutput(); + if ($start_time !== null) { + $message .= ' (' . Helper::formatTime(microtime(true) - $start_time) . ')'; + } if ($output->isVerbose()) { logger()->info(strip_ansi_colors($message)); } else { - $output->writeln(($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); + // wipe the current indicator line so our persistent ✔ line doesn't get appended to a spinner + $clear = (self::$indicator !== null && $output->isDecorated() && !$no_ansi) ? "\x0D\x1B[2K" : ''; + $output->writeln($clear . ($no_ansi ? 'strip_ansi_colors' : 'strval')(ConsoleColor::green(($indent ? ' ' : '') . '✔ ') . $message)); logger()->debug(strip_ansi_colors($message)); } } diff --git a/src/StaticPHP/Util/Pgo/PgoContext.php b/src/StaticPHP/Util/Pgo/PgoContext.php new file mode 100644 index 000000000..80a8464ab --- /dev/null +++ b/src/StaticPHP/Util/Pgo/PgoContext.php @@ -0,0 +1,265 @@ + + */ + public const array TRAINABLE = [ + 'cli' => 'build-cli', + 'micro' => 'build-micro', + 'cgi' => 'build-cgi', + 'fpm' => 'build-fpm', + 'embed' => 'build-embed', + 'frankenphp' => 'build-frankenphp', + ]; + + public const array SHUTDOWN_PATCHES = [ + 'php-src' => 'spc_pgo_flush_php_main.patch', + 'frankenphp' => 'spc_pgo_flush_frankenphp.patch', + ]; + + /** @var list */ + private array $trainableSapis = []; + + public function __construct( + public readonly string $mode, + public readonly string $profileRoot, + ) { + if (!in_array($mode, [self::MODE_INSTRUMENT, self::MODE_CS_INSTRUMENT, self::MODE_USE], true)) { + throw new WrongUsageException("PgoContext: unknown mode '{$mode}'"); + } + } + + public static function registerOptions(BaseCommand $cmd): void + { + $cmd->addOption('pgi', null, null, 'PGO instrument pass: build with -fprofile-generate so the resulting binary writes .profraw on shutdown.'); + $cmd->addOption('cs-pgi', null, null, 'PGO context-sensitive instrument pass: -fprofile-use= + -fcs-profile-generate. Requires a prior --pgi/--pgo cycle.'); + $cmd->addOption('pgo', null, null, 'PGO use pass: merge the collected .profraw into .profdata, then rebuild with -fprofile-use.'); + } + + /** + * @param array $sapis + * @param array $build_options + */ + public static function tryFromInput(InputInterface $input, array $sapis, array &$build_options): ?self + { + $modes = array_filter(['pgi', 'cs-pgi', 'pgo'], fn ($m) => (bool) $input->getOption($m)); + if (count($modes) > 1) { + throw new WrongUsageException('--pgi, --cs-pgi, and --pgo are mutually exclusive'); + } + $picked = array_values($modes)[0] ?? null; + if ($picked === null) { + return null; + } + $mode = match ($picked) { + 'pgi' => self::MODE_INSTRUMENT, + 'cs-pgi' => self::MODE_CS_INSTRUMENT, + 'pgo' => self::MODE_USE, + }; + $ctx = new self($mode, BUILD_ROOT_PATH . '/pgo-data'); + $ctx->setTrainableSapis($sapis); + + match ($mode) { + self::MODE_INSTRUMENT => $ctx->setupInstrument(), + self::MODE_CS_INSTRUMENT => $ctx->setupCsInstrument(), + self::MODE_USE => $ctx->mergeProfiles(), + }; + + if ($ctx->isInstrument() || $ctx->isCsInstrument()) { + $build_options['no-strip'] = true; + } + ApplicationContext::set(self::class, $ctx); + return $ctx; + } + + public function isInstrument(): bool + { + return $this->mode === self::MODE_INSTRUMENT; + } + + public function isCsInstrument(): bool + { + return $this->mode === self::MODE_CS_INSTRUMENT; + } + + public function isUse(): bool + { + return $this->mode === self::MODE_USE; + } + + /** + * @param list $sapis + */ + public function setTrainableSapis(array $sapis): void + { + $resolved = []; + foreach ($sapis as $sapi) { + $r = $this->resolveSapi($sapi); + if (!in_array($r, $resolved, true)) { + $resolved[] = $r; + } + } + if ($resolved === []) { + throw new WrongUsageException( + 'PGO: no trainable SAPI selected; supply one of ' . implode(', ', array_keys(self::TRAINABLE)) + ); + } + $this->trainableSapis = $resolved; + } + + /** @return list */ + public function trainableSapis(): array + { + return $this->trainableSapis; + } + + /** + * Static-embed mode links libphp.a into frankenphp, sharing a single binary + * and profdata. Shared-embed keeps them separate. + */ + public function resolveSapi(string $sapi): string + { + if ($sapi === 'embed' && getenv('SPC_CMD_VAR_PHP_EMBED_TYPE') === 'static') { + return 'frankenphp'; + } + return $sapi; + } + + public function rawDir(string $sapi): string + { + return $this->profileRoot . '/' . $sapi; + } + + public function csRawDir(string $sapi): string + { + return $this->profileRoot . '/cs-' . $sapi; + } + + public function profDataFile(string $sapi): string + { + return $this->profileRoot . '/' . $sapi . '.profdata'; + } + + public function cflagsFor(string $sapi): string + { + $sapi = $this->resolveSapi($sapi); + if ($this->mode === self::MODE_USE && !is_file($this->profDataFile($sapi))) { + return ''; + } + return match ($this->mode) { + 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', + self::MODE_USE => '-fprofile-use=' . $this->profDataFile($sapi) + . ' -Wno-error=profile-instr-unprofiled' + . ' -Wno-error=profile-instr-out-of-date' + . ' -Wno-backend-plugin', + default => throw new WrongUsageException("PgoContext: unreachable mode '{$this->mode}'"), + }; + } + + public function ldflagsFor(string $sapi): string + { + $resolved = $this->resolveSapi($sapi); + $flags = $this->cflagsFor($sapi); + $patterns = ['/\s*-Wno-error=\S+/', '/\s*-Wno-backend-plugin/']; + if ($resolved === 'frankenphp') { + $patterns[] = '/\s*-fprofile-use=\S+/'; + $patterns[] = '/\s*-fcs-profile-generate=\S+/'; + } + return trim((string) preg_replace($patterns, '', $flags)); + } + + public function applyEnvFor(string $sapi): void + { + self::overwritePgoFlags('SPC_CMD_VAR_PHP_MAKE_EXTRA_CFLAGS', $this->cflagsFor($sapi)); + self::overwritePgoFlags('SPC_CMD_VAR_PHP_MAKE_EXTRA_LDFLAGS_PROGRAM', $this->ldflagsFor($sapi)); + } + + public function setupInstrument(): void + { + FileSystem::removeDir($this->profileRoot); + FileSystem::createDir($this->profileRoot); + foreach ($this->trainableSapis as $sapi) { + FileSystem::createDir($this->rawDir($sapi)); + } + } + + public function setupCsInstrument(): void + { + foreach ($this->trainableSapis as $sapi) { + if (!is_file($this->profDataFile($sapi))) { + throw new WrongUsageException( + "PGO --phase=cs-instrument: missing {$sapi}.profdata; run --phase=instrument and --phase=use first" + ); + } + FileSystem::createDir($this->csRawDir($sapi)); + } + } + + public function mergeProfiles(): void + { + foreach ($this->trainableSapis as $sapi) { + $this->mergeSapi($sapi); + } + } + + private function mergeSapi(string $sapi): void + { + $raws = glob($this->rawDir($sapi) . '/*.profraw') ?: []; + $csRaws = glob($this->csRawDir($sapi) . '/*.profraw') ?: []; + if ($raws === [] && $csRaws === []) { + if ($sapi === 'frankenphp') { + logger()->warning( + 'PGO --phase=use: no .profraw for frankenphp (cgo-glue PGO will be skipped); ' . + 'run --phase=instrument, exercise frankenphp longer, then re-run --phase=use' + ); + return; + } + throw new WrongUsageException( + "PGO --phase=use: no .profraw for {$sapi}; run --phase=instrument, exercise the binary, then re-run --phase=use" + ); + } + $out = $this->profDataFile($sapi); + $inputs = array_merge($raws, $csRaws); + $argv = implode(' ', array_map('escapeshellarg', $inputs)); + $profdata = PKG_ROOT_PATH . '/llvm-tools/bin/llvm-profdata'; + shell()->exec(escapeshellarg($profdata) . ' merge --failure-mode=warn -output=' . escapeshellarg($out) . ' ' . $argv); + if (!is_file($out) || filesize($out) === 0) { + throw new WrongUsageException("PGO --phase=use: empty merge output for {$sapi}"); + } + logger()->info("PGO merged {$sapi}: " . filesize($out) . ' bytes'); + } + + private static function overwritePgoFlags(string $var, string $append): void + { + $cur = (string) getenv($var); + $cur = preg_replace('/\s*-f(cs-)?profile-(generate|use)=\S+/', '', $cur) ?? $cur; + $cur = preg_replace('/\s*-fprofile-update=atomic/', '', $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(trim($cur) . ' ' . $append)); + } +} diff --git a/src/StaticPHP/Util/PkgConfigUtil.php b/src/StaticPHP/Util/PkgConfigUtil.php index d5b03757f..ba3026aa7 100644 --- a/src/StaticPHP/Util/PkgConfigUtil.php +++ b/src/StaticPHP/Util/PkgConfigUtil.php @@ -67,7 +67,7 @@ public static function getModuleVersion(string $pkg_config_str): string public static function getCflags(string $pkg_config_str): string { // get other things - $result = self::execWithResult("pkg-config --static --cflags-only-other {$pkg_config_str}"); + $result = self::execWithResult("pkg-config --static --cflags {$pkg_config_str}"); return trim($result); } diff --git a/src/StaticPHP/Util/SPCConfigUtil.php b/src/StaticPHP/Util/SPCConfigUtil.php index 63b0e90ef..fd2460eeb 100644 --- a/src/StaticPHP/Util/SPCConfigUtil.php +++ b/src/StaticPHP/Util/SPCConfigUtil.php @@ -5,8 +5,10 @@ namespace StaticPHP\Util; use StaticPHP\Config\PackageConfig; +use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\WrongUsageException; use StaticPHP\Package\LibraryPackage; +use StaticPHP\Package\PackageInstaller; use StaticPHP\Package\PhpExtensionPackage; use StaticPHP\Runtime\SystemTarget; @@ -118,6 +120,10 @@ public function config(array $packages = [], bool $include_suggests = false): ar * [Helper function] * Get configuration for a specific extension(s) dependencies. * + * Uses the installer's resolved package set as the source of truth — only libraries that + * are actually enabled in this build appear in the result. The resolved set already + * reflects the user's `--with-suggests` choice. + * * @param array|PhpExtensionPackage $extension_packages Extension instance or list * @return array{ * cflags: string, @@ -125,30 +131,30 @@ public function config(array $packages = [], bool $include_suggests = false): ar * libs: string * } */ - public function getExtensionConfig(array|PhpExtensionPackage $extension_packages, bool $include_suggests = false): array + public function getExtensionConfig(array|PhpExtensionPackage $extension_packages): array { if (!is_array($extension_packages)) { $extension_packages = [$extension_packages]; } - return $this->config( - packages: array_map(fn ($y) => $y->getName(), $extension_packages), - include_suggests: $include_suggests, - ); + $names = array_map(fn ($y) => $y->getName(), $extension_packages); + return $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names)); } /** * [Helper function] * Get configuration for a specific library(s) dependencies. * - * @param array|LibraryPackage $lib Library instance or list - * @param bool $include_suggests Whether to include suggested libraries + * Like {@see getExtensionConfig()}, draws from the resolved package set so we never + * link against a library that wasn't built. + * + * @param array|LibraryPackage $lib Library instance or list * @return array{ * cflags: string, * ldflags: string, * libs: string * } */ - public function getLibraryConfig(array|LibraryPackage $lib, bool $include_suggests = false): array + public function getLibraryConfig(array|LibraryPackage $lib): array { if (!is_array($lib)) { $lib = [$lib]; @@ -157,35 +163,31 @@ public function getLibraryConfig(array|LibraryPackage $lib, bool $include_sugges $this->no_php = true; $save_libs_only_deps = $this->libs_only_deps; $this->libs_only_deps = true; - $ret = $this->config( - packages: array_map(fn ($y) => $y->getName(), $lib), - include_suggests: $include_suggests, - ); + $names = array_map(fn ($y) => $y->getName(), $lib); + $ret = $this->configWithResolvedPackages($this->collectEnabledLinkPackages($names)); $this->no_php = $save_no_php; $this->libs_only_deps = $save_libs_only_deps; return $ret; } /** - * Get build configuration for a package and its sub-dependencies within a resolved set. + * Get build configuration for a package's sub-dependencies within a resolved set. * - * This is useful when you need to statically link something against a specific - * library and all its transitive dependencies. It properly handles optional - * dependencies by only including those that were actually resolved. + * Walks both depends and suggests edges — the resolved set is the filter, so anything + * reachable but unbuilt is naturally excluded. No `include_suggests` knob is needed. * * @param string $package_name The package to get config for * @param string[] $resolved_packages The full resolved package list - * @param bool $include_suggests Whether to include resolved suggests * @return array{ * cflags: string, * ldflags: string, * libs: string * } */ - public function getPackageDepsConfig(string $package_name, array $resolved_packages, bool $include_suggests = false): array + public function getPackageDepsConfig(string $package_name, array $resolved_packages): array { // Get sub-dependencies within the resolved set - $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, $include_suggests); + $sub_deps = DependencyResolver::getSubDependencies($package_name, $resolved_packages, include_suggests: true); if (empty($sub_deps)) { return [ @@ -299,6 +301,24 @@ public function getFrameworksString(array $extensions): string return implode(' ', $list); } + /** + * For each input package name, gather its transitive deps within the installer's resolved + * set (walking depends + suggests edges), plus the package itself, deduped and in build order. + * + * @param string[] $package_names Input package names + * @return string[] Resolved packages to link against + */ + private function collectEnabledLinkPackages(array $package_names): array + { + $resolved = array_keys(ApplicationContext::get(PackageInstaller::class)->getResolvedPackages()); + $out = []; + foreach ($package_names as $name) { + $sub = DependencyResolver::getSubDependencies($name, $resolved, include_suggests: true); + $out = [...$out, ...$sub, $name]; + } + return array_values(array_unique($out)); + } + private function hasCpp(array $packages): bool { foreach ($packages as $package) { diff --git a/src/bootstrap.php b/src/bootstrap.php index 4e640fea6..65d8a0274 100644 --- a/src/bootstrap.php +++ b/src/bootstrap.php @@ -60,5 +60,9 @@ // load core registry Registry::loadRegistry(ROOT_DIR . '/spc.registry.yml'); +// in vendor mode, auto-load the local working directory registry if it exists +if (spc_mode(SPC_MODE_VENDOR) && file_exists(WORKING_DIR . '/spc.registry.yml')) { + Registry::loadRegistry(WORKING_DIR . '/spc.registry.yml'); +} // load registries from environment variable SPC_REGISTRIES Registry::loadFromEnvOrOption(); diff --git a/src/globals/functions.php b/src/globals/functions.php index 549ef498d..c824bd3df 100644 --- a/src/globals/functions.php +++ b/src/globals/functions.php @@ -256,10 +256,30 @@ function clean_spaces(string $string): string */ function deduplicate_flags(string $flags): string { - $tokens = preg_split('/\s+/', trim($flags)); + // Flags that take their value as a separate token. + static $paired = [ + '-Xclang', '-Xpreprocessor', '-Xlinker', '-Xassembler', + '-framework', '-arch', '-target', + '-include', '-imacros', '-isystem', '-isysroot', '-iquote', '-idirafter', + '-MT', '-MF', '-MQ', + ]; + + $tokens = preg_split('/\s+/', trim($flags)) ?: []; + + // Group paired flag+value into a single atom before dedup. + $atoms = []; + $n = count($tokens); + for ($i = 0; $i < $n; ++$i) { + if (in_array($tokens[$i], $paired, true) && $i + 1 < $n) { + $atoms[] = $tokens[$i] . ' ' . $tokens[$i + 1]; + ++$i; + } else { + $atoms[] = $tokens[$i]; + } + } // Reverse, unique, reverse back - keeps last occurrence of duplicates - $deduplicated = array_reverse(array_unique(array_reverse($tokens))); + $deduplicated = array_reverse(array_unique(array_reverse($atoms))); return implode(' ', $deduplicated); } diff --git "a/src/globals/patch/spc_fix_avx512_cache_before_80400.patch\342\200\216" b/src/globals/patch/spc_fix_avx512_cache_before_80400.patch similarity index 100% rename from "src/globals/patch/spc_fix_avx512_cache_before_80400.patch\342\200\216" rename to src/globals/patch/spc_fix_avx512_cache_before_80400.patch 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..6bf905bf0 --- /dev/null +++ b/src/globals/patch/spc_pgo_flush_php_main.patch @@ -0,0 +1,12 @@ +--- a/main/main.c ++++ b/main/main.c +@@ -2563,6 +2563,9 @@ + #endif + + zend_observer_shutdown(); ++ ++ { extern int __llvm_profile_write_file(void) __attribute__((weak)); ++ if (__llvm_profile_write_file) __llvm_profile_write_file(); } + } + /* }}} */ + diff --git a/src/globals/scripts/zig-cc.sh b/src/globals/scripts/zig-cc.sh index 56ae95055..743ee0bf7 100755 --- a/src/globals/scripts/zig-cc.sh +++ b/src/globals/scripts/zig-cc.sh @@ -1,9 +1,14 @@ #!/usr/bin/env bash SCRIPT_DIR="$(dirname "${BASH_SOURCE[0]}")" -BUILDROOT_ABS="${BUILD_ROOT_PATH:-$(realpath "$SCRIPT_DIR/../../../buildroot/include" 2>/dev/null || true)}" +BUILDROOT_INC="${BUILD_INCLUDE_PATH:-$SCRIPT_DIR/../../../buildroot/include}" +BUILDROOT_ABS="$(realpath "$BUILDROOT_INC" 2>/dev/null || true)" PARSED_ARGS=() +is_buildroot_inc() { + [[ -n "$BUILDROOT_ABS" && "$1" == "$BUILDROOT_ABS" ]] +} + while [[ $# -gt 0 ]]; do case "$1" in -isystem) @@ -11,13 +16,13 @@ while [[ $# -gt 0 ]]; do ARG="$1" shift ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" - [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG") + is_buildroot_inc "$ARG_ABS" && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem" "$ARG") ;; -isystem*) ARG="${1#-isystem}" shift ARG_ABS="$(realpath "$ARG" 2>/dev/null || true)" - [[ "$ARG_ABS" == "$BUILDROOT_ABS" ]] && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") + is_buildroot_inc "$ARG_ABS" && PARSED_ARGS+=("-I$ARG") || PARSED_ARGS+=("-isystem$ARG") ;; -march=*|-mcpu=*) OPT_NAME="${1%%=*}" @@ -32,6 +37,14 @@ while [[ $# -gt 0 ]]; do PARSED_ARGS+=("${OPT_NAME}=${OPT_VALUE}") shift ;; + -mtune=generic) + PARSED_ARGS+=("-mtune=baseline") + shift + ;; + -Wlogical-op|-Wduplicated-cond|-Wduplicated-branches|-Wno-clobbered|-Wjump-misses-init|-Wformat-truncation|-Warray-bounds=*|-Wimplicit-fallthrough=*) + # GCC-only warning flags that clang/zig doesn't recognize; drop to silence -Wunknown-warning-option noise + shift + ;; *) PARSED_ARGS+=("$1") shift @@ -39,6 +52,26 @@ 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*|-fcs-profile-generate*) NEED_PROFILE_RT=1 ;; + -shared) NEED_CRT=1 ;; + esac +done +[[ "$SPC_COMPILER_EXTRA" == *-fprofile-generate* || "$SPC_COMPILER_EXTRA" == *-fcs-profile-generate* ]] && NEED_PROFILE_RT=1 + +RT_DIR="${SPC_COMPILER_RT_DIR:-}" +if [[ $IS_LINK -eq 1 && $NEED_PROFILE_RT -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/libclang_rt.profile.a" ]]; then + PARSED_ARGS+=("$RT_DIR/libclang_rt.profile.a" "-Wl,-u,__llvm_profile_runtime") +fi +if [[ $IS_LINK -eq 1 && $NEED_CRT -eq 1 && -n "$RT_DIR" && -f "$RT_DIR/clang_rt.crtbegin.o" && -f "$RT_DIR/clang_rt.crtend.o" ]]; then + PARSED_ARGS+=("$RT_DIR/clang_rt.crtbegin.o" "$RT_DIR/clang_rt.crtend.o") +fi + [[ -n "$SPC_TARGET" ]] && TARGET="-target $SPC_TARGET" || TARGET="" if [[ "$SPC_TARGET" =~ \.[0-9]+\.[0-9]+ ]]; then