状态:分析 + 方案,待 review。全部对 HEAD 重新核实过,不是照抄 issue。
七条都是 2026-08-11/12 写的,分支此后动过。下面每一条先给核实结论(还在?
行号变了没?issue 的推断成立吗?),再给方案。三处 issue 自身需要修正,标了
| # | 核实 | 性质 | 改动面 | 建议 |
|---|---|---|---|---|
| 416 | 仍在,但 |
正确性(后果远小于所述) | 小 | 先复现 |
| 422 | 仍在,且真因比 issue 说的更好修 | 正确性(MSVC 不可用) | 小 | 先做 |
| 412 | 三条全在 |
文档/覆盖 | 小 | 先做 |
| 418 | 两条全在,但字段有同名的活字段 |
死代码 | 小 | 做 |
| 415 | 仍在(Origin 无 Artifact 档) |
可观测性 | 中 | 做 |
| 417 | 仍在,但真因未定位 | 首次体验 | 中 | 先探针 |
| 421 | 仍在; |
能力边界 | 大 | 拆成三档 |
仍在。行号已从 issue 写的 1362-1381 变到 ninja_backend.cppm:1648 / :1658:
case LinkUnit::Binary:
case LinkUnit::TestBinary:
if (has_std_artifacts) ins += " " + escape_ninja_path(std_o_dst);
case LinkUnit::SharedLibrary:
if (has_std_artifacts) ins += " " + escape_ninja_path(std_o_dst);has_std_artifacts 的含义是「这条工具链有预建的 std 模块」,与这个链接单元是否
需要无关 —— 这一点确实成立。但 issue 声称的后果不成立,见下面两处修正。
把条件从「工具链有 std」收窄到「这个单元的闭包里真的有人 import std」。
判定数据已经在手,不需要新的扫描:模块图里每个 TU 的 requires_ 是扫描器
填的,import std 会出现在里面。所以:
needs_std(link_unit) = 该单元的任一 TU 的 requires_ 含 "std" / "std.compat"
∪ 该单元链接的任一【本工程】静态库/对象的同一判定(传递闭包)
import std,但它 import 的模块接口写了,链接时同样需要 std.o。
修正 1:std.o 不可能是 libstdc++.so.6 的来源。
$ nm --defined-only obj/std.o → 1 个符号:_ZGIW3std(模块 std 的全局初始化器)
$ nm --undefined-only obj/std.o → 0 个
$ ls -l obj/std.o → 1280 字节
零个未定义符号,所以它拖不动任何共享库。issue 把
「纯 C 的 compat 包多一条 NEEDED libstdc++.so.6」归因给 std.o,这条因果不成立。
真正的嫌疑是链接驱动:ninja_backend.cppm 一律用 $cxx(g++)链接,而 g++
总是加 -lstdc++;全仓 --as-needed 只用在 -latomic 那一处
(flags.cppm:240),所以 -lstdc++ 会无条件成为 NEEDED,与 std.o 无关。
修正 2:头号症状在本机产物上复现不出来。
$ readelf -d ~/.mcpp/registry/subos/default/lib/libXau.so.6 | grep NEEDED
libc.so.6 ← 只有这一条,没有 libstdc++.so.6
$ nm -D libXau.so.6 | grep -c _ZGIW → 0
这份产物的来源版本不明(可能是 #414 之后重建的)。实施前必须先用当前 mcpp
重新构建一个纯 C 的 compat 包并 readelf -d,确认症状是否还在 ——
否则可能在修一个已经不存在的问题,而真正的 -lstdc++ 通道没人动。
- 仍然该做:
std.o无条件链进纯 C 单元本身就是错的(一个不该在那里的对象), 即使它的后果比 issue 说的小得多。 - 但判据要拆开:「
readelf -d没有libstdc++.so.6」这条不是收窄std.o就能达成的,它属于-lstdc++/--as-needed/ 链接驱动那条线。两件事要分成 两个 issue,否则会出现「改完了,判据仍然不满足」。 - 风险比想象中低:
std.o唯一的符号是模块初始化器,而import std的 TU 会 引用它。所以谓词若漏判(该链没链),结果是链接期 undefined_ZGIW3std—— 响亮的失败,不是静默错误。这让递归传递闭包不必一次写到完美。
仍在,而且真因比 issue 推测的更具体。src/toolchain/msvc.cppm:529 构建 std 的
命令是:
cl /nologo <std-flag> /EHsc /O2 /W0 <extraRef> /c <src> /ifcOutput <ifc> /Fo:<obj>
一个 /MT 或 /MD 都没有 —— 用的是 cl 的默认(/MT)。工程在
cxx_runtime = "host-coupled" 下编 /MD,于是 _MSVC_MT / _MSVC_MD 对不上,
C5050 之后是真正的 C2375。
issue 说「要么 std 构建遵守 cxx_runtime,要么 std 缓存按它分键」。核实发现
std 的缓存身份键里已经含 std_build_commands(实际命令行)
(src/toolchain/stdmod.cppm:136,且 metadata_matches 的 14 个键里也有它)。
所以只要把 runtime flag 加进那条命令,缓存目录会自动随之分叉,两者不可能再 静默背离。不需要单独设计缓存键。
msvc.cppm的 std 命令构造函数接收「有效 runtime 契约」,发/MT或/MD(以及 debug profile 下的/MTd//MDd)。- 契约来源必须与工程 TU 同一处推导,否则就是又一个「同一决策两处推导」。
工程侧的推导在
src/build/flags.cppm(dist::parse_contract/contractByRole), std 侧要读同一个结果。 - 顺带:GCC / Clang 侧不需要动 —— 它们的 std 模块不带 runtime ABI 开关。 但注释要写明为什么只有 MSVC 需要,否则下一个人会以为漏了。
cxx_runtime = "host-coupled"的工程在 MSVC 上能构建并运行(现在是失败)- 同一台机器上,
host-coupled与self-contained两个工程各自拿到不同的 std 缓存目录 —— 用ls <cache_root>/std/直接看,两个 key ⚠️ 反向也要验:只改cxx_runtime不改别的,std 必须重建。否则说明 flag 没进std_build_commands,缓存键没分叉,问题只是被当前的冷缓存掩盖了
三条全在:
| 位置 | 现状 | |
|---|---|---|
| 1. 劝退用户的假 note | src/toolchain/lifecycle.cppm:664 |
仍在 |
| 2. 与自身断言矛盾的头注释 | tests/e2e/95_msvc_system_toolchain.sh:9 |
仍在 |
3. 217 在两个平台整条被跳过 |
tests/e2e/217_module_extensions.sh:2 # requires: gcc |
仍在 |
issue 建议「补一条 219」。219 已被 219_runtime_search_farm_is_last.sh 占用。
新测试要另取编号(当前最大是 234,故用 235)。
- 删
lifecycle.cppm:664那句 note。它的实际效果是劝退一个能用的功能。 替换成陈述现状还是直接删掉,取决于是否有已知限制要说——若无,直接删, 「没有消息」比「过期的消息」好。 - 删
95头注释第 9 行。 - 新增
tests/e2e/235_module_extensions_msvc_llvm.sh:不依赖gcc能力, 按平台选默认工具链(Windows→msvc,macOS→llvm),验证.ixx走完 build→link→run。
.ixx + module_extensions + MSVC 这条组合推理上必然通,但推理不是测量 ——
这正是 #411 里指出的同一形状。
两条都仍在,但 cxxRuntimeTests 这个名字下有两个字段,只有一个是死的:
| 字段 | 位置 | 状态 |
|---|---|---|
BuildConfig::cxxRuntimeTests |
types.cppm:450 |
活的 —— toml.cppm:967 解析,flags.cppm:760/848 读 |
TargetEntry::cxxRuntimeTests |
types.cppm:653 |
死的 —— 解析处(toml.cppm:1379)只读标量 cxx_runtime,应用处(prepare.cppm:1154)只应用 cxxRuntime |
contractByRole 全仓两处命中(flags.cppm:62 声明、:875 写),确认无读取方。
实施时不要 grep cxxRuntimeTests 后一把删 —— 会删掉活的那个。
TargetEntry::cxxRuntimeTests:删。 理由:per-target 通道目前只支持标量, 而标量已覆盖所有角色。补全表形式解析是扩表面积,而这个通道还没有用户要求它。 一个不生效的配置项比没有更糟 —— 但补全它等于新增一个需要长期维护的语义。contractByRole:接出去,不删。 它记录「每个角色实际拿到的契约(降级之后)」, 而 #414 之后共享库的契约按格式分档,「我这个.so到底拿到了哪一档」是用户会 真的问的问题,现在只能靠readelf自己看。写进resolution.json,让mcpp why/mcpp doctor能回答。
TargetEntry::cxxRuntimeTests不存在,且[target.<triple>].cxx_runtime_tests写在 manifest 里会报未知键(而不是被静默忽略)contractByRole有真实消费方并被测试覆盖:一个共享库工程的resolution.json里能读到它拿到的档位,且与readelf的实际观测一致
仍在。src/platform/runtime_search.cppm 的 Origin 枚举只有
Payload / Package / SubosFarm / HostDefault,没有 Artifact 档(全文件 0 处
命中 "Artifact")。$ORIGIN 由 src/build/plan.cppm:475 在 per-unit flags 通道
单独发出。
模块自称「Search order = decreasing immutability. This is the one invariant in this module」,而最关键的那个目录不在这个排序里 —— #414 修的正是它排错了位置。
runtime_search.cppm:Origin加Artifact,rank()置于Package与SubosFarm之间,补to_string();⚠️ is_machine_local()返回 false ——$ORIGIN随产物走,不是机器局部的。 这一条写错会让pack的判断反过来(虽然pack当前不 import 这个模块, 但那是巧合,不是契约)。plan.cppm:679runtime_search_closure():把产物输出目录加进去。- 显示侧:
prepare.cppm写记录、doctor.cppm打标签。
重档(让闭包成为唯一的 rpath 生产者)不做:$ORIGIN 本质是 per-unit 的
(只有消费共享库的单元才需要),挪进全局闭包要给闭包引入 per-unit 概念,
改动量与风险明显更大,而收益只是消灭「两个生产者」这个洁癖。
e2e 219 能把「记录的闭包」与「产物的 DT_RPATH」逐项比对并通过,
不对 $ORIGIN 做任何过滤或例外 —— 现在它必须过滤,那个过滤就是缺口的证据。
仍在。两条消息在 src/platform/elf_runtime.cppm:773 / :791,按产物逐条发,
所以一个图形工程刷 13 条。
issue 自己写了「精确接缝还需要一次探针确认 —— 不要照着这个推理直接改」。 这条要照办。当前证据只支持这个描述:
- 首次运行:
binding.loader与binding.library_dirs为空,而binding.search_dirs有值(已记下<subos>/lib) - 第二次运行:两者都有值,警告消失
- 磁盘上二者首次运行时都存在
「binding 求值早于 farm 落盘」是自然读法,不是已证事实。search_dirs 有值而 另外两个没有,说明填充它们的不是同一段代码或同一时刻 —— 这一点本身就值得先查。
- 探针:在
runtime_binding.cppm:337-380的填充点打一条 verbose,记录 「此刻<subos>/lib64、<subos>/lib是否存在、里面有没有libc.so.6和ld-linux-*」。全新 MCPP_HOME 跑一次,把真实接缝钉死。 - 按探针结果二选一:
- 若确是时序:把 binding 的求值推迟到 farm 落盘之后(或在首次构建时重求值一次)。
- 若是查找逻辑在首次运行时的目录形态下失效(例如那时还是符号链接的符号链接): 修查找,而不是改时序。
- 无论哪一种,诊断都要改:同一个原因对 13 个产物刷 26 条,是噪声不是信息。 改成 binding 层面只说一次「此刻无法求值,原因 X」。
即使 rule B 完全正常,它也抓不到 #414 那个崩溃。
elf_runtime.cppm:761-801 只比对 libc / PT_INTERP 的同一性,不管其它 SONAME
解析到谁。修这个 issue 不能替代 #414 的顺序修复,也不要当成它的兜底。
全新 MCPP_HOME 的第一次构建:要么 rule B 给出真实判决(pass/mismatch), 要么 binding 只说一次「此刻还无法求值」,而不是对每个产物各刷两条。
两条限制都仍在:scanner.cppm:660(条件 import)、:666(头单元)。扫描器是
纯词法的,用 if_depth > 0 判定,不看条件是否可判定。
docs/05-mcpp-toml.md:414(中文版 §2.3 同):
defines… also reaches the P1689 module scan, which is what makes a macro-guardedimportresolvable
这句话对 mcpp 不成立。 defines 确实会进编译器的 P1689 扫描(.ddi),
但 mcpp 自己的前置扫描器在看到条件块里有 import 时就直接报错,根本走不到
宏求值。用户按文档写一个宏保护的 import,拿到的是 forbidden in M1。
这一条不需要设计,只需要把文档改对 —— 而且它是七条里唯一一个「文档承诺了一个 不存在的能力」,优先级应当最高。
档 A(必做,零风险):把文档改对。 说明 defines 到达的是编译器的扫描,
而 mcpp 的前置扫描器目前拒绝一切条件块内的 import。
档 B(建议做):诊断可操作化。 现在只说 "forbidden in M1"。改成:
- 指出是哪个宏在保护它;
- 若该宏出现在
[build].defines/[targets.*].defines里,直接说 「这个条件是可判定的,当前未实现求值(见 #421)」; - 若不在(例如
__cpp_lib_stacktrace这种工具链内建宏),说 「依赖工具链内建宏,不可判定」。
代价很小,而它把「适配一个真实工程」的排查成本从「逐个文件试」降到「读一条消息」。
档 C(不建议现在做):真正求值可判定的条件。
若条件只依赖 defines 里出现过的宏,扫描器信息是足够的。但这意味着扫描器要引入
一个最小预处理器(#if 表达式求值、defined()、嵌套、#elif),而
「实现一个不完整的预处理器」的失败形态是静默扫错依赖图,不是报错。
M1 的取舍(纯词法 + 硬拒)正是为了避免这个。要做也应当先把边界写死:
只支持 #ifdef X / #ifndef X / #if defined(X) 三种字面形态,别的一律仍然拒。
头单元:维持现状(拒绝)。头单元本身是泥潭,而 issue 的报告者也说
「不是说 M1 的取舍不对」。但档 B 的诊断应当同时指出替代写法
(把 import <h>; 改成 GMF 里的 #include <h>),这是他实际采用的做法。
module : private; 在 GCC 16 的 P1689 扫描路径下报 module already declared
(编译路径报的是 sorry, unimplemented: private module fragment)。这是 GCC 的
消息差异,不是 mcpp 的缺陷。扫描器既然已经在做词法解析,识别出这一句并附一句
「当前工具链未实现私有模块片段」能省掉一轮误导性排查。可做可不做。
- #421 档 A —— 改一句文档,消除「承诺了不存在的能力」。零风险,最高性价比。
- #422 —— MSVC 上
host-coupled现在根本不能用,而修法很小且缓存键白送。 - #416 ——
⚠️ 先复现:用当前 mcpp 重建一个纯 C compat 包,确认readelf -d里还有没有libstdc++.so.6。症状若已不在,这条降级为 「清理一个不该在那里的对象」,并把-lstdc++/--as-needed另开一条。 - #412 —— 两条删字 + 一条真正的覆盖补齐(注意编号用 235)。
- #418 —— 死字段;
⚠️ 注意两个同名字段只有一个该删。 - #415 —— 可观测性,改动中等,判据明确(219 不再需要过滤
$ORIGIN)。 - #417 —— 先探针再修,不要照 issue 的推理直接动时序。
- #421 档 B —— 诊断可操作化。
- #421 档 C / 私有片段透传 —— 暂不做。
七条里有四条(412、418、421 档 A、415)本质是同一件事: 写下来的状态与实际行为脱节 —— 过期的 note、不生效的字段、承诺了不存在能力的 文档、自称唯一排序却漏了最关键一项的模型。它们都不是「功能缺失」,而是 「代码与它自己的说明书不一致」,而这类问题的共同代价是:读的人据此做决定, 然后浪费掉的时间不会归因到这里。