Skip to content

Commit 6df778f

Browse files
committed
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()).
1 parent 582a88e commit 6df778f

6 files changed

Lines changed: 263 additions & 5 deletions

File tree

config/pkg/target/llvm-tools.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
llvm-tools:
2+
type: target
3+
artifact:
4+
binary: custom
5+
depends:
6+
- zig
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Package\Artifact;
6+
7+
use StaticPHP\Artifact\ArtifactDownloader;
8+
use StaticPHP\Artifact\Downloader\DownloadResult;
9+
use StaticPHP\Artifact\Downloader\Type\CheckUpdateResult;
10+
use StaticPHP\Artifact\Downloader\Type\GitHubTokenSetupTrait;
11+
use StaticPHP\Attribute\Artifact\AfterBinaryExtract;
12+
use StaticPHP\Attribute\Artifact\CustomBinary;
13+
use StaticPHP\Attribute\Artifact\CustomBinaryCheckUpdate;
14+
use StaticPHP\DI\ApplicationContext;
15+
use StaticPHP\Exception\BuildFailureException;
16+
use StaticPHP\Exception\DownloaderException;
17+
use StaticPHP\Package\PackageBuilder;
18+
19+
class llvm_tools
20+
{
21+
use GitHubTokenSetupTrait;
22+
23+
public const array TOOLS = ['llvm-objcopy', 'llvm-strip', 'llvm-profdata'];
24+
25+
/** Install prefix for the locally-built llvm-tools. */
26+
public static function path(): string
27+
{
28+
return PKG_ROOT_PATH . '/llvm-tools';
29+
}
30+
31+
/** Path to a binary under llvm-tools/bin (llvm-objcopy, llvm-strip, llvm-profdata, …). */
32+
public static function binary(string $name = 'llvm-strip'): string
33+
{
34+
return self::path() . '/bin/' . $name;
35+
}
36+
37+
/** True when every required TOOLS binary is present and executable. */
38+
public static function isInstalled(): bool
39+
{
40+
foreach (self::TOOLS as $t) {
41+
$p = self::binary($t);
42+
if (!is_file($p) || !is_executable($p)) {
43+
return false;
44+
}
45+
}
46+
return true;
47+
}
48+
49+
#[CustomBinary('llvm-tools', [
50+
'linux-x86_64',
51+
'linux-aarch64',
52+
'macos-x86_64',
53+
'macos-aarch64',
54+
])]
55+
public function downBinary(ArtifactDownloader $downloader): DownloadResult
56+
{
57+
$llvmVersion = $this->detectLlvmVersion()
58+
?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first');
59+
$tarball = "llvm-project-{$llvmVersion}.src.tar.xz";
60+
$url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-{$llvmVersion}/{$tarball}";
61+
$tarballPath = DOWNLOAD_PATH . '/' . $tarball;
62+
default_shell()->executeCurlDownload($url, $tarballPath, headers: $this->getGitHubTokenHeaders(), retries: $downloader->getRetry());
63+
return DownloadResult::archive($tarball, ['url' => $url, 'version' => $llvmVersion], extract: '{source_path}/llvm-tools', verified: false, version: $llvmVersion);
64+
}
65+
66+
#[CustomBinaryCheckUpdate('llvm-tools', [
67+
'linux-x86_64',
68+
'linux-aarch64',
69+
'macos-x86_64',
70+
'macos-aarch64',
71+
])]
72+
public function checkUpdateBinary(?string $old_version, ArtifactDownloader $downloader): CheckUpdateResult
73+
{
74+
$llvmVersion = $this->detectLlvmVersion()
75+
?? throw new DownloaderException('Could not detect a clang version on host; install zig or clang first');
76+
return new CheckUpdateResult(
77+
old: $old_version,
78+
new: $llvmVersion,
79+
needUpdate: $old_version === null || $llvmVersion !== $old_version,
80+
);
81+
}
82+
83+
#[AfterBinaryExtract('llvm-tools', [
84+
'linux-x86_64',
85+
'linux-aarch64',
86+
'macos-x86_64',
87+
'macos-aarch64',
88+
])]
89+
public function postExtract(string $target_path): void
90+
{
91+
$this->buildForHost($target_path);
92+
}
93+
94+
public function buildForHost(?string $sourceRoot = null): void
95+
{
96+
$sourceRoot ??= SOURCE_PATH . '/llvm-tools';
97+
if (self::isInstalled()) {
98+
return;
99+
}
100+
$llvmDir = "{$sourceRoot}/llvm";
101+
if (!is_dir($llvmDir)) {
102+
throw new BuildFailureException("llvm-tools: missing source at {$llvmDir} (extraction layout changed?)");
103+
}
104+
$buildDir = "{$sourceRoot}/build";
105+
$installDir = self::path();
106+
$binDir = self::path() . '/bin';
107+
f_mkdir($buildDir, recursive: true);
108+
f_mkdir($binDir, recursive: true);
109+
110+
$cmakeArgs = implode(' ', array_map('escapeshellarg', [
111+
'-S', $llvmDir,
112+
'-B', $buildDir,
113+
'-DCMAKE_BUILD_TYPE=Release',
114+
'-DLLVM_ENABLE_PROJECTS=',
115+
'-DLLVM_TARGETS_TO_BUILD=',
116+
'-DLLVM_INCLUDE_BENCHMARKS=OFF',
117+
'-DLLVM_INCLUDE_TESTS=OFF',
118+
'-DLLVM_INCLUDE_EXAMPLES=OFF',
119+
'-DLLVM_INCLUDE_DOCS=OFF',
120+
'-DLLVM_ENABLE_ZLIB=OFF',
121+
'-DLLVM_ENABLE_ZSTD=OFF',
122+
'-DLLVM_ENABLE_LIBXML2=OFF',
123+
'-DLLVM_ENABLE_TERMINFO=OFF',
124+
'-DLLVM_ENABLE_LIBEDIT=OFF',
125+
'-DLLVM_ENABLE_LIBPFM=OFF',
126+
'-DLLVM_BUILD_LLVM_DYLIB=OFF',
127+
'-DLLVM_LINK_LLVM_DYLIB=OFF',
128+
'-DBUILD_SHARED_LIBS=OFF',
129+
'-DCMAKE_C_COMPILER=' . zig::binary('zig-cc'),
130+
'-DCMAKE_CXX_COMPILER=' . zig::binary('zig-c++'),
131+
'-DCMAKE_INSTALL_PREFIX=' . $installDir,
132+
]));
133+
$jobs = ApplicationContext::get(PackageBuilder::class)->concurrency;
134+
$targetArgs = implode(' ', array_map(fn ($t) => '--target ' . escapeshellarg($t), self::TOOLS));
135+
136+
shell()
137+
->setEnv(['SPC_TARGET' => GNU_ARCH . '-linux-musl'])
138+
->exec('cmake ' . $cmakeArgs)
139+
->exec('cmake --build ' . escapeshellarg($buildDir) . ' ' . $targetArgs . " -j {$jobs}");
140+
141+
foreach (self::TOOLS as $t) {
142+
$built = "{$buildDir}/bin/{$t}";
143+
if (!is_file($built)) {
144+
throw new BuildFailureException("llvm-tools: missing build output {$built}");
145+
}
146+
copy($built, self::binary($t));
147+
chmod(self::binary($t), 0755);
148+
}
149+
}
150+
151+
private function detectLlvmVersion(): ?string
152+
{
153+
if (!zig::isInstalled()) {
154+
return null;
155+
}
156+
[$rc, $out] = shell()->execWithResult(escapeshellarg(zig::binary()) . ' cc --version', false);
157+
if ($rc !== 0) {
158+
return null;
159+
}
160+
return preg_match('/clang version (\d+\.\d+\.\d+)/', implode("\n", $out), $m) ? $m[1] : null;
161+
}
162+
}

