状态:分析 + 方案,待 review。三条全部在 HEAD 上实测复现,不是照抄 issue。
| # | 核实 | 性质 | 影响面 | 改动面 |
|---|---|---|---|---|
| 427 | 复现,且 |
可用性(硬失败) | Linux 上所有工具链安装路径 | 小 |
| 426 | 复现,且 NEEDED 不是一个 |
分发正确性 | 纯 C / 纯汇编链接单元 | 中偏大 |
| main 红 | 复现,且 |
CI 阻塞 | 三平台 e2e | 小 |
三条互不依赖,可并行。#427 应优先:它是唯一的硬失败,且已发布三周。
沙箱内复现(mcpp 2026.8.15.1):
Runtime SubOS 'default' does not describe itself: … (no `subos_info` block) …
Where the C runtime comes from a payload, there is now no declared runtime to
bind to — mcpp declines to guess a version, so the link falls back to the host
and the hermeticity check will say so.
Resolving toolchain
error: toolchain post-install fixup: cannot fix up gcc toolchain
'…/xim-x-gcc/16.1.0': default SubOS has no RuntimeBinding identity (…)
输出自相矛盾。 前三行完整描述了一套降级方案(「declines to guess … the hermeticity check will say so」),第四行就地为同一个事实杀死构建。降级被设计过、 被打印给用户,然后另一处调用点把它作废。
沙箱那份 subos/default/.xlings.json:
-rw-rw-r-- 1 speak speak 16 Jun 22 04:30 …/subos/default/.xlings.json
{"workspace":{}}
16 字节,6 月 22 日 —— 一个早于 subos_info 写入器的 SubOS。宿主机上同一路径的
文件有完整的块,所以宿主机正常。沙箱不是成因,它只是保存了一个旧 SubOS。
于是真实触发条件是:默认 SubOS 由早于该块的 xlings 创建。同样能达成的还有全新
但未升级的 xlings、容器/CI 镜像里的旧 subos、任何未跑过 xlings self update 的机器。
mcpp 自己的诊断文字就写着 A newer xlings writes this block,即它已知这是一个
版本差,却按矛盾处理。
| 动作 | 结果 |
|---|---|
mcpp build |
❌ toolchain post-install fixup: … |
mcpp build,allow_host_libs = true |
❌ 同样失败 |
mcpp toolchain install gcc@16.1.0 |
❌ post-install fixup failed: … |
两条推论:
allow_host_libs救不了 —— 致命判定发生在 hermeticity 策略之前且无条件, 所以这不是「不够 hermetic 就拒绝」,是「不知道就拒绝」。- 影响面不止
mcpp build。lifecycle.cppm:587调用时不传runtimeId,因此mcpp toolchain install在 Linux 上必定走这条兜底,即受影响的是所有工具链 安装路径。
只向沙箱的 subos/default/.xlings.json 补一个 subos_info 块(runtime: glibc@2.39),其它一律不动:
Resolved gcc@16.1.0 → @mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0/bin/g++
Compiling ctl427 v0.1.0 (.)
Finished dev [unoptimized + debuginfo] in 0.11s
构建完全成功。所以沙箱内既非只读、也无第二道墙;唯一的阻塞是这个 gate。 (实验后已还原该文件。)
第一层:同一个决定有两处推导。
绑定解析器按项目选中的 SubOS 取路径:
// src/platform/runtime_binding.cppm:162
if (selection.mode == Mode::McppDefault || selection.subosName == "default")
return cfg.xlingsHome() / "subos" / "default";
return selection.ownerRoot / ".mcpp" / ".xlings" / "subos" / selection.subosName;fixup 的兜底则硬编码:
// src/toolchain/post_install.cppm:545
auto info = mcpp::xlings::subos::read(cfg.xlingsHome() / "subos" / "default");这不只是重复,而且结果可以不同:项目选了 subos = "foo" 时,兜底会拿
default 的 glibc 去 patch 载荷。这是一条独立于 #427 的正确性缺陷。
第二层:兜底违反它自己调用点写下的架构。 prepare.cppm:953 的注释:
// Resolve one exact runtime contract before resolving/fixing a toolchain.
// The fixup is itself a consumer of RuntimeBinding: doing it first would
// recreate #392 by letting directory order choose a libc and only later
// discovering what the project selected.「fixup 是 RuntimeBinding 的消费者」是明确的设计,而 subos/default 兜底正是这段
注释禁止的「让别的东西去挑 libc」。这不需要新设计 —— 它是一次没做完的迁移:
runtimeId 参数已经加上并已从四个调用点传入,旧的自行推导没有删。
第三层:降级分支是死代码。 被 gate 保护的函数本来就正确处理空值:
// src/toolchain/post_install.cppm:439
if (!glibcLibDir.empty() && … ) { …patchelf… }
else {
mcpp::ui::warning("could not locate sandbox glibc/gcc/patchelf paths; "
"gcc-built binaries may have unresolved PT_INTERP/RUNPATH");
}外层 gate 保证它永远拿不到空值,于是这个 else 从未执行过。与
repair-placed-where-flow-never-reaches 是同一形状。
221 正是为这条规则写的(「DATA THAT IS MISSING OR NEWER MUST NOT INVALIDATE THE PROGRAM THAT READS IT」),却抓不到它:
- 221 建的是项目级 SubOS(
[xlings] subos = "bare"→.mcpp/.xlings/subos/bare), 而 fixup 读的是硬编码的<xlings home>/subos/default。两者不相交,所以 221 的 空 SubOS 对这条代码路径毫无作用。 - 跑 221 的机器(开发机、CI runner)其
default都描述得出自己,gate 永远通过。 - 221 的绿色一半断言「加上
allow_host_libs必须能构建」—— 这一条在 #427 的现场 实测是失败的。即该断言是对的,只是从未指向出问题的 SubOS。
教训:一个「缺失必须降级」的测试,必须让缺失发生在被读取的那个对象上。 测试建了一个 SubOS,被测代码读的是另一个。
A. 删掉第二处推导(核心)。
ensure_post_install_fixup 不再自行读 SubOS。运行时身份只能来自调用方传入的
RuntimeBinding 快照。调用方拿不到,fixup 就拿不到 —— 这正是「fixup 是消费者」的
含义。删除 post_install.cppm:543-553 整段兜底,subos_info 的 include 一并移除。
副产品:「项目选了 foo,却按 default 打补丁」这条缺陷随之消失,无需单独修。
A′. mcpp toolchain install 必须补上解析,否则 A 会把它变成永久跳过。
(自我 review 发现,见 R1。)
lifecycle.cppm:587 调用时不传 runtimeId,而 toolchain_install(cfg, …) 手里
只有 cfg,没有任何 RuntimeBinding。单做 A,这条路径在 Linux 上会 runtimeId 恒空
⇒ 永远跳过 fixup —— 而它正是最需要 fixup 的路径(「without it a fresh-sandbox
glibc gcc cannot find the C library」)。
修正:toolchain_install 自己调用一次 resolve_runtime_binding(与 prepare.cppm:962
同一个解析器、同样的默认 selection),把 runtimeId 与 libraryDirs.front() 传下去。
这比被删的兜底更正确:它尊重 selection、走统一的降级与 note,而不是硬编码
subos/default。
B. 未知降级,矛盾照旧失败。
runtimeId为空 ⇒glibcLibDir为空 ⇒ 直接调用 fixup 体,由那个已存在的else发一次 warning 并跳过 patch。这恰好回到 2026.8.8.4 的行为,而二分表已证明该版本在 同一沙箱同一工程上成功。runtimeId非空但载荷缺失/版本对不上 ⇒ 仍然std::unexpected。 这是矛盾不是缺失:身份被声明了却兑现不了,猜测会让同一份 mcpp.toml 在不同机器上 得到不同 ABI。select_glibc_payload_lib现有的检查保持致命。
判据的分界线就是这一句:未知降级,矛盾失败。
C. 严重程度归调用方,不归被调方。
ensure_post_install_fixup 返回「做了什么 / 跳过了什么」,而不是自行决定生死:
struct FixupOutcome { bool applied; std::string skippedReason; };
std::expected<FixupOutcome, std::string> ensure_post_install_fixup(…);mcpp build(prepare.cppm四处):skippedReason非空时以 info 级说明一次 ——「工具链未按运行时打补丁,原因 X;若后续报stdlib.h not found或 hermeticity 失败,即由此而来」。构建继续。mcpp toolchain install(lifecycle.cppm:587):用户显式要求安装,以 warning 级 报告并给出xlings self update这一条可操作指令,退出码仍为 0(安装本身成功了)。
同一事实两种量级是合理的;不合理的是量级被写死在被调方,让 build 无从选择。
D. 顺带修一处诊断。 现在的错误文本把用户指向
/home/speak/.mcpp/registry/data/xpkgs/xim-x-gcc/16.1.0 —— 一个他不该写的目录。
降级后的 warning 应指向真正的动作:xlings self update。
新增 tests/e2e/237_default_subos_without_info.sh,# requires: elf:
- 构造一个独立 MCPP_HOME,其
subos/default/.xlings.json写成{"workspace":{}}—— 即沙箱现场的字面复制。⚠️ 必须是<home>/subos/default,不是项目级 SubOS, 否则重蹈 221 的假绿。 - 断言
mcpp build成功,且输出说明了降级(「一个没人打印的降级与没有降级 无法区分」)。 - 反向:把
subos_info.runtime写成一个不存在的 glibc 版本,断言构建失败, 且消息说的是载荷缺失而非「无法描述自己」。这一条防止 A/B 把矛盾也放行。 - 断言
mcpp toolchain install在同一 home 下退出 0。
单测 tests/unit/test_post_install.cpp:ensure_post_install_fixup 传入空
runtimeId 时返回 applied=false, skippedReason≠"",而非 error。
- 一个
subos/default/.xlings.json只有{"workspace":{}}的 Linux 机器上,mcpp build与mcpp toolchain install均成功; - 同一机器上
git grep -n '"subos" / "default"' src/toolchain/无结果; subos_info.runtime指向不存在的载荷时仍然失败;- 宿主机(SubOS 描述完整)行为逐字节不变 —— 用
build.ninja归一化 diff 核对。
纯 C 共享库([targets.purec] kind = "shared",一个只含 C 函数的 .c):
build.ninja: rule cxx_shared
command = $cxx -shared @$out.rsp -o $out $ldflags …
cxx = …/xim-x-gcc/16.1.0/bin/g++
同一个 .o、同一份 ldflags,只换驱动:
| 驱动 | NEEDED |
|---|---|
g++ |
libstdc++.so.6 libm.so.6 libgcc_s.so.1 libc.so.6 |
gcc |
libc.so.6 |
nm -D --undefined-only 显示该 .so 唯一像 C++ 的未定义符号是
__cxa_finalize@GLIBC_2.2.5 —— glibc 的弱符号,不来自 libstdc++。即三个 NEEDED
全部由驱动带入,零真实依赖。purec_add 在两种链接下都正常导出、可 dlopen。
issue 低估了收益:少的不是一个 NEEDED,是三个。
issue 列的两条中,--as-needed 不能作为主方案,理由与「优雅」无关:
- 它不跨平台。
--as-needed是 GNU ld / lld 的特性。macOS 的 ld64 没有它 (最接近的-dead_strip_dylibs语义不同且作用于整条链接线),MSVC 的link.exe没有对应概念。选驱动则三平台都成立。 - 它治不了这个错误。 一个纯 C 的库由 C++ 驱动链接,即使
NEEDED被裁掉, 命令行仍然是错的 —— C++ 驱动还会带入-lm、C++ 的启动/异常段落与不同的默认库 顺序。--as-needed只是把症状扫掉。 - 它的作用域会越界。 现在全仓唯一一处
--as-needed用的是-Wl,--push-state,--as-needed -latomic -Wl,--pop-state(flags.cppm:240), 括起来正是为了把作用域限死。全局启用会波及用户显式命名的库,以及只被dlopen使用的库 —— GPU/GL 那条链已经为此付过代价。
结论:方案 1(按内容选驱动)。方案 2 不作为替代,可在方案 1 之后单独评估。
数据已经在,只是没有到达发射链接边的那一步。 CompileUnit 携带
mcpp::SourceKind kind(ModuleInterface / Cxx / C / GasAsm / NasmAsm);
LinkUnit 只有 std::vector<path> objects。
改动分三处,换驱动只是其中一处 —— 只换驱动会半途而废,见 (3)。
(1) 判定谓词,照 unit_needs_std 的形状写。
ninja_backend.cppm:1169-1179 已经有一个完全同形的先例:按 cu.object 建一张表,
再对 lu.objects 查表。照抄结构即可,不需要给 LinkUnit 加字段(加字段会牵动
Plan 的序列化与缓存键,而这个决定只有发射时才用得到):
std::unordered_map<std::string, bool> objectIsCxx;
for (auto& cu : plan.compileUnits)
objectIsCxx[cu.object.generic_string()] =
cu.kind == SourceKind::ModuleInterface || cu.kind == SourceKind::Cxx;
auto unit_needs_cxx_runtime = [&](const LinkUnit& lu) {
for (auto& o : lu.objects) {
auto it = objectIsCxx.find(o.generic_string());
if (it == objectIsCxx.end()) return true; // ⚠️ 未知 ⇒ 保守
if (it->second) return true;
}
return false;
};unit_needs_std 的关键差异是查不到时的取值。 那个查不到取 false
(不链 std.o),这个查不到必须取 true。action 产出的对象
(prepare.cppm:5182 / 5420-5422)不在 plan.compileUnits 里,语言未知,按 C 链
会得到未定义符号。
静态依赖不需要单独传播:kind = "lib" 依赖的对象经 append_package_objects
(plan.cppm:1547)直接进入 lu.objects,其 cu.kind 就在表里。共享库依赖由动态
链接器解决,其自身的 NEEDED 与本单元的驱动无关。
(2) 规则选择,并同时堵住 std.compat.o。
ninja_backend.cppm:1691-1710 的 switch 按谓词选 c_link / c_shared
(用 $cc,该变量已存在)或 cxx_link / cxx_shared。cxx_archive 走 ar,
与驱动无关,不变。
switch 里 std.compat.o 没有收窄(自我 review 发现,见 R4):
if (has_std_artifacts && unit_needs_std(lu)) ins += std_o_dst; // #423 已收窄
if (has_std_compat) ins += compat_o_dst; // ← 无条件std.compat.o 由 C++ TU 编出,链进纯 C 单元会带来真实的 libstdc++ 依赖。实测配置
下 has_std_compat 为 false 故当前不发作,但这是 #416 修了一半留下的不对称。方案 B
必须一并处理:C 链接单元既不收 std.o 也不收 std.compat.o,且 std.compat.o
补上与 std.o 相同的 unit_needs_std 收窄。
(3) ldflags 里的 C++ 专属 token 必须同时不发。
只换驱动是不够的 —— flags.ld 与 unit_ldflags 都可能带 C++ 运行时 token:
| token | 来源 | C 链接下的后果 |
|---|---|---|
-lstdc++exp |
flags.cppm:965(MinGW + libstdc++) |
显式命名的库,照样链进去 —— 修复失效 |
-stdlib=libc++ |
linkmodel.cppm:178/189 |
clang 警告并忽略 |
--rtlib=compiler-rt --unwindlib=libunwind |
linkmodel.cppm:189 |
改变 C 链接的运行时选择 |
-static-libstdc++ |
契约表经 unit_ldflags(dist::Format::Pe) |
gcc 接受但无意义 |
做法:让 compute_flags 一次算出两份 ld,即 CompileFlags 增加 ldC,与 ld
在同一个函数体内并行产生;ninja_backend.cppm:485 发两个全局
ldflags / c_ldflags。ld 的计算路径一个字符不动,所以「C++ 链接命令逐字节
不变」这条反向判据是由构造保证的,而不是靠测试碰运气。
unit_ldflags 侧同理:契约表按 dist::Format 取值时一并按链接语言取。
-static 也来自契约表,但它表达的是
PE 的「自包含」语义、与语言无关,C 单元必须保留它。
跨平台:
| 平台 | 现状 | 变化 |
|---|---|---|
| Linux / MinGW (gcc) | g++ |
纯 C 单元改 gcc;MinGW 另需去掉 -lstdc++exp |
| macOS (clang) | clang++ |
纯 C 单元改 clang,并去掉 -stdlib=libc++ 等三项 |
| MSVC | separateLinker 走 $ld(link.exe) |
无变化 —— 驱动不参与链接。加一条断言把这个事实钉住 |
不新增开关。 需要强制的用户已经有 ldflags = ["-lstdc++"]。多一个
linker_language 键就是多一处可以与真相不一致的声明。
规模修正:因 (3),改动面从「中」上修为「中偏大」。若要拆两步,(1)+(2) 单独落地
在 Linux/gcc 上即可得到实测的全部收益(该配置下 ldflags 不含任何 C++ token),
但不可在 macOS / MinGW 上只做 (1)+(2) —— 那会得到一个「换了驱动却仍带
-lstdc++exp」的半吊子状态,比不改更难诊断。
- 纯 C 目标静态链接了一个 C++ 静态库:传递规则(第 2 条第二项)覆盖已声明的
依赖。若用户用裸
ldflags = ["-lfoo"]引入一个 C++ 库,则会得到未定义_Z…—— 响亮失败,不是静默错误,且可由用户加-lstdc++解决。这是可接受的方向。 - 不影响
import std:模块接口单元的kind是ModuleInterface,必然 true。 - 与 #416 的
std.o收窄正交:那条管「链不链这个对象」,这条管「用哪个驱动」。 issue #426 已明确证否「症状源自std.o」。
- 纯 C 共享库工程
readelf -d中没有libstdc++.so.6/libm.so.6/libgcc_s.so.1,且导出符号与可dlopen性不变; - 一个 C++ 工程的链接命令与产物逐字节不变(归一化 diff
build.ninja); - 一个「纯 C 可执行 + 依赖一个 C++ 共享库」的工程仍能链接并运行 —— 传递规则的正向判据;
- macOS 上纯 C 单元的
otool -L不含libc++; - MSVC 上
build.ninja的链接规则无变化。
bb53e81(发版收尾的 pin bump)后,三平台 e2e 全红,失败点同一处:
FAIL: bench/matrix.json
reference_mcpp=2026.8.11.3 but .xlings.json bootstraps mcpp 2026.8.15.1 —
the reference arm IS the bootstrapped binary, so these two must agree …
.xlings.json 被 bump 到 2026.8.15.1,bench/matrix.json 的 reference_mcpp
留在 2026.8.11.3。守卫是 #423 加的,由 tests/e2e/233_bench_matrix.sh:269 触发。
它断言的危险 —— 「the old column silently becomes some other release」——
已经被 run-standard.sh 自己防住了:
# bench/run-standard.sh:125-136
for c in "$HOME"/.xlings/data/runtimedir/mcpp-"$REFERENCE_MCPP"-*/mcpp; do
got="$("$c" --version …)"
if [ "$got" = "$REFERENCE_MCPP" ]; then REF_BIN="$c"; break; fi
echo " note: $c reports '$got', not '$REFERENCE_MCPP'; ignoring it"
done
[ -n "$REF_BIN" ] || echo " note: no mcpp@$REFERENCE_MCPP binary found; …"参照臂按精确版本取路径,并要求二进制自述该版本,取不到就明说列缺失。 两个 pin 不一致时不会量错东西,只会在没装那个版本时少一列。
而守卫断言的前提「the reference arm IS the bootstrapped binary」不成立:bench 标准集
不在任何 workflow 里(grep -rn run-standard .github/workflows 无结果),它在开发机上
手工跑,runtimedir 下常年并存多个版本。参照臂是被显式点名的那个,不是被
bootstrap 装的那个。
更根本的一点:bootstrap pin 是自举起点,判据是上界而非义务,可以合理滞后于最新 发布;bench 的参照臂语义是「上一个已发布版本」。把两者钉成相等,是给一个 bench 旋钮 强加了发布流程的约束。
-
删除
233_bench_matrix.sh:262-272的跨文件相等断言。 保留同文件里两条真实的 不变量:reference_mcpp必须是精确版本;matrix.json的编译器 pin 必须与bench/src/toolchain.cppm一致(那一条是真耦合 —— 一处决定装什么、一处决定要哪个 路径,不一致会让每个 cell 报一个路径错误)。 -
让报告自述它实际量到的版本。
report.py:237现在把列名写死成mcpp (old)/mcpp (旧版),应改为显示实测版本(mcpp 2026.8.11.3);README pin 表里的那一行 同样由生成而非手写。⚠️ 不能改 engine 标签(自我 review 发现,见 R6)。journal 的 cell 只有engine / compiler / profile / scenario,参照臂的 engine 标签就是mcpp(run-standard.sh:280),不含版本;而 engine 标签是 resume 的键,改它会让 已有 journal 全部失效、整套数据重跑。修正做法:run-standard.sh:131已经拿到并 校验过该二进制自述的版本($got),把它连同解析出的路径写进 journal 旁的meta.json,report.py从那里取。纯增量,不动 resume 键。
第 2 条是把守卫想买的性质结构化地买到:漂移不再靠断言拦截,而是无处藏身 —— 报告直接写着它量的是哪个版本。
- main 三平台 e2e 恢复绿;
matrix.json与.xlings.json故意写成不同版本时,233通过,而生成的报告表头 显示matrix.json里那个版本;- 删掉
runtimedir下的参照二进制后,报告显示该列缺失而非显示错误数据。
main 红(独立,最短) ──► 先做,恢复 CI 信号
#427(独立,硬失败) ──► 并行,优先级最高
#426(独立,中偏大) ──► 并行
三条无共享代码路径:#427 在 src/toolchain/,#426 在 src/build/{plan,ninja_backend,flags},
main 红在 tests/e2e/ 与 bench/tools/。可在同一 PR 内并行实现,分三组 commit。
- #427:宿主机(SubOS 完整)的
build.ninja与产物必须逐字节不变。 若变了,说明改动动到了正常路径,而不只是缺失路径。 - #426:C++ 工程的链接命令必须逐字节不变。
- main 红:必须能构造出「两个 pin 不同」且
233通过的情形,否则说明只是把断言挪了地方。
对上面三份方案逐条找洞,每条都对 HEAD 核实过。其中 R1 是方案 A 的真缺陷:按初稿 实施会引入一个新的、更隐蔽的问题。
toolchain_install(const GlobalConfig& cfg, …)(lifecycle.cppm)手里只有 cfg,
没有任何 RuntimeBinding,:587 因此不传 runtimeId。删掉兜底之后,这条路径在
Linux 上 runtimeId 恒空 ⇒ 永远走降级分支 ⇒ 永远不打补丁。
而 fixup 的存在理由正是这条路径:「without it a fresh-sandbox glibc gcc cannot find the C library (stdlib.h not found)」。初稿等于把一个硬失败换成一个静默的坏安装 —— 比原缺陷更难诊断。
修正(已并入方案 A′):toolchain_install 自己调用一次 resolve_runtime_binding,
与 prepare.cppm:962 用同一个解析器,把 runtimeId 与 libraryDirs.front() 传下去。
教训:「删掉重复推导」只在所有调用方都能提供那个事实时成立。删之前必须逐个 调用方核对谁能提供 —— 五个调用点里有一个不能。
初稿的隐忧:健康机器上 runtimeId 若也可能为空,删掉兜底就成了回归。
核实:resolve_runtime_binding 在缺 subos_info 时不早退
(runtime_binding.cppm:277「CONTRADICTION vs ABSENCE」那段),后面还会用
<subos>/lib*/libc.so.6 的物理链接反推身份(managed_glibc_identity,:352)。
所以只有既无声明、又无物理 libc 的 SubOS 才会得到空身份。
沙箱现场正是如此 —— 那个 subos 的 lib/ 里没有 libc.so.6,物理推导也无从下手。
即降级路径只覆盖真正一无所知的情形,不会波及正常机器。
降级写出的 .mcpp-fixup.json 其 glibcLib 为空,与打过补丁的指纹不同 ⇒ 用户后来跑
了 xlings self update、身份可知之后,fixup 会自动重跑。这条性质由现有的内容
指纹机制免费提供,但必须在测试里钉住,否则将来有人「优化」成布尔标记就会退化成
「补好了 xlings 却仍不打补丁」。
ninja_backend.cppm:1697 / :1707:
if (has_std_artifacts && unit_needs_std(lu)) ins += std_o_dst; // #423 已收窄
if (has_std_compat) ins += compat_o_dst; // ← 无条件std.compat.o 由 C++ TU 编出,链进纯 C 单元会带来真实的 libstdc++ 依赖(不同于
#426 那个纯粹由驱动带入、可完全去掉的)。实测配置下 has_std_compat 为 false,故
当前不发作 —— 但这是 #423 留下的不对称,方案 B 必须一并收窄,否则修好了驱动却在另
一个配置上重新引入同一个症状。已并入方案 B (2)。
方案 B (3) 初稿说「C 单元拿不到 C++ 运行时那几项」,措辞过宽:契约表(dist::Format::Pe)
同时提供 -static,它表达的是 PE 的自包含语义、与语言无关。C 单元丢掉它会破坏
build-mcpp-helper-self-containment 记的那条结论。已在方案 B (3) 收窄措辞。
核实:journal 的 cell 只有 engine / compiler / profile / scenario
(report.py:74),参照臂的 engine 标签就是 mcpp(run-standard.sh:280),不含版本。
而 engine 标签是 resume 的键 —— 把它改成 mcpp@2026.8.11.3 会让所有已有 journal
失配、整套标准集重跑,代价远大于收益。
修正:run-standard.sh:131 已经取到并校验过该二进制自述的版本,写进 journal 旁的
meta.json 即可,report.py 从那里取。纯增量,不动 resume 键。已并入方案 C2。
一次 mcpp build 里 ensure_post_install_fixup 最多被调四次(manifest 工具链、
默认工具链、MinGW 首次、build.mcpp host 工具链)。降级提示要按 (kind, payloadRoot)
去重,否则同一条消息刷四遍 —— 与 #417 那半个「说一次就够」是同一条规矩。
R4 让方案 B 多收一处 std.compat.o,但仍不与 A / C 争同一个文件。三条依旧可并行,
只是 B 内部多一步。
| 编号 | 性质 | 处置 |
|---|---|---|
| R1 | 方案缺陷 —— 会引入更隐蔽的新问题 | 已改方案(新增 A′) |
| R6 | 方案缺陷 —— 做法不可行且代价大 | 已改方案(C2 改为 meta.json) |
| R4 | 遗漏 —— 方案不完整 | 已并入方案 B (2) |
| R5 | 措辞过宽,会误删 | 已收窄方案 B (3) |
| R3 / R7 | 补充,防止将来退化 | 已并入方案 A |
| R2 | 隐忧证否,方案不变 | 记录核实结论 |
三条方案在改动后自洽。A′ 是必须与 A 同时落地的,单做 A 会造成回归。
方案在实施中被现实修正了四处。记在这里,因为「方案说 X,代码做了 Y」不写下来就 是下一个人要重新发现的东西。
R6 断定 journal 不含参照臂的版本、只能另写 meta.json。核实代码后:mbench 按
二进制自述的版本给每条臂命名(report.py 见到的是 mcpp@2026.8.11.3),版本
一直都在键里。short_name 只是把它丢掉了。
所以列名改动是三行:return "mcpp " + base[len("mcpp@"):],不需要新数据源。
meta.json 仍然写,但作用变小且不同 —— 它记的是请求 vs 实际解析到的路径,
即「matrix.json 要 2026.8.11.3,这台机器上解析到了/没解析到」,这是 engine 键
表达不了的。
方案 A 的测试计划写的是 e2e 构造一个 subos/default 缺描述的 MCPP_HOME。写出来
才发现:任何低成本 e2e 都用符号链接继承工具链载荷,而 ensure_post_install_fixup
第一件事就是对继承载荷提前返回(「owner is responsible for its fixup」)——
被测代码一行都执行不到。
这正是 §六里 221 的形状,差点原样重演一次。改为:
tests/unit/test_post_install.cpp—— 载荷是测试自己 registry 里的真实目录, 门的四个分支(降级/矛盾/不留标记/无需 fixup)逐条钉住;tests/e2e/237—— 只钉用户可见的契约:构建不再死在 fixup、allow_host_libs能到链接、toolchain install退出 0。
方案 B 的 e2e 初稿在一个工程里放纯 C、混合、C++ 三个 target。实测:同一个包的
对象会进入该包每一个链接单元 —— libpurec.so 拿到了 mixed_cxx.o,于是「纯 C 单元」
在这个布局下根本不存在。改成三个独立工程。
初版的绿色一半是「加上 allow_host_libs = true 必须能构建」。本机绿,CI 红:
ld: cannot find crt1.o / crti.o / -lm
落回宿主需要宿主真有一份 C 运行时,而 runner 上一个都没有。方向与
shared-gcc-payload-specs-poisoning 记的那次相反 —— 那次是本机有历史污染,这次是
本机有 runner 没有的宿主开发文件。两者共同点:断言依赖了运行机器。
改成不依赖机器的对照:同一个 home、同一批载荷、同一个工程,只给 SubOS 补上
subos_info 块,必须构建成功。这同时更强 —— 它证明第 1 部分的失败确实只是降级,
而不是这个环境本来就编不出东西。
绑定哪个 glibc 也不能猜:向真实 home 的 subos_info.runtime 要,取不到才回落到目录
扫描。装了两个 glibc 载荷的机器上,ls | head -1 会按字母序挑中工具链没有针对它打过
补丁的那个,让对照因为与被控变量无关的原因失败。
方案 A2 原说「降级写出的 marker 其 glibcLib 为空,身份可知后会自动重跑」。实现时
改为根本不写 marker:让「什么都没做」有机会被读成「已经做过」是不必要的风险,
而不写 marker 的代价只是每次构建重新判断一次(纯内存)。单测
ASkippedFixupLeavesNoMarkerBehind 钉住这一点。