Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .agents/docs/2026-08-23-llvm-musl-target-design.md
Original file line number Diff line number Diff line change
@@ -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 的二进制,不重复分发)
- `<prefix>/musl/<triple>/`: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=<musl-gcc>
-rtlib=libgcc -unwindlib=libgcc -nostdinc++ -isystem <libcxx>/include/c++/v1
-nostdlib++ -L<libcxx>/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@<v> --target <triple>-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 结构里的显式
开关而非渲染层字符串(倾向后者先行,够用)
87 changes: 84 additions & 3 deletions src/build/flags.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -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 `<triple>-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))
{
// <xim-x-...>/<version> — 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;
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down
90 changes: 84 additions & 6 deletions src/build/prepare.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand All @@ -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).
Expand Down Expand Up @@ -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
Expand Down
42 changes: 41 additions & 1 deletion src/toolchain/stdmod.cppm
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -249,8 +251,46 @@ std::expected<StdModule, StdModError> 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 <triple>/).
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
Expand Down
Loading
Loading