|
| 1 | +# Linux sysroot 缺少内核头文件导致 std module 预编译失败 |
| 2 | + |
| 3 | +## 现象 |
| 4 | + |
| 5 | +在用户机器上(非 CI),使用 LLVM 或 GCC 工具链执行 `mcpp run` / `mcpp build` 时, |
| 6 | +std module 预编译失败: |
| 7 | + |
| 8 | +``` |
| 9 | +/home/speak/.mcpp/registry/subos/default/usr/include/bits/local_lim.h:38:10: |
| 10 | +fatal error: 'linux/limits.h' file not found |
| 11 | +``` |
| 12 | + |
| 13 | +GCC 和 Clang 均受影响,问题是系统性的。 |
| 14 | + |
| 15 | +## 根因 |
| 16 | + |
| 17 | +### 直接原因:M5.5 sysroot 覆盖逻辑 |
| 18 | + |
| 19 | +`cli.cppm:1192-1203` 中的 M5.5 逻辑,将 `tc->sysroot` 强制覆盖为 mcpp 自己的 |
| 20 | +subos(`~/.mcpp/registry/subos/default`): |
| 21 | + |
| 22 | +```cpp |
| 23 | +if (!isMuslTc) { |
| 24 | + if (auto cfg = get_cfg(); cfg) { |
| 25 | + auto mcppSubos = (*cfg)->xlingsHome() / "subos" / "default"; |
| 26 | + if (std::filesystem::exists(mcppSubos / "usr" / "include")) { |
| 27 | + tc->sysroot = mcppSubos; |
| 28 | + } |
| 29 | + } |
| 30 | +} |
| 31 | +``` |
| 32 | + |
| 33 | +### 触发 commit |
| 34 | + |
| 35 | +**`063fb6f`** — 将 MCPP_HOME 从 xpkgs 包目录改为 `~/.mcpp/`,使 M5.5 |
| 36 | +找到一个存在但不完整的 subos。 |
| 37 | + |
| 38 | +### CI/e2e 未发现的原因 |
| 39 | + |
| 40 | +CI 的 subos 由 `xlings self install` 完整初始化(含内核头文件),e2e 测试 |
| 41 | +通过 `_inherit_toolchain.sh` 继承宿主完整 subos。 |
| 42 | + |
| 43 | +--- |
| 44 | + |
| 45 | +## 设计分析:当前问题的本质 |
| 46 | + |
| 47 | +### 当前架构的矛盾 |
| 48 | + |
| 49 | +mcpp 的工具链管理存在一个架构层面的矛盾: |
| 50 | + |
| 51 | +``` |
| 52 | + ┌─────────────────────────┐ |
| 53 | + │ xlings 下载 payload │ |
| 54 | + │ ~/.xlings/data/xpkgs/ │ |
| 55 | + │ (cfg/specs 路径正确) │ |
| 56 | + └──────────┬──────────────┘ |
| 57 | + │ copy (因 XLINGS_HOME 传播不可靠) |
| 58 | + ▼ |
| 59 | + ┌─────────────────────────┐ |
| 60 | + │ mcpp sandbox 副本 │ |
| 61 | + │ ~/.mcpp/registry/xpkgs/ │ |
| 62 | + │ (cfg/specs 路径 stale!) │ |
| 63 | + └──────────┬──────────────┘ |
| 64 | + │ |
| 65 | + ┌──────────────┼──────────────┐ |
| 66 | + ▼ ▼ ▼ |
| 67 | + GCC: specs Clang macOS: Clang Linux: |
| 68 | + fixup 修复 --no-default 没有修复 ← BUG |
| 69 | + 路径 ✅ -config ✅ 用 subos 补救 ✗ |
| 70 | +``` |
| 71 | + |
| 72 | +**三个平台/工具链的 stale path 问题,用了三种不同的解法:** |
| 73 | + |
| 74 | +| 工具链 | stale path 来源 | 当前解法 | 状态 | |
| 75 | +|--------|-----------------|---------|------| |
| 76 | +| GCC (Linux) | specs 文件 | `fixup_gcc_specs()` 重写 specs | ✅ 正确但只修了 specs,没修 `-print-sysroot` | |
| 77 | +| Clang (macOS) | clang++.cfg | `--no-default-config` + xcrun | ✅ 但只在 macOS | |
| 78 | +| Clang (Linux) | clang++.cfg | M5.5 subos 覆盖 | ❌ subos 不完整 | |
| 79 | + |
| 80 | +**根本问题:mcpp 复制了 payload 但没有统一处理 stale path。** |
| 81 | + |
| 82 | +### mcpp 对 xlings 的依赖边界不清 |
| 83 | + |
| 84 | +当前 mcpp 对 xlings 有三层依赖: |
| 85 | + |
| 86 | +| 层次 | 内容 | 应该依赖? | |
| 87 | +|------|------|-----------| |
| 88 | +| 包下载 | xlings 作为包索引 + 下载工具 | ✅ 是 | |
| 89 | +| payload 路径 | xpkgs 下具体包的文件结构 | ✅ 是 | |
| 90 | +| subos | xlings 的沙箱 sysroot | ❌ 不应该 | |
| 91 | + |
| 92 | +M5.5 的问题就在于跨越了第三层边界——用 xlings 的内部实现细节(subos) |
| 93 | +来补救 mcpp 自身的路径问题。 |
| 94 | + |
| 95 | +--- |
| 96 | + |
| 97 | +## 设计方案 |
| 98 | + |
| 99 | +### 核心原则 |
| 100 | + |
| 101 | +**mcpp 只把 xlings 当包索引 + 下载工具。工具链的编译环境由 payload 自描述, |
| 102 | +mcpp 忠实读取,不替换、不覆盖。** |
| 103 | + |
| 104 | +### 方案:Payload-first + 统一 stale path 处理 |
| 105 | + |
| 106 | +#### 1. 工具链 sysroot 来源:只从 payload 获取 |
| 107 | + |
| 108 | +``` |
| 109 | +┌─────────────────────────────────────────────────┐ |
| 110 | +│ sysroot 解析优先级 │ |
| 111 | +│ │ |
| 112 | +│ 1. compiler -print-sysroot (GCC 原生支持) │ |
| 113 | +│ → 路径存在则使用 │ |
| 114 | +│ │ |
| 115 | +│ 2. payload cfg 文件解析 (Clang clang++.cfg) │ |
| 116 | +│ → 解析 --sysroot= 行,路径存在则使用 │ |
| 117 | +│ │ |
| 118 | +│ 3. macOS: xcrun --show-sdk-path │ |
| 119 | +│ │ |
| 120 | +│ 4. 空 (不传 --sysroot,让编译器用自身默认值) │ |
| 121 | +│ │ |
| 122 | +│ ✗ 不再有 subos fallback │ |
| 123 | +└─────────────────────────────────────────────────┘ |
| 124 | +``` |
| 125 | + |
| 126 | +**改动**: |
| 127 | +- 删除 `cli.cppm` M5.5 subos 覆盖代码 |
| 128 | +- `probe.cppm` 增加 cfg 文件解析作为第 2 优先级 |
| 129 | + |
| 130 | +#### 2. 复制 payload 时统一修复 stale path |
| 131 | + |
| 132 | +当前只有 GCC 做了 specs fixup,Clang 只在 macOS 做了 `--no-default-config`。 |
| 133 | +应该统一为:**凡是复制了 payload,就修复其中的路径配置。** |
| 134 | + |
| 135 | +``` |
| 136 | + copy payload 后 |
| 137 | + │ |
| 138 | + ┌────────┼────────┐ |
| 139 | + ▼ ▼ ▼ |
| 140 | + GCC specs Clang cfg 其他配置 |
| 141 | + │ │ |
| 142 | + ▼ ▼ |
| 143 | + rewrite_gcc rewrite_cfg |
| 144 | + _specs() _paths() |
| 145 | + │ │ |
| 146 | + ▼ ▼ |
| 147 | + 新 sysroot 新 sysroot |
| 148 | + 新 rpath 新 -isystem |
| 149 | + 新 -L/-rpath |
| 150 | +``` |
| 151 | + |
| 152 | +**具体做法**:在 `package_fetcher.cppm` 复制 payload 后(或在 `cli.cppm` |
| 153 | +toolchain install 后),对 Clang cfg 做类似 `fixup_gcc_specs` 的路径重写: |
| 154 | + |
| 155 | +```cpp |
| 156 | +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, |
| 157 | + const std::filesystem::path& newSysroot) { |
| 158 | + auto cfgPath = payloadRoot / "bin" / "clang++.cfg"; |
| 159 | + if (!std::filesystem::exists(cfgPath)) return; |
| 160 | + |
| 161 | + // 读取 cfg,将旧 sysroot/isystem/rpath 路径替换为 payload 实际位置 |
| 162 | + // ... |
| 163 | +} |
| 164 | +``` |
| 165 | +
|
| 166 | +**但这里有一个关键设计选择:新路径指向哪里?** |
| 167 | +
|
| 168 | +#### 3. 关于 sysroot 本身从哪来 |
| 169 | +
|
| 170 | +工具链需要 C 库头文件(glibc headers + linux kernel headers)。来源有三种: |
| 171 | +
|
| 172 | +| 来源 | 说明 | 优劣 | |
| 173 | +|------|------|------| |
| 174 | +| 系统 `/usr/include` | 宿主机自带 | 简单,但不可控,不同发行版不同 | |
| 175 | +| xlings subos | xlings 管理的沙箱 sysroot | 可控,但 mcpp 需依赖 xlings 内部结构 | |
| 176 | +| payload 自带 | 工具链包自带 sysroot(如 musl-gcc) | 最干净,但需要上游包支持 | |
| 177 | +
|
| 178 | +**推荐策略**: |
| 179 | +
|
| 180 | +- **短期**:信任 payload 自身配置的 sysroot 路径。xlings 安装 GCC/LLVM 时 |
| 181 | + 已经配置好了 sysroot(指向 xlings 自己的 subos),mcpp 只需忠实读取。 |
| 182 | + 如果路径存在且有效,就用它。如果路径无效,不传 `--sysroot`,让编译器 |
| 183 | + 用系统默认路径。 |
| 184 | +
|
| 185 | +- **中期**:推动 xlings 上游让 LLVM/GCC 包的 cfg/specs 使用相对路径或 |
| 186 | + 可配置路径,避免硬编码绝对路径。这从源头消除 stale path 问题。 |
| 187 | +
|
| 188 | +- **长期**:mcpp 自带轻量 sysroot 管理(类似 Zig 的做法:打包 libc headers |
| 189 | + 作为 mcpp 自身的资源),彻底不依赖宿主系统或 xlings 的 sysroot。但这是 |
| 190 | + 大工程,不急。 |
| 191 | +
|
| 192 | +--- |
| 193 | +
|
| 194 | +## 修复方案(基于以上设计) |
| 195 | +
|
| 196 | +### Phase 1:修复当前 bug(最小改动) |
| 197 | +
|
| 198 | +#### P1-1:删除 M5.5 subos 覆盖 |
| 199 | +
|
| 200 | +**文件**:`src/cli.cppm:1192-1203` |
| 201 | +
|
| 202 | +**删除**整个代码块。工具链的 sysroot 由 payload 决定,mcpp 不介入。 |
| 203 | +
|
| 204 | +同时删除 `cli.cppm:1001` 的 subos 注释和 `cli.cppm:1178` 的 "glibc subos" 注释。 |
| 205 | +
|
| 206 | +#### P1-2:`probe_sysroot` 增加 cfg 解析 |
| 207 | +
|
| 208 | +**文件**:`src/toolchain/probe.cppm:254-272` |
| 209 | +
|
| 210 | +`-print-sysroot` 失败后(Clang 不支持),解析 payload 中 `clang++.cfg` |
| 211 | +的 `--sysroot=` 行: |
| 212 | +
|
| 213 | +```cpp |
| 214 | +std::filesystem::path |
| 215 | +probe_sysroot(const std::filesystem::path& compilerBin, |
| 216 | + const std::string& envPrefix) { |
| 217 | + // 1. -print-sysroot (GCC) |
| 218 | + auto r = run_capture(std::format("{}{} -print-sysroot {}", |
| 219 | + envPrefix, |
| 220 | + mcpp::xlings::shq(compilerBin.string()), |
| 221 | + mcpp::platform::null_redirect)); |
| 222 | + if (r) { |
| 223 | + auto s = trim_line(*r); |
| 224 | + if (!s.empty() && std::filesystem::exists(s)) return s; |
| 225 | + } |
| 226 | +
|
| 227 | + // 2. Parse payload cfg (Clang) |
| 228 | + auto cfgPath = compilerBin.parent_path() |
| 229 | + / (compilerBin.stem().string() + ".cfg"); |
| 230 | + if (std::filesystem::exists(cfgPath)) { |
| 231 | + std::ifstream ifs(cfgPath); |
| 232 | + std::string line; |
| 233 | + while (std::getline(ifs, line)) { |
| 234 | + constexpr std::string_view prefix = "--sysroot="; |
| 235 | + if (line.starts_with(prefix)) { |
| 236 | + auto val = trim_line(std::string(line.substr(prefix.size()))); |
| 237 | + if (!val.empty() && std::filesystem::exists(val)) |
| 238 | + return val; |
| 239 | + } |
| 240 | + } |
| 241 | + } |
| 242 | +
|
| 243 | + // 3. macOS: xcrun SDK |
| 244 | + if (auto sdk = mcpp::platform::macos::sdk_path()) |
| 245 | + return *sdk; |
| 246 | + return {}; |
| 247 | +} |
| 248 | +``` |
| 249 | + |
| 250 | +**Phase 1 效果**: |
| 251 | +- Clang:cfg 中的 `--sysroot=~/.xlings/subos/default` 被正确读取, |
| 252 | + `tc->sysroot` 不再为空。stdmod.cppm 和 flags.cppm 传递正确的 sysroot。 |
| 253 | +- GCC:`-print-sysroot` 正常工作(如果路径存在);若不存在则 sysroot 为空, |
| 254 | + GCC 用默认系统路径(`/usr/include`)。 |
| 255 | +- 不再依赖 subos。 |
| 256 | + |
| 257 | +### Phase 2:统一 Clang stale cfg 处理(消除隐患) |
| 258 | + |
| 259 | +#### P2-1:`stdmod.cppm` — 所有有 cfg 的 Clang 都走 `--no-default-config` |
| 260 | + |
| 261 | +**文件**:`src/toolchain/stdmod.cppm:103-116` |
| 262 | + |
| 263 | +将 macOS 特有的 `--no-default-config` 逻辑泛化为"有 cfg 文件的 Clang": |
| 264 | + |
| 265 | +```cpp |
| 266 | +std::string sysroot_flag; |
| 267 | +if (is_clang(tc)) { |
| 268 | + auto cfgPath = tc.binaryPath.parent_path() |
| 269 | + / (tc.binaryPath.stem().string() + ".cfg"); |
| 270 | + if (std::filesystem::exists(cfgPath)) { |
| 271 | + // Bypass cfg (may have stale paths after payload copy). |
| 272 | + // Provide correct flags from payload structure directly. |
| 273 | + auto llvmRoot = tc.binaryPath.parent_path().parent_path(); |
| 274 | + auto libcxxInclude = llvmRoot / "include" / "c++" / "v1"; |
| 275 | + sysroot_flag = " --no-default-config"; |
| 276 | + sysroot_flag += std::format(" -isystem'{}'", libcxxInclude.string()); |
| 277 | + if (!tc.sysroot.empty()) |
| 278 | + sysroot_flag += std::format(" --sysroot='{}'", tc.sysroot.string()); |
| 279 | + else if (auto sdk = mcpp::platform::macos::sdk_path()) |
| 280 | + sysroot_flag += std::format(" --sysroot='{}'", sdk->string()); |
| 281 | + } else if (!tc.sysroot.empty()) { |
| 282 | + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); |
| 283 | + } |
| 284 | +} else if (!tc.sysroot.empty()) { |
| 285 | + sysroot_flag = std::format(" --sysroot='{}'", tc.sysroot.string()); |
| 286 | +} |
| 287 | +``` |
| 288 | + |
| 289 | +#### P2-2:`flags.cppm` — 同步修改 |
| 290 | + |
| 291 | +**文件**:`src/build/flags.cppm:96-111` |
| 292 | + |
| 293 | +同步 P2-1 的逻辑:将 `is_macos_clang` 条件改为"检测到 cfg 文件存在"。 |
| 294 | + |
| 295 | +**Phase 2 效果**: |
| 296 | +- Linux 和 macOS Clang 走统一路径 |
| 297 | +- 不再依赖 cfg 中的路径碰巧有效 |
| 298 | +- mcpp 从 payload 结构推导出正确的 `-isystem` 和 `--sysroot` |
| 299 | + |
| 300 | +### Phase 3(未来):复制 payload 时重写 cfg 路径 |
| 301 | + |
| 302 | +在 `package_fetcher.cppm` 或 `cli.cppm` toolchain install 后,添加 |
| 303 | +`fixup_clang_cfg()`,类似 `fixup_gcc_specs()` 的做法: |
| 304 | + |
| 305 | +```cpp |
| 306 | +void fixup_clang_cfg(const std::filesystem::path& payloadRoot, |
| 307 | + const std::filesystem::path& oldXlingsHome, |
| 308 | + const std::filesystem::path& newRegistryHome) { |
| 309 | + // 重写 clang++.cfg 中的路径: |
| 310 | + // --sysroot=<old> → --sysroot=<new> |
| 311 | + // -isystem <old> → -isystem <new> |
| 312 | + // -L<old> → -L<new> |
| 313 | + // -rpath,<old> → -rpath,<new> |
| 314 | +} |
| 315 | +``` |
| 316 | +
|
| 317 | +这样即使不用 `--no-default-config`,cfg 路径也是正确的。 |
| 318 | +但需要 mcpp 管理自己的 sysroot 内容(确保完整性),所以这是更远期的方向。 |
| 319 | +
|
| 320 | +--- |
| 321 | +
|
| 322 | +## 修改总结 |
| 323 | +
|
| 324 | +| Phase | 修改 | 文件 | 效果 | |
| 325 | +|-------|------|------|------| |
| 326 | +| P1-1 | 删除 M5.5 | cli.cppm | 去除 subos 依赖 | |
| 327 | +| P1-2 | cfg 解析 sysroot | probe.cppm | Clang 获取正确 sysroot | |
| 328 | +| P2-1 | 统一 --no-default-config | stdmod.cppm | 消除 stale cfg 隐患 | |
| 329 | +| P2-2 | 同步 P2-1 | flags.cppm | 常规编译也用正确路径 | |
| 330 | +| P3 | cfg 路径重写 | package_fetcher/cli | 从根源修复 stale path | |
| 331 | +
|
| 332 | +**Phase 1 修复 bug,Phase 2 消除隐患,Phase 3 完善架构。** |
| 333 | +
|
| 334 | +--- |
| 335 | +
|
| 336 | +## 测试补充 |
| 337 | +
|
| 338 | +### 新增 e2e 测试:无 subos 下的 import std |
| 339 | +
|
| 340 | +```bash |
| 341 | +#!/usr/bin/env bash |
| 342 | +# requires: import-std |
| 343 | +# Test that import std works without mcpp's subos sysroot. |
| 344 | +# Regression test: M5.5 subos override must not be required. |
| 345 | +set -euo pipefail |
| 346 | +
|
| 347 | +TMP=$(mktemp -d) |
| 348 | +trap "rm -rf $TMP" EXIT |
| 349 | +
|
| 350 | +export MCPP_HOME="$TMP/mcpp-home" |
| 351 | +export MCPP_INHERIT_SUBOS=0 |
| 352 | +source "$(dirname "$0")/_inherit_toolchain.sh" |
| 353 | +
|
| 354 | +mkdir -p "$TMP/proj/src" |
| 355 | +cd "$TMP/proj" |
| 356 | +
|
| 357 | +cat > mcpp.toml <<'EOF' |
| 358 | +[package] |
| 359 | +name = "sysroot_test" |
| 360 | +version = "0.1.0" |
| 361 | +EOF |
| 362 | +
|
| 363 | +cat > src/main.cpp <<'EOF' |
| 364 | +import std; |
| 365 | +int main() { std::println("sysroot ok"); } |
| 366 | +EOF |
| 367 | +
|
| 368 | +"$MCPP" build |
| 369 | +"$MCPP" run | grep -q "sysroot ok" |
| 370 | +``` |
0 commit comments