src/Package/Artifact/zig.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,23 @@
1515

1616
class zig
1717
{
18+
/** Directory zig extracts into. */
19+
public static function path(): string
20+
{
21+
return PKG_ROOT_PATH . '/zig';
22+
}
23+
24+
/** Path to a binary inside the zig install dir (zig, zig-cc, zig-c++, zig-ar, …). */
25+
public static function binary(string $name = 'zig'): string
26+
{
27+
return self::path() . '/' . $name;
28+
}
29+
30+
public static function isInstalled(): bool
31+
{
32+
return is_file(self::binary());
33+
}
34+
1835
#[CustomBinary('zig', [
1936
'linux-x86_64',
2037
'linux-aarch64',

src/StaticPHP/DI/ApplicationContext.php

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,25 @@ public static function has(string $id): bool
9898
return self::getContainer()->has($id);
9999
}
100100

101+
/**
102+
* Resolve $id, returning null if it can't be constructed.
103+
* PHP-DI's has() returns true for any autowirable class even when get()
104+
* would throw on missing scalar args — for "is this resolvable right now"
105+
* semantics use this.
106+
*
107+
* @template T
108+
* @param class-string<T> $id
109+
* @return null|T
110+
*/
111+
public static function tryGet(string $id): mixed
112+
{
113+
try {
114+
return self::getContainer()->get($id);
115+
} catch (\Throwable) {
116+
return null;
117+
}
118+
}
119+
101120
/**
102121
* Set a service in the container.
103122
* Use sparingly - prefer configuration-based definitions.
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace StaticPHP\Doctor\Item;
6+
7+
use Package\Artifact\llvm_tools;
8+
use StaticPHP\Attribute\Doctor\CheckItem;
9+
use StaticPHP\Attribute\Doctor\FixItem;
10+
use StaticPHP\Attribute\Doctor\OptionalCheck;
11+
use StaticPHP\DI\ApplicationContext;
12+
use StaticPHP\Doctor\CheckResult;
13+
use StaticPHP\Package\PackageInstaller;
14+
use StaticPHP\Toolchain\Interface\ToolchainInterface;
15+
use StaticPHP\Toolchain\ZigToolchain;
16+
17+
#[OptionalCheck([self::class, 'optionalCheck'])]
18+
class LlvmToolsCheck
19+
{
20+
public static function optionalCheck(): bool
21+
{
22+
return ApplicationContext::get(ToolchainInterface::class) instanceof ZigToolchain;
23+
}
24+
25+
/** @noinspection PhpUnused */
26+
#[CheckItem('if llvm-tools (objcopy/strip/profdata) are built', level: 798)]
27+
public function checkLlvmTools(): CheckResult
28+
{
29+
if (llvm_tools::isInstalled()) {
30+
return CheckResult::ok(llvm_tools::path() . '/bin');
31+
}
32+
return CheckResult::fail('llvm-tools are not built', 'build-llvm-tools');
33+
}
34+
35+
#[FixItem('build-llvm-tools')]
36+
public function fixLlvmTools(): bool
37+
{
38+
$installer = new PackageInstaller(interactive: false);
39+
$installer->addInstallPackage('llvm-tools');
40+
$installer->run(true);
41+
new llvm_tools()->buildForHost();
42+
return llvm_tools::isInstalled();
43+
}
44+
}

