From f27ec773a1c0adabe084b468258fc6160b1e82ab Mon Sep 17 00:00:00 2001 From: crazywhalecc Date: Mon, 18 May 2026 10:55:39 +0800 Subject: [PATCH] Refactor clang runtime bits support for zig integration --- config/pkg/target/clang-runtime-bits.yml | 4 + src/Package/Artifact/clang_runtime_bits.php | 163 ++++++++++++++++++++ src/Package/Artifact/zig.php | 147 ------------------ src/StaticPHP/Doctor/Item/ZigCheck.php | 30 ++++ 4 files changed, 197 insertions(+), 147 deletions(-) create mode 100644 config/pkg/target/clang-runtime-bits.yml create mode 100644 src/Package/Artifact/clang_runtime_bits.php diff --git a/config/pkg/target/clang-runtime-bits.yml b/config/pkg/target/clang-runtime-bits.yml new file mode 100644 index 00000000..aba94f5f --- /dev/null +++ b/config/pkg/target/clang-runtime-bits.yml @@ -0,0 +1,4 @@ +clang-runtime-bits: + type: target + artifact: + binary: custom diff --git a/src/Package/Artifact/clang_runtime_bits.php b/src/Package/Artifact/clang_runtime_bits.php new file mode 100644 index 00000000..60eb11aa --- /dev/null +++ b/src/Package/Artifact/clang_runtime_bits.php @@ -0,0 +1,163 @@ +detectZigLlvmVersion() + ?? throw new DownloaderException('Could not detect bundled clang version from zig cc --version; ensure zig is installed'); + $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: '{pkg_root_path}/clang-runtime-bits', verified: false, version: $llvmVersion); + } + + #[CustomBinaryCheckUpdate('clang-runtime-bits', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult + { + $llvmVersion = $this->detectZigLlvmVersion() + ?? throw new DownloaderException('Could not detect bundled clang version from zig cc --version; ensure zig is installed'); + return new CheckUpdateResult( + old: $old_version, + new: $llvmVersion, + needUpdate: $old_version === null || $llvmVersion !== $old_version, + ); + } + + #[AfterBinaryExtract('clang-runtime-bits', [ + 'linux-x86_64', + 'linux-aarch64', + ])] + public function postExtract(string $target_path): void + { + $zig = PKG_ROOT_PATH . '/zig/zig'; + $libDir = PKG_ROOT_PATH . '/zig/lib'; + $profileLib = "{$libDir}/libclang_rt.profile.a"; + $crtBegin = "{$libDir}/clang_rt.crtbegin.o"; + $crtEnd = "{$libDir}/clang_rt.crtend.o"; + if (file_exists($profileLib) && file_exists($crtBegin) && file_exists($crtEnd)) { + return; + } + + $llvmVersion = $this->detectZigLlvmVersion(); + if ($llvmVersion === null) { + logger()->warning('[clang-runtime-bits] could not detect bundled clang version; skipping runtime bit build (PGO + shared libs without __dso_handle will fail to link)'); + return; + } + logger()->info("Building clang runtime bits for LLVM {$llvmVersion} (zig's bundled clang)"); + + f_mkdir($libDir, recursive: true); + if (!file_exists($profileLib)) { + $this->buildProfileRuntime($zig, $target_path, $profileLib); + } + if (!file_exists($crtBegin) || !file_exists($crtEnd)) { + $this->buildCrtObjects($zig, $target_path, $crtBegin, $crtEnd); + } + } + + private function buildProfileRuntime(string $zig, string $srcRoot, string $libPath): void + { + $profileSrc = "{$srcRoot}/lib/profile"; + $profileInc = "{$srcRoot}/include"; + if (!is_dir($profileSrc)) { + logger()->warning("[clang-runtime-bits] profile src dir missing at {$profileSrc} — PGO will not work"); + return; + } + $sources = array_merge( + glob("{$profileSrc}/*.c") ?: [], + glob("{$profileSrc}/*.cpp") ?: [] + ); + // Keep Linux-only compilation units; the others bring in OS-specific headers + // we can't satisfy without their respective SDKs. + $skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap']; + $sources = array_filter($sources, function ($f) use ($skip) { + foreach ($skip as $s) { + if (str_contains($f, $s)) { + return false; + } + } + return true; + }); + + $objDir = "{$srcRoot}/obj-profile"; + f_mkdir($objDir, recursive: true); + $cflags = '-c -O2 -fPIC -fvisibility=hidden ' . + '-I' . escapeshellarg($profileInc) . ' ' . + '-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1'; + $objs = []; + foreach ($sources as $src) { + $obj = $objDir . '/' . pathinfo($src, PATHINFO_FILENAME) . '.o'; + $cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($obj) . ' ' . escapeshellarg($src) . ' 2>&1'; + if (!$this->runZigCmd($cmd, $obj, "failed to compile {$src}")) { + return; + } + $objs[] = $obj; + } + $arCmd = escapeshellarg($zig) . ' ar rcs ' . escapeshellarg($libPath) . ' ' . implode(' ', array_map('escapeshellarg', $objs)) . ' 2>&1'; + if (!$this->runZigCmd($arCmd, $libPath, 'zig ar failed')) { + return; + } + logger()->info('[clang-runtime-bits] libclang_rt.profile.a installed (' . filesize($libPath) . ' bytes)'); + } + + private function buildCrtObjects(string $zig, string $srcRoot, string $crtBegin, string $crtEnd): void + { + $beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c"; + $endSrc = "{$srcRoot}/lib/builtins/crtend.c"; + if (!is_file($beginSrc) || !is_file($endSrc)) { + logger()->error("[clang-runtime-bits] crtbegin/crtend source missing under {$srcRoot}/lib/builtins — shared libs will lack __dso_handle"); + return; + } + $cflags = '-c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY'; + foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) { + $cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($dst) . ' ' . escapeshellarg($src) . ' 2>&1'; + if (!$this->runZigCmd($cmd, $dst, "failed to compile {$src}")) { + return; + } + } + logger()->info('[clang-runtime-bits] clang_rt.crtbegin.o + clang_rt.crtend.o installed (' . filesize($crtBegin) . ' + ' . filesize($crtEnd) . ' bytes)'); + } + + private function runZigCmd(string $cmd, string $dst, string $errPrefix): bool + { + exec($cmd, $out, $rc); + if ($rc !== 0 || !is_file($dst)) { + logger()->warning("[clang-runtime-bits] {$errPrefix}: " . implode("\n", $out)); + return false; + } + return true; + } + + private function detectZigLlvmVersion(): ?string + { + $zig = PKG_ROOT_PATH . '/zig/zig'; + if (!is_file($zig)) { + return null; + } + $verLine = trim((string) shell_exec(escapeshellarg($zig) . ' cc --version 2>/dev/null')); + if (!preg_match('/clang version (\d+\.\d+\.\d+)/', $verLine, $m)) { + return null; + } + return $m[1]; + } +} diff --git a/src/Package/Artifact/zig.php b/src/Package/Artifact/zig.php index 775c466a..18a06c4d 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -12,7 +12,6 @@ use StaticPHP\Attribute\Artifact\CustomBinary; use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate; use StaticPHP\Exception\DownloaderException; use StaticPHP\Runtime\SystemTarget; -use StaticPHP\Util\FileSystem; class zig { @@ -132,151 +131,5 @@ class zig chmod("{$target_path}/zig-ld.lld", 0755); chmod("{$target_path}/zig-ranlib", 0755); chmod("{$target_path}/zig-objcopy", 0755); - - // Build the clang runtime bits zig 0.15+ doesn't ship: profile runtime - // (so -fprofile-generate actually emits .profraw) and crtbegin/crtend - // (so shared libs get __dso_handle and the __cxa_finalize atexit hook). - // These get auto-linked by the zig-cc wrapper when the right flags fly past. - $this->buildClangRuntimeBits($target_path); - } - - /** - * Detect the bundled clang version and build the missing clang runtime - * archives into `/lib/`. Compiles from a 2 MB compiler-rt-.src - * tarball — far cheaper than fetching the 2 GB prebuilt LLVM tarball. - */ - private function buildClangRuntimeBits(string $zig_bin_dir): void - { - if (PHP_OS_FAMILY !== 'Linux') { - return; - } - $libDir = "{$zig_bin_dir}/lib"; - $profileLib = "{$libDir}/libclang_rt.profile.a"; - $crtBegin = "{$libDir}/clang_rt.crtbegin.o"; - $crtEnd = "{$libDir}/clang_rt.crtend.o"; - if (file_exists($profileLib) && file_exists($crtBegin) && file_exists($crtEnd)) { - return; - } - - $zig = "{$zig_bin_dir}/zig"; - $verLine = trim((string) shell_exec(escapeshellarg($zig) . ' cc --version 2>/dev/null')); - if (!preg_match('/clang version (\d+\.\d+\.\d+)/', $verLine, $m)) { - logger()->warning('[zig] could not detect bundled clang version; skipping runtime bit build (PGO + shared libs without __dso_handle will fail to link)'); - return; - } - $llvmVersion = $m[1]; - logger()->info("Building clang runtime bits for LLVM {$llvmVersion} (zig's bundled clang)"); - - $srcRoot = $this->fetchCompilerRtSource($llvmVersion); - if ($srcRoot === null) { - return; - } - - f_mkdir($libDir, recursive: true); - if (!file_exists($profileLib)) { - $this->buildProfileRuntime($zig, $srcRoot, $profileLib); - } - if (!file_exists($crtBegin) || !file_exists($crtEnd)) { - $this->buildCrtObjects($zig, $srcRoot, $crtBegin, $crtEnd); - } - FileSystem::removeDir($srcRoot); - } - - private function fetchCompilerRtSource(string $llvmVersion): ?string - { - $tarball = "compiler-rt-{$llvmVersion}.src.tar.xz"; - $url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}"; - $tarballPath = DOWNLOAD_PATH . '/' . $tarball; - if (!file_exists($tarballPath)) { - try { - default_shell()->executeCurlDownload($url, $tarballPath); - } catch (\Throwable $e) { - logger()->warning("[zig] failed to download {$tarball}: {$e->getMessage()}"); - return null; - } - } - $srcRoot = PKG_ROOT_PATH . "/compiler-rt-src-{$llvmVersion}"; - FileSystem::removeDir($srcRoot); - FileSystem::createDir($srcRoot); - try { - default_shell()->executeTarExtract($tarballPath, $srcRoot, 'xz'); - } catch (\Throwable $e) { - logger()->warning("[zig] failed to extract {$tarball}: {$e->getMessage()}"); - return null; - } - return $srcRoot; - } - - private function buildProfileRuntime(string $zig, string $srcRoot, string $libPath): void - { - $profileSrc = "{$srcRoot}/lib/profile"; - $profileInc = "{$srcRoot}/include"; - if (!is_dir($profileSrc)) { - logger()->warning("[zig] profile src dir missing at {$profileSrc} — PGO will not work"); - return; - } - $sources = array_merge( - glob("{$profileSrc}/*.c") ?: [], - glob("{$profileSrc}/*.cpp") ?: [] - ); - // Keep Linux-only compilation units; the others bring in OS-specific headers - // we can't satisfy without their respective SDKs. - $skip = ['/PlatformAIX', '/PlatformDarwin', '/PlatformFuchsia', '/PlatformOther', '/PlatformWindows', '/WindowsMMap']; - $sources = array_filter($sources, function ($f) use ($skip) { - foreach ($skip as $s) { - if (str_contains($f, $s)) { - return false; - } - } - return true; - }); - - $objDir = "{$srcRoot}/obj-profile"; - f_mkdir($objDir, recursive: true); - $cflags = '-c -O2 -fPIC -fvisibility=hidden ' . - '-I' . escapeshellarg($profileInc) . ' ' . - '-DCOMPILER_RT_HAS_ATOMICS=1 -DCOMPILER_RT_HAS_FCNTL_LCK=1 -DCOMPILER_RT_HAS_UNAME=1'; - $objs = []; - foreach ($sources as $src) { - $obj = $objDir . '/' . pathinfo($src, PATHINFO_FILENAME) . '.o'; - $cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($obj) . ' ' . escapeshellarg($src) . ' 2>&1'; - if (!$this->runZigCmd($cmd, $obj, "failed to compile {$src}")) { - return; - } - $objs[] = $obj; - } - $arCmd = escapeshellarg($zig) . ' ar rcs ' . escapeshellarg($libPath) . ' ' . implode(' ', array_map('escapeshellarg', $objs)) . ' 2>&1'; - if (!$this->runZigCmd($arCmd, $libPath, 'zig ar failed')) { - return; - } - logger()->info('[zig] libclang_rt.profile.a installed (' . filesize($libPath) . ' bytes)'); - } - - private function buildCrtObjects(string $zig, string $srcRoot, string $crtBegin, string $crtEnd): void - { - $beginSrc = "{$srcRoot}/lib/builtins/crtbegin.c"; - $endSrc = "{$srcRoot}/lib/builtins/crtend.c"; - if (!is_file($beginSrc) || !is_file($endSrc)) { - logger()->error("[zig] crtbegin/crtend source missing under {$srcRoot}/lib/builtins — shared libs will lack __dso_handle"); - return; - } - $cflags = '-c -O2 -fPIC -fvisibility=hidden -DCRT_HAS_INITFINI_ARRAY'; - foreach ([[$beginSrc, $crtBegin], [$endSrc, $crtEnd]] as [$src, $dst]) { - $cmd = escapeshellarg($zig) . ' cc ' . $cflags . ' -o ' . escapeshellarg($dst) . ' ' . escapeshellarg($src) . ' 2>&1'; - if (!$this->runZigCmd($cmd, $dst, "failed to compile {$src}")) { - return; - } - } - logger()->info('[zig] clang_rt.crtbegin.o + clang_rt.crtend.o installed (' . filesize($crtBegin) . ' + ' . filesize($crtEnd) . ' bytes)'); - } - - private function runZigCmd(string $cmd, string $dst, string $errPrefix): bool - { - exec($cmd, $out, $rc); - if ($rc !== 0 || !is_file($dst)) { - logger()->warning("[zig] {$errPrefix}: " . implode("\n", $out)); - return false; - } - return true; } } diff --git a/src/StaticPHP/Doctor/Item/ZigCheck.php b/src/StaticPHP/Doctor/Item/ZigCheck.php index ec140fd7..93bb6749 100644 --- a/src/StaticPHP/Doctor/Item/ZigCheck.php +++ b/src/StaticPHP/Doctor/Item/ZigCheck.php @@ -39,4 +39,34 @@ class ZigCheck $installer->run(true); return $installer->isPackageInstalled('zig'); } + + /** @noinspection PhpUnused */ + #[CheckItem('if clang runtime bits are built', limit_os: 'Linux', level: 799)] + public function checkClangRuntimeBits(): ?CheckResult + { + // Skip if zig is not installed yet (zig check runs at level 800) + if (!new PackageInstaller()->addInstallPackage('zig')->isPackageInstalled('zig')) { + return null; + } + $libDir = PKG_ROOT_PATH . '/zig/lib'; + if (file_exists("{$libDir}/libclang_rt.profile.a") + && file_exists("{$libDir}/clang_rt.crtbegin.o") + && file_exists("{$libDir}/clang_rt.crtend.o") + ) { + return CheckResult::ok("{$libDir}/libclang_rt.profile.a"); + } + return CheckResult::fail('clang runtime bits are not built', 'build-clang-runtime-bits'); + } + + #[FixItem('build-clang-runtime-bits')] + public function fixClangRuntimeBits(): bool + { + $installer = new PackageInstaller(interactive: false); + $installer->addInstallPackage('clang-runtime-bits'); + $installer->run(true); + $libDir = PKG_ROOT_PATH . '/zig/lib'; + return file_exists("{$libDir}/libclang_rt.profile.a") + && file_exists("{$libDir}/clang_rt.crtbegin.o") + && file_exists("{$libDir}/clang_rt.crtend.o"); + } }