Skip to content

Commit acc1a71

Browse files
committed
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
1 parent 0e67801 commit acc1a71

5 files changed

Lines changed: 444 additions & 56 deletions

File tree

Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
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

Comments
 (0)