Skip to content

Commit cf38941

Browse files
committed
release: 2026.8.11.2 —— 文档、xlings pin、设计与实施记录
版本两处同步(mcpp.toml + src/version.cppm),内带 xlings pin 升到 2026.8.11.2 并同步全部 workflow/action 引用点;check_version_pins.sh 通过。bootstrap pin (.xlings.json)不动 —— 它必须指向一个已发布并进入索引的版本,发布收尾时才前移。 文档: - 08-toolchain-internals(中英)新增 §2.3 运行期搜索闭包:四种 origin、次序与 理由、两条护栏、host_default 为什么只对非 hermetic 产物成立、退出声明。 - 同一份的 §2.1 改掉一句现在是假的话:「contract 缺失/不兼容会 hard error」。 改成矛盾报错、缺席降级,并写明 Linux 上降级确实会失去 payload-first。 - 02-pack-and-release 补一句所有档位都不携带构建机路径,并指出 system 档不是 「保留构建时的样子」而是「目标机提供一切」—— 后者无法用本机绝对路径表达。 设计与实施记录进 .agents/docs。两份文档都在顶部标了与实施的出入,每一处都是被 实测推翻的:system 档早就剥干净(所以那一项从「实现」变成「补测试」)、 farm 不能复用 linkIntent 字段(会流进 LD_LIBRARY_PATH)、载荷目录不能只读 linkRuntimeDirs(GCC 上是空的)。
1 parent 1edbc59 commit cf38941

14 files changed

Lines changed: 1040 additions & 25 deletions

.agents/docs/2026-08-11-graphics-runtime-search-closure-and-binding-degradation.md

