From acc1a71a7dd9a0a15e076fcef4f6f41d1b93abd3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:11:40 +0800 Subject: [PATCH 1/4] fix: remove subos sysroot override, use payload paths for toolchain sysroot (#62) Remove M5.5 logic that forced tc->sysroot to mcpp's xlings subos (~/.mcpp/registry/subos/default). The subos created by `xlings self init` lacks linux kernel headers (linux/limits.h, asm/, asm-generic/), causing std module precompilation to fail on user machines. Root cause: commit 063fb6f changed MCPP_HOME to ~/.mcpp/, where the subos exists but is incomplete. M5.5 only checked exists(usr/include) and overwrote the toolchain's correct sysroot. CI never caught this because its subos is fully populated by `xlings self install`. Design principle: mcpp uses xlings only as a package index + download tool. Sysroot comes from the toolchain payload itself, not from subos. Changes: - cli.cppm: delete M5.5 subos sysroot override entirely - probe.cppm: parse clang++.cfg --sysroot= when -print-sysroot fails (Clang doesn't support -print-sysroot), so tc->sysroot reflects the payload's actual configuration - stdmod.cppm: generalize --no-default-config from macOS-only to all Clang toolchains with a cfg file (cfg paths become stale after mcpp copies the payload to its sandbox) - flags.cppm: sync the same --no-default-config logic for regular compilation flags --- ...21-linux-sysroot-missing-kernel-headers.md | 370 ++++++++++++++++++ src/build/flags.cppm | 38 +- src/cli.cppm | 26 +- src/toolchain/probe.cppm | 27 +- src/toolchain/stdmod.cppm | 39 +- 5 files changed, 444 insertions(+), 56 deletions(-) create mode 100644 .agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md diff --git a/.agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md b/.agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md new file mode 100644 index 0000000..01f5391 --- /dev/null +++ b/.agents/docs/2026-05-21-linux-sysroot-missing-kernel-headers.md @@ -0,0 +1,370 @@ +# Linux sysroot 缺少内核头文件导致 std module 预编译失败 + +## 现象 + +在用户机器上(非 CI),使用 LLVM 或 GCC 工具链执行 `mcpp run` / `mcpp build` 时, +std module 预编译失败: + +``` +/home/speak/.mcpp/registry/subos/default/usr/include/bits/local_lim.h:38:10: +fatal error: 'linux/limits.h' file not found +``` + +GCC 和 Clang 均受影响,问题是系统性的。 + +## 根因 + +### 直接原因:M5.5 sysroot 覆盖逻辑 + +`cli.cppm:1192-1203` 中的 M5.5 逻辑,将 `tc->sysroot` 强制覆盖为 mcpp 自己的 +subos(`~/.mcpp/registry/subos/default`): + +```cpp +if (!isMuslTc) { + if (auto cfg = get_cfg(); cfg) { + auto mcppSubos = (*cfg)->xlingsHome() / "subos" / "default"; + if (std::filesystem::exists(mcppSubos / "usr" / "include")) { + tc->sysroot = mcppSubos; + } + } +} +``` + +### 触发 commit + +**`063fb6f`** — 将 MCPP_HOME 从 xpkgs 包目录改为 `~/.mcpp/`,使 M5.5 +找到一个存在但不完整的 subos。 + +### CI/e2e 未发现的原因 + +CI 的 subos 由 `xlings self install` 完整初始化(含内核头文件),e2e 测试 +通过 `_inherit_toolchain.sh` 继承宿主完整 subos。 + +--- + +## 设计分析:当前问题的本质 + +### 当前架构的矛盾 + +mcpp 的工具链管理存在一个架构层面的矛盾: + +``` + ┌─────────────────────────┐ + │ xlings 下载 payload │ + │ ~/.xlings/data/xpkgs/ │ + │ (cfg/specs 路径正确) │ + └──────────┬──────────────┘ + │ copy (因 XLINGS_HOME 传播不可靠) + ▼ + ┌─────────────────────────┐ + │ mcpp sandbox 副本 │ + │ ~/.mcpp/registry/xpkgs/ │ + │ (cfg/specs 路径 stale!) │ + └──────────┬──────────────┘ + │ + ┌──────────────┼──────────────┐ + ▼ ▼ ▼ + GCC: specs Clang macOS: Clang Linux: + fixup 修复 --no-default 没有修复 ← BUG + 路径 ✅ -config ✅ 用 subos 补救 ✗ +``` + +**三个平台/工具链的 stale path 问题,用了三种不同的解法:** + +| 工具链 | stale path 来源 | 当前解法 | 状态 | +|--------|-----------------|---------|------| +| GCC (Linux) | specs 文件 | `fixup_gcc_specs()` 重写 specs | ✅ 正确但只修了 specs,没修 `-print-sysroot` | +| Clang (macOS) | clang++.cfg | `--no-default-config` + xcrun | ✅ 但只在 macOS | +| Clang (Linux) | clang++.cfg | M5.5 subos 覆盖 | ❌ subos 不完整 | + +**根本问题:mcpp 复制了 payload 但没有统一处理 stale path。** + +### mcpp 对 xlings 的依赖边界不清 + +当前 mcpp 对 xlings 有三层依赖: + +| 层次 | 内容 | 应该依赖? | +|------|------|-----------| +| 包下载 | xlings 作为包索引 + 下载工具 | ✅ 是 | +| payload 路径 | xpkgs 下具体包的文件结构 | ✅ 是 | +| subos | xlings 的沙箱 sysroot | ❌ 不应该 | + +M5.5 的问题就在于跨越了第三层边界——用 xlings 的内部实现细节(subos) +来补救 mcpp 自身的路径问题。 + +--- + +## 设计方案 + +### 核心原则 + +**mcpp 只把 xlings 当包索引 + 下载工具。工具链的编译环境由 payload 自描述, +mcpp 忠实读取,不替换、不覆盖。** + +### 方案:Payload-first + 统一 stale path 处理 + +#### 1. 工具链 sysroot 来源:只从 payload 获取 + +``` +┌─────────────────────────────────────────────────┐ +│ sysroot 解析优先级 │ +│ │ +│ 1. compiler -print-sysroot (GCC 原生支持) │ +│ → 路径存在则使用 │ +│ │ +│ 2. payload cfg 文件解析 (Clang clang++.cfg) │ +│ → 解析 --sysroot= 行,路径存在则使用 │ +│ │ +│ 3. macOS: xcrun --show-sdk-path │ +│ │ +│ 4. 空 (不传 --sysroot,让编译器用自身默认值) │ +│ │ +│ ✗ 不再有 subos fallback │ +└─────────────────────────────────────────────────┘ +``` + +**改动**: +- 删除 `cli.cppm` M5.5 subos 覆盖代码 +- `probe.cppm` 增加 cfg 文件解析作为第 2 优先级 + +#### 2. 复制 payload 时统一修复 stale path + +当前只有 GCC 做了 specs fixup,Clang 只在 macOS 做了 `--no-default-config`。 +应该统一为:**凡是复制了 payload,就修复其中的路径配置。** + +``` + copy payload 后 + │ + ┌────────┼────────┐ + ▼ ▼ ▼ + GCC specs Clang cfg 其他配置 + │ │ + ▼ ▼ + rewrite_gcc rewrite_cfg + _specs() _paths() + │ │ + ▼ ▼ + 新 sysroot 新 sysroot + 新 rpath 新 -isystem + 新 -L/-rpath +``` + +**具体做法**:在 `package_fetcher.cppm` 复制 payload 后(或在 `cli.cppm` +toolchain install 后),对 Clang cfg 做类似 `fixup_gcc_specs` 的路径重写: + +```cpp +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, + const std::filesystem::path& newSysroot) { + auto cfgPath = payloadRoot / "bin" / "clang++.cfg"; + if (!std::filesystem::exists(cfgPath)) return; + + // 读取 cfg,将旧 sysroot/isystem/rpath 路径替换为 payload 实际位置 + // ... +} +``` + +**但这里有一个关键设计选择:新路径指向哪里?** + +#### 3. 关于 sysroot 本身从哪来 + +工具链需要 C 库头文件(glibc headers + linux kernel headers)。来源有三种: + +| 来源 | 说明 | 优劣 | +|------|------|------| +| 系统 `/usr/include` | 宿主机自带 | 简单,但不可控,不同发行版不同 | +| xlings subos | xlings 管理的沙箱 sysroot | 可控,但 mcpp 需依赖 xlings 内部结构 | +| payload 自带 | 工具链包自带 sysroot(如 musl-gcc) | 最干净,但需要上游包支持 | + +**推荐策略**: + +- **短期**:信任 payload 自身配置的 sysroot 路径。xlings 安装 GCC/LLVM 时 + 已经配置好了 sysroot(指向 xlings 自己的 subos),mcpp 只需忠实读取。 + 如果路径存在且有效,就用它。如果路径无效,不传 `--sysroot`,让编译器 + 用系统默认路径。 + +- **中期**:推动 xlings 上游让 LLVM/GCC 包的 cfg/specs 使用相对路径或 + 可配置路径,避免硬编码绝对路径。这从源头消除 stale path 问题。 + +- **长期**:mcpp 自带轻量 sysroot 管理(类似 Zig 的做法:打包 libc headers + 作为 mcpp 自身的资源),彻底不依赖宿主系统或 xlings 的 sysroot。但这是 + 大工程,不急。 + +--- + +## 修复方案(基于以上设计) + +### Phase 1:修复当前 bug(最小改动) + +#### P1-1:删除 M5.5 subos 覆盖 + +**文件**:`src/cli.cppm:1192-1203` + +**删除**整个代码块。工具链的 sysroot 由 payload 决定,mcpp 不介入。 + +同时删除 `cli.cppm:1001` 的 subos 注释和 `cli.cppm:1178` 的 "glibc subos" 注释。 + +#### P1-2:`probe_sysroot` 增加 cfg 解析 + +**文件**:`src/toolchain/probe.cppm:254-272` + +`-print-sysroot` 失败后(Clang 不支持),解析 payload 中 `clang++.cfg` +的 `--sysroot=` 行: + +```cpp +std::filesystem::path +probe_sysroot(const std::filesystem::path& compilerBin, + const std::string& envPrefix) { + // 1. -print-sysroot (GCC) + auto r = run_capture(std::format("{}{} -print-sysroot {}", + envPrefix, + mcpp::xlings::shq(compilerBin.string()), + mcpp::platform::null_redirect)); + if (r) { + auto s = trim_line(*r); + if (!s.empty() && std::filesystem::exists(s)) return s; + } + + // 2. Parse payload cfg (Clang) + auto cfgPath = compilerBin.parent_path() + / (compilerBin.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + std::ifstream ifs(cfgPath); + std::string line; + while (std::getline(ifs, line)) { + constexpr std::string_view prefix = "--sysroot="; + if (line.starts_with(prefix)) { + auto val = trim_line(std::string(line.substr(prefix.size()))); + if (!val.empty() && std::filesystem::exists(val)) + return val; + } + } + } + + // 3. macOS: xcrun SDK + if (auto sdk = mcpp::platform::macos::sdk_path()) + return *sdk; + return {}; +} +``` + +**Phase 1 效果**: +- Clang:cfg 中的 `--sysroot=~/.xlings/subos/default` 被正确读取, + `tc->sysroot` 不再为空。stdmod.cppm 和 flags.cppm 传递正确的 sysroot。 +- GCC:`-print-sysroot` 正常工作(如果路径存在);若不存在则 sysroot 为空, + GCC 用默认系统路径(`/usr/include`)。 +- 不再依赖 subos。 + +### Phase 2:统一 Clang stale cfg 处理(消除隐患) + +#### P2-1:`stdmod.cppm` — 所有有 cfg 的 Clang 都走 `--no-default-config` + +**文件**:`src/toolchain/stdmod.cppm:103-116` + +将 macOS 特有的 `--no-default-config` 逻辑泛化为"有 cfg 文件的 Clang": + +```cpp +std::string sysroot_flag; +if (is_clang(tc)) { + auto cfgPath = tc.binaryPath.parent_path() + / (tc.binaryPath.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + // Bypass cfg (may have stale paths after payload copy). + // Provide correct flags from payload structure directly. + auto llvmRoot = tc.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); + if (!tc.sysroot.empty()) + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); + else if (auto sdk = mcpp::platform::macos::sdk_path()) + sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + } else if (!tc.sysroot.empty()) { + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); + } +} else if (!tc.sysroot.empty()) { + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); +} +``` + +#### P2-2:`flags.cppm` — 同步修改 + +**文件**:`src/build/flags.cppm:96-111` + +同步 P2-1 的逻辑:将 `is_macos_clang` 条件改为"检测到 cfg 文件存在"。 + +**Phase 2 效果**: +- Linux 和 macOS Clang 走统一路径 +- 不再依赖 cfg 中的路径碰巧有效 +- mcpp 从 payload 结构推导出正确的 `-isystem` 和 `--sysroot` + +### Phase 3(未来):复制 payload 时重写 cfg 路径 + +在 `package_fetcher.cppm` 或 `cli.cppm` toolchain install 后,添加 +`fixup_clang_cfg()`,类似 `fixup_gcc_specs()` 的做法: + +```cpp +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, + const std::filesystem::path& oldXlingsHome, + const std::filesystem::path& newRegistryHome) { + // 重写 clang++.cfg 中的路径: + // --sysroot= → --sysroot= + // -isystem → -isystem + // -L → -L + // -rpath, → -rpath, +} +``` + +这样即使不用 `--no-default-config`,cfg 路径也是正确的。 +但需要 mcpp 管理自己的 sysroot 内容(确保完整性),所以这是更远期的方向。 + +--- + +## 修改总结 + +| Phase | 修改 | 文件 | 效果 | +|-------|------|------|------| +| P1-1 | 删除 M5.5 | cli.cppm | 去除 subos 依赖 | +| P1-2 | cfg 解析 sysroot | probe.cppm | Clang 获取正确 sysroot | +| P2-1 | 统一 --no-default-config | stdmod.cppm | 消除 stale cfg 隐患 | +| P2-2 | 同步 P2-1 | flags.cppm | 常规编译也用正确路径 | +| P3 | cfg 路径重写 | package_fetcher/cli | 从根源修复 stale path | + +**Phase 1 修复 bug,Phase 2 消除隐患,Phase 3 完善架构。** + +--- + +## 测试补充 + +### 新增 e2e 测试:无 subos 下的 import std + +```bash +#!/usr/bin/env bash +# requires: import-std +# Test that import std works without mcpp's subos sysroot. +# Regression test: M5.5 subos override must not be required. +set -euo pipefail + +TMP=$(mktemp -d) +trap "rm -rf $TMP" EXIT + +export MCPP_HOME="$TMP/mcpp-home" +export MCPP_INHERIT_SUBOS=0 +source "$(dirname "$0")/_inherit_toolchain.sh" + +mkdir -p "$TMP/proj/src" +cd "$TMP/proj" + +cat > mcpp.toml <<'EOF' +[package] +name = "sysroot_test" +version = "0.1.0" +EOF + +cat > src/main.cpp <<'EOF' +import std; +int main() { std::println("sysroot ok"); } +EOF + +"$MCPP" build +"$MCPP" run | grep -q "sysroot ok" +``` diff --git a/src/build/flags.cppm b/src/build/flags.cppm index e7278c7..4b85913 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -88,23 +88,29 @@ CompileFlags compute_flags(const BuildPlan& plan) { include_flags += " -I" + escape_path(abs); } - // Sysroot + config override for macOS. - // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and - // -isystem paths from the original install location. When the package is - // copied to mcpp's sandbox, these paths become stale. We pass - // --no-default-config to ignore the cfg and provide correct paths. + // Sysroot + config override for Clang with a driver config file. + // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem + // paths from the original install location. After mcpp copies the package + // to its sandbox, these paths may become stale. Detect the cfg and bypass + // it with --no-default-config, providing correct flags from the payload. std::string sysroot_flag; - bool is_macos_clang = mcpp::toolchain::is_clang(plan.toolchain) - && (plan.toolchain.targetTriple.find("apple") != std::string::npos - || plan.toolchain.targetTriple.find("darwin") != std::string::npos); - if (is_macos_clang) { - auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); - auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; - sysroot_flag += " -isystem" + escape_path(libcxxInclude); - if (auto sdk = mcpp::platform::macos::sdk_path()) - sysroot_flag += " --sysroot=" + escape_path(*sdk); - f.sysroot = sysroot_flag; + if (mcpp::toolchain::is_clang(plan.toolchain)) { + auto cfgPath = plan.toolchain.binaryPath.parent_path() + / (plan.toolchain.binaryPath.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + sysroot_flag += " -isystem" + escape_path(libcxxInclude); + if (!plan.toolchain.sysroot.empty()) + sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); + else if (auto sdk = mcpp::platform::macos::sdk_path()) + sysroot_flag += " --sysroot=" + escape_path(*sdk); + f.sysroot = sysroot_flag; + } else if (!plan.toolchain.sysroot.empty()) { + sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); + f.sysroot = sysroot_flag; + } } else if (!plan.toolchain.sysroot.empty()) { sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); f.sysroot = sysroot_flag; diff --git a/src/cli.cppm b/src/cli.cppm index 172e124..5c00bcd 100644 --- a/src/cli.cppm +++ b/src/cli.cppm @@ -998,9 +998,8 @@ prepare_build(bool print_fingerprint, // ─── Toolchain resolution (docs/21) ──────────────────────────────── // Priority chain: // 1. mcpp.toml [toolchain]. → resolve_xpkg_path → abs path - // 2. $MCPP_HOME/registry/subos//bin/g++ (xlings sandbox subos) - // 3. $CXX env var - // 4. PATH g++ (with warning) + // 2. $CXX env var + // 3. PATH g++ (with warning) std::filesystem::path explicit_compiler; std::optional cfg_opt; auto get_cfg = [&]() -> std::expected { @@ -1174,10 +1173,8 @@ prepare_build(bool print_fingerprint, if (!tc) return std::unexpected(tc.error().message); // For musl-gcc the toolchain is fully self-contained - // (`/x86_64-linux-musl/{include,lib}` is its own sysroot), and - // pointing it at mcpp's glibc subos breaks compilation. Skip the - // sysroot injection in that case — musl-gcc's `-dumpmachine` reports - // `x86_64-linux-musl`, which is also the marker we use elsewhere. + // (`/x86_64-linux-musl/{include,lib}` is its own sysroot). + // musl-gcc's `-dumpmachine` reports `x86_64-linux-musl`. bool isMuslTc = tc->targetTriple.find("-musl") != std::string::npos; // A musl toolchain only really makes sense with static linkage — @@ -1189,18 +1186,9 @@ prepare_build(bool print_fingerprint, m->buildConfig.linkage = "static"; } - // M5.5: prefer mcpp's xlings-managed subos as sysroot — it has glibc - // headers + libs in the conventional layout that GCC expects. The - // -print-sysroot output from a freshly-built GCC often points at - // some build-time path that doesn't exist on the user's machine. - if (!isMuslTc) { - if (auto cfg = get_cfg(); cfg) { - auto mcppSubos = (*cfg)->xlingsHome() / "subos" / "default"; - if (std::filesystem::exists(mcppSubos / "usr" / "include")) { - tc->sysroot = mcppSubos; - } - } - } + // Sysroot comes from the toolchain payload itself (GCC -print-sysroot, + // Clang clang++.cfg). mcpp does not override it — the payload is + // self-describing. See docs: 2026-05-21-linux-sysroot-missing-kernel-headers.md // Resolve dependencies: walk the **transitive** graph from the main // manifest, BFS-style. Each unique `(namespace, shortName)` is fetched diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index ef25032..5f6c106 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -254,6 +254,7 @@ probe_target_triple(const std::filesystem::path& compilerBin, std::filesystem::path probe_sysroot(const std::filesystem::path& compilerBin, const std::string& envPrefix) { + // 1. Ask the compiler directly (works for GCC; Clang often doesn't support it). auto r = run_capture(std::format("{}{} -print-sysroot {}", envPrefix, mcpp::xlings::shq(compilerBin.string()), @@ -262,10 +263,28 @@ probe_sysroot(const std::filesystem::path& compilerBin, auto s = trim_line(*r); if (!s.empty() && std::filesystem::exists(s)) return s; } - // macOS fallback: use xcrun to discover the SDK path. - // The sysroot is used for regular compilation flags (flags.cppm) but - // skipped for std module precompilation on macOS (stdmod.cppm) to - // avoid breaking SDK internal header dependencies. + + // 2. Parse the compiler driver config file (Clang .cfg). + // xlings-installed Clang ships a clang++.cfg alongside the binary + // with --sysroot pointing to the payload's associated sysroot. + { + auto stem = compilerBin.stem().string(); + auto cfgPath = compilerBin.parent_path() / (stem + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + std::ifstream ifs(cfgPath); + std::string line; + while (std::getline(ifs, line)) { + constexpr std::string_view prefix = "--sysroot="; + if (line.starts_with(prefix)) { + auto val = trim_line(std::string(line.substr(prefix.size()))); + if (!val.empty() && std::filesystem::exists(val)) + return val; + } + } + } + } + + // 3. macOS fallback: use xcrun to discover the SDK path. if (auto sdk = mcpp::platform::macos::sdk_path()) return *sdk; return {}; diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index 0220d75..dd806d1 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -93,24 +93,29 @@ std::expected ensure_built( sm.objectPath = sm.cacheDir / "std.o"; // Build sysroot + include flags for std module precompilation. - // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and - // -isystem paths from the original install location. When the LLVM package - // is copied to mcpp's sandbox, these cfg paths become stale (still point - // to the original xlings directory). We override both: - // --sysroot → current active SDK (from xcrun) - // --no-default-config → ignore stale cfg entirely - // -isystem → correct libc++ headers in the sandbox copy + // + // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem + // paths from the original install location. After mcpp copies the package + // to its sandbox, these cfg paths may become stale. We detect a cfg file + // and bypass it with --no-default-config, providing correct flags derived + // from the payload's actual binary location and the probed sysroot. std::string sysroot_flag; - bool is_macos = tc.targetTriple.find("apple") != std::string::npos - || tc.targetTriple.find("darwin") != std::string::npos; - if (is_macos && is_clang(tc)) { - // Ignore the stale clang++.cfg and provide correct flags directly. - auto llvmRoot = tc.binaryPath.parent_path().parent_path(); - auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; - sysroot_flag = " --no-default-config"; - sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); - if (auto sdk = mcpp::platform::macos::sdk_path()) - sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + if (is_clang(tc)) { + auto cfgPath = tc.binaryPath.parent_path() + / (tc.binaryPath.stem().string() + ".cfg"); + if (std::filesystem::exists(cfgPath)) { + // Bypass potentially-stale cfg; provide correct flags directly. + auto llvmRoot = tc.binaryPath.parent_path().parent_path(); + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; + sysroot_flag = " --no-default-config"; + sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); + if (!tc.sysroot.empty()) + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); + else if (auto sdk = mcpp::platform::macos::sdk_path()) + sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + } else if (!tc.sysroot.empty()) { + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); + } } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); } From 0f8d0e82fe0a09dcffd64f0c794be4577e4598f3 Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:17:51 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20GCC=20sysroot=20fallback=20=E2=80=94?= =?UTF-8?q?=20remap=20stale=20build-time=20path=20to=20registry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GCC bakes the build-time sysroot into the binary via --with-sysroot. For xlings-built GCC this is a path like /.xlings/subos/default that doesn't exist on the user's machine. When -print-sysroot returns such a non-existent path ending in subos/default, remap it to the equivalent sysroot relative to the compiler's own xpkgs directory. This is payload-derived (from the compiler binary's location in the registry), not a config-level dependency on subos. --- src/toolchain/probe.cppm | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/toolchain/probe.cppm b/src/toolchain/probe.cppm index 5f6c106..0cac4f2 100644 --- a/src/toolchain/probe.cppm +++ b/src/toolchain/probe.cppm @@ -262,6 +262,21 @@ probe_sysroot(const std::filesystem::path& compilerBin, if (r) { auto s = trim_line(*r); if (!s.empty() && std::filesystem::exists(s)) return s; + + // GCC bakes the build-time sysroot into the binary. For xlings-built + // GCC this is a path like /.xlings/subos/default that + // doesn't exist on the user's machine. If the reported path ends + // with subos/default, look for the equivalent sysroot relative to + // the compiler's own xpkgs directory (payload-derived). + if (!s.empty() && s.ends_with("subos/default")) { + if (auto xpkgs = mcpp::xlings::paths::xpkgs_from_compiler(compilerBin)) { + // xpkgs is /data/xpkgs → registry = xpkgs/../.. + auto registrySysroot = xpkgs->parent_path().parent_path() + / "subos" / "default"; + if (std::filesystem::exists(registrySysroot / "usr" / "include")) + return registrySysroot; + } + } } // 2. Parse the compiler driver config file (Clang .cfg). From b3d3e88a9029a37ff03e16dcb775bb7c887b4f8a Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:35:49 +0800 Subject: [PATCH 3/4] fix: add target-specific libc++ include path for --no-default-config When bypassing clang++.cfg with --no-default-config, we must provide both libc++ include paths that the cfg originally supplied: -isystem /include/c++/v1 -isystem /include//c++/v1 The target-specific path contains __config_site which is required by __config. Without it, std module precompilation fails with '__config_site' file not found. --- src/build/flags.cppm | 8 ++++++++ src/toolchain/stdmod.cppm | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 4b85913..5a9537c 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -102,6 +102,14 @@ CompileFlags compute_flags(const BuildPlan& plan) { auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += " -isystem" + escape_path(libcxxInclude); + // Target-specific libc++ headers (e.g. __config_site) live under + // include//c++/v1/. Add if present. + if (!plan.toolchain.targetTriple.empty()) { + auto targetInclude = llvmRoot / "include" + / plan.toolchain.targetTriple / "c++" / "v1"; + if (std::filesystem::exists(targetInclude)) + sysroot_flag += " -isystem" + escape_path(targetInclude); + } if (!plan.toolchain.sysroot.empty()) sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); else if (auto sdk = mcpp::platform::macos::sdk_path()) diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index dd806d1..ffb5aa8 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -109,6 +109,14 @@ std::expected ensure_built( auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); + // Target-specific libc++ headers (e.g. __config_site) live under + // include//c++/v1/. Add if present. + if (!tc.targetTriple.empty()) { + auto targetInclude = llvmRoot / "include" + / tc.targetTriple / "c++" / "v1"; + if (std::filesystem::exists(targetInclude)) + sysroot_flag += std::format(" -isystem'{}'", targetInclude.string()); + } if (!tc.sysroot.empty()) sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); else if (auto sdk = mcpp::platform::macos::sdk_path()) From 265cde68474c6167063bfa46288c30d7b4dc0c0d Mon Sep 17 00:00:00 2001 From: sunrisepeak Date: Thu, 21 May 2026 09:52:03 +0800 Subject: [PATCH 4/4] fix: restrict --no-default-config to macOS only On Linux, clang++.cfg contains essential linker flags (-fuse-ld=lld, --rtlib=compiler-rt, --unwindlib=libunwind). Using --no-default-config strips these, causing "cannot find crtbeginS.o" link failures because clang falls back to system GNU ld looking for GCC runtime objects. On Linux, let the cfg apply normally. The cfg's --sysroot points to the xlings subos which is valid and complete. Pass --sysroot explicitly only when needed (to override a stale cfg value), leveraging the fact that command-line --sysroot takes precedence over the cfg's value. Keep --no-default-config for macOS only, where the cfg-baked paths genuinely become stale (pointing to CommandLineTools SDK when Xcode SDK is active). --- src/build/flags.cppm | 26 +++++++++++++++----------- src/toolchain/stdmod.cppm | 29 +++++++++++++++++------------ 2 files changed, 32 insertions(+), 23 deletions(-) diff --git a/src/build/flags.cppm b/src/build/flags.cppm index 5a9537c..b894d0f 100644 --- a/src/build/flags.cppm +++ b/src/build/flags.cppm @@ -88,13 +88,19 @@ CompileFlags compute_flags(const BuildPlan& plan) { include_flags += " -I" + escape_path(abs); } - // Sysroot + config override for Clang with a driver config file. - // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem - // paths from the original install location. After mcpp copies the package - // to its sandbox, these paths may become stale. Detect the cfg and bypass - // it with --no-default-config, providing correct flags from the payload. + // Sysroot + config override. + // + // On macOS, xlings LLVM's clang++.cfg has hardcoded paths that become + // stale after copying. Use --no-default-config + explicit flags. + // + // On Linux, the cfg contains essential linker flags (-fuse-ld=lld, + // --rtlib=compiler-rt). Let the cfg apply normally; pass --sysroot + // explicitly to override any stale sysroot value in the cfg. std::string sysroot_flag; - if (mcpp::toolchain::is_clang(plan.toolchain)) { + bool is_macos_clang = mcpp::toolchain::is_clang(plan.toolchain) + && (plan.toolchain.targetTriple.find("apple") != std::string::npos + || plan.toolchain.targetTriple.find("darwin") != std::string::npos); + if (is_macos_clang) { auto cfgPath = plan.toolchain.binaryPath.parent_path() / (plan.toolchain.binaryPath.stem().string() + ".cfg"); if (std::filesystem::exists(cfgPath)) { @@ -102,18 +108,16 @@ CompileFlags compute_flags(const BuildPlan& plan) { auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += " -isystem" + escape_path(libcxxInclude); - // Target-specific libc++ headers (e.g. __config_site) live under - // include//c++/v1/. Add if present. if (!plan.toolchain.targetTriple.empty()) { auto targetInclude = llvmRoot / "include" / plan.toolchain.targetTriple / "c++" / "v1"; if (std::filesystem::exists(targetInclude)) sysroot_flag += " -isystem" + escape_path(targetInclude); } - if (!plan.toolchain.sysroot.empty()) - sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); - else if (auto sdk = mcpp::platform::macos::sdk_path()) + if (auto sdk = mcpp::platform::macos::sdk_path()) sysroot_flag += " --sysroot=" + escape_path(*sdk); + else if (!plan.toolchain.sysroot.empty()) + sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot); f.sysroot = sysroot_flag; } else if (!plan.toolchain.sysroot.empty()) { sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot); diff --git a/src/toolchain/stdmod.cppm b/src/toolchain/stdmod.cppm index ffb5aa8..bd1b67b 100644 --- a/src/toolchain/stdmod.cppm +++ b/src/toolchain/stdmod.cppm @@ -94,33 +94,38 @@ std::expected ensure_built( // Build sysroot + include flags for std module precompilation. // - // xlings LLVM ships a clang++.cfg with hardcoded --sysroot and -isystem - // paths from the original install location. After mcpp copies the package - // to its sandbox, these cfg paths may become stale. We detect a cfg file - // and bypass it with --no-default-config, providing correct flags derived - // from the payload's actual binary location and the probed sysroot. + // On macOS, xlings LLVM's clang++.cfg contains hardcoded --sysroot and + // -isystem paths that become stale when the package is copied to mcpp's + // sandbox. We use --no-default-config to bypass the cfg and provide + // correct flags derived from the payload's actual location + xcrun SDK. + // + // On Linux, the cfg also contains linker flags (-fuse-ld=lld, + // --rtlib=compiler-rt, --unwindlib=libunwind) that are essential. Using + // --no-default-config would strip these, causing link failures. Instead, + // we let the cfg apply normally and only pass --sysroot to override the + // sysroot if needed (command-line --sysroot takes precedence over cfg). std::string sysroot_flag; - if (is_clang(tc)) { + bool is_macos = tc.targetTriple.find("apple") != std::string::npos + || tc.targetTriple.find("darwin") != std::string::npos; + if (is_macos && is_clang(tc)) { auto cfgPath = tc.binaryPath.parent_path() / (tc.binaryPath.stem().string() + ".cfg"); if (std::filesystem::exists(cfgPath)) { - // Bypass potentially-stale cfg; provide correct flags directly. + // Bypass stale macOS cfg; provide correct flags directly. auto llvmRoot = tc.binaryPath.parent_path().parent_path(); auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; sysroot_flag = " --no-default-config"; sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); - // Target-specific libc++ headers (e.g. __config_site) live under - // include//c++/v1/. Add if present. if (!tc.targetTriple.empty()) { auto targetInclude = llvmRoot / "include" / tc.targetTriple / "c++" / "v1"; if (std::filesystem::exists(targetInclude)) sysroot_flag += std::format(" -isystem'{}'", targetInclude.string()); } - if (!tc.sysroot.empty()) - sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); - else if (auto sdk = mcpp::platform::macos::sdk_path()) + if (auto sdk = mcpp::platform::macos::sdk_path()) sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); + else if (!tc.sysroot.empty()) + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); } else if (!tc.sysroot.empty()) { sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); }