Skip to content

Commit e9d088e

Browse files
committed
feat: payload-first sysroot — assemble compile env from xpkgs payloads
Replace --sysroot dependency on xlings subos with fine-grained -isystem paths derived from sibling xpkgs payloads (glibc, linux-headers). Phase 2: PayloadPaths model - Add PayloadPaths struct to Toolchain model (glibcInclude, glibcLib, linuxInclude) - probe_payload_paths() finds sibling glibc and linux-headers xpkgs via find_sibling_package() which searches across all index prefixes - Falls back to host /usr/include for linux kernel headers if no xpkg found Phase 3: Payload-first flags - flags.cppm: use -isystem for glibc + linux-headers instead of --sysroot; Clang with cfg uses --no-default-config + explicit flags including -fuse-ld=lld, --rtlib=compiler-rt, --unwindlib=libunwind - stdmod.cppm: unified Clang cfg bypass on all platforms for std module precompile (no linker needed, so --no-default-config is safe) Phase 4: Clang cfg fixup - fixup_clang_cfg() rewrites clang++.cfg paths after payload copy, similar to fixup_gcc_specs() for GCC - Called during `mcpp toolchain install llvm` Phase 5: Sysroot dependency auto-install - Toolchain install ensures glibc and linux-headers xpkgs are installed before the main toolchain package
1 parent 265cde6 commit e9d088e

8 files changed

