Skip to content

Commit 728c2c5

Browse files
committed
fix(build,toolchain): make the llvm-musl path actually reach the target
Four defects the first end-to-end run surfaced, all found by building a module TU against the llvm-musl-libcxx payload: - pin override: the target vocabulary pin silently replaced --toolchain (it arrives via the env side channel, which the pin guard never read), so `--toolchain llvm --target x86_64-linux-musl` resolved gcc. The flag now blocks the pin on its own, regardless of target origin. - triple propagation: the target-triple correction was scoped to freestanding only, but llvm-on-musl is the same one-binary case — the clang frontend's -dumpmachine answers with the host while the build targets musl, leaving the output dir, cache key and flags on the host triple (E1 for a hosted target). Freestanding-only side effects (hasImportStd off, the picolibc sysroot lookup) stay scoped. - std module over the wrong libc: the std precompile used the shared host-flags producer (host libc++ headers + glibc), which fails on the payload's std.cppm with __config_site / bits/alltypes.h not found. llvm-musl now builds the std BMI from the payload headers over the musl-gcc sysroot (both payload naming shapes, with the xim- prefix). - payload C runtime leak: the clangWithCfg PayloadFirst link flags name the HOST's glibc, whose loader became a static binary's PT_INTERP. Excluded for llvm-musl; its branch already assembles the target C runtime (musl sysroot, crt via --gcc-toolchain). Verified end to end: mcpp build --toolchain llvm@22.1.8 --target x86_64-linux-musl on a hello-module project produces a fully static x86_64 musl ELF (no PT_INTERP, not a dynamic executable) that runs. Unit suite 91 passed; the 1 test_elf_runtime failure is the known environmental one (host /lib64/libtinfo.so.6 symlink chain), unchanged. Refs #491
1 parent 42d2b6d commit 728c2c5

3 files changed

Lines changed: 76 additions & 8 deletions

File tree

