Skip to content

Commit 520ae65

Browse files
committed
fix(integrity): roll back legacy packages on failed reinstall (InstallStash)
A content-complete *legacy* package (no .mcpp_ok marker) was deleted outright by clean_incomplete_install() on the resolve path before a reinstall. If the reinstall then failed (xlings extracting to $XLINGS_HOME, network error, etc.), the working package was gone for good — e.g. xim-x-zlib vanished, leaving clang's RUNPATH dangling and `clang++ --version` failing with 'libz.so.1: cannot open shared object' (exit 127), with no way to recover short of a manual reinstall. Add InstallStash, an RAII guard that replaces the bare clean on the resolve path: it renames the no-marker dir aside instead of deleting it, commits (drops the backup) on each success path, and on any failure path its destructor: - keeps the new install if verdir now has a marker (reinstall won); - else restores a looks_complete_legacy() stash and adopts it (writes the marker) so the next resolve trusts it instead of repeating the delete-then-fail cycle; - else discards genuine half-extracted residue (historical semantics). Wire it into Fetcher::resolve_xpkg_path with stash.commit() before each success return. Add 5 gtest cases covering restore / commit / keep-new-complete / discard-residue / noop-when-marked. Also lands the cross-repo root-cause + implementation doc under .agents/docs/ (covers this and the compat.xcb codegen fix in mcpp-index).
1 parent 72037f8 commit 520ae65

4 files changed

Lines changed: 550 additions & 5 deletions

