Skip to content

Commit fd91ffd

Browse files
feat: 图形栈闭合与分发档位 —— 标签、一条没人依赖的边、一个被钉住的 pin (#405, #407) (2026.8.10.2) (#408)
图形栈不可用不是一个 bug,是三层各自独立的故障。追到代码之后,其中两层的既有结论 是错的 —— 按它们去修,修完还是坏的。 ## 三条被测量推翻的结论 1. #405 的 issue 根因是错的(我自己写的那条,而且是这个 issue 上第二次)。 它说「谓词漏判 ⇒ 不发 std 的 stage 边」。生成物否掉了它:边就在 build.ninja 第 65 行。 `scan_packages` 的 packages 本来就含依赖包根,谓词恒为真。真因是**没有任何边依赖 那条边**,ninja 于是从不执行它。按 issue 里的修法(entry.json 记 imports_std) 作用在一个已经为真的谓词上 —— 改完仍然坏。 2. mcpp-index 上 8 个图形成员全红不是数据缺陷。`xim:libglvnd@>=1.7.0.1` 在 xlings 2026.8.9.2 起解析正常(四段版本 semver 重写),红的原因是 index CI 钉在 mcpp 2026.8.8.2,它内带 xlings 2026.8.8.1 —— 正好落在修复之前。 xim-pkgindex 一个字都不用改。 3. 图形拿不到 GPU 的直接原因在 mcpp 自己的链接命令行里:全仓没有一处 `--disable-new-dtags`,唯一相关的一处显式写了 `--enable-new-dtags`。 ## 修复 - **#405** 缓存命中时,被恢复的包传递依赖的 std BMI 没有消费者。修法把它放进 `_mcpp_staged_cache` —— 那个聚合本来就是为「stage 边丢掉编译边携带的次序」建的, std BMI 是同一缺陷早一条边。不动 cache key / entry schema,现有缓存全部有效。 缓存 miss 时依赖在本地编译、把 std 边带进图,所以第一个构建它的人永远是好的、 之后每个人都坏 —— 这就是它伪装成升级回归的方式。 - **#407** 三种模式写同一个 build.ninja,快路径只比源码 mtime。改成让图自己声明形态 (`# mcpp:graph=normal|test`),快路径校验它即将重放的那张图。**读取侧不变式**, 并同时删掉 #387 留下的写入侧修补 —— 写入侧要求每个未来的图重写者都记得调用, 这正是 `mcpp test` 那半边在 `--configure-only` 修好之后仍然坏着的原因。 ## 新增 - **加载器标签契约**(`mcpp.build.loader_contract`):可执行 DT_RPATH、库 DT_RUNPATH。 DT_RUNPATH 只对携带它的对象**自己发起**的 dlopen 生效,而图形程序到驱动的三到四层 dlopen 都不是它发起的 —— 是 libGLX.so.0 代发的。所以决定能否上 GPU 的是标签不是路径。 反过来在库上强制 RPATH 有害(打断 eglInitialize),所以一分为二。链接期与 pack 的 patchelf 期读同一条契约。这是 xlings 图形栈设计里 E2(构建侧)的 mcpp 那一半。 - **rule E**:标签校验落在产物上,写进 resolution.json 的 `loader_tags`,warn-first。 记录而不只是告警 —— 只在沉默中通过的检查,和根本没跑的检查,输出完全相同。 - **pack 不再残留构建机路径**:此前只重写主二进制,bundle 进来的每个 .so 都保留着 指向构建机 xlings store 的绝对 RUNPATH。「依赖 xlings 生态」是设计选择, 「依赖这一台机器的这一份 store」是缺陷,而且在构建它的机器上跑得好好的。 **不碰动态加载器** —— 它不是被搜索的库,它是执行搜索的程序;patchelf 改它会让 self-contained 档在 main 之前段错误(30_pack_modes 抓到的)。 - **HOST-REQUIREMENTS**:自包含有底。驱动只能来自目标机器(与内核模块锁步 + 禁止 再分发),所以诚实产出是 bundle 加一份声明。带 discovery 列,因为几种发现机制 互不通用。 - **自带 libc 的档硬拒宿主能力**:self-contained 与 static 在 plan 期失败并指出改用 vendored。两者坏在同一件事上(#392/#401 的两个方向),此前都链得过去然后运行时崩。 - **`[[runtime.requirements]] discovery`**:声明式,mcpp 绝不推断 —— 从能力名推断 就是把 provider 专属知识写进 mcpp,`test_runtime_contract` 正是为此设的门。 - **artifact 身份判决**:resolution.json 每个 artifact 带 `identity` (ok/mismatch/missing/unverified),跟随符号链接。这是 mcpp 已经对私有 libc 执行的 规则的推广,纯路径事实。`why runtime` 的 `(none declared)` 改为 `(not declared by the environment — nothing to verify)`:有名无物是未验证,不是通过。 ## 变更 - 内带 xlings 升到 2026.8.10.4(16 个 pin 由 check_version_pins.sh 机器校验)。 ## 本地验证 - unit 77/77 通过(含新增 test_loader_contract:契约、图形态、宿主要求、身份判决)。 - e2e 183 通过 / 25 失败 / 8 跳过。**25 条里 24 条用已发布的 2026.8.8.2 逐条复现**, 同一个环境根因:本机共享 gcc 载荷的 specs 被历史安装污染 —— `--dynamic-linker` 指向已被改名的 glibc/2.44,rpath 里还有约 40 条来自已删除沙箱的 /tmp/tmp.* 条目。 第 25 条(30_pack_modes)是真回归,已定位并修复(不得 patchelf 动态加载器)。 - 每条新 e2e 都先证伪过:撤掉对应修改必须变红,并已实测。 Co-authored-by: speak-agent <248744407+speak-agent@users.noreply.github.com>
1 parent 3f237ed commit fd91ffd

39 files changed

Lines changed: 3962 additions & 79 deletions

.agents/docs/2026-08-09-issue-triage-full-sweep.md

Lines changed: 676 additions & 0 deletions
Large diffs are not rendered by default.

.agents/docs/2026-08-10-graphics-closure-and-distribution-tiers-design.md

Lines changed: 754 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
# 实施计划:图形栈闭合与分发档位
2+
3+
> 设计:`2026-08-10-graphics-closure-and-distribution-tiers-design.md`
4+
> 分支:`feat/graphics-closure-and-distribution-tiers`
5+
> 单 PR、一档全发。基线 `main` `3f237ed`(`2026.8.10.1`)。
6+
7+
---
8+
9+
## 0. 模块划分
10+
11+
三条**协议**各自独占一个 `.cppm`,因为它们每一条都有**两个以上的读写方**,
12+
而"同一决策两处推导"是这个仓库反复付过学费的形状。
13+
14+
| 新模块 | 协议内容 | 谁写 | 谁读 |
15+
|---|---|---|---|
16+
| `src/build/graph_shape.cppm`<br>`mcpp.build.graph_shape` | `build.ninja` 首行 `# mcpp:graph=<shape>` | `ninja_backend` | `execute`(两条快路径) |
17+
| `src/build/loader_contract.cppm`<br>`mcpp.build.loader_contract` | 加载器标签契约:哪种产物要哪个标签、flag 怎么拼、判决怎么表达 | `plan`(链接期)、`pack`(patchelf 期) | `runtime_validation`(rule E) |
18+
| `src/pack/host_requirements.cppm`<br>`mcpp.pack.host_requirements` | `HOST-REQUIREMENTS` 文本格式 + 到 xpkg `[runtime].requirements` 的投影 | `pack` | `publish`(J)、一致性检查 |
19+
20+
平台特化一律留在 `src/platform/`,不外溢:
21+
22+
| 平台模块 | 本轮改动 |
23+
|---|---|
24+
| `src/platform/elf_runtime.cppm` | 新增 `SearchPathTag`(`None`/`Rpath`/`Runpath`/`Both`)与 `searchPathTag` 字段。**今天两个标签被合并进 `runpaths` 就丢了**,rule E 要的正是被丢掉的那一位 |
25+
| `src/platform/runtime_env_contract.cppm` | 补一条:DT_RUNPATH 只对**携带它的对象自己发起**`dlopen` 生效;第三方对象代为 `dlopen` 时不生效 |
26+
27+
`loader_contract` **不认识 ELF** —— 它表达"可执行文件要 RPATH、库要 RUNPATH"这条策略,
28+
读标签的动作委托给 `platform::elf`。这样 macOS/Windows 只是"契约不适用",而不是散落 `#ifdef`
29+
30+
**判断"是不是可执行文件"统一用 `PT_INTERP` 是否存在**(`facts.interp` 非空),
31+
不用 `ET_EXEC` —— PIE 可执行文件是 `ET_DYN`,和共享库同型。xlings 的 `_has_pt_interp` 同判据。
32+
33+
---
34+
35+
## 1. 步骤
36+
37+
每步给:改哪、测什么、**怎么先证伪****每一步的测试必须先看到红。**
38+
39+
### 步 1 — A:pin + 版本(无行为变更,先落地,让后面每一步都在新 pin 上验)
40+
41+
- `src/xlings.cppm:47` `kXlingsVersion``"2026.8.10.4"`
42+
- `src/version.cppm:34` `MCPP_VERSION` → 下一个补丁号
43+
- `mcpp.toml``version` 同步(第一组两个文件,同一个 commit)
44+
- **不动** `.xlings.json` 的 bootstrap pin
45+
- 验:`.github/tools/check_version_pins.sh` 绿
46+
47+
### 步 2 — 平台层:把标签这一位保住
48+
49+
`src/platform/elf_runtime.cppm`:
50+
51+
```cpp
52+
enum class SearchPathTag { None, Rpath, Runpath, Both };
53+
// ElfRuntimeFacts 新增:
54+
SearchPathTag searchPathTag = SearchPathTag::None;
55+
```
56+
57+
在已有的 `kDtRpath` / `kDtRunpath` 分支里各置一位;`runpaths` 的既有语义
58+
(两者都在时以 RUNPATH 为准)**不变** —— 那是加载器物理,现有调用方依赖它。
59+
60+
- 单测 `tests/unit/test_elf_runtime.cpp`:三个 fixture(仅 RPATH / 仅 RUNPATH / 两者)
61+
断言 `searchPathTag` 与 `runpaths` 各自正确。
62+
- **证伪**:两者都在时若只读第一个命中,`Both` 会退化成 `Rpath` —— fixture 必须能抓到。
63+
64+
### 步 3 — B:#405
65+
66+
`src/build/ninja_backend.cppm:1053–1083`,`staged` 非空且 `has_std_artifacts` 时,
67+
把 `std_bmi_dst`(以及 `has_std_compat` 时的 `compat_bmi_dst`)一并放进聚合。
68+
69+
- e2e `tests/e2e/212_cached_dep_std_is_ordered.sh`
70+
- 依赖选 `mcpplibs.cmdline@0.0.2`(其模块 `import std`),消费方**刻意不写 `import std`**
71+
- 清该包 pkg 缓存 → 项目 A(miss)必须 OK → 项目 B(hit)必须也 OK
72+
- **图断言**:`_mcpp_staged_cache : phony` 那一行必须含 `std.gcm`
73+
- ⚠️ **全程不删任何产物**
74+
- **证伪**:撤掉 push_back,B 必须红
75+
76+
### 步 4 — C:#407
77+
78+
1. `mcpp.build.graph_shape`:`enum class GraphShape { Normal, WithTests }`、
79+
`header_line(shape)`、`read_shape(ninjaPath)`。**读写同一处拼写。**
80+
2. `BuildPlan` 加 `graphShape`;`prepare_build` 按 `includeDevDeps || !extraTargets.empty()` 置位
81+
3. `emit_ninja_string` 首行写 header
82+
4. `try_fast_build` / `try_fast_run`:`read_shape() != Normal` ⇒ 当 miss
83+
5. 删 `configure.cppm:101` 的调用与 `execute.cppm:288` 的 `forget_build_cache_entry`
84+
85+
- e2e `213_build_after_test_is_not_the_test_graph.sh`:
86+
`build → test → build`,第三次之后 `bin/<target>` 必须存在且是**这次**产的;
87+
把 `tests/*.cpp` 写坏后 `mcpp build` 必须仍绿。**不删产物。**
88+
- 既有 `211_configure_only_cdb.sh` 必须保持绿(它断言同一条可观察行为)
89+
- **顺序**:先加 header 检查 → 跑 211 绿 → 再删 `forget_build_cache_entry`。两步不合并。
90+
- **证伪**:去掉 header 检查,213 必须红
91+
92+
### 步 5 — D:DT_RPATH 契约
93+
94+
`mcpp.build.loader_contract`:
95+
96+
```cpp
97+
enum class ArtifactForm { Executable, SharedLibrary, StaticArchive, NotElf };
98+
enum class RequiredTag { Rpath, Runpath, NotApplicable };
99+
RequiredTag required_tag(ArtifactForm); // 策略,平台无关
100+
std::optional<std::string> link_flag(RequiredTag); // "-Wl,--disable-new-dtags"
101+
struct TagVerdict { enum class Status { Ok, Violation, NotApplicable }; … };
102+
TagVerdict check(const mcpp::platform::elf::ElfRuntimeFacts&, ArtifactForm);
103+
```
104+
105+
- **链接期**:`plan.cppm` 按 link unit 的 kind 追加 `link_flag(...)`,
106+
`shared_library_link_flags`(`:391`)同层;**必须排在所有 `--enable-new-dtags` 之后**
107+
- **校验期**:`runtime_validation.cppm` 加 rule E,**warn-first**
108+
- e2e `214_executable_carries_dt_rpath.sh`:
109+
- gcc 与 clang 各建 bin + shared lib
110+
- **测试自身先断言默认值是 RUNPATH**(不带 flag 编一个),否则将来链接器默认一变它静默空转
111+
- bin 必须 `Rpath`,lib 必须 `Runpath`
112+
- **证伪**:去掉 flag,bin 那条必须红
113+
114+
### 步 6 — E:dev 档身份一致
115+
116+
`src/build/runtime_validation.cppm`(或近邻)实现原生求解:
117+
118+
```
119+
dispatch(libGLX.so.0)→ DT_RPATH → vendor 目录 → 解符号链接 → 真实载荷路径
120+
→ 与声明 provider 身份比对
121+
```
122+
123+
- 三值 `OK` / `MISMATCH` / `NOT_DECLARED`,由 `mcpp why runtime` 呈现
124+
- `.wiring` **只做增强**:存在则附加显示 `state`,并标注按本产物标签重新求值;
125+
缺失 / 未知 `state``unverified`;未知 key ⇒ 忽略
126+
- 单测:构造 fixture 目录树(符号链接指向 `0.1.1`、声明 `0.1.2`)⇒ 必须 `MISMATCH`
127+
- **证伪**:把符号链接改成指向 `0.1.2` ⇒ 必须 `OK`;删掉 `.wiring`**主判据不变**
128+
129+
### 步 7 — F:pack
130+
131+
- **F1** `pack.cppm:350` `set_runpath`:可执行文件加 `--force-rpath`(走 `loader_contract`)
132+
- **F2**`toBundle` 里每个 ELF 同样重写为 `$ORIGIN`(库保持 RUNPATH)
133+
- **F3/F4** `mcpp.pack.host_requirements`:从 plan 的 `RuntimeRequirement` 生成清单
134+
- e2e `215_pack_has_no_build_machine_paths.sh`:
135+
遍历 bundle 内**每个** ELF,原生解析,**不得出现 xlings store 前缀**;
136+
可执行 `Rpath`、库 `Runpath`;有能力需求时 `HOST-REQUIREMENTS` 非空
137+
- **证伪**:去掉 F2,必须红
138+
139+
### 步 8 — G:自带 libc 的档拒绝能力型需求
140+
141+
- `pack`/`plan` 期:`static``self-contained` + 存在 `phase=="run"` 的能力型
142+
`RuntimeRequirement` ⇒ 失败,错误里说**哪一条能力 / 为什么不相容 / 改用 `vendored`**
143+
- e2e `216_selfcontained_refuses_host_capability.sh`:两档都必须红
144+
- **证伪**:换 `--mode vendored` ⇒ 两条都绿
145+
146+
### 步 9 — J:xpkg-app 投影
147+
148+
- `publish``[runtime].requirements``host_requirements` **同一个函数**产出
149+
- 一致性检查(单测):同一 plan 下两种投影逐条相等
150+
- **证伪**:改掉一处 ⇒ 检查红
151+
152+
### 步 10 — 文档
153+
154+
- `docs/` 里 pack / publish / runtime 三处随改动更新
155+
- 设计文档标记实施状态
156+
- `CHANGELOG.md`
157+
158+
### 步 11 — 本地验证
159+
160+
`mcpp build``mcpp test`(unit)→ e2e 全量。记录真实输出,不概括。
161+
162+
### 步 12–15 — PR / CI / 合入 / 发版 / index / 生态验收
163+
164+
`.agents/skills/mcpp-release/SKILL.md`
165+
166+
---
167+
168+
## 2. 顺序
169+
170+
```
171+
步1(pin/版本)
172+
└→ 步2(平台层标签位)──→ 步5(D)──→ 步7(F1/F2)
173+
└→ 步3(B #405) └→ 步8(G)
174+
└→ 步4(C #407) └→ 步9(J)
175+
步6(E)独立(只需步2)
176+
```
177+
178+
步 3 / 步 4 与其余互不相干,可先落地先验证。
179+
180+
---
181+
182+
## 3. 风险与守则
183+
184+
- **每个 e2e 先红后绿**,红的输出要贴进 PR 描述。
185+
- **`212` / `213` 全程不删产物** —— 删了未修的二进制也会绿。
186+
- **`214` 先断言默认标签是 RUNPATH** —— 否则将来默认一变它静默空转。
187+
- **步 4 的两小步不合并** —— 先加检查、211 绿,再删旧机制。
188+
- **rule E warn-first** —— 结论只来自一台 NVIDIA/X11/x86_64 机器。
189+
- 跨仓阻塞项:xlings E2b 若无声明式退出即落地(设计 §1.3.1),**与本 PR 无耦合**,单独跟。

0 commit comments

Comments
 (0)