src/build/flags.cppm

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1183,7 +1183,12 @@ CompileFlags compute_flags(const BuildPlan& plan) {
11831183
// but the link line has a hard 128KiB ceiling (MAX_ARG_STRLEN) that real
11841184
// workspaces already spend 43% of.
11851185
std::string payload_ld;
1186-
if (isClangWithCfg
1186+
// llvm-musl excluded: the clangWithCfg PayloadFirst flags here name the
1187+
// HOST's glibc (its loader ends up as a static binary's PT_INTERP), while
1188+
// the isLlvmMusl branch above already assembled the target's complete C
1189+
// runtime (musl sysroot + crt via --gcc-toolchain).
1190+
if (!isLlvmMusl
1191+
&& isClangWithCfg
11871192
&& lm.mode == mcpp::toolchain::CLibMode::PayloadFirst)
11881193
payload_ld = lm.link_flags(ninjaEsc);
11891194
// GCC: replace the payload's patched `*link:` with the pristine one, so

src/build/prepare.cppm

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1253,9 +1253,11 @@ prepare_build(bool print_fingerprint,
12531253
// costs nobody anything and needs no coordination.
12541254
//
12551255
// It counts as user-explicit, so mcpp will not quietly revise it.
1256+
bool toolchainFromFlag = false;
12561257
if (const char* tcEnv = std::getenv("MCPP_TOOLCHAIN"); tcEnv && *tcEnv) {
12571258
tcSpec = std::string(tcEnv);
12581259
tcOrigin = TcOrigin::ManifestToolchain;
1260+
toolchainFromFlag = true;
12591261
}
12601262
if (!tcSpec.has_value()) {
12611263
auto cfg = get_cfg();
@@ -1436,8 +1438,17 @@ prepare_build(bool print_fingerprint,
14361438
// which is exactly the promise the fallback is built on ("mcpp
14371439
// revises its own defaults, never yours"). A target the user asked
14381440
// for (--target, or [build] target) still wins, as it always has.
1441+
// `hasToolchainOverride` above covers the manifest's [target.X]
1442+
// section. `--toolchain` must stop the pin on its own, regardless of
1443+
// where the target came from: the flag form of the promise
1444+
// `pinWouldOverruleUser` keeps for remembered targets ("mcpp revises
1445+
// its own defaults, never yours") — measured, `--toolchain llvm
1446+
// --target x86_64-linux-musl` used to resolve gcc@16.1.0 because the
1447+
// musl row's pin silently replaced the flag.
14391448
const bool pinWouldOverruleUser =
1440-
targetFromGlobalDefault && tc_origin_is_user_explicit(tcOrigin);
1449+
toolchainFromFlag
1450+
|| (targetFromGlobalDefault
1451+
&& tc_origin_is_user_explicit(tcOrigin));
14411452
if (known && !hasToolchainOverride && !known->pin.empty()
14421453
&& !pinWouldOverruleUser) {
14431454
tcSpec = std::string(known->pin);
@@ -1823,15 +1834,22 @@ prepare_build(bool print_fingerprint,
18231834
// `tc.targetTriple`, so correcting it here corrects all of them at once —
18241835
// which is the point of there being one field rather than five answers.
18251836
//
1826-
// ⚠️ Scoped to freestanding on purpose. The hosted cross targets already
1827-
// resolve a per-target binary, and overwriting their probed triple would
1828-
// replace a measured fact with an assumed one for no gain.
1837+
// ⚠️ Scoped to freestanding AND the llvm-musl family on purpose. The
1838+
// other hosted cross targets already resolve a per-target binary, and
1839+
// overwriting their probed triple would replace a measured fact with an
1840+
// assumed one for no gain. llvm-musl is the same one-binary case as
1841+
// freestanding: the clang frontend's -dumpmachine answers with the host
1842+
// while the build targets musl, so without this the output dir, cache key
1843+
// and flag layer all stay on the host triple (E1 for a hosted target).
18291844
if (!overrides.target_triple.empty()) {
1830-
if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple);
1831-
want && want->is_freestanding())
1845+
auto want = mcpp::toolchain::triple::parse(overrides.target_triple);
1846+
const bool llvmMuslTarget = want && want->is_musl()
1847+
&& mcpp::toolchain::is_clang(*tc);
1848+
if (want && (want->is_freestanding() || llvmMuslTarget))
18321849
{
18331850
tc->targetTriple = want->str();
18341851

1852+
if (want->is_freestanding()) {
18351853
// `import std` is structurally hosted, and turning it off is the
18361854
// SAME fact as the line above, not a second policy: libc++'s
18371855
// std.cppm is one module over the whole library, including the
@@ -1852,9 +1870,14 @@ prepare_build(bool print_fingerprint,
18521870
tc->hasImportStd = false;
18531871
tc->stdModuleSource.clear();
18541872
tc->stdCompatSource.clear();
1873+
}
18551874

18561875
// ── The target's C library, resolved like its compiler ─────────
18571876
//
1877+
// (freestanding only: the C library column names picolibc, whose
1878+
// payload layout a freestanding spec describes. llvm-musl resolves
1879+
// its libc++ one block below instead.)
1880+
if (want->is_freestanding())
18581881
// The row in kKnownTargets names it, exactly as it names the
18591882
// toolchain pin, and it is installed through the same channel a
18601883
// project's `[xlings] deps` use (see the materialization above).

src/toolchain/stdmod.cppm

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,14 @@ import std;
3434
import mcpp.home;
3535
import mcpp.libs.json;
3636
import mcpp.platform;
37+
import mcpp.platform.xlings;
3738
import mcpp.toolchain.clang;
3839
import mcpp.toolchain.detect;
3940
import mcpp.toolchain.fingerprint;
4041
import mcpp.toolchain.gcc;
4142
import mcpp.toolchain.hostflags;
4243
import mcpp.toolchain.linkmodel;
44+
import mcpp.toolchain.model;
4345
import mcpp.toolchain.msvc;
4446

4547
export namespace mcpp::toolchain {
@@ -249,8 +251,46 @@ std::expected<StdModule, StdModError> ensure_built(
249251
HostFlagOptions hopt;
250252
hopt.cfgBypass = HostFlagOptions::CfgBypass::Always;
251253
hopt.clangStdlibSelect = true;
252-
std::string sysroot_flag =
254+
std::string sysroot_flag;
255+
// llvm-musl: the shared producer reconstructs the HOST's header world,
256+
// which is wrong for the target whose std.cppm this is — the failure is
257+
// the freestanding doc's `__config_site not found` one layer up. The
258+
// target's musl libc++ headers and C library come from the payload
259+
// prepare resolved (targetSysroot*) plus the musl-gcc sysroot.
260+
if (is_clang(tc) && is_musl_target(tc)
261+
&& !tc.targetSysrootInclude.empty())
262+
{
263+
// The musl C headers come from the musl-gcc payload's sysroot; the
264+
// frontend locates it the same way the link side does (a
265+
// triple-named sibling of the llvm payload under xpkgs).
266+
auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(tc.binaryPath);
267+
std::string sysroot;
268+
if (xpkgs) {
269+
// Two payload shapes, matching the registry's naming: the
270+
// triple-named cross package and the host-native musl-gcc one
271+
// (whose sysroot still lives under <triple>/).
272+
const std::string shapes[] = {
273+
std::format("xim-x-{}-gcc", tc.targetTriple), "xim-x-musl-gcc"};
274+
for (auto const& shape : shapes) {
275+
auto gccRoot = *xpkgs / shape;
276+
std::error_code ec;
277+
if (!std::filesystem::is_directory(gccRoot, ec)) continue;
278+
for (auto& e : std::filesystem::directory_iterator(gccRoot, ec))
279+
if (e.is_directory()) {
280+
sysroot = (e.path() / tc.targetTriple).string();
281+
break;
282+
}
283+
if (!sysroot.empty()) break;
284+
}
285+
}
286+
sysroot_flag = std::format(
287+
" --no-default-config --target={} -nostdinc++ -stdlib=libc++"
288+
" --sysroot={} -isystem'{}'",
289+
tc.targetTriple, sysroot, tc.targetSysrootInclude.string());
290+
} else {
291+
sysroot_flag =
253292
render_tokens(host_compile_tokens(tc, hopt, shellEsc));
293+
}
254294

255295
// Deployment target appended here rather than passed to the producer
256296
// ONLY to keep this command string byte-identical to what earlier

0 commit comments

Comments
 (0)