File tree

Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
# 跨仓库修复方案:残缺/被删依赖被 `.mcpp_ok` 盲区放过
2+
3+
> 状态:草案 / 待实施
4+
> 影响面:经 `imgui[docking-full]`(或任何 X11/libxcb 链)构建的项目;切换/重装 llvm 等工具链的用户
5+
> 涉及仓库:`mcpplibs/mcpp-index`(配方层) + `mcpp`(核心层);附带 toolchain/ABI 可用性
6+
>
7+
> 本方案覆盖同一系统性病根的**两个实证**:
8+
> - **案例 A**:`compat.xcb` 头文件生成空转 → 残缺包被标记完整(§2.2)
9+
> - **案例 B**:llvm 工具链的 `xim-x-zlib` 运行时被严格清理删除且未还原(§2.4)
10+
> 二者都是「`.mcpp_ok` 只证进程/布局、不证内容」+「strict marker-only 清理对 legacy 包先删后赌重装」的不同表现。
11+
12+
---
13+
14+
## 1. 现象
15+
16+
用户项目仅声明:
17+
18+
```toml
19+
[dependencies]
20+
imgui = { version = "0.0.6", features = ["docking-full"] }
21+
```
22+
23+
`mcpp build/run` 在编译传递依赖 `compat.xcb`(libxcb)时报:
24+
25+
```
26+
compat-x-compat.xcb/1.17.0/src/xcb.h:209:10: fatal error: xproto.h: No such file or directory
27+
209 | #include "xproto.h"
28+
```
29+
30+
用户项目本身无问题,根因在依赖的打包与安装链路。
31+
32+
---
33+
34+
## 2. 根因(两层)
35+
36+
### 2.1 关键事实
37+
38+
`libxcb` 的协议头 `xproto.h` / `bigreq.h` / `xc_misc.h`(及对应 `.c`)**不是源码自带**,而是在 `install()` 阶段用 libxcb 自带的代码生成器 `c_client.py` 读取 `xcb-proto``*.xml` **现场生成**的。
39+
40+
注意:它与 `xorgproto` 提供的 `X11/Xproto.h`(大写、完全是另一个文件)无关——编译命令里挂的 `xorgproto``-I` 帮不上忙。
41+
42+
### 2.2 配方层缺陷(`mcpplibs/mcpp-index` · `pkgs/c/compat.xcb.lua`)
43+
44+
**Bug A — `resolve_python()` 兜底挑错文件。**
45+
xim-python 的 `bin/` 同时存在 `python3.13``python3.13-config`。当 `python3`/`python` 符号链接不存在、走到兜底分支时:
46+
47+
```lua
48+
local matches = os.files(path.join(python.bin, "python3.*")) or {}
49+
table.sort(matches)
50+
if #matches > 0 then
51+
return matches[#matches] -- 取“最后一个”
52+
end
53+
```
54+
55+
glob `python3.*` 同时命中两者,`table.sort` 升序后 `["python3.13", "python3.13-config"]`,`matches[#matches]` 取最后一个 = **`python3.13-config`**(配置助手脚本,不是解释器)。把 `c_client.py` 喂给它:**退出码 0、不报错、零产出**
56+
57+
**Bug B — `install()` 不校验生成产物。**
58+
生成步骤即便空转也无人发现,只能等后续 `copy_public_headers``os.cp(src/xproto.h, ...)` 撞缺文件才崩;错误信息含糊,且在某些环境下不一定崩,从而把残缺包"装完"。
59+
60+
### 2.3 核心层盲区(`mcpp` · `src/pm/package_fetcher.cppm` + `src/fallback/install_integrity.cppm`)
61+
62+
完整性标记 `.mcpp_ok` 的写入条件是:
63+
64+
- `package_fetcher.cppm`:`xlings 退 0 且 verdir 存在` → 写标记;
65+
- copy 兜底路径:仅凭 `looks_complete_legacy`(看目录里有没有 `src/` 之类的**布局启发式**)→ 写标记。
66+
67+
**它证明的是"安装进程跑完了 / 布局看起来像",而不是"产物正确"。** 于是配方退 0 但产物缺失的残缺包被打上 `.mcpp_ok`、被永久信任,问题被拖到编译期才以 `xproto.h not found` 爆出来。
68+
69+
这与既有的 `#120 空壳+缓存血统``包解析 identity-first 盲取首命中` 属同一类系统性问题。
70+
71+
### 2.4 案例 B:llvm 工具链 `xim-x-zlib` 运行时被删且未还原
72+
73+
**现象**:切到 llvm 后 `mcpp run`
74+
75+
```
76+
'... clang++ --version' exited with status 127
77+
# 真实 stderr:
78+
clang++: error while loading shared libraries: libz.so.1: cannot open shared object file
79+
```
80+
81+
`mcpp toolchain install llvm` 重装也不好。
82+
83+
**关键事实(实测)**:
84+
85+
- clang 的 ELF 解释器是 **xim 私有 glibc 加载器**(`xim-x-glibc/.../ld-linux`),**不搜系统 `/usr/lib`**,只认二进制 RUNPATH。clang 的 RUNPATH 把 `libz.so.1` 指向 `xim-x-zlib/<ver>/lib``subos/default/lib`
86+
- `xim-x-zlib/<ver>` 版本目录**整个被删**(只剩空壳,甚至出现嵌套 `xim-x-zlib/xim-x-zlib` 残骸);`subos/default/lib/libz.so.1` 软链**悬空**
87+
- clang 二进制本身完好——补上任意可用的 libz 路径后 `clang++ --version` 立即成功。
88+
- `ldd clang` 却显示一切正常:因为 `ldd`**系统**加载器(能找到系统 libz),而 clang 实跑用**私有**加载器(不看系统路径)——这是排查时的迷惑点。
89+
90+
**根因**:这些 xim 运行时包(zlib/glibc/libxml2)都是**早于标记系统的 legacy 包,无 `.mcpp_ok`**`install_integrity.cppm``is_install_complete` 是 strict marker-only,解析/安装路径上对无标记目录执行 `clean_incomplete_install`(**先删、再赌"一次性重装"**)。这次对 `xim-x-zlib` 的重装**没有还原成功**(典型:xlings 装到 `$XLINGS_HOME` 没落回 sandbox、或静默失败),于是 zlib 版本目录被删却没补回。clang 因此加载不到 `libz.so.1` → 127。
91+
92+
**为何 `toolchain install llvm` 无效**:它只重装 llvm 本体(有标记、会重解压),而 `libz.so.1` 来自**独立的 xpkg `xim-x-zlib`**,llvm 安装不会去还原这个缺失的兄弟包。
93+
94+
与案例 A 同根:`.mcpp_ok` 不证内容 + strict 清理对 legacy 包"先删后赌重装"。
95+
96+
---
97+
98+
## 3. 修复方案(按仓库)
99+
100+
### 3.1 仓库 `mcpplibs/mcpp-index``pkgs/c/compat.xcb.lua`(治本,优先)
101+
102+
**修 Bug A:兜底只保留真正的解释器。**
103+
104+
```lua
105+
-- 兜底:只保留真正的解释器(python3 / python3.<minor>),
106+
-- 排除 -config / -gdb 等同前缀助手脚本;取最规范(最短)的一个。
107+
local interpreters = {}
108+
for _, m in ipairs(os.files(path.join(python.bin, "python3*")) or {}) do
109+
if path.filename(m):match("^python3%.?%d*$") then
110+
table.insert(interpreters, m)
111+
end
112+
end
113+
table.sort(interpreters)
114+
if #interpreters > 0 then
115+
return interpreters[1]
116+
end
117+
```
118+
119+
正则 `^python3%.?%d*$` 命中 `python3` / `python3.13`,拒绝 `python3.13-config`(含 `-config`)。
120+
121+
**修 Bug B:生成后显式校验,缺产物即 `return false`**
122+
123+
```lua
124+
os.cd(srcdir)
125+
for _, name in ipairs({ "xproto", "bigreq", "xc_misc" }) do
126+
local cmd = string.format(...) -- 不变
127+
os.exec(cmd)
128+
if not os.isfile(path.join(srcdir, name .. ".h"))
129+
or not os.isfile(path.join(srcdir, name .. ".c")) then
130+
log.error("c_client.py did not generate %s.{h,c} (python=%s)", name, python)
131+
return false
132+
end
133+
end
134+
```
135+
136+
两处合并后:不再挑错解释器;即便挑错,也在**安装期硬失败**,绝不产出残缺包。
137+
138+
> `pkgs/c/compat.xcb-proto.lua` 无需改:协议头本就设计成在 `compat.xcb``install()` 里生成,`xcb-proto` 只负责提供 `*.xml` + `xcbgen`,职责正确。
139+
140+
**验证**:删除已装的 xcb 版本目录触发重装,确认重装成功且 `src/` 下出现 `xproto.{h,c}` / `bigreq.{h,c}` / `xc_misc.{h,c}`,`include/xcb/` 下出现公共头,且写入了 `.mcpp_ok`;随后下游项目能编过 libxcb。
141+
142+
### 3.2 仓库 `mcpp` — 完整性标记应校验"内容"而非"进程退出码"(防御纵深)
143+
144+
目标:即便某个配方有 bug,残缺安装也不该被标记为完整。
145+
146+
- **让配方声明产物清单**:在 `mcpp` 段支持可选 `verify = { "include/xcb/xproto.h", ... }`(相对 install dir 的必存路径);`mark_install_complete()` 之前逐一 `exists` 校验,缺任一即判失败、清理目录、不写标记。
147+
- **新装路径禁用 `looks_complete_legacy` 打标记**:布局启发式分不清"半成品"与"完整包",仅应用于"老包一次性收编",不应作为刚装完的完整性判定(见 `package_fetcher.cppm` copy-fallback 分支)。
148+
- **legacy 包"先删后重装"必须可回滚(案例 B 核心)**:`clean_incomplete_install` 在 resolve 路径上对**无标记但内容完好的 legacy 包**直接 `remove_all` 是危险的——一旦后续重装没把它落回 sandbox(xlings 装到 `$XLINGS_HOME`、静默失败等),就把一个**本来能用**的包删没了。要求:
149+
- 删除前先**备份/暂存**(rename 到 `.bak`,而非直接 `remove_all`);
150+
- 重装确认还原成功(目标产物存在)后才删除备份;**还原失败则回滚备份并报错**,绝不留下"删了没补"的空洞;
151+
- 或更稳:对 legacy-complete 包采用**"原地收编"**(校验通过即补写 `.mcpp_ok`),避免无谓的删-装往返(见 §4 恢复:全局副本 `$XLINGS_HOME/data/xpkgs/...` 通常就是好的)。
152+
- **被删依赖的连带破坏要可检测**:工具链 RUNPATH 指向的运行时兄弟包(zlib/glibc/libxml2…)被删后,`subos/default/lib` 会留**悬空软链**`mcpp self doctor` 应扫描 toolchain RUNPATH 与 subos 软链,发现悬空/缺失即报可操作提示。
153+
154+
**改动点**:`src/pm/package_fetcher.cppm`(写标记前插入 verify 校验;legacy 删除改为 rename-备份+失败回滚)、`src/fallback/install_integrity.cppm`(新增"按清单校验"入口;`clean_incomplete_install` 增加可回滚语义;收紧新装路径)、`src/doctor.cppm`(RUNPATH/软链悬空体检)。
155+
156+
**测试(TDD)**:
157+
1. "安装产出残缺包应被判失败、不得写 `.mcpp_ok`、并清理目录"(案例 A);
158+
2. "legacy 无标记包在重装失败时必须回滚还原、不得被删空"(案例 B);
159+
3. "doctor 能报出工具链 RUNPATH 指向的缺失运行时包"。
160+
161+
> 关系:3.1 的 Bug B 对**本包**已是充分修复(install 返回 false → xlings 非 0 → 核心层不写标记);3.2 是让**所有包**都不再被"退 0 但产物缺失"骗过,**且不再因清理把好包删坏**
162+
163+
### 3.3 工具链 / ABI(环境配置 + `mcpp` 可用性)
164+
165+
现象:默认工具链一度解析成 `gcc 15.1.0-musl`,而 GUI 依赖(glfw / xcb 等)是 glibc 包 → `ABI mismatch: dependency 'compat.glfw' requires abi=glibc but the resolved toolchain ... is abi=musl`
166+
167+
- **环境层(用户侧)**:glibc 依赖即用 glibc 默认工具链:`mcpp toolchain default gcc@<glibc-version>`
168+
- **`mcpp` 可用性(建议)**:
169+
1. ABI 报错改为**可操作**——直接提示应执行的 `mcpp toolchain default <glibc-toolchain>`;
170+
2. 排查 **global default 与实际解析不一致**:`mcpp toolchain default` 显示默认已是某 glibc 工具链,但 `mcpp run` / `toolchain list``*` 却解析/标记为 musl,疑似存在 project-local 或 resolved-context 覆盖,需厘清解析优先级。
171+
172+
---
173+
174+
## 4. 恢复手段(给已中招的用户)
175+
176+
当前没有 `mcpp reinstall <pkg>` 子命令;重装靠"标记缺失自动触发":
177+
178+
```bash
179+
# 方式 A:只删标记,触发自清 + 重装
180+
rm "$MCPP_HOME/registry/data/xpkgs/compat-x-compat.xcb/1.17.0/.mcpp_ok"
181+
182+
# 方式 B:删整个版本目录(更干净)
183+
rm -rf "$MCPP_HOME/registry/data/xpkgs/compat-x-compat.xcb/1.17.0"
184+
185+
# 然后重建
186+
mcpp build
187+
```
188+
189+
> `$MCPP_HOME` 默认 `~/.mcpp`,`$XLINGS_HOME` 默认 `~/.xlings`
190+
> 注意:**裸重装会重跑同一个有 bug 的配方**,务必先落地 3.1 的配方修复(或先按 3.1 逻辑手动补齐生成产物)再重装,否则会再次产出残缺包。
191+
192+
**案例 B(工具链运行时被删,如 `xim-x-zlib`)**:全局 `$XLINGS_HOME/data/xpkgs/<pkg>/<ver>` 通常还保留着完好副本,直接还原回 sandbox 并打标记即可:
193+
194+
```bash
195+
SRC="$XLINGS_HOME/data/xpkgs/xim-x-zlib/1.3.1"
196+
DST="$MCPP_HOME/registry/data/xpkgs/xim-x-zlib/1.3.1"
197+
rm -rf "$MCPP_HOME/registry/data/xpkgs/xim-x-zlib/xim-x-zlib" # 清掉嵌套空壳残骸(若有)
198+
cp -a "$SRC" "$DST"
199+
printf '1\n' > "$DST/.mcpp_ok" # 打标记,避免再次被严格清理删掉
200+
test -e "$MCPP_HOME/registry/subos/default/lib/libz.so.1" && echo "subos 软链已恢复"
201+
```
202+
203+
> 验证:`env LD_LIBRARY_PATH=<llvm>/lib <llvm>/bin/clang++ --version` 应正常输出版本,`mcpp run` 用 llvm 工具链可编译运行。
204+
205+
---
206+
207+
## 5. 实施顺序与验收
208+
209+
| 步骤 | 仓库 | 内容 | 验收 |
210+
| --- | --- | --- | --- |
211+
| 1 | `mcpplibs/mcpp-index` | 3.1 配方双修复(案例 A) | 删目录重装成功、生成产物齐全、下游编过 |
212+
| 2 | `mcpp` | 3.2 产物校验 + 收紧新装标记 + legacy 删除可回滚 + doctor 体检 | 案例 A/B 两个失败测试 + doctor 测试通过;回归既有安装路径 |
213+
| 3 | `mcpp` / 文档 | 3.3 ABI 报错可操作化 + 解析不一致排查 | 报错含修复命令;default 与实际解析一致 |
214+
215+
建议顺序:**先 1(治本、覆盖所有装该 X11 链的人),再 2(防御纵深,同时根治案例 B 的"删好包"风险),最后 3(可用性)**
216+
217+
---
218+
219+
## 6. 经验沉淀
220+
221+
- "进程退出码 0" ≠ "产物正确";凡是 `install()` 里有**代码生成 / 外部子进程**的配方,都应在生成后**显式校验产物**并在缺失时 `return false`
222+
- 完整性标记(`.mcpp_ok`)只应在**内容校验**通过后写入;布局启发式不能用于新装的完整性判定。
223+
- **清理 ≠ 可丢**:对无标记但内容完好的 legacy 包,"先删后赌重装"会把好包删坏;删除须可回滚,还原失败须报错,优先"原地收编"。
224+
- xim 工具链是**自带私有 glibc 加载器**的可执行文件,只认 RUNPATH、不搜系统库;`ldd`(系统加载器)的结论不代表实跑——排查共享库问题以**实跑 stderr** 为准。
225+
- 工具链 ABI 与依赖 ABI 必须一致;musl 工具链不能链接 glibc 预期的源码包。
226+
227+
---
228+
229+
## 7. 实施拆解(Implementation Breakdown)
230+
231+
本次目标:跨仓库**实现 → 打通 → 测试 → PR → CI 全绿 → 生态 OK**。聚焦两个问题:
232+
233+
- **问题 1(xcb)**:`compat.xcb` 头生成空转 → 残缺包 → `xproto.h not found`(配方层)
234+
- **问题 2(mcpp run / llvm 报错)**:legacy 运行时包(`xim-x-zlib`)被严格清理删除且未还原 → `clang++` 加载 `libz.so.1` 失败 127(核心层)
235+
236+
### 7.1 问题 1 — `mcpplibs/mcpp-index`
237+
238+
**改动**:`pkgs/c/compat.xcb.lua`
239+
240+
|| 内容 | 验收 |
241+
|---|---|---|
242+
| P1-a | `resolve_python()` 兜底:`os.files("python3*")` + `path.filename(m):match("^python3%.?%d*$")` 过滤,排除 `*-config`;取排序首项 | 单元/手测:在只有 `python3.13` + `python3.13-config` 的 bin 下返回 `python3.13` |
243+
| P1-b | `install()` 生成循环后校验 `name..".h"``name..".c"` 存在,缺则 `log.error + return false` | 删 xcb 版本目录重装成功;故意误配 python 时安装**硬失败**而非产残缺 |
244+
245+
**测试**:
246+
1. `lua5.4 -e "assert(loadfile('pkgs/c/compat.xcb.lua','t'))"`(CI lint 同款语法门)。
247+
2. 本地:删 `xpkgs/compat-x-compat.xcb/<ver>``mcpp build`(imgui 项目)→ 确认 `src/xproto.{h,c}` 生成、`include/xcb/` 公共头齐、`.mcpp_ok` 写入、libxcb 编过。
248+
3. CI:`validate.yml``smoke_compat_imgui.sh` / `smoke_compat_imgui_window.sh` 会构建 X11 链,直接覆盖。
249+
250+
**PR**:分支 `fix/xcb-codegen-python-resolve` → PR 到 `mcpplibs/mcpp-index` → 等 `lint` + `mirror-cn-reachable` + `smoke-linux` + `smoke-portable` 全绿。
251+
252+
### 7.2 问题 2 — `mcpp`(核心层)
253+
254+
**改动**:`src/fallback/install_integrity.cppm` + `src/pm/package_fetcher.cppm`(+ 可选 `src/doctor.cppm`)
255+
256+
|| 内容 | 验收 |
257+
|---|---|---|
258+
| P2-a | `clean_incomplete_install` 增加**可回滚**语义:resolve 路径上对无标记目录,先 `rename → .mcpp_residue.bak` 而非 `remove_all`;调用方在重装确认成功后再删备份,失败则 `rename` 回滚 | 单元测试:重装失败时原目录内容必须原样还原 |
259+
| P2-b | `mark_install_complete` 前可选**产物清单校验**(配方声明 `verify`);新装路径不再用 `looks_complete_legacy` 打标记 | 单元测试:残缺产物不得写 `.mcpp_ok` |
260+
| P2-c(可选) | `mcpp self doctor` 扫工具链 RUNPATH 与 `subos/default/lib` 悬空软链 | doctor 能报出缺失的 `xim-x-zlib` |
261+
262+
**测试**:
263+
1. 单元/集成测试(见 §3.2 TDD 三项):案例 A(残缺包判失败)、案例 B(重装失败回滚)、doctor 体检。
264+
2. 本地 e2e:构造"无标记 legacy 包 + 重装失败"场景,验证目录被回滚而非删空。
265+
3. 回归:既有安装路径(正常装包、copy-fallback)不受影响。
266+
267+
**PR**:分支 `fix/install-integrity-rollback` → PR 到 `mcpp-community/mcpp` → 等其 CI(构建 + 测试矩阵)全绿。
268+
269+
### 7.3 顺序与"生态 OK"判据
270+
271+
1.**问题 1**(配方,解锁所有 X11 链用户),本地 + CI smoke 验证;
272+
2.**问题 2**(核心,根治"删好包"),单元 + e2e;
273+
3. **生态 OK** = mcpp-index 的 imgui smoke 全绿 + mcpp 自身测试矩阵全绿 + 一个真实 imgui 项目 `mcpp run` 起得来 + llvm 工具链切换不再 127。
274+
275+
### 7.4 进度
276+
277+
- [x] 文档落 `.agents/docs/`,补实施拆解(本节)
278+
- [x] P1-a / P1-b 实现(`compat.xcb.lua`)+ lua lint + 过滤逻辑单测
279+
- [x] 问题 1 PR:`mcpplibs/mcpp-index#43`(lint ✓ / mirror-cn ✓ / smoke 运行中)
280+
- [x] P2-a 实现:`InstallStash` 可回滚守卫(`install_integrity.cppm` + `package_fetcher.cppm`)+ 5 项 gtest 单测
281+
- [ ] 问题 2 构建 + 单测通过 + PR + CI 绿
282+
- [ ] 生态 OK 复核
283+
284+
> 注:问题 1 的本地 e2e 被本会话反复删包导致的环境损坏("python build dependency not found",非配方问题)阻塞;已确认 main 的 `validate`(含 imgui→xcb smoke)在干净 CI 为绿,故以 PR CI smoke 为权威验证。
285+
> P2-b(配方声明 `verify` 产物清单 + 新装禁用 looks_complete_legacy 打标记)与 P2-c(doctor 体检)留作后续 PR,本次先落最小且根治案例 B 的 P2-a。

0 commit comments

Comments
 (0)