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/llvm_tools.php b/src/Package/Artifact/llvm_tools.php new file mode 100644 index 000000000..714cab37f --- /dev/null +++ b/src/Package/Artifact/llvm_tools.php @@ -0,0 +1,162 @@ +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, headers: $this->getGitHubTokenHeaders(), 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'; + if (self::isInstalled()) { + return; + } + $llvmDir = "{$sourceRoot}/llvm"; + if (!is_dir($llvmDir)) { + throw new BuildFailureException("llvm-tools: missing source at {$llvmDir} (extraction layout changed?)"); + } + $buildDir = "{$sourceRoot}/build"; + $installDir = self::path(); + $binDir = self::path() . '/bin'; + 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=' . zig::binary('zig-cc'), + '-DCMAKE_CXX_COMPILER=' . zig::binary('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, self::binary($t)); + chmod(self::binary($t), 0755); + } + } + + private function detectLlvmVersion(): ?string + { + if (!zig::isInstalled()) { + return null; + } + [$rc, $out] = shell()->execWithResult(escapeshellarg(zig::binary()) . ' 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/zig.php b/src/Package/Artifact/zig.php index 95520aa4b..e1c76bb03 100644 --- a/src/Package/Artifact/zig.php +++ b/src/Package/Artifact/zig.php @@ -15,6 +15,23 @@ class zig { + /** Directory zig extracts into. */ + public static function path(): string + { + return PKG_ROOT_PATH . '/zig'; + } + + /** Path to a binary inside the zig install dir (zig, zig-cc, zig-c++, zig-ar, …). */ + public static function binary(string $name = 'zig'): string + { + return self::path() . '/' . $name; + } + + public static function isInstalled(): bool + { + return is_file(self::binary()); + } + #[CustomBinary('zig', [ 'linux-x86_64', 'linux-aarch64', 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/LlvmToolsCheck.php b/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php new file mode 100644 index 000000000..60383869c --- /dev/null +++ b/src/StaticPHP/Doctor/Item/LlvmToolsCheck.php @@ -0,0 +1,44 @@ +addInstallPackage('llvm-tools'); + $installer->run(true); + new llvm_tools()->buildForHost(); + return llvm_tools::isInstalled(); + } +} diff --git a/src/StaticPHP/Package/PackageBuilder.php b/src/StaticPHP/Package/PackageBuilder.php index 582fa474d..49150c0c9 100644 --- a/src/StaticPHP/Package/PackageBuilder.php +++ b/src/StaticPHP/Package/PackageBuilder.php @@ -4,6 +4,7 @@ namespace StaticPHP\Package; +use Package\Artifact\llvm_tools; use StaticPHP\Config\PackageConfig; use StaticPHP\DI\ApplicationContext; use StaticPHP\Exception\SPCException; @@ -11,6 +12,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 +181,18 @@ 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') + ?: (ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain + ? llvm_tools::binary('llvm-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 +206,12 @@ public function extractDebugInfo(string $binary_path): string */ public function stripBinary(string $binary_path): void { + $strip = ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain + ? llvm_tools::binary('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'), });