Compare commits

...

1 Commits

Author SHA1 Message Date
henderkes
6df778f92f llvm-tools: build host llvm-objcopy/strip/profdata under ZigToolchain
Add an `llvm-tools` target artifact that downloads llvm-project source
matching the version of clang shipped by the active zig install,
builds llvm-objcopy, llvm-strip and llvm-profdata into
PKG_ROOT_PATH/llvm-tools/bin, and exposes them through the same
path/binary/isInstalled static surface as the other artifacts.

A new LlvmToolsCheck doctor item runs when the active toolchain is
ZigToolchain and reports whether the three tools are built, with a
fix that installs the package and runs the build.

PackageBuilder now picks the right tool when the active toolchain is
ZigToolchain:
- extractDebugInfo() honours OBJCOPY from the environment, then falls
  back to llvm-tools' llvm-objcopy under Zig and plain objcopy
  otherwise.
- stripBinary() uses llvm-strip under Zig and plain strip otherwise.

System strip/objcopy refuse zig-produced archives and bitcode
sections, so without this the strip stage breaks LTO builds. Other
toolchains keep using the system binaries.

ApplicationContext::tryGet() wraps the container's get() in a
try/catch and returns null on failure, so PackageBuilder can ask
"which toolchain is active right now" without PHP-DI throwing on
autowirable-but-unconstructable classes.

Depends on v3c/artifact-static-helpers (uses zig::isInstalled()
and zig::binary()).
2026-05-24 21:50:57 +07:00
6 changed files with 263 additions and 5 deletions

View File

@@ -0,0 +1,6 @@
llvm-tools:
type: target
artifact:
binary: custom
depends:
- zig

View File

@@ -0,0 +1,162 @@
<?php
declare(strict_types=1);
namespace Package\Artifact;
use StaticPHP\Artifact\ArtifactDownloader;
use StaticPHP\Artifact\Downloader\DownloadResult;
use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult;
use StaticPHP\Artifact\Downloader\Type\GitHubTokenSetupTrait;
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
use StaticPHP\Attribute\Artifact\CustomBinary;
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Exception\BuildFailureException;
use StaticPHP\Exception\DownloaderException;
use StaticPHP\Package\PackageBuilder;
class llvm_tools
{
use GitHubTokenSetupTrait;
public const array TOOLS = ['llvm-objcopy', 'llvm-strip', 'llvm-profdata'];
/** Install prefix for the locally-built llvm-tools. */
public static function path(): string
{
return PKG_ROOT_PATH . '/llvm-tools';
}
/** Path to a binary under llvm-tools/bin (llvm-objcopy, llvm-strip, llvm-profdata, …). */
public static function binary(string $name = 'llvm-strip'): string
{
return self::path() . '/bin/' . $name;
}
/** True when every required TOOLS binary is present and executable. */
public static function isInstalled(): bool
{
foreach (self::TOOLS as $t) {
$p = self::binary($t);
if (!is_file($p) || !is_executable($p)) {
return false;
}
}
return true;
}
#[CustomBinary('llvm-tools', [
'linux-x86_64',
'linux-aarch64',
'macos-x86_64',
'macos-aarch64',
])]
public function downBinary(ArtifactDownloader $downloader): DownloadResult
{
$llvmVersion = $this->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;
}
}

View File

@@ -15,6 +15,23 @@ use StaticPHP\Runtime\SystemTarget;
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',

View File

@@ -98,6 +98,25 @@ class ApplicationContext
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<T> $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.

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace StaticPHP\Doctor\Item;
use Package\Artifact\llvm_tools;
use StaticPHP\Attribute\Doctor\CheckItem;
use StaticPHP\Attribute\Doctor\FixItem;
use StaticPHP\Attribute\Doctor\OptionalCheck;
use StaticPHP\DI\ApplicationContext;
use StaticPHP\Doctor\CheckResult;
use StaticPHP\Package\PackageInstaller;
use StaticPHP\Toolchain\Interface\ToolchainInterface;
use StaticPHP\Toolchain\ZigToolchain;
#[OptionalCheck([self::class, 'optionalCheck'])]
class LlvmToolsCheck
{
public static function optionalCheck(): bool
{
return ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain;
}
/** @noinspection PhpUnused */
#[CheckItem('if llvm-tools (objcopy/strip/profdata) are built', level: 798)]
public function checkLlvmTools(): CheckResult
{
if (llvm_tools::isInstalled()) {
return CheckResult::ok(llvm_tools::path() . '/bin');
}
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 llvm_tools::isInstalled();
}
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
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\SPCInternalException;
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 @@ class PackageBuilder
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 @@ class PackageBuilder
*/
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'),
});