Lines changed: 500 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 333 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,333 @@
1+
# 实施计划:运行期搜索闭包 与 binding 降级
2+
3+
> 设计:`2026-08-11-graphics-runtime-search-closure-and-binding-degradation.md`
4+
> 分支:`feat/runtime-search-closure-and-binding-degradation`
5+
> 基线:`main` `1bc6074`(`2026.8.11.1`)。**单 PR、一档全发,版本 `2026.8.11.2`**
6+
>
7+
> **实施状态:已完成。** 实施过程中改动本计划的四处,均在对应小节以
8+
> **【实施修正】** 标出 —— 每一处都是被**实测**推翻的,不是改主意。
9+
10+
---
11+
12+
## 0. 模块划分:协议独占一个 `.cppm`,平台特化留在 `platform/`
13+
14+
三个及以上的读写方共享同一条规则 ⇒ 独占一个模块。这是本仓库反复付学费后立下的形状
15+
(`loader_contract` / `graph_shape` / `host_requirements` 都是这么来的)。
16+
17+
| 新模块 | 协议内容 | 谁写 | 谁读 |
18+
|---|---|---|---|
19+
| **`src/platform/runtime_search.cppm`**<br>`mcpp.platform.runtime_search` | **运行期搜索路径契约**:一条目录的**来源****次序****是否机器本地** | `runtime_binding`(组装)、`plan`(链接期) | `elf_runtime`(闭包解析)、`pack`(剥离判定)、`runtime_validation`(记录) |
20+
21+
**为什么放 `platform/` 而不是 `build/`**:它描述的是**加载器怎么搜**(物理),
22+
不是 mcpp 怎么选(策略);而且 `elf_runtime`(platform)必须读它 ——
23+
`build/` 会让 `platform → build` 反向依赖。
24+
25+
**它只 `import std`**,不认识 `RuntimeBinding`、不认识 ELF。纯策略,可单测,零耦合。
26+
27+
平台特化各归各位,不外溢:
28+
29+
| 平台位置 | 本轮改动 |
30+
|---|---|
31+
| `src/platform/runtime_binding.cppm` | farm 目录发现(与既有 libc 探测同一次遍历);`declared`/`note` 降级字段 |
32+
| `src/platform/elf_runtime.cppm` | 宿主默认目录按 binding 分档;`Status::Unresolvable` |
33+
| `src/platform/linux/``macos``windows` | **不动** —— farm 是"有没有 DT_RPATH"的函数,已由 `platform::supports_rpath` 表达 |
34+
35+
---
36+
37+
## 1. M1 — `src/platform/runtime_search.cppm`(新)
38+
39+
```cpp
40+
export module mcpp.platform.runtime_search;
41+
import std;
42+
43+
export namespace mcpp::platform::search {
44+
45+
// 一条运行期搜索目录的来源。次序与"是否可分发"都由它决定,
46+
// 调用方不再各自推导。
47+
enum class Origin {
48+
Payload, // 不可变载荷目录(<store>/xim-x-glibc/2.39/lib64)
49+
Package, // 包描述符声明的 runtime 目录
50+
SubosFarm, // subos 符号链接农场(<subos>/lib)—— 可变
51+
HostDefault, // 宿主加载器的内建默认目录 —— 仅非 hermetic binding 适用
52+
};
53+
54+
// 次序 = 不可变性递减。
55+
int rank(Origin);
56+
57+
// 这条目录是否是"这台机器的私有状态"⇒ 不得随产物分发。
58+
bool is_machine_local(Origin); // Payload / SubosFarm ⇒ true
59+
60+
std::string_view to_string(Origin);
61+
62+
struct Dir { std::filesystem::path path; Origin origin; };
63+
64+
// 唯一一次排序 + 去重。stable,同 rank 内保持插入序。
65+
std::vector<Dir> ordered(std::vector<Dir> dirs);
66+
}
67+
```
68+
69+
**`rank` 的理由写进注释,因为它是本轮唯一的不变式**:
70+
71+
> 载荷目录不可变(装一次不再动),farm 每次 `xlings install` 都重写符号链接。
72+
> 载荷在前 ⇒ libc / libm / libstdc++ 永远从被 pin 的载荷解析,farm 只补没人提供的。
73+
> farm 在前 ⇒ 一次安装能在事后悄悄换掉一个**已经构建好**的产物的 libc。
74+
75+
---
76+
77+
## 2. M2 — `src/platform/runtime_binding.cppm`
78+
79+
### 2.1 结构
80+
81+
```cpp
82+
struct RuntimeBinding {
83+
84+
bool declared = false; // subos 是否自我描述(subos_info 块存在)
85+
std::string note; // 降级原因;非空则调用方必须呈现
86+
std::vector<std::filesystem::path> searchDirs; // farm 视图目录(可变)
87+
};
88+
```
89+
90+
`libraryDirs`(载荷,不可变)与 `searchDirs`(farm,可变)**必须是两个字段** ——
91+
合成一个就把 rank 的信息丢了,而 rank 是本轮的全部。
92+
93+
### 2.2 `resolve_runtime_binding`:矛盾报错,缺席降级
94+
95+
| 情况 | 今天 | 改为 |
96+
|---|---|---|
97+
| 点名的 subos 目录不存在 | error | **error(保留)** —— 用户输入无法被满足 |
98+
|`.xlings.json` / 无 `subos_info`| error | `declared=false` + `note`,**返回可用 binding** |
99+
| `runtime` 字段为空 | error | 同上 |
100+
| `schema > kSupportedSchema` | error | 读懂的字段照用 + `note`(与 `subos_info::read` 对齐) |
101+
| `schema < kSupportedSchema` | error | 照用 |
102+
103+
降级 binding 的内容:`platform`/`arch`/`providerId`/`subosDir`/`selection` 照填,
104+
`runtimeId` 留空,`loader`/`libc` 无值 ⇒ rule A/B 自然落到 `Inconclusive` 并说明
105+
**是因为没有声明**,而不是因为查过了。
106+
107+
### 2.3 farm 发现:与既有 libc 探测同一次遍历
108+
109+
今天 `{subosDir/"lib64", subosDir/"lib"}` 那个循环找的是 `libc.so.6`(找到即 `break`)。
110+
farm 需要的是**目录本身是否存在**,两件事一次走完:
111+
112+
```
113+
for candidate in {lib64, lib}:
114+
if is_directory(candidate): searchDirs.push_back(candidate) // farm
115+
if is_regular_file(candidate/libc.so.6) and libraryDirs.empty():
116+
…既有的 canonical → 载荷目录 → libraryDirs / loader…
117+
```
118+
119+
**不新增第二处布局知识。** 只在 `if constexpr (is_linux)` 内。
120+
121+
### 2.4 序列化
122+
123+
- `search_dirs``declared``note` 进 JSON。
124+
- `searchDirs` + `declared` **`canonical_contract`** ⇒ contract hash 变 ⇒
125+
farm 变化会正确地让快路径与校验缓存失效。**这会让所有既有缓存失效一次,是预期的。**
126+
- `deserialize` 的完整性检查放宽:`declared=false` 的 binding 允许 `runtimeId` 为空
127+
(今天 `schema==0 || runtimeId.empty()` 直接判 incomplete)。
128+
129+
---
130+
131+
## 3. M3 — `src/platform/elf_runtime.cppm`
132+
133+
### 3.1 搜索顺序按契约,宿主默认目录分档
134+
135+
```cpp
136+
// resolve_needed 内
137+
dirs = requester.runpaths (expand $ORIGIN)
138+
+ additionalSearchDirs
139+
+ binding.libraryDirs // Origin::Payload
140+
+ binding.searchDirs // Origin::SubosFarm
141+
+ (is_hermetic(binding) ? {} : host_library_dirs()); // ← 分档
142+
```
143+
144+
**`is_hermetic(binding)` = `binding.loader.has_value()`** —— 产物的 `PT_INTERP` 指向私有
145+
加载器时,宿主的内建默认目录**不在它的搜索路径里**。今天无条件加宿主目录,是
146+
`validation: pass` + `cannot open shared object file` 同时成立的直接成因。
147+
148+
非 hermetic(`gcc@system`、macOS、Windows)保持原状:那里宿主目录**确实**是默认值。
149+
150+
### 3.2 第四种判决
151+
152+
```cpp
153+
enum class Status { Pass, ProvenMismatch, Unresolvable, Inconclusive };
154+
```
155+
156+
hermetic binding 下一个解析不到的 `NEEDED` 是**可证的失败**(私有 loader 一定打不开),
157+
把它塞进 `Inconclusive` 是把可证的事说成没查过。
158+
159+
- `elf_runtime.cppm:759` 的 `inconclusive(...)` 在 hermetic 下改走 `unresolvable(...)`,
160+
非 hermetic 保持 `inconclusive`(宿主可能在 `ld.so.cache` 里有,mcpp 不读 cache)。
161+
- `runtime_validation`:`has_proven_mismatch()` → `has_blocking_failure()`,
162+
收下 `ProvenMismatch | Unresolvable`;`status_name`/`parse_status` 补 `"unresolvable"`。
163+
- `ninja_backend.cppm:1794` 的门同步。
164+
- `doctor.cppm:279` 的三分支补第四支。
165+
166+
---
167+
168+
## 4. M4 — `src/build/plan.cppm`:farm 进闭包,末位
169+
170+
### 【实施修正 ①】不能塞进 `linkIntent.runtimeSearchDirs`
171+
172+
原计划让 farm 复用那个字段。**不行**:它有三个消费者,其中一个是
173+
174+
```
175+
plan.linkIntent.runtimeSearchDirs → plan.runtimeLibraryDirs → compute_run_env()
176+
→ LD_LIBRARY_PATH(`mcpp run` 的子进程环境)
177+
```
178+
179+
而 farm 进 `LD_LIBRARY_PATH` 正是设计 §6「不做什么」第三条禁掉的东西 ——
180+
它会污染 `mcpp run` 拉起的每一个子进程,包括宿主二进制(实测会让 `xdg-open` /
181+
`notify-send` 死于 `__pointer_chk_guard`)。
182+
183+
**改为 `BuildPlan` 上的独立字段** `runtimeSearch`(`vector<search::Dir>`,
184+
全部四种 origin 的有序记录),farm 的**唯一**消费者是 `flags.cppm` 渲染的
185+
`-Wl,-rpath` 尾巴。**per-object 可达,绝不 per-process。**
186+
187+
### 【实施修正 ②】载荷目录不能只读 `linkRuntimeDirs`
188+
189+
`plan.toolchain.linkRuntimeDirs` **只有 clang 会填**(`clang.cppm:142`)。
190+
GCC 的载荷 `-rpath` 来自**链接模型**(`lm.libDirs`)。第一版记录出来只有一条
191+
farm,而产物 DT_RPATH 有三条 —— 记录与产物不一致,正是这份设计要消灭的形状。
192+
193+
改为向**发出它们的同一个函数**要:`resolve_link_model(plan.toolchain).libDirs`
194+
(纯函数,可在 plan 层调用),再叠 `linkRuntimeDirs`,顺序与 `flags.cppm` 的拼接一致。
195+
196+
### 【实施修正 ③】装配点在 `merge_runtime_binding_contract`,不在 `build_plan`
197+
198+
`plan.runtimeBinding` 在 `prepare.cppm:5313` 才被赋值,晚于 `make_plan` 返回。
199+
装配放进 `merge_runtime_binding_contract`(紧随其后调用),那里三个输入齐全。
200+
201+
**次序天然正确,不需要额外机制**(已核 `flags.cppm:975-978`):
202+
203+
```
204+
f.ld = full_static + link_toolchain_flags + b_flag + runtime_dirs
205+
+ link_intent_ld + atomic_ld + payload_ld + user_ldflags + link_extra
206+
↑ 载荷 -L/-rpath ↑ linkIntent(farm 在其末尾)
207+
```
208+
209+
且 `runtimeSearchDirs` 的既有语义正是我们要的(`flags.cppm:804-806` 原文):
210+
*"contributes RUNPATH only; it must never become a link-time `-L` path"* ——
211+
链接期已由 `--sysroot` 覆盖,这里只补运行期,还省下链接行长度。
212+
213+
### 4.1 两条护栏
214+
215+
| 护栏 | 判据 |
216+
|---|---|
217+
| **交叉目标** | `targetTriple` 非空且(`os != "linux"` 或 `arch != binding.arch`)⇒ 不发 |
218+
| **非 ELF** | `elfTarget == false` ⇒ 不发(与 `loader_tag_flag` 同一个判据,复用) |
219+
220+
---
221+
222+
## 5. M5 — pack:`215` 扩面(剥离不需要写代码)
223+
224+
### 【实施修正 ④】5.1 的前提是错的 —— `system` 档早就剥干净了
225+
226+
原计划断言「`Mode::None` 是唯一不重写 rpath 的档」。**实测推翻**:
227+
228+
```console
229+
$ mcpp pack --mode system && tar -xzf …-system.tar.gz
230+
$ readelf -d bin/glprobe | grep RPATH
231+
(RPATH) Library rpath: [] ← 已清空
232+
$ readelf -p .interp bin/glprobe
233+
/lib64/ld-linux-x86-64.so.2 ← 已改回平台标准解释器
234+
$ grep -rl "$HOME/.mcpp" <bundle>/ ← 无命中
235+
```
236+
237+
`pack.cppm:718``Mode::None` 把每个依赖都标 skip ⇒ `toBundle` 空 ⇒
238+
`rpath = ""``set_search_path` 整体清空。**这一项从"实现"变成"补测试"。**
239+
240+
「从 farm 解析到的 `NEEDED` 升级为 host requirement」也**不做**:
241+
`HOST-REQUIREMENTS` 存在的理由是记录**产物本身看不出来**的东西(经 dlopen 链到达
242+
的驱动),而 `NEEDED` 本来就写在产物里 —— 再抄一遍是冗余,还会污染
243+
`mcpp publish``[runtime].requirements` 的投影。
244+
245+
### 5.2 `215` 的两处扩面(设计 §2.6 核出来的)
246+
247+
```bash
248+
STORE="$MCPP_HOME/registry/data/xpkgs" # ← 今天只到这里
249+
MACHINE_LOCAL="$MCPP_HOME" # ← farm 在 registry/subos/…,不在 store 下
250+
```
251+
252+
并补 `--mode system` 的用例 —— 今天 215 只跑默认 `vendored` 档。
253+
254+
---
255+
256+
## 6. M6 — `XLINGS_SUBOS_LD_PATHS=0`:声明式退出
257+
258+
mcpp 在**驱动 ninja 之前**把它设进自己的进程环境(子进程继承 ⇒ 覆盖 ninja / 驱动 / ld),
259+
**不进 ninja 命令行**(链接行有 128KiB 上限)。
260+
261+
- 今天 = 无操作(xlings 还没读它);
262+
- xlings E2b 落地当天自动生效,mcpp 的 DT_RPATH 仍只含 mcpp 决定的内容;
263+
- 键名与语义在 `runtime_search.cppm` 里以常量声明一次,**不散落**
264+
265+
---
266+
267+
## 7. M7 — 可观测性
268+
269+
| 载体 | 补什么 |
270+
|---|---|
271+
| `resolution.json` | `runtime_search` 数组:`[{path, origin}]`,**保序** |
272+
| `mcpp why runtime` | `search:` 行按 origin 展开;binding 未声明时打印 `note` |
273+
| `mcpp doctor` | `declared=false` 作为 **info** 呈现并说明影响范围 |
274+
275+
---
276+
277+
## 8. 测试
278+
279+
### 8.1 单测(`tests/unit/`)
280+
281+
| 文件 | 断言 |
282+
|---|---|
283+
| `test_runtime_search.cpp`(新) | `rank` 次序;`ordered` 去重且 stable;`is_machine_local` 逐值 |
284+
| `test_subos_info.cpp`(补) | schema 高于支持值时**不失败**,填 note |
285+
| `test_runtime_contract.cpp`(补) | 降级 binding 的 serialize↔deserialize 往返;contract hash 含 searchDirs |
286+
287+
### 8.2 e2e
288+
289+
| # | 文件 | 断言 | 防空转 |
290+
|---|---|---|---|
291+
| T1 | `219_runtime_search_farm_is_last.sh` | 可执行文件 `DT_RPATH` **最后一项**是 binding 的 farm |**生成物** |
292+
| T2 | 同上 | `libc.so.6` 解析到载荷目录,不是 farm | 次序反了它先红 |
293+
| T3 | `220_farm_only_needed_runs.sh` | 一个只有 farm 提供的 `NEEDED` 的产物 **rc=0 真的跑起来** | **唯一能戳破假绿的断言**;库从 farm∖载荷 差集里取,差集空则 **skip 并打印原因** |
294+
| T4 | 同上 | 谁都提供不了的 `NEEDED` ⇒ 构建**变红**并指名 | 防止状态枚举没接到失败门 |
295+
| T5 | `215`(扩) | `--mode system` 产物不含任何 `$MCPP_HOME` 路径 | 前缀扩到整个 home |
296+
| T6 | `221_subos_without_info_still_builds.sh` | `subos_info` 缺失的 fixture 能 `mcpp build` | **不要求任何图形能力**,Windows/macOS 都要真跑到 |
297+
298+
**`# requires:` 只用 `run_all.sh` 真授予的能力**;T6 **不得**`elf`/`gcc`,否则它在
299+
Windows 上被跳过,而 Windows 正是它要防的回归。
300+
301+
---
302+
303+
## 9. 文档
304+
305+
| 文件 | 改什么 |
306+
|---|---|
307+
| `docs/08-toolchain-internals.md` | 新增"运行期搜索闭包"一节:四种 origin、次序与理由 |
308+
| `docs/02-pack-and-release.md` | `system` 档会剥机器本地路径并升为 host requirement |
309+
| `docs/11-machine-output.md` | `resolution.json``runtime_search` 字段 |
310+
| `docs/zh/` 对应件 | 同步 |
311+
| 设计文档 | 顶部标注实施状态与 PR 号 |
312+
313+
---
314+
315+
## 10. 版本与 pin
316+
317+
- `mcpp.toml` `[package].version` + `src/version.cppm` `MCPP_VERSION`**`2026.8.11.2`**(同一 commit)
318+
- `src/xlings.cppm` `kXlingsVersion`**最新 xlings**(实施时以 `xlings --version` / 索引为准)
319+
- `.xlings.json` 的 bootstrap pin **本 PR 不动**(发布并进索引后才前移)
320+
- `bash .github/tools/check_version_pins.sh` 必须过
321+
322+
---
323+
324+
## 11. 实施顺序
325+
326+
**唯一不可交换:M3 在 M4 之前。** 先加路径再补判据 = 在不会响的报警器上加功能。
327+
328+
```
329+
M1 契约模块 → M2 binding(降级+farm) → M3 闭包判据 → M4 链接期 → M5 pack → M6 退出声明 → M7 观测 → 测试 → 文档 → 版本
330+
```
331+
332+
M2 的降级半边(§2.2)与图形无关,是正在阻塞 Windows 用户的回归 —— 它在同一个 PR 里,
333+
**提交上独立成一个 commit**,以便必要时单独 cherry-pick。

