Skip to content

Commit fdad165

Browse files
authored
feat: 编译器是能力,不是配置 —— payload 版本权威 + specs 污染 + xlings pin (2026.8.8.2) (#378)
一个 payload 交付两样可分离的东西:编译的**能力**,以及关于如何链接的**主张**。mcpp 要前者,后者自己给 —— 链接行才是构建决策该待的地方,因为它是逐次构建而变的那一个。四个看起来无关的缺陷收敛到这一条上。 设计:`.agents/docs/2026-08-08-payload-version-and-contract-drift-design.md`(含 §9 实施回写)。替代已关闭的 #377。 ## 1. 产物加载哪个 glibc,由权威决定,不再靠目录顺序 旧规则向目录问「那个 glibc」并取 `readdir` 的第一项。只装一个时它永远正确,所以从没有东西逼它正确 —— 而一条带 `xim:glibc@>=2.38` 的依赖就足以装进第二个。mcpp-index 上真实发生过:编译侧取 2.44,产物 interpreter(冻结在 gcc specs 里)仍是 2.39,二进制引用 `GLIBC_2.42` 却跑在没有它的运行时上,**报错落在与那条依赖无关的包上**。 解析顺序:`[xlings] subos` 自述 → 活动 subos 自述 → 兼容记录(specs / cfg / **PT_INTERP**)→ 单例 payload。计入工具链指纹(11 字段)。 两条判据是落地时被 CI 逼出来的,已写回设计: - **没得选就不算猜。** 恰好一个 glibc payload 时没有选择可言,拒答等于拒答一个只有一个答案的问题。两个及以上才沉默 —— 那才是事故的形状。 - **化石不算权威。** `specs`/`.cfg`/`PT_INTERP` 都是过去某刻的记录,payload 被换掉时没人回头改。一条记录只有在它指的东西还在,才算数。subos 自己的声明同样适用。 把「查不到」也当成「有歧义」的代价是产物掉到宿主 loader —— 比猜错版本更糟,因为它连沙箱都出去了。 ## 2. 不再改写 GCC 的 `specs` 旧重写用单路径 needle 配双路径 replacement,每个跑过它的 home 都漏下一条:一台开发机产出的**每个** gcc 产物里都带 **68 条**陈旧 `RUNPATH`,全指向已删除的 `mktemp` 目录。没有东西发现它们,因为一条死 RUNPATH 只多花一点搜索时间。 改为 `-specs=` 一份逐构建生成的干净文件(`-dumpspecs` 取**内建** `*link:`,去掉 loader/rpath)。不带 `+` 的 `-specs=` 是**替换**,payload 分毫未动。逐构建、不需写权限,继承来的只读 payload 也能用。 删掉 `*link:` 也删掉了它提供的东西,因此 loader 与每条 rpath 由 mcpp 显式写在链接行上 —— 包括**编译器自己的 `lib64`**(`libgcc_s.so.1` 在那)。 ## 3. 落后于 pin 的 vendored xlings 会被替换 —— 但只在替换品确实更新时 一个 home 会永远留着第一次获取的 xlings(实测 2026.8.2.1 对 pin 2026.8.6.3)。`subos_info` 是 2026.8.5.1 才有的,所以那台机器上 subos 的自述一直被太旧的客户端丢弃,#352 的修复无论 mcpp 多新都无法生效。 修复的第一版直接删了重取,把 2026.8.2.1 换成系统的 **0.4.51** —— 更旧、且同样缺那个特性。现在先给替换品定价再动手。 ## 4. `--sysroot` 的判据从「存在」改为「归属」 gcc 把 `--sysroot=<...>/.xlings/subos/default` 当字符串烙进去,而一台机器上有很多同名目录 —— 烙入的路径经常**存在、却属于另一个 checkout**。实测:本仓库的构建解析到了无关仓库下的 sysroot。归属先于可用性。 --- ## 验证 | | | |---|---| | 单测 | **67/67**(新增 5 个文件、28 条断言) | | 本地全量 e2e | **186 passed / 6 failed / 8 skipped**,6 个失败全在已记录基线内(03/09/20/33/59/98),其中 03/09/20/155/184 逐一用**已发布的 2026.8.6.3** 复现确认;`178` 反而转绿 | | CI | **18/18 全绿** | | 版本 | 2026.8.8.2;xlings pin → 2026.8.7.1(索引最新;上游 2026.8.8.1 未收录,pin 过去会让 CI 装不到) | runtime binding 四条来源逐条实测:新建自述 subos、注入 `glibc@2.99`(不存在)、挪开 `specs` **且**把 gcc 自身 `PT_INTERP` 改指宿主、三条来源全打掉只剩单例。 ## 这一轮真正的教训 **十轮 CI 抓出 11 个缺陷,其中 8 个在本地定向子集全绿时不可见。** 形状高度一致:修补落在控制流或本机状态到不了的地方。 - 兼容回落读的是**本 PR 让 mcpp 不再写**的文件 —— 每台老机器都还留着它,所以本机永远绿 - 归属谓词加在 `probe_sysroot` **第一步就 return 掉**的函数里 - `--no-cache` 删 `target/`,带走了我放进去的 spec;e2e 201 只跑暖构建,**那个专为此机制存在的测试看不见它** - 我给 201 加的 PayloadFirst 分支,**把修复拿掉后照样通过** —— 本机 gcc 的 baked sysroot 指向另一个仓库且存在,把它拉回了 Sysroot 模式。守卫因此改成**指名模式**的单测,并逐一验证过红→绿 - 判据本身也错过:**「跑不跑得起来」在任何有宿主工具链的机器上都是 yes**,哪怕产物正在偷用宿主的库。换成「是否从 `/lib`、`/usr/lib` 加载任何库」 - 最后一条是**误报**:`ldd` 用宿主 loader 内省,总把 payload 解释器报成解析到宿主 —— 一个会拦下正确工作的检查,代价一样大 以及一个低级错误:`mcpp.toml` 的 `[toolchain] default` 被本地试验写成 llvm,`git add -A` 一起提交,五个 job 连带失败。**多个 job 同时开始失败,几乎总是指向一处共同输入被改动。**
1 parent 463dedf commit fdad165

43 files changed

Lines changed: 2691 additions & 190 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
# 编译器是能力 —— 跨仓实施计划
2+
3+
> **For agentic workers:** 逐 task 执行,每个 task 自带 red→green→commit。**T1 是所有后续 task 的前提**(没有它,后面每一步的观测都不可信)。
4+
5+
**Goal:** 让 mcpp 停止依赖编译器的安装期配置,把「用哪份 libc」收敛成一个权威;顺带把已经污染的产物清干净,并让多 subos 构建真正可用。
6+
7+
**设计文档:**
8+
- `.agents/docs/2026-08-07-xlings-as-runtime-substrate-design.md`(运行时底座;S1/S3 与撤销 `c_runtime` 的理由)
9+
- `.agents/docs/2026-08-08-payload-version-and-contract-drift-design.md`(本计划的主体)
10+
11+
**Tech Stack:** C++23 modules,gtest(`tests/unit/`),shell e2e(`tests/e2e/`),Lua(mcpp-index)。
12+
13+
## Global Constraints
14+
15+
- **不改 payload**`patchelf_walk`(让编译器自己能跑)保留;**写 payload 内配置的一律删掉**
16+
- **权威唯一**:`--runtime``[xlings] subos``subos_info.runtime`**报错,不猜**。删掉「扫目录挑一个」。
17+
- **平台差异下沉** `src/platform/`,`build/` 里不写 `#ifdef`
18+
- **每一处「注释描述行为」必须有对应断言**(R-C)。本轮已发现三处注释与代码不符。
19+
- **跨仓 wire format 的 fixture 取自写入方**(R-A),不得手写。
20+
- **一条测试若为覆盖某路径而存在,必须先断言走了那条路径**(R-B)。
21+
- **不新增任何让产物链宿主 libc 的开关**(8-07 §5.2 已否)。
22+
23+
---
24+
25+
### T1: 三处「注释撒谎」先钉住 —— 后续观测的地基
26+
27+
**Files:** `tests/unit/test_linkmodel.cpp`(新断言)、`src/xlings.cppm``src/toolchain/post_install.cppm`(改注释)
28+
29+
本轮发现三处注释描述了代码没有的行为。**先让它们不能再撒谎**,否则后面每一步都可能在错误的前提上推理。
30+
31+
- [ ] **Step 1** 写失败单测:`find_sibling_tool` 的注释说「first (highest)」,断言**两个版本目录时返回的是排序后最高的那个**
32+
- [ ] **Step 2** 跑,确认 FAIL(今天返回 readdir 第一个)
33+
- [ ] **Step 3** 二选一并使注释与代码一致:①真的排序;②改注释为「未定义顺序」并让调用方不得依赖。**本计划选 ②**——T2 会让它不再被用于选版本
34+
- [ ] **Step 4** 另两处:`fixup_gcc_specs` 的「Idempotent」(跨 home 不幂等)、`execute.cppm` reader 的「旧缓存视为 miss」(T5 修)——先把注释改成事实
35+
- [ ] **Step 5** Commit
36+
37+
---
38+
39+
### T2: 权威降级链 —— 没有权威就不进 PayloadFirst
40+
41+
**Files:** `src/toolchain/probe.cppm``src/xlings/subos_info.cppm``src/build/prepare.cppm`
42+
**Test:** `tests/unit/test_toolchain_probe.cpp`(新)
43+
44+
**Interfaces:**
45+
```cpp
46+
// probe 不再自己选版本;版本由权威给出
47+
std::optional<PayloadPaths> probe_payload_paths(
48+
const std::filesystem::path& compilerBin,
49+
std::string_view runtimeBinding); // "glibc@2.39";空 ⇒ 不进 PayloadFirst
50+
```
51+
52+
- [ ] **Step 1** 单测:给定 `glibc@2.39` 且 home 内有 2.39/2.44 两份 ⇒ **必须返回 2.39**;给定空 binding ⇒ **返回 nullopt**(不猜)
53+
- [ ] **Step 2** 跑,FAIL
54+
- [ ] **Step 3** 实现;`prepare.cppm` 按 `--runtime` → `[xlings] subos` 的 `subos_info.runtime` 顺序解析,末级不猜
55+
- [ ] **Step 4** 跑,PASS
56+
- [ ] **Step 5** Commit
57+
58+
---
59+
60+
### T3: fingerprint 加入 runtime 轴 —— 多 subos 的前置
61+
62+
**Files:** `src/toolchain/fingerprint.cppm`、`tests/unit/test_fingerprint.cpp`
63+
64+
不加这条,切 subos 不改 fingerprint ⇒ `target/<fp>/` 被复用 ⇒ 里面是对另一份 glibc 编译的对象。**比今天更坏。**
65+
66+
- [ ] **Step 1** 单测:两个只有 `runtimeBinding` 不同的输入 ⇒ **fingerprint 必须不同**
67+
- [ ] **Step 2/3/4** red → 加字段 → green
68+
- [ ] **Step 5** Commit
69+
70+
---
71+
72+
### T4: GCC 也显式发 loader/rpath + clean specs
73+
74+
**Files:** `src/toolchain/linkmodel.cppm`、`src/build/flags.cppm`、`src/toolchain/post_install.cppm`(删 `fixup_gcc_specs`)
75+
**Test:** `tests/unit/test_linkmodel.cpp`、`tests/e2e/201_gcc_clean_specs.sh`(新)
76+
77+
**已实测**:`-Wl,--dynamic-linker` 压得过 specs;`-specs=<无 `+` 定义>` 是替换;`g++ -dumpspecs` 给内建 specs(`/tmp` 路径 0 条);真实构建(模块 + `import std` + 共享库)RUNPATH 68→2、死路径 0、可运行。
78+
79+
- [ ] **Step 1** e2e:gcc 构建的产物 **RUNPATH 中不得有 `/tmp/tmp.`**,且 interpreter 是权威指定的那份,且能跑
80+
- [ ] **Step 2** 跑,FAIL(今天有 34+ 条)
81+
- [ ] **Step 3** ①去掉 `linkmodel` 的 `if (clangDriver)` 门;②构建期生成 clean specs 到 **build dir**,`-specs=` 传入;③**删掉 `fixup_gcc_specs`**(保留 `patchelf_walk`)
82+
- [ ] **Step 4** 跑,PASS
83+
- [ ] **Step 5** Commit
84+
85+
---
86+
87+
### T5: 并回 #377 的两处修复
88+
89+
**Files:** `src/xlings/subos_info.cppm`、`src/build/execute.cppm`、`tests/unit/test_subos_info.cpp`、`tests/e2e/200_subos_env_reaches_program.sh`
90+
91+
已在 `fix/fast-run-stale-cache-subos` 分支上完成并 18/18 CI 通过,**原样带过来**:
92+
93+
- [ ] **Step 1** `git cherry-pick` 该分支的三个 commit(wire format / 旧缓存 / e2e fixture)
94+
- [ ] **Step 2** 跑单测 + e2e,确认仍绿
95+
- [ ] **Step 3** Commit(或保留 cherry-pick 的原始提交)
96+
97+
---
98+
99+
### T6: D4 沙箱 xlings 版本比对 + D5 陈旧 sysroot
100+
101+
**Files:** `src/fallback/xlings_binary.cppm`、`src/doctor.cppm`、`src/fallback/probe_sysroot.cppm`
102+
103+
- [ ] **Step 1** 单测/e2e:vendored xlings 版本**低于 pin** ⇒ 被替换;**高于 pin** ⇒ 不动
104+
- [ ] **Step 2** red
105+
- [ ] **Step 3** 实现;doctor 增加两条 finding:①xlings 低于 pin;②`--sysroot` 指向当前项目之外
106+
- [ ] **Step 4** green
107+
- [ ] **Step 5** Commit
108+
109+
---
110+
111+
### T7: 文档 + 版本 + PR
112+
113+
- [ ] `docs/03-toolchains.md`:权威降级链、`[xlings] subos`、多 subos 构建
114+
- [ ] `docs/05-mcpp-toml.md`:`[xlings] subos` 与 runtime 的关系
115+
- [ ] 中文版同步
116+
- [ ] `MCPP_VERSION` / `mcpp.toml` → `2026.8.8.2`;`kXlingsVersion` → 最新;`check_version_pins.sh` 通过
117+
- [ ] 全量 `mcpp test` + 关键 e2e
118+
- [ ] 开 PR
119+
120+
---
121+
122+
### T8: mcpp-index 图形栈重新落地(D1 修好之后)
123+
124+
**Files(mcpp-index):** `pkgs/c/compat.glx-runtime.lua`、`pkgs/c/compat.glfw.lua`
125+
126+
- [ ] **Step 1** 新增 e2e/验证:**装 `xim:graphics` 前后**,各跑一次与图形无关的成员,断言产物 `PT_INTERP` 与 `readelf -V` 的 glibc 符号上界**不变**。这是上次事故真正缺失的那个测试
127+
- [ ] **Step 2** 重新应用 `f44e896` 的两个文件改动(deps 放平台层)
128+
- [ ] **Step 3** CI 全绿后合入
129+
130+
---
131+
132+
## Self-Review
133+
134+
| 设计文档条目 | Task |
135+
|---|---|
136+
| 8-08 §3.1 原则(编译器当能力) | T4 |
137+
| 8-08 §3.2 权威降级链 / D1 | T2 |
138+
| 8-08 §3.5① 去掉 clangDriver 门 | T4 |
139+
| 8-08 §3.5② clean specs 进 build dir | T4 |
140+
| 8-08 §3.5③ 删 `fixup_gcc_specs` / D6 | T4 |
141+
| 8-08 §3.6 多 subos + fingerprint 前置 | T2 + T3 |
142+
| 8-08 D4 沙箱 xlings / D5 sysroot | T6 |
143+
| 8-08 D2 wire format / D3 旧缓存 | T5 |
144+
| 8-08 §4.2 R-A/R-B/R-C | T1(R-C)+ 各 task 的测试形态 |
145+
| 8-07 §1.5 图形栈迁移 | T8 |
146+
| 8-07 §3-S2 不加链宿主 libc 的开关 | Global Constraints(不做) |
147+
148+
**未覆盖且是有意的**:8-07 §7「xlings 落盘 exports」—— 8-08 已说明它不再是关键路径(权威改为 `subos_info.runtime`)。Q6(交叉/musl/MinGW 下 `-specs=` 替换)在 T4 的 e2e 里只覆盖 native;**其余平台列为 PR 中显式声明的未验项**,不假装验过。

0 commit comments

Comments
 (0)