Lines changed: 597 additions & 46 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
# 设计方案:Payload-first 工具链环境管理
2+
3+
## 背景
4+
5+
mcpp 当前通过 `--sysroot` 指向 xlings subos 来为工具链提供 C 库头文件和内核头文件。
6+
这导致了对 xlings 内部结构(subos)的隐式依赖,且 subos 的完整性不可控。
7+
8+
## 设计原则
9+
10+
**mcpp 只把 xlings 当包索引 + 资源下载工具。所有编译环境由 mcpp 从 xpkgs payload
11+
细粒度组装,不依赖 xlings 的 subos 或其他内部功能。**
12+
13+
## 当前问题
14+
15+
xlings 中相关的 xpkgs 包:
16+
17+
| 包名 | 内容 | 路径 |
18+
|------|------|------|
19+
| `xim-x-glibc/2.39` | glibc 头文件 + 运行时库 | `include/` (130 头文件) + `lib64/` (libc.so, crt*.o, ld-linux) |
20+
| `scode-x-linux-headers/5.11.1` | Linux 内核头文件 | `include/linux/`, `include/asm/`, `include/asm-generic/` |
21+
| `xim-x-gcc/16.1.0` | GCC 编译器 | C++ 标准库头文件 + 编译器内建头文件 + 运行时库 |
22+
| `xim-x-llvm/20.1.7` | LLVM/Clang 编译器 | libc++ 头文件 + clang 内建头文件 + 运行时库 |
23+
24+
这些包已经在 xpkgs 中了,但 mcpp 没有直接利用它们的路径,而是依赖 subos
25+
(subos 实际上是 xlings 从这些包 + 宿主系统组装的一个合并目录)。
26+
27+
## 目标设计
28+
29+
### 核心思路
30+
31+
mcpp 在 detect 阶段从工具链 binary 位置推导出 sibling xpkgs(glibc、linux-headers),
32+
在 flags 阶段直接用这些 payload 路径组装编译和链接参数,不使用 `--sysroot`
33+
34+
### 路径推导链
35+
36+
```
37+
compiler binary (e.g. ~/.mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++)
38+
39+
├── xpkgs_from_compiler() → ~/.mcpp/registry/data/xpkgs/
40+
41+
├── find_sibling_tool("glibc")
42+
│ → ~/.mcpp/registry/data/xpkgs/xim-x-glibc/2.39/
43+
│ ├── include/ → glibc 头文件 (-isystem)
44+
│ └── lib64/ → 运行时库 (-L, -rpath, -B for crt*.o)
45+
46+
└── find_sibling_tool("linux-headers") [优先 scode-x-linux-headers]
47+
→ ~/.mcpp/registry/data/xpkgs/scode-x-linux-headers/5.11.1/
48+
└── include/ → linux/, asm/, asm-generic/ (-isystem)
49+
```
50+
51+
### 环境检查
52+
53+
mcpp 在 detect 阶段主动检查工具链环境完整性,而不是被动等到编译报错。
54+
55+
```cpp
56+
struct SysrootPaths {
57+
std::filesystem::path glibcInclude; // glibc headers
58+
std::filesystem::path linuxInclude; // linux kernel headers
59+
std::filesystem::path glibcLib; // glibc runtime (lib64/)
60+
std::filesystem::path dynamicLinker; // ld-linux-x86-64.so.2
61+
};
62+
63+
// detect 阶段调用:
64+
std::expected<SysrootPaths, DetectError>
65+
probe_sysroot_paths(const std::filesystem::path& compilerBin);
66+
```
67+
68+
检查项:
69+
70+
| 检查 | 验证文件 | 失败提示 |
71+
|------|---------|---------|
72+
| glibc 头文件 | `glibc/include/features.h` | `glibc xpkg not found; run: mcpp toolchain install gcc` |
73+
| linux 内核头文件 | `linux-headers/include/linux/limits.h` | `linux-headers package missing; will use host /usr/include as fallback` |
74+
| glibc 运行时 | `glibc/lib64/libc.so.6` | `glibc runtime not found` |
75+
| 动态链接器 | `glibc/lib64/ld-linux-x86-64.so.2` | `dynamic linker not found` |
76+
77+
### 编译 flags 组装
78+
79+
#### GCC(当前用 --sysroot,改为 -isystem + -L)
80+
81+
**修改前**:
82+
```
83+
g++ -std=c++23 --sysroot=<subos> ...
84+
```
85+
86+
**修改后**:
87+
```
88+
g++ -std=c++23 \
89+
-isystem <glibc>/include \
90+
-isystem <linux-headers>/include \
91+
-L<glibc>/lib64 \
92+
...
93+
```
94+
95+
注意:GCC 使用相对路径自动找到自己的 C++ 标准库头文件和编译器内建头文件,
96+
不需要额外的 `-isystem`。只需提供 glibc 和 linux-headers 的路径。
97+
98+
#### Clang(当前依赖 cfg,改为 --no-default-config + 显式路径)
99+
100+
**修改前**:
101+
```
102+
# cfg 中的路径(指向 ~/.xlings/,可能 stale)
103+
clang++ --sysroot=<xlings-subos> -isystem <xlings-llvm>/include/c++/v1 ...
104+
```
105+
106+
**修改后**:
107+
```
108+
clang++ --no-default-config \
109+
-stdlib=libc++ \
110+
-isystem <llvm>/include/c++/v1 \
111+
-isystem <llvm>/include/<triple>/c++/v1 \
112+
-isystem <glibc>/include \
113+
-isystem <linux-headers>/include \
114+
-fuse-ld=lld \
115+
--rtlib=compiler-rt \
116+
--unwindlib=libunwind \
117+
-L<llvm>/lib/<triple> \
118+
-L<glibc>/lib64 \
119+
...
120+
```
121+
122+
这里 mcpp 显式提供了 cfg 中所有必要的 flags,不再依赖 cfg 文件。
123+
Linux 和 macOS 走同一套逻辑(macOS 用 xcrun SDK 替代 glibc/linux-headers)。
124+
125+
### Toolchain 数据模型扩展
126+
127+
```cpp
128+
struct Toolchain {
129+
// ... 现有字段 ...
130+
131+
// 替代 sysroot 的细粒度路径
132+
struct PayloadPaths {
133+
std::filesystem::path glibcInclude; // glibc headers
134+
std::filesystem::path glibcLib; // glibc lib64
135+
std::filesystem::path linuxInclude; // linux kernel headers
136+
std::filesystem::path dynamicLinker; // ld-linux path
137+
};
138+
std::optional<PayloadPaths> payloadPaths; // 非空 = 使用 payload 模式
139+
};
140+
```
141+
142+
`payloadPaths` 有值时,flags 组装使用细粒度路径;
143+
`payloadPaths` 为空时(系统编译器、用户自定义),回退到 `sysroot` 或不传。
144+
145+
### GCC specs fixup 的演进
146+
147+
当前 `fixup_gcc_specs()` 重写 specs 中的 dynamic-linker 和 rpath 路径,
148+
指向 glibc xpkg 的 `lib64/`。这已经是 payload-first 的做法。
149+
150+
未来可以考虑在 specs fixup 中同时重写 sysroot:
151+
```
152+
# specs 中添加:
153+
*sysroot:
154+
<glibc-xpkg-root>
155+
```
156+
这样 GCC 自身就知道正确的 sysroot,不需要命令行 `--sysroot`
157+
但这需要更深入理解 specs 格式,作为 Phase 3 推进。
158+
159+
### Clang cfg fixup(新增)
160+
161+
类似 `fixup_gcc_specs()`,在 toolchain install 时重写 `clang++.cfg`
162+
163+
```cpp
164+
void fixup_clang_cfg(const std::filesystem::path& payloadRoot,
165+
const PayloadPaths& paths) {
166+
auto cfgPath = payloadRoot / "bin" / "clang++.cfg";
167+
// 重写:
168+
// --sysroot=<old> → 删除(由 mcpp 显式传递)
169+
// -isystem <old> → -isystem <llvm>/include/c++/v1
170+
// -L<old> → -L<llvm>/lib/<triple>
171+
// -Wl,--dynamic-linker=<old> → -Wl,--dynamic-linker=<glibc>/lib64/ld-linux
172+
// -Wl,-rpath,<old> → -Wl,-rpath,<glibc>/lib64
173+
}
174+
```
175+
176+
这样即使不用 `--no-default-config`,cfg 中的路径也是自洽的。
177+
178+
## 实施路线
179+
180+
### Phase 1(当前 PR #62 已完成)
181+
182+
- 删除 M5.5 subos 覆盖
183+
- `probe_sysroot` 增加 cfg 解析 + GCC stale path remap
184+
- macOS Clang 保持 `--no-default-config` + xcrun
185+
186+
**效果**:修复了 bug,但 GCC sysroot 仍依赖 registry subos,Clang 仍依赖 cfg 中的
187+
xlings 路径。
188+
189+
### Phase 2:Payload-first 路径探测
190+
191+
- Toolchain 模型增加 `PayloadPaths` 字段
192+
- `probe_sysroot_paths()` 通过 `find_sibling_tool()` 定位 glibc 和 linux-headers xpkgs
193+
- 环境检查:detect 阶段验证关键头文件和库文件存在
194+
- 缺失 linux-headers 时给出明确提示,或自动触发安装
195+
196+
**改动文件**:
197+
- `src/toolchain/model.cppm` — PayloadPaths 结构
198+
- `src/toolchain/probe.cppm` — probe_sysroot_paths() 实现
199+
- `src/toolchain/detect.cppm` — 调用 probe_sysroot_paths
200+
201+
### Phase 3:Payload-first flags 组装
202+
203+
- `flags.cppm` 使用 PayloadPaths 组装 `-isystem` / `-L` / `-B` 替代 `--sysroot`
204+
- `stdmod.cppm` 同步使用 PayloadPaths
205+
- GCC 和 Clang 走统一的 flags 组装逻辑
206+
- 去掉对 cfg 文件的运行时依赖
207+
208+
**改动文件**:
209+
- `src/build/flags.cppm` — 核心 flags 组装
210+
- `src/toolchain/stdmod.cppm` — std module 预编译
211+
- `src/toolchain/clang.cppm` — clang std module build commands
212+
213+
### Phase 4:cfg/specs fixup 统一
214+
215+
- toolchain install 时对 Clang cfg 做 fixup(类似 `fixup_gcc_specs`)
216+
- 使 payload 完全自洽,不依赖 xlings 原始路径
217+
- 去掉 `--no-default-config`(cfg 本身已正确)
218+
219+
**改动文件**:
220+
- `src/cli.cppm` — fixup_clang_cfg() + install 流程集成
221+
222+
### Phase 5(可选):依赖声明
223+
224+
在 xpkgs 包描述中声明 sysroot 依赖关系:
225+
```toml
226+
[toolchain.gcc]
227+
requires = ["glibc", "linux-headers", "binutils"]
228+
```
229+
mcpp 在 toolchain install 时自动检查和安装这些依赖包。
230+
231+
## 设计对比
232+
233+
| 维度 | 当前(subos) | 目标(payload-first) |
234+
|------|-------------|---------------------|
235+
| sysroot 来源 | xlings subos(合并目录) | glibc + linux-headers xpkgs |
236+
| 路径控制 | 被动依赖 xlings 初始化 | mcpp 主动探测和组装 |
237+
| 环境检查 | 无(编译时才报错) | detect 阶段验证 |
238+
| cfg/specs | cfg stale + specs fixup | 两者都 fixup,路径自洽 |
239+
| 跨平台 | macOS/Linux/Windows 各自特殊处理 | 统一的 PayloadPaths 抽象 |
240+
| 错误提示 | `linux/limits.h not found` | `linux-headers package missing` |
241+
| xlings 依赖 | subos + xpkgs + cfg 路径 | 只依赖 xpkgs 文件 |