.github/actions/setup-macos-llvm/action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ inputs:
1515
# Floor imposed by the index, not a routine bump — see
1616
# .github/actions/bootstrap-mcpp/action.yml for why 0.4.69 is required
1717
# (two packages named `lua` in one repo need openxlings/xlings#381).
18-
default: '2026.8.11.1'
18+
default: '2026.8.11.2'
1919

2020
runs:
2121
using: composite

.github/workflows/bootstrap-macos.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ jobs:
1717
# Dormant (workflow_dispatch only), but kept in step with the rest —
1818
# check_version_pins.sh holds it there. Floor: 0.4.69, below which the
1919
# index cannot resolve two packages that share a short name.
20-
XLINGS_VERSION: '2026.8.11.1'
20+
XLINGS_VERSION: '2026.8.11.2'
2121
steps:
2222
- uses: actions/checkout@v4
2323

.github/workflows/ci-fresh-install.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ jobs:
152152
env:
153153
XLINGS_NON_INTERACTIVE: '1'
154154
run: |
155-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.1
155+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.2
156156
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
157157
158158
- name: Install mcpp and config mirror
@@ -293,7 +293,7 @@ jobs:
293293

294294
- name: Install xlings + mcpp
295295
run: |
296-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.1
296+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.2
297297
# Deliberately NOT writing to $GITHUB_PATH here. On container
298298
# images that declare no PATH in their config (opensuse/
299299
# tumbleweed), appending a single dir to GITHUB_PATH makes the
@@ -364,7 +364,7 @@ jobs:
364364
# (older ones carry minos=15 and refuse to start).
365365
# v0.4.51+: in-process sha256 — this image has no sha256sum
366366
# binary, so pinned fetches failed before it.
367-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.1
367+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.2
368368
echo "$HOME/.xlings/subos/current/bin" >> "$GITHUB_PATH"
369369
370370
- name: Install mcpp and config mirror

.github/workflows/ci-linux-e2e.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ jobs:
133133
134134
- name: Bootstrap xlings + released mcpp
135135
run: |
136-
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.1
136+
curl -fsSL https://raw.githubusercontent.com/openxlings/xlings/main/tools/other/quick_install.sh | bash -s v2026.8.11.2
137137
export PATH="$HOME/.xlings/subos/current/bin:$PATH"
138138
xlings update
139139
xlings install mcpp -y -g

0 commit comments

Comments
 (0)