Skip to content

Commit b0747c9

Browse files
authored
feat(toolchain): hermetic link model — CRT/loader from one resolver, fixup pipeline, hermeticity check (v0.0.83, fixes #195) (#196)
* feat(toolchain): linkmodel — single resolver for the C-library link axis ToolchainLinkModel (CRT dir / lib dirs / loader / system includes, payload-first with --sysroot fallback) + ClangDriverModel (cfg-bypass driver flags), replacing the four divergent copies of this knowledge. Loader names come from data: declared payload exports (.xpkg-exports.json) -> per-arch triple map -> glob, never a hardcoded x86_64 string. Part of the hermetic toolchain link model (.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md, issue #195). * fix(build): CRT discovery via linkmodel — payload link gains -B (fixes #195) flags.cppm, stdmod.cppm and build_program.cppm host_base_flags now derive their sysroot/payload flags from the shared link model. The clang-with-cfg payload link path previously emitted only -L/-rpath/--dynamic-linker; the driver resolves Scrt1.o/crti.o/crtn.o through -B prefixes and sysroot paths only, so on hosts without a system toolchain it passed bare CRT names that lld cannot open (issue #195), and on hosts with one it silently linked the host's CRT. The model's link_flags carry -B<glibc-payload-lib>, so CRT objects now resolve inside the payload everywhere. build.mcpp host compiles stop trusting the sibling clang cfg (an install-time-generated, per-machine artifact) and use the same explicit flags as the main build. * refactor(toolchain): one post-install fixup pipeline + deterministic cfg regeneration ensure_post_install_fixup(cfg, payloadRoot, pkg) is now the single entry for toolchain post-install fixups (gcc: patchelf + specs; llvm: patchelf(lib) + cfg), called from all three install paths — explicit `toolchain install`, default-toolchain auto-install, and manifest [toolchain] auto-install. The manifest path previously ran NO fixup, so a fresh llvm auto-install kept its stale install-time cfg and unpatched runtime libs (how the #195 reporter's cfg still carried an install-time --sysroot while other installs didn't). Idempotent via a content-fingerprinted marker (<payload>/.mcpp-fixup.json: schema + kind + fixup rev + glibc lib) — drifted inputs re-run the fixup. The ownership guard (inherited/symlinked payloads are not ours to patch) now covers llvm too. fixup_clang_cfg no longer line-patches whatever a given install produced: it regenerates the cfg deterministically from the link model, so the same payload yields byte-identical cfgs on every machine and install path — and a human running clang++ directly now gets hermetic CRT discovery (-B) too. All loader paths come from resolve_loader (no ld-linux-x86-64 hardcodes). * refactor(probe): stop mining the clang cfg for --sysroot (diagnostic only) The cfg-mined sysroot was dead trust: mcpp's fixup pipeline regenerates the cfg without a --sysroot line (the C library comes from the payload link model), so the mined value existed only on never-fixed-up installs and pointed at an environment directory the payload doesn't own. Builds derive everything from the link model; the cfg serves humans running clang++ directly. The parse is kept as a debug log. * feat(build): hermetic link check — assert CRT/loader resolve inside the sandbox Dry-run the driver (-###) with the exact ldflags the build will use and assert every CRT object plus the EFFECTIVE dynamic linker (last occurrence wins — the driver emits its built-in default before the -Wl override) resolve under the sandbox's xpkgs registry or the toolchain sysroot. This converts two previously silent failure modes into one actionable build-time diagnostic: on hosts WITH a system toolchain the driver would quietly link the host's CRT (contamination that made CI green a false signal), and on hosts WITHOUT one it passed bare CRT names lld cannot open (issue #195). Sandbox toolchains only — a PATH/system compiler is the user's explicit choice of the host world. Verdict cached per flag-set (.mcpp-hermetic-ok). Escape hatches: [build] allow_host_libs = true or MCPP_ALLOW_HOST_LIBS=1. * refactor: loader paths from data everywhere — no ld-linux-x86-64 hardcodes fixup_gcc_specs detects the baked loader name from the specs content and replaces it with resolve_loader()'s answer (any glibc arch); the specs dir is discovered instead of assuming x86_64-linux-gnu. mcpp pack derives the BundleProject PT_INTERP distro path from the loader soname ldd resolved for the binary being packed (LSB /lib64 vs /lib), instead of a hardcoded x86_64 string. The only remaining literal loader names live in linkmodel.cppm's per-arch triple map. * test(e2e): 86_llvm_hermetic_link + llvm suite unpinned from 20.1.7 86 asserts, via a -### dry-run with the exact ldflags mcpp generated, that every CRT object and the effective dynamic linker resolve inside the sandbox (an xpkgs path) — failing on both regression modes of issue #195: bare CRT names (link failure on hosts without a system toolchain) and host-CRT contamination (silent on hosts with one). The llvm e2e scripts (36-41, 47, 65) source the new _llvm_env.sh instead of hardcoding llvm@20.1.7: MCPP_E2E_LLVM_VERSION overrides, default is the newest installed payload — so new toolchain versions get coverage the day they're installed instead of never. * ci: hermetic e2e job — no host toolchain container debian:stable-slim with NO compiler and NO host Scrt1.o: the only environment class that faithfully reproduces issue #195 (standard runners ship libc6-dev, so a sandbox toolchain leaking to the host CRT still links green there). Bootstraps xlings + released mcpp, builds the PR code with the sandbox gcc only, then runs the #195 manifest-llvm reproduction plus the hermetic e2e subset. * chore: 0.0.83 + changelog (hermetic toolchain link model) * fix(hermetic): allow the canonical xpkgs registry for symlink-inherited payloads mcpp passes symlink-view paths on the command line, but the clang driver reports its own resource dir (clang_rt.crt*) through the canonical path; with payloads symlink-inherited from another MCPP_HOME (the e2e isolation pattern) that canonical registry must be allowed too. * docs: hermetic toolchain link model design (issue #195) * fix(fixup): specs-grammar-safe loader detection + macOS keeps cfg-trust semantics Two regressions the first CI round caught (exactly the environments local verification can't fake): 1. gcc specs corruption (linux): the rewritten detect walked the loader path to 'whitespace/:;' — but specs embed the baked loader inside %-spec conditionals (%{mmusl:...;:/baked/ld-linux-x86-64.so.2}), so the scan swallowed closing braces and the rewrite corrupted the spec grammar ('braced spec body ... is invalid' from every g++ run after). Now: path-character whitelist scan, skip pristine /lib* multilib defaults, unit-tested against the real spec grammar (incl. aarch64 loader names). detect_baked_loader is exported for the tests. Fixup rev bumped (hermetic-2) so payloads stamped by the broken pass re-run the fixup. 2. macOS host/cfg semantics: the cfg-bypass host flags and the -nostdinc++/-stdlib=libc++ cfg regeneration are LINUX semantics; a bare macOS link has no libc++abi handling (that lives in the main build's needs_explicit_libcxx path) and died with undefined __cxa_* / __gxx_personality_v0. build.mcpp host compiles keep trusting the cfg off-Linux, and the macOS cfg keeps its historical shape (--sysroot=<sdk> + payload libc++ headers only). * ci: TEMP-DEBUG — capture core backtrace for the exit-time segfault (#196) * fix(fixup): patchelf a copy + atomic rename — never rewrite live mappings The fixup pipeline now runs on every toolchain install path, which means the process executing it can itself be linked against the very libraries being patched (a self-hosted mcpp loads the sandbox glibc/libgcc_s). In-place patchelf rewrites the backing file of those live mappings and corrupts the running process — reproduced deterministically on fresh- sandbox CI as an exit-time SIGSEGV in _dl_fini jumping to an unrelocated address (0x45a0), while a control run of main on the same fresh sandbox passed (the old code only ever patched from the statically-linked bootstrap mcpp, so the hazard was structural but unexposed). patchelf now operates on a copy and atomically rename()s it into place: the patched content gets a fresh inode, live processes keep the old one. * Revert "ci: TEMP-DEBUG — capture core backtrace for the exit-time segfault (#196)" This reverts commit 24d138e.
1 parent 5bfb2a1 commit b0747c9

29 files changed

Lines changed: 1886 additions & 467 deletions

.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md

Lines changed: 347 additions & 0 deletions
Large diffs are not rendered by default.

.github/workflows/ci-linux-e2e.yml

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,76 @@ jobs:
117117
# Warm musl once so fresh-home e2e tests inherit the payload.
118118
"$MCPP" toolchain install gcc 15.1.0-musl
119119
bash tests/e2e/run_all.sh
120+
121+
# ──────────────────────────────────────────────────────────────────
122+
# Hermetic (no host toolchain): the ONLY environment class that
123+
# faithfully reproduces issue #195. Standard runners ship gcc +
124+
# libc6-dev, so a sandbox toolchain that leaks to the host's CRT
125+
# still links "green" there; this container has no compiler and no
126+
# host Scrt1.o, so any leak fails loudly. Builds PR code with the
127+
# bootstrap mcpp, then runs the llvm flow end-to-end.
128+
# ──────────────────────────────────────────────────────────────────
129+
hermetic:
130+
name: hermetic e2e (no host toolchain, container)
131+
runs-on: ubuntu-24.04
132+
container: debian:stable-slim
133+
timeout-minutes: 60
134+
env:
135+
XLINGS_NON_INTERACTIVE: '1'
136+
steps:
137+
- name: Install base utilities (NO compiler)
138+
run: |
139+
apt-get update -qq
140+
apt-get install -y -qq curl ca-certificates git xz-utils unzip
141+
# The whole point of this job: no host toolchain, no host CRT.
142+
! command -v gcc
143+
! command -v cc
144+
test ! -e /usr/lib/x86_64-linux-gnu/Scrt1.o
145+
test ! -e /usr/lib/gcc
146+
147+
- uses: actions/checkout@v4
148+
149+
# Payload cache (downloads only — the container still has no host
150+
# toolchain, which is the property under test).
151+
- name: Cache mcpp sandbox payloads
152+
uses: actions/cache@v4
153+
with:
154+
path: ~/.mcpp
155+
key: mcpp-hermetic-${{ hashFiles('mcpp.toml') }}
156+
restore-keys: |
157+
mcpp-hermetic-
158+
159+
- name: Bootstrap xlings + released mcpp
160+
run: |
161+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v0.4.62
162+
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
163+
xlings update
164+
xlings install mcpp -y -g
165+
MCPP_BOOT="$HOME/.xlings/subos/current/bin/mcpp"
166+
"$MCPP_BOOT" --version
167+
"$MCPP_BOOT" self config --mirror GLOBAL
168+
echo "MCPP_BOOT=$MCPP_BOOT" >> "$GITHUB_ENV"
169+
170+
- name: Build PR mcpp from source (sandbox gcc only)
171+
run: |
172+
"$MCPP_BOOT" build
173+
MCPP=$(realpath "$(find target -type f -name mcpp -printf '%T@ %p\n' | sort -rn | head -1 | cut -d' ' -f2)")
174+
test -x "$MCPP"
175+
"$MCPP" --version
176+
echo "MCPP=$MCPP" >> "$GITHUB_ENV"
177+
178+
- name: "issue #195 reproduction: manifest llvm toolchain, fresh"
179+
run: |
180+
cd "$(mktemp -d)"
181+
"$MCPP" new hello195
182+
cd hello195
183+
printf '\n[toolchain]\nlinux = "llvm@22.1.8"\n' >> mcpp.toml
184+
printf 'import std;\nint main() { std::println("hello {}", 195); return 0; }\n' > src/main.cpp
185+
"$MCPP" run
186+
187+
- name: Hermetic llvm e2e subset
188+
run: |
189+
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
190+
export MCPP
191+
bash tests/e2e/86_llvm_hermetic_link.sh
192+
bash tests/e2e/37_llvm_import_std.sh

CHANGELOG.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,37 @@
33
> 本文件追踪 `mcpp-community/mcpp` 公开仓的版本演进。
44
> 格式参考 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/)
55
6+
## [0.0.83] — 2026-07-07
7+
8+
### 修复
9+
10+
- **Linux llvm 工具链链接失败 `cannot open Scrt1.o/crti.o/crtn.o`(#195)**:clang-with-cfg
11+
的 payload 链接路径此前只带 `-L/-rpath/--dynamic-linker`,缺少 CRT 启动对象的发现前缀
12+
`-B<glibc payload lib>`——driver 查找 `Scrt1.o/crti.o/crtn.o` 只走 `-B` 前缀与 sysroot
13+
派生路径,不查 `-L`。在装有宿主 libc6-dev 的机器上 driver 会静默兜底宿主 `/lib` 的 CRT
14+
(污染式"假绿"),在没有的机器(如全新 WSL2)上则把裸文件名传给 lld 直接失败。
15+
16+
### 新增 / 架构
17+
18+
- **工具链链接模型单一化(hermetic toolchain link model)**:新增 `mcpp.toolchain.linkmodel`
19+
作为「如何对该工具链的 C 库编译/链接」的唯一解析器(payload-first,--sysroot 回退),
20+
`flags` / `stdmod` / `build_program` / cfg 再生全部消费同一模型,消除四份漂移实现;
21+
动态链接器名按 声明式 payload 元数据 → 按 triple 的 arch 映射 → glob 三级解析,全链
22+
不再硬编码 `ld-linux-x86-64.so.2`(aarch64 glibc 的 loader 障碍随之消除)。详见
23+
`.agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md`
24+
- **post-install fixup 归位为统一管线**:`ensure_post_install_fixup` 成为所有工具链安装
25+
路径(显式 install / 默认工具链 auto-install / manifest `[toolchain]` auto-install)共享
26+
的唯一 fixup 入口,内容指纹 marker 幂等;此前 manifest 路径不跑任何 fixup。clang cfg
27+
由行级补丁改为从链接模型**确定性再生**(同一 payload 在任何机器/安装路径产出一致 cfg,
28+
人类直接使用 `clang++` 同样获得 hermetic 的 CRT 发现)。
29+
- **hermetic 链接校验**:构建前用 `-###` 干跑断言 CRT 对象与生效 dynamic linker 全部解析
30+
在沙箱(xpkgs registry)内,越界即报错并指明泄漏路径;逃生阀
31+
`[build] allow_host_libs = true` / `MCPP_ALLOW_HOST_LIBS=1`。按 flag 集缓存判定。
32+
- **测试与 CI**:新增 e2e `86_llvm_hermetic_link.sh`(`-###` 前缀断言,双向防「链接失败」
33+
与「宿主污染」回归);llvm e2e 解除 20.1.7 硬 pin(`MCPP_E2E_LLVM_VERSION`,默认最新
34+
已装 payload);ci-linux-e2e 新增 **无宿主工具链容器 job**(debian:stable-slim,无 gcc /
35+
无宿主 CRT)——唯一能真实复现 #195 环境类的 CI 形态。
36+
637
## [0.0.71] — 2026-06-29
738

839
### 新增

mcpp.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "mcpp"
3-
version = "0.0.82"
3+
version = "0.0.83"
44
description = "Modern C++ build & package management tool"
55
license = "Apache-2.0"
66
authors = ["mcpp-community"]

src/build/build_program.cppm

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,10 @@ export module mcpp.build.build_program;
1414

1515
import std;
1616
import mcpp.manifest;
17+
import mcpp.platform;
1718
import mcpp.platform.process;
1819
import mcpp.toolchain.fingerprint; // hash_file / hash_string (FNV-1a, 16 hex)
20+
import mcpp.toolchain.linkmodel; // shared C-library / clang-cfg-bypass model
1921
import mcpp.toolchain.model; // Toolchain, PayloadPaths, is_clang/is_musl_target
2022
import mcpp.toolchain.registry; // archive_tool
2123
import mcpp.ui;
@@ -116,21 +118,63 @@ std::string env_value(const std::string& name) {
116118
// only the native cases; these are passed as separate argv tokens (no shell).
117119
std::vector<std::string> host_base_flags(const mcpp::toolchain::Toolchain& tc) {
118120
std::vector<std::string> f;
119-
// Clang reads its sibling `<clang>.cfg` by default, which wires libc++ + the
120-
// sysroot. A simple host compile trusts it (the main build bypasses the cfg
121-
// for reproducibility; here correctness on a fresh box is all we need).
122-
if (mcpp::toolchain::is_clang(tc)) return f;
121+
const auto lm = mcpp::toolchain::resolve_link_model(tc);
122+
123+
// Clang with a bundled cfg on LINUX: bypass it (--no-default-config) and
124+
// provide everything explicitly, same as the main build — the cfg is an
125+
// install-time-generated artifact, so trusting it here while bypassing
126+
// it in the main build meant two different toolchains for one project.
127+
// On macOS/Windows keep trusting the cfg: the macOS link additionally
128+
// needs the platform's libc++abi/unwind handling that the main build's
129+
// needs_explicit_libcxx path owns (duplicating it for a host compile
130+
// produced undefined __cxa_*/__gxx_personality_v0), and the fixup
131+
// pipeline regenerates the cfg deterministically anyway.
132+
if (mcpp::toolchain::is_clang(tc)) {
133+
if constexpr (!mcpp::platform::is_linux) return f;
134+
const auto dm = mcpp::toolchain::resolve_clang_driver(tc);
135+
if (dm.hasCfg) {
136+
f.push_back("--no-default-config");
137+
f.push_back("-nostdinc++");
138+
f.push_back("-stdlib=libc++");
139+
for (auto& inc : dm.cxxIncludes) f.push_back("-isystem" + inc.string());
140+
f.push_back("-fuse-ld=lld");
141+
f.push_back("--rtlib=compiler-rt");
142+
f.push_back("--unwindlib=libunwind");
143+
for (auto& d : dm.libDirs) {
144+
f.push_back("-L" + d.string());
145+
f.push_back("-Wl,-rpath," + d.string());
146+
}
147+
}
148+
if (lm.mode == mcpp::toolchain::CLibMode::Sysroot) {
149+
f.push_back("--sysroot=" + lm.sysroot.string());
150+
} else if (lm.mode == mcpp::toolchain::CLibMode::PayloadFirst) {
151+
for (auto& inc : lm.systemIncludes) f.push_back("-isystem" + inc.string());
152+
f.push_back("-B" + lm.crtDir.string()); // Scrt1.o/crti.o discovery
153+
for (auto& d : lm.libDirs) {
154+
f.push_back("-L" + d.string());
155+
f.push_back("-Wl,-rpath," + d.string());
156+
}
157+
if (!lm.loader.empty())
158+
f.push_back("-Wl,--dynamic-linker=" + lm.loader.string());
159+
}
160+
// Runtime lib dirs so the produced program can load private libs in-tree.
161+
for (auto& d : tc.linkRuntimeDirs) {
162+
f.push_back("-L" + d.string());
163+
f.push_back("-Wl,-rpath," + d.string());
164+
}
165+
return f;
166+
}
123167

124168
// GCC: a fresh sandbox g++ needs --sysroot to find the C library + the
125169
// include-fixed headers; without a sysroot, wire the glibc payload directly.
126-
if (!tc.sysroot.empty()) {
127-
f.push_back("--sysroot=" + tc.sysroot.string());
128-
} else if (tc.payloadPaths) {
129-
auto& pp = *tc.payloadPaths;
130-
f.push_back("-idirafter"); f.push_back(pp.glibcInclude.string());
131-
if (!pp.linuxInclude.empty()) { f.push_back("-idirafter"); f.push_back(pp.linuxInclude.string()); }
132-
f.push_back("-B" + pp.glibcLib.string()); // crt1.o/crti.o discovery
133-
f.push_back("-L" + pp.glibcLib.string()); // -lc/-lm resolution
170+
if (lm.mode == mcpp::toolchain::CLibMode::Sysroot) {
171+
f.push_back("--sysroot=" + lm.sysroot.string());
172+
} else if (lm.mode == mcpp::toolchain::CLibMode::PayloadFirst) {
173+
for (auto& inc : lm.systemIncludes) {
174+
f.push_back("-idirafter"); f.push_back(inc.string());
175+
}
176+
f.push_back("-B" + lm.crtDir.string()); // crt1.o/crti.o discovery
177+
for (auto& d : lm.libDirs) f.push_back("-L" + d.string()); // -lc/-lm
134178
}
135179
// binutils -B so the driver finds ld/as (GCC, non-musl; musl ships its own).
136180
if (!mcpp::toolchain::is_musl_target(tc)) {

src/build/flags.cppm

Lines changed: 39 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import mcpp.build.plan;
1616
import mcpp.platform;
1717
import mcpp.toolchain.clang;
1818
import mcpp.toolchain.detect;
19+
import mcpp.toolchain.linkmodel;
1920
import mcpp.toolchain.provider;
2021
import mcpp.toolchain.registry;
2122

@@ -153,34 +154,28 @@ CompileFlags compute_flags(const BuildPlan& plan) {
153154
include_flags += " -I" + escape_path(abs);
154155
}
155156

156-
// Sysroot / payload paths.
157-
//
158-
// Payload-first: when PayloadPaths are available (glibc + linux-headers
159-
// xpkgs found), use -isystem for each payload include dir. This avoids
160-
// dependency on xlings subos.
161-
//
162-
// For Clang with a cfg file: use --no-default-config to bypass
163-
// potentially-stale paths, then provide all flags explicitly.
164-
//
165-
// Fallback: if no PayloadPaths, use --sysroot from probe_sysroot().
157+
// Sysroot / payload paths — resolved ONCE by the toolchain link model
158+
// (mcpp.toolchain.linkmodel, the single source of truth shared with
159+
// stdmod / build_program / the cfg fixup; see
160+
// .agents/docs/2026-07-07-hermetic-toolchain-link-model-design.md).
161+
// Payload-first, --sysroot fallback; for Clang with a cfg file we bypass
162+
// the (install-time-generated, non-reproducible) cfg with
163+
// --no-default-config and provide everything explicitly.
164+
const auto dm = mcpp::toolchain::resolve_clang_driver(plan.toolchain);
165+
const auto lm = mcpp::toolchain::resolve_link_model(plan.toolchain);
166+
const mcpp::toolchain::PathEscape ninjaEsc =
167+
[](const std::filesystem::path& p) { return escape_path(p); };
168+
166169
std::string compile_toolchain_flags;
167170
std::string link_toolchain_flags;
168-
bool isClangWithCfg = false;
169-
std::filesystem::path cfgPath;
171+
const bool isClangWithCfg = dm.hasCfg;
170172
// LLVM root of a clang-with-cfg toolchain — used by the macOS link
171173
// path below to locate libc++.a/libc++abi.a for staticStdlib.
172174
std::filesystem::path llvmRootForStdlib;
173-
if (mcpp::toolchain::is_clang(plan.toolchain)) {
174-
cfgPath = plan.toolchain.binaryPath.parent_path()
175-
/ (plan.toolchain.binaryPath.stem().string() + ".cfg");
176-
isClangWithCfg = std::filesystem::exists(cfgPath);
177-
}
178175

179176
if (isClangWithCfg) {
180-
// Clang with cfg: bypass cfg and provide all paths explicitly.
181-
auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path();
182-
auto libcxxInclude = llvmRoot / "include" / "c++" / "v1";
183-
compile_toolchain_flags = " --no-default-config -nostdinc++";
177+
// --no-default-config -nostdinc++ + libc++ headers.
178+
compile_toolchain_flags = dm.compile_flags(ninjaEsc);
184179
// macOS deployment target: make the resolved value explicit on
185180
// the command line so (a) the ninja commands don't depend on env
186181
// propagation and (b) the value participates in the BMI
@@ -193,71 +188,22 @@ CompileFlags compute_flags(const BuildPlan& plan) {
193188
compile_toolchain_flags +=
194189
" -mmacosx-version-min=" + macosDeploymentTarget;
195190
}
196-
llvmRootForStdlib = llvmRoot;
197-
// libc++ headers
198-
compile_toolchain_flags += " -isystem" + escape_path(libcxxInclude);
199-
if (!plan.toolchain.targetTriple.empty()) {
200-
auto targetInclude = llvmRoot / "include"
201-
/ plan.toolchain.targetTriple / "c++" / "v1";
202-
if (std::filesystem::exists(targetInclude))
203-
compile_toolchain_flags += " -isystem" + escape_path(targetInclude);
204-
}
205-
// C library + kernel headers from payload
206-
if (plan.toolchain.payloadPaths) {
207-
auto& pp = *plan.toolchain.payloadPaths;
208-
compile_toolchain_flags += " -isystem" + escape_path(pp.glibcInclude);
209-
if (!pp.linuxInclude.empty())
210-
compile_toolchain_flags += " -isystem" + escape_path(pp.linuxInclude);
211-
} else if (auto sdk = mcpp::platform::macos::sdk_path()) {
212-
auto sysroot_flag = " --sysroot=" + escape_path(*sdk);
213-
compile_toolchain_flags += sysroot_flag;
214-
link_toolchain_flags += sysroot_flag;
215-
} else if (!plan.toolchain.sysroot.empty()) {
216-
auto sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot);
217-
compile_toolchain_flags += sysroot_flag;
218-
link_toolchain_flags += sysroot_flag;
219-
}
220-
// Linker flags that cfg normally provides
221-
link_toolchain_flags = " --no-default-config" + link_toolchain_flags
222-
+ " -stdlib=libc++ -fuse-ld=lld --rtlib=compiler-rt --unwindlib=libunwind";
191+
llvmRootForStdlib = dm.llvmRoot;
192+
// C library headers (payload -isystem, or --sysroot fallback).
193+
compile_toolchain_flags += lm.compile_flags(ninjaEsc);
194+
// Linker flags that cfg normally provides. The payload C-runtime
195+
// flags (-B/-L/loader) are appended via payload_ld below.
196+
link_toolchain_flags = " --no-default-config";
197+
if (lm.mode == mcpp::toolchain::CLibMode::Sysroot)
198+
link_toolchain_flags += lm.link_flags(ninjaEsc);
199+
link_toolchain_flags +=
200+
mcpp::toolchain::ClangDriverModel::kLinkDriverFlags;
223201
f.sysroot = link_toolchain_flags;
224-
} else if (!plan.toolchain.sysroot.empty()) {
225-
// GCC (or Clang without cfg): use --sysroot from probe.
226-
// GCC requires --sysroot for include-fixed headers (stdlib.h wrapper).
227-
// Supplement with -isystem for linux kernel headers from payload
228-
// if the probed sysroot is missing them.
229-
auto sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot);
230-
compile_toolchain_flags = sysroot_flag;
231-
link_toolchain_flags = sysroot_flag;
232-
// Self-contained musl toolchains ship their own kernel headers in the
233-
// sysroot; for a cross target the host (x86) linux-headers payload is
234-
// the wrong arch, so don't supplement it.
235-
if (!mcpp::toolchain::is_musl_target(plan.toolchain)
236-
&& plan.toolchain.payloadPaths && !plan.toolchain.payloadPaths->linuxInclude.empty()) {
237-
auto sysrootLinux = plan.toolchain.sysroot / "usr" / "include" / "linux" / "limits.h";
238-
if (!std::filesystem::exists(sysrootLinux))
239-
compile_toolchain_flags += " -isystem" + escape_path(plan.toolchain.payloadPaths->linuxInclude);
240-
}
241-
f.sysroot = link_toolchain_flags;
242-
} else if (plan.toolchain.payloadPaths) {
243-
// No usable sysroot: wire the C library headers from the payload.
244-
// For GCC use -idirafter (appended after the built-in dirs) so that
245-
// libstdc++'s #include_next wrappers can reach them; -isystem would
246-
// place them BEFORE the built-ins, invisible to #include_next.
247-
auto& pp = *plan.toolchain.payloadPaths;
248-
const bool clangTc = mcpp::toolchain::is_clang(plan.toolchain);
249-
auto inc_flag = [&](const std::filesystem::path& p) {
250-
return (clangTc ? " -isystem" : " -idirafter") + escape_path(p);
251-
};
252-
compile_toolchain_flags += inc_flag(pp.glibcInclude);
253-
if (!pp.linuxInclude.empty())
254-
compile_toolchain_flags += inc_flag(pp.linuxInclude);
255-
// Link-time C runtime: a usable --sysroot would have provided the
256-
// startup objects and core libs implicitly. Without one, point the
257-
// driver at the glibc payload lib dir: -B for crt1.o/crti.o discovery,
258-
// -L for -lm/-lc resolution.
259-
link_toolchain_flags += " -B" + escape_path(pp.glibcLib);
260-
link_toolchain_flags += " -L" + escape_path(pp.glibcLib);
202+
} else if (lm.mode != mcpp::toolchain::CLibMode::None) {
203+
// GCC (or Clang without cfg): --sysroot from probe, or the payload
204+
// headers + C runtime (-B for crt discovery, -L for -lc/-lm).
205+
compile_toolchain_flags = lm.compile_flags(ninjaEsc);
206+
link_toolchain_flags = lm.link_flags(ninjaEsc);
261207
f.sysroot = link_toolchain_flags;
262208
}
263209

@@ -358,16 +304,15 @@ CompileFlags compute_flags(const BuildPlan& plan) {
358304
}
359305
}
360306

361-
// For Clang with payload paths: add glibc lib + dynamic linker to link flags.
307+
// For Clang with payload paths: the payload C runtime — -B so the driver
308+
// resolves Scrt1.o/crti.o/crtn.o inside the payload (the driver never
309+
// consults -L for CRT objects; without -B it silently falls back to the
310+
// host's /lib or, on hosts without a system toolchain, passes bare names
311+
// that lld cannot open — issue #195), -L/-rpath for -lc/-lm, and the
312+
// payload's dynamic linker.
362313
std::string payload_ld;
363-
if (isClangWithCfg && plan.toolchain.payloadPaths) {
364-
auto& pp = *plan.toolchain.payloadPaths;
365-
payload_ld += " -L" + escape_path(pp.glibcLib);
366-
payload_ld += " -Wl,-rpath," + escape_path(pp.glibcLib);
367-
auto loader = pp.glibcLib / "ld-linux-x86-64.so.2";
368-
if (std::filesystem::exists(loader))
369-
payload_ld += " -Wl,--dynamic-linker=" + escape_path(loader);
370-
}
314+
if (isClangWithCfg && lm.mode == mcpp::toolchain::CLibMode::PayloadFirst)
315+
payload_ld = lm.link_flags(ninjaEsc);
371316

372317
std::string link_extra;
373318
if (prof.lto) link_extra += " -flto";

0 commit comments

Comments
 (0)