src/build/flags.cppm

Lines changed: 61 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -88,41 +88,60 @@ CompileFlags compute_flags(const BuildPlan& plan) {
8888
include_flags += " -I" + escape_path(abs);
8989
}
9090

91-
// Sysroot + config override.
91+
// Sysroot / payload paths.
9292
//
93-
// On macOS, xlings LLVM's clang++.cfg has hardcoded paths that become
94-
// stale after copying. Use --no-default-config + explicit flags.
93+
// Payload-first: when PayloadPaths are available (glibc + linux-headers
94+
// xpkgs found), use -isystem for each payload include dir. This avoids
95+
// dependency on xlings subos.
9596
//
96-
// On Linux, the cfg contains essential linker flags (-fuse-ld=lld,
97-
// --rtlib=compiler-rt). Let the cfg apply normally; pass --sysroot
98-
// explicitly to override any stale sysroot value in the cfg.
97+
// For Clang with a cfg file: use --no-default-config to bypass
98+
// potentially-stale paths, then provide all flags explicitly.
99+
//
100+
// Fallback: if no PayloadPaths, use --sysroot from probe_sysroot().
99101
std::string sysroot_flag;
100-
bool is_macos_clang = mcpp::toolchain::is_clang(plan.toolchain)
101-
&& (plan.toolchain.targetTriple.find("apple") != std::string::npos
102-
|| plan.toolchain.targetTriple.find("darwin") != std::string::npos);
103-
if (is_macos_clang) {
104-
auto cfgPath = plan.toolchain.binaryPath.parent_path()
105-
/ (plan.toolchain.binaryPath.stem().string() + ".cfg");
106-
if (std::filesystem::exists(cfgPath)) {
107-
auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path();
108-
auto libcxxInclude = llvmRoot / "include" / "c++" / "v1";
109-
sysroot_flag = " --no-default-config";
110-
sysroot_flag += " -isystem" + escape_path(libcxxInclude);
111-
if (!plan.toolchain.targetTriple.empty()) {
112-
auto targetInclude = llvmRoot / "include"
113-
/ plan.toolchain.targetTriple / "c++" / "v1";
114-
if (std::filesystem::exists(targetInclude))
115-
sysroot_flag += " -isystem" + escape_path(targetInclude);
116-
}
117-
if (auto sdk = mcpp::platform::macos::sdk_path())
118-
sysroot_flag += " --sysroot=" + escape_path(*sdk);
119-
else if (!plan.toolchain.sysroot.empty())
120-
sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot);
121-
f.sysroot = sysroot_flag;
102+
bool isClangWithCfg = false;
103+
std::filesystem::path cfgPath;
104+
if (mcpp::toolchain::is_clang(plan.toolchain)) {
105+
cfgPath = plan.toolchain.binaryPath.parent_path()
106+
/ (plan.toolchain.binaryPath.stem().string() + ".cfg");
107+
isClangWithCfg = std::filesystem::exists(cfgPath);
108+
}
109+
110+
if (isClangWithCfg) {
111+
// Clang with cfg: bypass cfg and provide all paths explicitly.
112+
auto llvmRoot = plan.toolchain.binaryPath.parent_path().parent_path();
113+
auto libcxxInclude = llvmRoot / "include" / "c++" / "v1";
114+
sysroot_flag = " --no-default-config";
115+
// libc++ headers
116+
sysroot_flag += " -stdlib=libc++";
117+
sysroot_flag += " -isystem" + escape_path(libcxxInclude);
118+
if (!plan.toolchain.targetTriple.empty()) {
119+
auto targetInclude = llvmRoot / "include"
120+
/ plan.toolchain.targetTriple / "c++" / "v1";
121+
if (std::filesystem::exists(targetInclude))
122+
sysroot_flag += " -isystem" + escape_path(targetInclude);
123+
}
124+
// C library + kernel headers from payload
125+
if (plan.toolchain.payloadPaths) {
126+
auto& pp = *plan.toolchain.payloadPaths;
127+
sysroot_flag += " -isystem" + escape_path(pp.glibcInclude);
128+
if (!pp.linuxInclude.empty())
129+
sysroot_flag += " -isystem" + escape_path(pp.linuxInclude);
130+
} else if (auto sdk = mcpp::platform::macos::sdk_path()) {
131+
sysroot_flag += " --sysroot=" + escape_path(*sdk);
122132
} else if (!plan.toolchain.sysroot.empty()) {
123-
sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot);
124-
f.sysroot = sysroot_flag;
133+
sysroot_flag += " --sysroot=" + escape_path(plan.toolchain.sysroot);
125134
}
135+
// Linker flags that cfg normally provides
136+
sysroot_flag += " -fuse-ld=lld --rtlib=compiler-rt --unwindlib=libunwind";
137+
f.sysroot = sysroot_flag;
138+
} else if (plan.toolchain.payloadPaths) {
139+
// GCC or Clang without cfg: use payload -isystem paths.
140+
auto& pp = *plan.toolchain.payloadPaths;
141+
sysroot_flag += " -isystem" + escape_path(pp.glibcInclude);
142+
if (!pp.linuxInclude.empty())
143+
sysroot_flag += " -isystem" + escape_path(pp.linuxInclude);
144+
f.sysroot = sysroot_flag;
126145
} else if (!plan.toolchain.sysroot.empty()) {
127146
sysroot_flag = " --sysroot=" + escape_path(plan.toolchain.sysroot);
128147
f.sysroot = sysroot_flag;
@@ -203,12 +222,23 @@ CompileFlags compute_flags(const BuildPlan& plan) {
203222
}
204223
}
205224

225+
// For Clang with payload paths: add glibc lib + dynamic linker to link flags.
226+
std::string payload_ld;
227+
if (isClangWithCfg && plan.toolchain.payloadPaths) {
228+
auto& pp = *plan.toolchain.payloadPaths;
229+
payload_ld += " -L" + escape_path(pp.glibcLib);
230+
payload_ld += " -Wl,-rpath," + escape_path(pp.glibcLib);
231+
auto loader = pp.glibcLib / "ld-linux-x86-64.so.2";
232+
if (std::filesystem::exists(loader))
233+
payload_ld += " -Wl,--dynamic-linker=" + escape_path(loader);
234+
}
235+
206236
if constexpr (mcpp::platform::is_windows) {
207237
f.ld = "";
208238
} else if constexpr (mcpp::platform::needs_explicit_libcxx) {
209239
f.ld = std::format("{}{}{} -lc++", full_static, static_stdlib, b_flag);
210240
} else {
211-
f.ld = std::format("{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag, runtime_dirs);
241+
f.ld = std::format("{}{}{}{}{}{}", full_static, static_stdlib, sysroot_flag, b_flag, runtime_dirs, payload_ld);
212242
}
213243

214244
return f;

0 commit comments

Comments
 (0)