diff --git a/.agents/docs/2026-08-23-llvm-musl-target-design.md b/.agents/docs/2026-08-23-llvm-musl-target-design.md new file mode 100644 index 00000000..fdc28aee --- /dev/null +++ b/.agents/docs/2026-08-23-llvm-musl-target-design.md @@ -0,0 +1,70 @@ +# llvm-musl 静态 target 支持 + +状态:设计(2026-08-23) + +## 问题 + +`mcpp build --target x86_64-linux-musl` 目前把 musl 目标硬绑到 gcc@16.1.0 +(`to_xim_package` 的 `Family::Gcc` + musl triple 分支)。GCC 16.1.0 的 +modules 实现有未修复的 ICE(BMI 读回期段错误,gdb 现场为编译器堆内 +乱码指针;尚未在 gcc bugzilla 立案),而 clang 22.1.8 编同一模块重度 +代码库全绿。 +llvm 家族缺的是 musl 侧的 payload:clang 自身不携带 musl libc/libc++。 + +## 前置事实(已在一个 ~240 个模块 TU 的工作区验证) + +1. clang 22.1.8 payload 的 `bin/clang.cfg` 强注 host glibc 路径;musl 构建 + 必须让驱动回退中性(`--no-default-config`,或等价地清空 cfg 影响)。 +2. musl libc++/libc++abi/libunwind 可由 host clang 以 + `LLVM_ENABLE_RUNTIMES` 交叉构建(x86_64 与 aarch64 双份,含 + `import std` 模块源 std.cppm);crt/libgcc 复用 musl-gcc payload + (`--gcc-toolchain` 指向它 + `-rtlib=libgcc -unwindlib=libgcc`)。 +3. clang 已知 bug 规避在用户代码侧(PCM wchar 误推导),不阻塞工具链。 + +## 设计 + +### 1. payload(分发层) + +新 xim 包 `llvm-musl`(每 target arch 一份资产): +- clang 前端(复用 llvm payload 的二进制,不重复分发) +- `/musl//`:sysroot(musl-gcc payload 借)+ 自建 libc++ 等 + 静态库与 std.cppm + +`to_xim_package`:`Family::Llvm` + musl triple 时映射到该包, +frontendCandidates 仍是 `clang++`。 + +### 2. linkmodel(渲染层) + +`resolve_link_model` 增加 llvm-musl 分支(在 `clangWithCfg` 分支之前): +- `mode = Sysroot`,root = payload 的 musl sysroot +- 额外 tokens:`--no-default-config --gcc-toolchain= + -rtlib=libgcc -unwindlib=libgcc -nostdinc++ -isystem /include/c++/v1 + -nostdlib++ -L/lib -lc++ -lc++abi` +- 全静态:`-static`(沿用 `supports_full_static` 的 target 语义) +- crt 查找:musl triple 前缀规则已存在(binutils_tool 的 cross 分支) + +### 3. stdmod(模块层) + +llvm-musl 的 std.pcm 必须 `--precompile` 自 payload 里的 musl libc++ +std.cppm(不能复用 build-cache 的 glibc 版)。在 stdmod 解析处按 +`is_musl_target && is_clang` 选源。 + +### 4. 别名与默认 + +- `llvm@ --target -linux-musl` 全形态可用 +- 不改变 gcc-musl 的现有默认;llvm-musl 是显式选择 + +## 验收 + +- 新包安装后 `mcpp build --toolchain llvm@22.1.8 --target + x86_64-linux-musl` 对该模块重度工作区编译通过 +- 产物 `file` 为 statically linked,`ldd` 报 not a dynamic executable +- aarch64 交叉 + qemu 冒烟 +- 现有 gcc/llvm host 路径零回归(tests/unit 全绿) + +## 开放问题 + +- `llvm-musl` 包的资产组装脚本放 xim-pkgindex 还是 mcpp 仓(倾向前者, + 遵循 payload 归属 xim 的既有分工) +- clang.cfg 的 `--no-default-config` 是否应做成 Toolchain 结构里的显式 + 开关而非渲染层字符串(倾向后者先行,够用) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 8b8ca8ec..4e4f18a9 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -29,6 +29,7 @@ import mcpp.toolchain.linkmodel; import mcpp.toolchain.model; import mcpp.toolchain.provider; import mcpp.toolchain.registry; +import mcpp.platform.xlings; export namespace mcpp::build { @@ -542,7 +543,64 @@ CompileFlags compute_flags(const BuildPlan& plan) { return ft && ft->is_freestanding(); }(); - if (!isFreestandingTarget) { + // The musl-gcc payload root for --gcc-toolchain: crt, libgcc and the + // musl libc itself live there. Located relative to the SAME xpkgs base + // the clang frontend came from (xpkgs_from_compiler), so the build + // cannot mix payloads from two homes. Two shapes, matching the registry's + // payload naming: the host-native `musl-gcc` package and the + // triple-named `-gcc` cross package. + const auto musl_gcc_toolchain = [&](const mcpp::toolchain::Toolchain& tc) + -> std::filesystem::path + { + auto base = mcpp::xlings::paths::xpkgs_from_compiler(tc.binaryPath); + if (!base) return {}; + auto pkg = std::string("x-") + tc.targetTriple + "-gcc"; + std::error_code ec; + for (auto& e : std::filesystem::directory_iterator(*base, ec)) { + auto name = e.path().filename().string(); + if (name.find(pkg) != std::string::npos + || (tc.targetTriple.find(mcpp::platform::host_arch) + != std::string::npos + && name.find("x-musl-gcc") != std::string::npos)) + { + // / — first version dir wins; payloads + // are single-version in practice. + std::error_code ec2; + for (auto& v : std::filesystem::directory_iterator( + e.path(), ec2)) + if (v.is_directory()) return v.path(); + } + } + return {}; + }; + + // llvm-musl: a clang frontend on a hosted musl target. The hosted + // clang-with-cfg path below reconstructs the HOST's world (glibc payload, + // host libc++), which for this target is exactly what must not reach the + // line — same class of bug as freestanding E1, one layer up. The target's + // musl libc++ rides in an xim payload resolved by prepare into + // targetSysroot*, and the C runtime (crt/libgcc/musl libc) comes from the + // musl-gcc payload via the driver's --gcc-toolchain. + const bool isLlvmMusl = mcpp::toolchain::is_clang(plan.toolchain) + && mcpp::toolchain::is_musl_target(plan.toolchain) + && !plan.toolchain.targetSysrootInclude.empty(); + + if (isLlvmMusl) { + // Compile side: neutral driver (--no-default-config kills the cfg's + // host glibc/loader pins), target triple, musl libc++ headers, the + // musl-gcc payload for crt/libgcc, and the target's own libc via + // --sysroot. HostFlagOptions knows none of this — it serves hosted + // gnu and macOS — so the tokens are assembled here, where every + // input (payload paths, gcc-toolchain dir) is already in hand. + compile_toolchain_flags = + " --no-default-config --target=" + plan.toolchain.targetTriple + + " --gcc-toolchain=" + ninjaEsc(musl_gcc_toolchain(plan.toolchain)) + + " -rtlib=libgcc -unwindlib=libgcc -nostdinc++" + + " -isystem " + ninjaEsc(plan.toolchain.targetSysrootInclude) + + " --sysroot=" + + ninjaEsc(musl_gcc_toolchain(plan.toolchain) + / plan.toolchain.targetTriple); + } else if (!isFreestandingTarget) { mcpp::toolchain::HostFlagOptions hopt; hopt.cfgBypass = mcpp::toolchain::HostFlagOptions::CfgBypass::Always; hopt.macosDeploymentTarget = macosDeploymentTarget; @@ -563,7 +621,25 @@ CompileFlags compute_flags(const BuildPlan& plan) { // x86-64 dynamic linker); it is added to the freestanding prefix. compile_toolchain_flags = " --no-default-config"; } - if (isClangWithCfg) { + if (isLlvmMusl) { + // Link side mirrors the compile side: neutral driver, target triple, + // gcc-toolchain for crt/libgcc, then the target's musl libc++ + // archives replace the driver's default stdlib selection entirely + // (-nostdlib++ + explicit -lc++/-lc++abi). The kLinkDriverFlags + // (compiler-rt/libunwind) are host selections and must not appear. + const auto gccTc = musl_gcc_toolchain(plan.toolchain); + link_toolchain_flags = + " --no-default-config --target=" + plan.toolchain.targetTriple + + " --gcc-toolchain=" + ninjaEsc(gccTc) + + " -rtlib=libgcc -unwindlib=libgcc -fuse-ld=lld --sysroot=" + + ninjaEsc(gccTc / plan.toolchain.targetTriple) + + " -nostdlib++ -L" + + ninjaEsc(plan.toolchain.targetSysrootRoot / "lib") + + " -lc++ -lc++abi"; + link_toolchain_flags_c = link_toolchain_flags; + f.sysroot = link_toolchain_flags; + llvmRootForStdlib = plan.toolchain.targetSysrootRoot; + } else if (isClangWithCfg) { llvmRootForStdlib = dm.llvmRoot; // Linker flags that cfg normally provides. The payload C-runtime // flags (-B/-L/loader) are appended via payload_ld below. @@ -1107,7 +1183,12 @@ CompileFlags compute_flags(const BuildPlan& plan) { // but the link line has a hard 128KiB ceiling (MAX_ARG_STRLEN) that real // workspaces already spend 43% of. std::string payload_ld; - if (isClangWithCfg + // llvm-musl excluded: the clangWithCfg PayloadFirst flags here name the + // HOST's glibc (its loader ends up as a static binary's PT_INTERP), while + // the isLlvmMusl branch above already assembled the target's complete C + // runtime (musl sysroot + crt via --gcc-toolchain). + if (!isLlvmMusl + && isClangWithCfg && lm.mode == mcpp::toolchain::CLibMode::PayloadFirst) payload_ld = lm.link_flags(ninjaEsc); // GCC: replace the payload's patched `*link:` with the pristine one, so diff --git a/src/build/prepare.cppm b/src/build/prepare.cppm index c6881e61..573a67de 100644 --- a/src/build/prepare.cppm +++ b/src/build/prepare.cppm @@ -32,6 +32,7 @@ import mcpp.toolchain.cppfly; import mcpp.toolchain.detect; import mcpp.toolchain.dialect; import mcpp.toolchain.fingerprint; +import mcpp.toolchain.model; import mcpp.toolchain.msvc; import mcpp.toolchain.registry; import mcpp.toolchain.stdmod; @@ -1252,9 +1253,11 @@ prepare_build(bool print_fingerprint, // costs nobody anything and needs no coordination. // // It counts as user-explicit, so mcpp will not quietly revise it. + bool toolchainFromFlag = false; if (const char* tcEnv = std::getenv("MCPP_TOOLCHAIN"); tcEnv && *tcEnv) { tcSpec = std::string(tcEnv); tcOrigin = TcOrigin::ManifestToolchain; + toolchainFromFlag = true; } if (!tcSpec.has_value()) { auto cfg = get_cfg(); @@ -1435,8 +1438,17 @@ prepare_build(bool print_fingerprint, // which is exactly the promise the fallback is built on ("mcpp // revises its own defaults, never yours"). A target the user asked // for (--target, or [build] target) still wins, as it always has. + // `hasToolchainOverride` above covers the manifest's [target.X] + // section. `--toolchain` must stop the pin on its own, regardless of + // where the target came from: the flag form of the promise + // `pinWouldOverruleUser` keeps for remembered targets ("mcpp revises + // its own defaults, never yours") — measured, `--toolchain llvm + // --target x86_64-linux-musl` used to resolve gcc@16.1.0 because the + // musl row's pin silently replaced the flag. const bool pinWouldOverruleUser = - targetFromGlobalDefault && tc_origin_is_user_explicit(tcOrigin); + toolchainFromFlag + || (targetFromGlobalDefault + && tc_origin_is_user_explicit(tcOrigin)); if (known && !hasToolchainOverride && !known->pin.empty() && !pinWouldOverruleUser) { tcSpec = std::string(known->pin); @@ -1822,15 +1834,22 @@ prepare_build(bool print_fingerprint, // `tc.targetTriple`, so correcting it here corrects all of them at once — // which is the point of there being one field rather than five answers. // - // ⚠️ Scoped to freestanding on purpose. The hosted cross targets already - // resolve a per-target binary, and overwriting their probed triple would - // replace a measured fact with an assumed one for no gain. + // ⚠️ Scoped to freestanding AND the llvm-musl family on purpose. The + // other hosted cross targets already resolve a per-target binary, and + // overwriting their probed triple would replace a measured fact with an + // assumed one for no gain. llvm-musl is the same one-binary case as + // freestanding: the clang frontend's -dumpmachine answers with the host + // while the build targets musl, so without this the output dir, cache key + // and flag layer all stay on the host triple (E1 for a hosted target). if (!overrides.target_triple.empty()) { - if (auto want = mcpp::toolchain::triple::parse(overrides.target_triple); - want && want->is_freestanding()) + auto want = mcpp::toolchain::triple::parse(overrides.target_triple); + const bool llvmMuslTarget = want && want->is_musl() + && mcpp::toolchain::is_clang(*tc); + if (want && (want->is_freestanding() || llvmMuslTarget)) { tc->targetTriple = want->str(); + if (want->is_freestanding()) { // `import std` is structurally hosted, and turning it off is the // SAME fact as the line above, not a second policy: libc++'s // std.cppm is one module over the whole library, including the @@ -1851,9 +1870,14 @@ prepare_build(bool print_fingerprint, tc->hasImportStd = false; tc->stdModuleSource.clear(); tc->stdCompatSource.clear(); + } // ── The target's C library, resolved like its compiler ───────── // + // (freestanding only: the C library column names picolibc, whose + // payload layout a freestanding spec describes. llvm-musl resolves + // its libc++ one block below instead.) + if (want->is_freestanding()) // The row in kKnownTargets names it, exactly as it names the // toolchain pin, and it is installed through the same channel a // project's `[xlings] deps` use (see the materialization above). @@ -1892,6 +1916,60 @@ prepare_build(bool print_fingerprint, } } + // ── The LLVM family's target C++ runtime on a hosted musl target ───── + // + // The gcc payload for *-linux-musl is self-contained: musl and libstdc++ + // ride inside it, and `effective_sysroot` above stays empty. A clang + // frontend brings no target libc at all — the same gap the bare-metal + // `sysroot` column closes for picolibc, one layer up. The target's row + // names an xim package carrying the target's musl libc++ (headers, + // archives, std.cppm); resolved here for the same reason the bare-metal + // sysroot is: the config is already open, and the flag builder only reads + // the result. + // + // Scoped to clang-on-musl so the gcc path is untouched. Absent package is + // not an error here either — the install may not have run on a first pass + // and the link will name what is missing. + if (mcpp::toolchain::is_clang(*tc) + && mcpp::toolchain::is_musl_target(*tc)) + { + if (const std::string want_libcxx = mcpp::toolchain::triple:: + effective_llvm_sysroot( + *mcpp::toolchain::triple::parse(tc->targetTriple), + sysroot_override(*m, *mcpp::toolchain::triple::parse( + tc->targetTriple))); + !want_libcxx.empty()) + { + if (auto cfg4 = get_cfg(); cfg4) { + auto ref = mcpp::xlings::paths::parse_xpkg_ref(want_libcxx); + auto xl = mcpp::config::make_xlings_env(**cfg4); + if (auto dir = mcpp::xlings::paths::xpkg_payload(xl, ref)) { + const auto inc = *dir / "include" / "c++" / "v1"; + const auto lib = *dir / "lib"; + std::error_code ec3; + tc->targetSysrootRoot = *dir; + tc->targetSysrootPkg = ref.name; + if (std::filesystem::is_directory(inc, ec3)) + tc->targetSysrootInclude = inc; + if (std::filesystem::is_directory(lib, ec3)) + tc->targetSysrootLib = lib; + // std.cppm from the payload replaces the host libc++ copy + // detection found — same reason the freestanding path + // clears it: a std BMI built over the wrong libc. + const auto stdcppm = *dir / "share" / "libc++" / "v1" + / "std.cppm"; + if (std::filesystem::exists(stdcppm, ec3)) + tc->stdModuleSource = stdcppm; + const auto compat = *dir / "share" / "libc++" / "v1" + / "std.compat.cppm"; + tc->stdCompatSource = std::filesystem::exists(compat, ec3) + ? compat + : std::filesystem::path{}; + } + } + } + } + // The Windows runtime identity, flowing BACK into the contract. // // Everything else about the runtime is known before a toolchain is diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 9b9c85ef..3089e581 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -34,12 +34,14 @@ import std; import mcpp.home; import mcpp.libs.json; import mcpp.platform; +import mcpp.platform.xlings; import mcpp.toolchain.clang; import mcpp.toolchain.detect; import mcpp.toolchain.fingerprint; import mcpp.toolchain.gcc; import mcpp.toolchain.hostflags; import mcpp.toolchain.linkmodel; +import mcpp.toolchain.model; import mcpp.toolchain.msvc; export namespace mcpp::toolchain { @@ -249,8 +251,46 @@ std::expected ensure_built( HostFlagOptions hopt; hopt.cfgBypass = HostFlagOptions::CfgBypass::Always; hopt.clangStdlibSelect = true; - std::string sysroot_flag = + std::string sysroot_flag; + // llvm-musl: the shared producer reconstructs the HOST's header world, + // which is wrong for the target whose std.cppm this is — the failure is + // the freestanding doc's `__config_site not found` one layer up. The + // target's musl libc++ headers and C library come from the payload + // prepare resolved (targetSysroot*) plus the musl-gcc sysroot. + if (is_clang(tc) && is_musl_target(tc) + && !tc.targetSysrootInclude.empty()) + { + // The musl C headers come from the musl-gcc payload's sysroot; the + // frontend locates it the same way the link side does (a + // triple-named sibling of the llvm payload under xpkgs). + auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(tc.binaryPath); + std::string sysroot; + if (xpkgs) { + // Two payload shapes, matching the registry's naming: the + // triple-named cross package and the host-native musl-gcc one + // (whose sysroot still lives under /). + const std::string shapes[] = { + std::format("xim-x-{}-gcc", tc.targetTriple), "xim-x-musl-gcc"}; + for (auto const& shape : shapes) { + auto gccRoot = *xpkgs / shape; + std::error_code ec; + if (!std::filesystem::is_directory(gccRoot, ec)) continue; + for (auto& e : std::filesystem::directory_iterator(gccRoot, ec)) + if (e.is_directory()) { + sysroot = (e.path() / tc.targetTriple).string(); + break; + } + if (!sysroot.empty()) break; + } + } + sysroot_flag = std::format( + " --no-default-config --target={} -nostdinc++ -stdlib=libc++" + " --sysroot={} -isystem'{}'", + tc.targetTriple, sysroot, tc.targetSysrootInclude.string()); + } else { + sysroot_flag = render_tokens(host_compile_tokens(tc, hopt, shellEsc)); + } // Deployment target appended here rather than passed to the producer // ONLY to keep this command string byte-identical to what earlier diff --git a/src/toolchain/triple.cppm b/src/toolchain/triple.cppm index c159d1db..88527def 100644 --- a/src/toolchain/triple.cppm +++ b/src/toolchain/triple.cppm @@ -112,6 +112,14 @@ struct TargetInfo { // it per-package bound a board-support package and a standard-library // subset alike to one libc, one ISA and one version. std::string_view sysroot; + // The TARGET's C++ runtime for the LLVM family on this target. The gcc + // payload for *-linux-musl is self-contained (musl + libstdc++ ride + // inside it), but a clang frontend brings no target libc at all — same + // gap the `sysroot` column closes for bare metal, one layer up. Names an + // xim package carrying the target's musl libc++ (headers, static + // archives, std.cppm). Empty = the LLVM family needs nothing beyond the + // frontend (hosted gnu, macOS, bare metal's own sysroot). + std::string_view llvmSysroot; bool defaultStatic; // target's default linkage is static }; @@ -119,15 +127,15 @@ struct TargetInfo { // from defaultStatic, so listing it here would duplicate it.) inline constexpr TargetInfo kKnownTargets[] = { // canonical tier note pin sysroot defaultStatic - { "x86_64-linux-gnu", "verified", "", "", "", false }, - { "x86_64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, - { "aarch64-linux-musl", "verified", "", "gcc@16.1.0", "", true }, - { "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", "", true }, - { "x86_64-windows-msvc", "verified", "PE", "", "", false }, - { "aarch64-macos", "verified", "", "", "", false }, - { "riscv64-linux-musl", "planned", "", "", "", true }, - { "aarch64-linux-gnu", "planned", "", "", "", false }, - { "x86_64-macos", "planned", "", "", "", false }, + { "x86_64-linux-gnu", "verified", "", "", "", "",false }, + { "x86_64-linux-musl", "verified", "", "gcc@16.1.0", "", "xim:llvm-musl-libcxx@22.1.8",true }, + { "aarch64-linux-musl", "verified", "", "gcc@16.1.0", "", "xim:llvm-musl-libcxx@22.1.8",true }, + { "x86_64-windows-gnu", "verified", "PE", "gcc@16.1.0", "", "",true }, + { "x86_64-windows-msvc", "verified", "PE", "", "", "",false }, + { "aarch64-macos", "verified", "", "", "", "",false }, + { "riscv64-linux-musl", "planned", "", "", "", "xim:llvm-musl-libcxx@22.1.8",true }, + { "aarch64-linux-gnu", "planned", "", "", "", "",false }, + { "x86_64-macos", "planned", "", "", "", "",false }, // Bare metal. `defaultStatic` is not a preference here — there is no // loader, so there is no other option. The pin is llvm on every host // because clang/lld are cross-compilers by construction: unlike the hosted @@ -136,8 +144,8 @@ inline constexpr TargetInfo kKnownTargets[] = { // which is the single place that decision is made. // The sysroot column is what keeps a bare-metal PACKAGE from having to // name a libc: the C library is the target's, like the compiler. - { "riscv64-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", true }, - { "riscv32-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", true }, + { "riscv64-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", "",true }, + { "riscv32-none-elf", "verified", "bare","llvm@22.1.8","xim:picolibc-riscv@1.8.12", "",true }, // ⚠️ AN EMPTY SYSROOT COLUMN, AND IT IS A STATEMENT RATHER THAN AN OMISSION. // // The two rows above name a C library because a project targeting them @@ -159,7 +167,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // an emulator. `xim:qemu-arm` provides `qemu-system-aarch64`; until a probe // has actually booted under it, claiming `verified` would be claiming the // measurement rather than reporting it. - { "aarch64-none-elf", "preview", "bare","llvm@22.1.8","", true }, + { "aarch64-none-elf", "preview", "bare","llvm@22.1.8","", "",true }, // ⚠️ THIS ROW EXISTS SO THAT A THIRD MACHINE CAN DISAGREE WITH THE FIRST // TWO, WHICH IS THE ONLY THING THAT TELLS AN ABSTRACTION FROM A HABIT. // @@ -180,7 +188,7 @@ inline constexpr TargetInfo kKnownTargets[] = { // The sysroot column is empty, the zero-libc tier, for the reason given // above `aarch64-none-elf`: the first consumer is `openarch`, which // references no C library symbol. - { "x86_64-none-elf", "preview", "bare","llvm@22.1.8","", true }, + { "x86_64-none-elf", "preview", "bare","llvm@22.1.8","", "",true }, }; inline std::span known_targets() { return kKnownTargets; } @@ -227,6 +235,18 @@ inline std::string effective_sysroot(const Triple& t, return {}; } +// The LLVM family's target C++ runtime for this target (see TargetInfo:: +// llvmSysroot). Same override-first shape as effective_sysroot so a project +// can pin a specific libc++ build for a target the table has not caught up +// with. Empty = the LLVM family needs nothing beyond its frontend. +inline std::string effective_llvm_sysroot(const Triple& t, + const std::string* override_) +{ + if (override_) return *override_; + if (auto* k = find_known_target(t)) return std::string(k->llvmSysroot); + return {}; +} + // Closest known-target canonical name for a mistyped `--target` (checked // against canonical names AND common alias spellings). nullopt when nothing // is plausibly close. diff --git a/tests/unit/test_toolchain_triple.cpp b/tests/unit/test_toolchain_triple.cpp index 6d4d9318..dcec82b4 100644 --- a/tests/unit/test_toolchain_triple.cpp +++ b/tests/unit/test_toolchain_triple.cpp @@ -256,3 +256,40 @@ TEST(Triple, EffectiveSysrootIsEmptyForHostedTargets) { ASSERT_TRUE(t.has_value()); EXPECT_EQ(effective_sysroot(*t, nullptr), ""); } + +// ── effective_llvm_sysroot: the LLVM family's target libc++ ───────────────── + +TEST(Triple, EffectiveLlvmSysrootNamesTheMuslLibcxxPayload) { + // The gcc payload for *-linux-musl is self-contained; a clang frontend + // is not, so the target row carries the libc++ payload for it. + auto t = parse("x86_64-linux-musl"); + ASSERT_TRUE(t.has_value()); + EXPECT_EQ(effective_llvm_sysroot(*t, nullptr), + "xim:llvm-musl-libcxx@22.1.8"); + auto a = parse("aarch64-linux-musl"); + ASSERT_TRUE(a.has_value()); + EXPECT_EQ(effective_llvm_sysroot(*a, nullptr), + "xim:llvm-musl-libcxx@22.1.8"); +} + +TEST(Triple, EffectiveLlvmSysrootOverrideWins) { + auto t = parse("x86_64-linux-musl"); + ASSERT_TRUE(t.has_value()); + const std::string kPinned = "xim:llvm-musl-libcxx@23.0.0"; + EXPECT_EQ(effective_llvm_sysroot(*t, &kPinned), kPinned); +} + +TEST(Triple, EffectiveLlvmSysrootIsEmptyWhereLlvmNeedsNothing) { + // Hosted gnu, macOS: the llvm frontend plus PayloadPaths cover it. + auto g = parse("x86_64-linux-gnu"); + ASSERT_TRUE(g.has_value()); + EXPECT_EQ(effective_llvm_sysroot(*g, nullptr), ""); + auto m = parse("aarch64-macos"); + ASSERT_TRUE(m.has_value()); + EXPECT_EQ(effective_llvm_sysroot(*m, nullptr), ""); + // Bare metal names its own C library in `sysroot`; the libc++ column + // stays empty there — the freestanding std subset is a package. + auto b = parse("riscv64-none-elf"); + ASSERT_TRUE(b.has_value()); + EXPECT_EQ(effective_llvm_sysroot(*b, nullptr), ""); +}