src/StaticPHP/Package/PackageBuilder.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,16 @@
44

55
namespace StaticPHP\Package;
66

7+
use Package\Artifact\llvm_tools;
78
use StaticPHP\Config\PackageConfig;
89
use StaticPHP\DI\ApplicationContext;
910
use StaticPHP\Exception\SPCException;
1011
use StaticPHP\Exception\SPCInternalException;
1112
use StaticPHP\Exception\WrongUsageException;
1213
use StaticPHP\Runtime\Shell\Shell;
1314
use StaticPHP\Runtime\SystemTarget;
15+
use StaticPHP\Toolchain\Interface\ToolchainInterface;
16+
use StaticPHP\Toolchain\ZigToolchain;
1417
use StaticPHP\Util\FileSystem;
1518
use StaticPHP\Util\GlobalPathTrait;
1619
use StaticPHP\Util\InteractiveTerm;
@@ -178,14 +181,18 @@ public function extractDebugInfo(string $binary_path): string
178181
if (SystemTarget::getTargetOS() === 'Darwin') {
179182
shell()->exec("dsymutil -f {$binary_path} -o {$debug_file}");
180183
} elseif (SystemTarget::getTargetOS() === 'Linux') {
184+
$objcopy = getenv('OBJCOPY')
185+
?: (ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain
186+
? llvm_tools::binary('llvm-objcopy')
187+
: 'objcopy');
181188
if ($eu_strip = LinuxUtil::findCommand('eu-strip')) {
182189
shell()
183190
->exec("{$eu_strip} -f {$debug_file} {$binary_path}")
184-
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
191+
->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}");
185192
} else {
186193
shell()
187-
->exec("objcopy --only-keep-debug {$binary_path} {$debug_file}")
188-
->exec("objcopy --add-gnu-debuglink={$debug_file} {$binary_path}");
194+
->exec("{$objcopy} --only-keep-debug {$binary_path} {$debug_file}")
195+
->exec("{$objcopy} --add-gnu-debuglink={$debug_file} {$binary_path}");
189196
}
190197
} else {
191198
logger()->debug('extractDebugInfo is only supported on Linux and macOS');
@@ -199,9 +206,12 @@ public function extractDebugInfo(string $binary_path): string
199206
*/
200207
public function stripBinary(string $binary_path): void
201208
{
209+
$strip = ApplicationContext::tryGet(ToolchainInterface::class) instanceof ZigToolchain
210+
? llvm_tools::binary('llvm-strip')
211+
: 'strip';
202212
shell()->exec(match (SystemTarget::getTargetOS()) {
203-
'Darwin' => "strip -S {$binary_path}",
204-
'Linux' => "strip --strip-unneeded {$binary_path}",
213+
'Darwin' => "{$strip} -S {$binary_path}",
214+
'Linux' => "{$strip} --strip-unneeded {$binary_path}",
205215
'Windows' => 'echo "Skip strip on Windows"', // Windows strip is not available for now
206216
default => throw new SPCInternalException('stripBinary is only supported on Linux and macOS'),
207217
});

0 commit comments

Comments
 (0)