Skip to content

Latest commit

 

History

History
326 lines (217 loc) · 27.5 KB

File metadata and controls

326 lines (217 loc) · 27.5 KB

Issue 分析报告 —— #254 / #256 / #257 / #258 / #259 / #261

日期:2026-07-22 基线:HEAD = af25d18(mcpp 0.0.101),工作树 clean 方法:每条 issue 的断言都回到 HEAD 源码逐行核对;#256、#257 的关键行为在本机用 mcpp 自带工具链(llvm 20.1.7 / 22.1.8 / gcc 16.1.0 / 系统 clang 18.1.3)实证,不依赖 issue 正文的转述。 架构判据沿用批次总账 §5.2:一数据模型、多文法、单一收敛漏斗;同一决策不许两处推导;外加一条本批次凸显的:失败必须响,不许静默降级


0. 结论速览

# 主题 裁决 断言准确度 归属 优先级
#261 windows scan-deps cmd /c 撞 8191 真 bug(构造性必现) 全部属实 mcpp P0
#257 clang 路径 purview #include 不触发重建 真 bug(correctness,静默错误结果) 全部属实,且是代码里已登记的 follow-up mcpp P0
#259 llvm 装完 exec 127 症状真,归因错;顺带暴露 3 个 mcpp 侧真 bug 现象属实,根因假设被证伪 需重定向(见 §5) P1
#258 [target.'cfg(os)'.build].flags 合理的特性请求,且内含 1 个真 bug(键被静默丢弃) 全部属实 mcpp P1
#254 per-OS 段按宿主而非 target 拼接 真 bug(设计层不自洽),作者已在 #253 设计文档 §2.3 自记 全部属实 mcpp P2
#256 clang 20/22 模块 BMI 崩溃 上游 clang 回归,非 mcpp 缺陷;三条 ask 均合理 本机完整复现 LLVM 上游 + mcpp 文档/CI P2(canary P1)

一句话总览:六条里五条成立,一条(#259)方向要改;而且四条(#257/#258/#259 + #261 的 cmd /c)共享同一条失效模式——引擎在遇到自己没准备好的情况时选择静默降级,详见 §7。


1. #261 —— windows scan-deps cmd /c 撞 8191 字符 【真 bug,P0】

1.1 核实

src/build/ninja_backend.cppm:646-677cxx_scan 规则三分支:

if (msvcDeps) {                          // /scanDependencies $out —— 文件参数,无重定向
} else if (plan.scanDepsPath.empty()) {  // GCC:-fdeps-file=$out —— 文件参数,无重定向
} else {                                 // Clang:clang-scan-deps
    if constexpr (mcpp::platform::is_windows) {
        append("  command = cmd /c \"$scan_deps -format=p1689 -- "
               "$cxx $local_includes $cxxflags $unit_cxxflags -c $in -o $compile_target > $out\"\n");
    } else { ... }
}

断言逐条成立:

  • cmd /c 包裹确实存在,且全仓仅此一处发射到 ninja 规则(:299 的另一处是 compile_commands.json 过滤器的字符串前缀判断,非规则)。

  • 触发面精确:windows × clang 工具链 × dyndep 开启 × clang-scan-deps 存在于 clang++。GCC(-fdeps-file=$out)与 MSVC(/scanDependencies $out)因为把输出写成参数而不是 shell 重定向,天然不需要 cmd /c——这本身就说明重定向是可以避免的。

  • -o 可用性我在本机实证(不是查文档):

    $ clang-scan-deps --help | grep '^  -o'
      -o <value>             Destination of the primary output      # 20.1.7 与 22.1.8 均有
    $ clang-scan-deps -format=p1689 -o out.ddi -- clang++ -std=c++23 -c m.cppm -o m.o
    rc=0  size=488   → 合法 P1689 JSON
    

    故 issue 提议的替换功能等价且已验证。仓库无任何最低 LLVM 版本闸(src/toolchain/clang.cppm:246-252find_scan_deps 只做路径探测,不查版本;provider.cppm:88-93 直接 has_scan_deps = true),实际下限就是自带工具链(20.1.7),-o 从 LLVM 17 起存在,兼容面无风险。

1.2 架构判断:issue 的修法对,但只对了一半

cmd /c放大器,不是唯一病灶。长度来源是 local_include_flags()(ninja_backend.cppm:102-127)每依赖一个 -I,无上界。去掉 cmd /c 把天花板从 8191 抬到 32767 —— 4 倍余量,对 48 个 -I 够,但对下一个 ffmpeg/opencv 级包不一定够。而且 cxx_object / cxx_module / c_object 在 windows 上同样没有任何长度兜底(rspfile 目前只在链接三规则上,:602 useRsp = separateLinker || is_windows,#247 引入)。今天 scan 先炸,只是因为它的命令行 = 编译命令行 + $scan_deps -format=p1689 -- 前缀 + > $out 后缀,严格更长

工业界基准很清楚:CMake/Ninja 生成器对所有可能超长的规则统一走 response file(RULE_LAUNCH_* / CMAKE_NINJA_FORCE_RESPONSE_FILE),而不是逐规则救火。仓库自己在 #247 已经建立了这个先例并写下了理由注释(:594-602)——只是没推广到编译/扫描边。

1.3 建议

  • P0(本周):合并两分支为 -o $out,消灭全仓唯一的 cmd /c。改动 8 行,cxx_scan 的 POSIX 命令文本也一并简化(去掉 > $out),两平台同形——符合"单一收敛漏斗"。
  • P1(随后):把 useRsp 的判据从"链接规则"上移为"windows 上所有含 $local_includes 的规则",cxx_scan/cxx_module/cxx_object/c_object 统一 rspfile。注意 #247 的教训(rsp 内容按 GNU 文法分词,反斜杠是转义符 → 节点名必须 generic_string())已经在 link_rule 里踩过,复用即可。
  • 单测:tests/unit/test_ninja_backend.cpp:516-519 已锁链接规则的 rsp 形状,照抄一条锁 cxx_scancmd /c

2. #257 —— clang 路径 purview #include 不触发模块接口重建 【真 bug,P0】

2.1 核实:这是代码里明写的已知缺口

src/build/ninja_backend.cppm:496-502:

const bool posixDepfile = !msvcDeps && !mcpp::platform::is_windows
    && plan.toolchain.compiler == mcpp::toolchain::CompilerId::GCC;

于是 clang 上 mmd_flag / mmd_filter 皆空,append_cxx_deps() 落到 append_deps() 而后者对非 MSVC 什么都不发 → cxx_modulecxx_object 在 clang 上完全没有 depfile.inc 对整个构建图不可见。

这个闸不是疏忽,是 0.0.97 主动加的。78bb1c9 "fix(build): scope #235 compile-edge depfile to GCC" 的提交信息与 :486-495 的注释都写着:

Clang's module system (.pcm) does not emit those and its module-TU depfile shape is different; applying the GCC-shaped filter there is untested and could yield a wrong dep set. Until Clang is verified, scope this to GCC —— header/purview rebuild tracking on Clang is a follow-up.

tests/e2e/118_purview_include_rebuild.sh:2# requires: gcc,所以 clang 腿零覆盖。scanner / p1689 都不回读 textual include(p1689.cppm:355 生成 .dep 却从不解析),也不存在任何源文件内容哈希/mtime 兜底——除 depfile 外没有第二道防线

2.2 关键新证据:那个"形状不同"的顾虑,实测不成立

当年 gate 掉的唯一理由是"clang 的 module-TU depfile 形状未知,GCC 的 awk 过滤器套上去可能出错"。我在本机把两边的形状打出来了:

$ clang++ -std=c++23 -fmodule-output=x.pcm -MMD -MF x.d -c x.cppm -o x.o   # 20.1.7 与 22.1.8 同
x.o: x.cppm ops.inc                       ← 干净的单条 make rule,ninja 直接可吃

$ g++ -std=c++23 -fmodules -MMD -MF x.d -c x.cppm -o x.o                    # gcc 16.1.0
x.o gcm.cache/x.gcm: x.cppm ops.inc
x.c++-module: gcm.cache/x.gcm             ← 正是 awk 过滤器要剥掉的 "reversed rules"
.PHONY: x.c++-module
gcm.cache/x.gcm:| x.o

结论:clang 根本不产生需要过滤的东西。正确修法不是"把 GCC 的过滤器移植到 clang",而是把两件事拆开——

  • posixDepfile(是否发 -MMD + deps = gcc + depfile)→ 放开到 GCC ∪ Clang;
  • mmd_filter(awk 剥离 reversed rules)→ 保持 GCC-only

代码上就是把 :496-502 的单一 bool 拆成两个谓词。风险极低,且 e2e 118 的 # requires: gcc 可以直接删掉,变成两工具链共同覆盖。

2.3 严重性:被 issue 低估了

issue 说"Harmless for CI (cold builds) but dangerous locally"。我认为要往上提一档:

  • 失效模式是静默给出错误产物,不是报错。构建系统的失效等级里,silent staleness 是最差的一档——它把编译期错误延后成运行期错误,而且怀疑对象总是先落到用户代码上。issue 自己就记录了它已经造成了一次误诊(在 #256 的 clang 调试过程中)。
  • 影响面不止 .inc:任何 purview 里的 #include(含 module; 之后再包的私有头)、任何被模块接口文本包含的生成头,在 clang 上都不重建。而 clang 恰是 macOS 默认腿与 windows 托管腿的编译器。
  • 从"依赖跟踪必须完备"这条构建系统第一原则看,当前状态是 mcpp 在 clang 上不满足最小正确性契约。0.0.97 当时的取舍(不 regress、留 follow-up)在发布压力下合理,但已经拖了 4 个版本,而实测表明代价只有几行。

3. #258 —— [target.'cfg(os)'.build] 缺 per-glob flags 【合理的特性请求 + 1 个真 bug,P1】

3.1 核实

断言 结论
ConditionalConfig 无 globFlags 字段 属实,types.cppm:276-292 只有 cflags/cxxflags/ldflags/sources/三类 deps
[target.<pred>.build] 只读四个键 属实,toml.cppm:976-987 四次 read_list
merge_conditional_sources_flags 不碰 globFlags 属实,prepare.cppm:429-441,加一行 insert 即可
xpkg 描述符per-OS flags 属实,机制是 xpkg.cppm:888-894文本体拼接——OS 段体追加到 base 体后跑同一解析循环,所以 flags 连同其余顶层键"免费"支持 per-OS,且 OS 条目自然排在 base 之后(last-wins 顺序天然正确)
死 glob 告警按构建的源集判定,不看文件系统 属实,globFlagHits 只在 apply_glob_flags 内自增,而它只被 all_files 循环调用(scanner.cppm:900/919,all_filesmodules.sources 展开)→ issue 评论里"磁盘上放锚点文件无效"的实测结论有代码依据
features.<n>.flags 在 mcpp.toml 里全局-only 属实,toml.cppm:320-327 在顶层 [features] 里;[target.*] 没有 features 子键 → OS 轴 × feature 轴在 mcpp.toml 里不可组合
optional / 允许零匹配的逃生口 属实,且两个解析器对未知键都硬错,所以今天写 optional = true 是 parse error 而非被忽略

3.2 顺带发现的真 bug:flags 在条件段里被静默丢弃

toml.cppm:976-987 的读法是"认得的四个键各读一次",没有未知键拒绝。作者在 [target.'cfg(windows)'.build] 里写 flags = [{...}],今天的行为是:解析成功、零告警、规则凭空消失、构建出一个 flags 缺失的产物。

这与本仓已确立的纪律直接冲突:xpkg 未知 feature 子键进 xpkgUnknownKeys 报 did-you-mean(0.0.100)、[build].flags 条目未知子键硬错、scan_overrides 零命中硬错。条件段是这条纪律的一个破口。即便 #258 的特性最终不做,这个静默吞键也该单独修(#227 封闭文法 allowlist 应覆盖 target.*.build.*)。

3.3 架构判断:请求成立,而且是补齐既有原则而非新增机制

  • 文法平价是本仓写在设计文档里的原则("一数据模型两文法")。今天的实况是 xpkg 描述符能表达、mcpp.toml 不能——即"manifest 包是二等公民"这个论证站得住。而且不对称的成因是偶然(xpkg 靠文本拼接免费获得,toml 走结构化解析所以要逐键实现),不是有意的能力分级。
  • removal 论证成立-U 之所以救不了,是因为它自身也需要 OS 条件化,递归回同一缺口;issue 正文对此的推理正确。而 merge_conditional_sources_flags 的 append 位置天然给出 last-wins 覆盖权,这正是表达"windows 上撤销 unix 的 -DHAVE_UNISTD_H"所需要的。
  • 实施成本:issue 给的 4 处清单准确无误,四处全部复用既有代码(parse_glob_flags_value 的签名 (value, ctx, std::vector<GlobFlags>& dst) 可直接指向 cc.globFlags;下游 scanner/backend/fingerprint 零改动,因为 0.0.101 已经把 globFlags 全量序列化进 per-package fingerprint)。唯一要盯的顺序不变量:merge 必须在 makePackageRoot 快照 buildConfig 之前(prepare.cppm:900-909 已有注释)——三个调用点 :912 / :1871 / :2762 都在其前。
  • 对 703 个 stub 文件的评估:这不是"包作者偷懒",是引擎缺表达力把语义泄漏成了文件系统结构。0.0.97 刚删掉过同类的 #233-era tu-stub 机制,现在因为另一个缺口在下游重新长出来——这是判断该做的强信号。

3.4 对 stopgap optional = true 的反对意见

issue 评论里提议的 optional=true / ?-前缀 我不建议做,理由是它会引入第二套抑制机制,与 #253 刚确立的路线("条目按条件存在,所以无从告警")相悖——那正是同一决策两处推导。#253 已经证明了正确形状:让规则在不该生效的维度上不进入 globFlags。OS 轴照抄即可,不需要新语义。如果 #258 主体要拖,宁可先只做 §3.2 的静默吞键修复(半天工作量,且能让作者立刻发现自己写的 flags 没生效)。


4. #254 —— per-OS 段按宿主拼接而非 resolved target 【真 bug,P2】

4.1 核实

两轴不自洽,逐条属实:

  • platform/common.cppm:95-105xpkg_platformconstexpr + #if defined(_WIN32)/__APPLE__/__linux__ —— mcpp 自己被编译时的宿主,无任何运行期输入。
  • xpkg.cppm:888-889 osKey = osOverride.empty() ? xpkg_platform : osOverrideosOverride全部非测试调用点:prepare.cppm:1589 / 1637 / 1814 三个构建路径全部走默认(宿主),唯一传 override 的是 lint 命令 mcpp xpkg parse --all-os(cmd_xpkg.cppm:132)。overrides.target_triple 从未穿进来。
  • cfgpred::context_for(prepare.cppm:76-107)注释明写 "Context = the RESOLVED target's coordinates",三个调用点(:911/:1872/:2763)都传 overrides.target_triple
  • 影响面比 issue 列的还宽一格:xpm 版本/资产表也走宿主键(xpkg.cppm:711-772list_xpkg_versions(luaContent, platform),pm/resolver.cppm:108kXpkgPlatform = xpkg_platform),即交叉编译时连"有哪些版本可选/下哪个 tarball"都按宿主判定,连错误文案都把宿主平台名硬写进去。
  • 交叉编译不是纸面能力:tests/e2e/102_mingw_cross_wine.sh112_build_mcpp_cross.sh 真跑 --target x86_64-windows-gnu 并用 wine 执行。没有一条 cross e2e 使用带 per-OS mcpp 段的 xpkg 依赖 → 缺口对 CI 完全不可见。

4.2 架构判断

这是教科书级的"同一决策两处推导":平台选择这一个决策,在 xpkg 通道按宿主编译期常量推,在 manifest 通道按 resolved target 运行期推。native 构建下 host == target 使两者巧合一致,于是缺陷被三平台 CI 完全掩盖——恰恰是 0.0.99 复盘里记下的那类隐性架构债(加新语义会变构建失败:#253 刚给 per-OS 段加了 features,这个错误轴上又多挂了一项)。

作者的处理方式是对的:在 #253 设计文档 §2.3 记录、明确不入该 PR、拆成独立 issue 并回引设计节。这是应该保持的工作方式。

4.3 建议(比 issue 的方向再收紧一步)

issue 说"pm/install 侧需单独辨析",正确但太软。建议直接上类型层约束,否则这类 bug 会反复:

  • 引入两个不可互换的类型(如 HostPlatform / TargetPlatform,或一个带 enum class PlatformAxis { Host, Target } 的强类型 wrapper),让 synthesize_from_xpkg_lua / list_xpkg_versions 在签名上必须声明自己要哪一轴。今天二者都收 std::string_view,混用没有任何编译期阻力。
  • 分类结论(已核实,可直接照用):prepare.cppm:1589/1637/1814 三处 = 编进用户构建的源码库包target;toolchain/lifecycle.cppm:135 = 宿主工具(且注释已写明为何要 host)→ 保持 host;pm/resolver.cppm:108 = 用户项目的库依赖 → target;cmd_xpkg.cppm = lint,遍历全 OS。
  • 锁定:e2e 加一条"host == target 时拼接结果逐字节不变"的回归,再加一条 mingw-cross 腿消费带 per-OS 段的 xpkg dep。

优先级 P2 合理——今天没有真实用户被咬(交叉编译尚未与 per-OS xpkg 依赖组合使用),但它是能力增长的路障:每加一个 per-OS 键,错误面就扩大一次。


5. #259 —— llvm 装完 exec 127 【症状真,根因假设被证伪 → 需重定向,P1】

5.1 现象属实

本机 ELF 证据与 issue 完全一致:

xim-x-llvm/22.1.8/bin/clang++ → interpreter …/xpkgs/xim-x-glibc/2.39/lib64/ld-linux-x86-64.so.2
RUNPATH …/xim-x-llvm/22.1.8/lib : …/xim-x-glibc/2.39/lib64 : …/xim-x-zlib/1.3.1/lib : …

解释器路径是绝对的、机器本地的、安装时写入的(不在发布 tarball 里)。glibc payload 缺失 ⇒ 所有 clang 二进制 exec 127。到这里都对。

5.2 但归因错了

issue 的判断是"looks like a missing runtime dependency in the llvm xim package"。核对索引:

-- xim-pkgindex pkgs/l/llvm.lua:25-31
xpm = { linux = {
    deps = { "xim:glibc@2.39", "xim:linux-headers@5.11.1", "xim:zlib@1.3.1", "xim:libxml2@2.13.5" },

llvm 声明了 glibc,自 8e432d99(#182,首次加 Linux x86_64 支持)起从未变过,与 gcc.lua 的声明形态一致。三份本地 checkout 全同。而且 llvm.lua 自己的 install hook 在 glibc 缺失时是故意 fail-loud 的(:234-239 log.error("glibc payload not found … refusing to write a host-dependent clang cfg"); return false)。

所以"llvm 包漏声明 glibc"不成立。真正的问题一定在声明有、实际没装上,而且没人喊这条链上。目前证据指向三个 mcpp 侧的静默点:

(a) 依赖安装结果被丢弃 —— toolchain/lifecycle.cppm:554-568:

for (auto dep : {"xim:glibc", "xim:linux-headers"}) {
    auto depPayload = fetcher.resolve_xpkg_path(dep, /*autoInstall=*/true, &progress);
    mcpp::log::debug("toolchain", std::format("dep {} result: {}", dep, depPayload ? "ok" : ...));
}

结果只进 debug 日志,不检查、不中断。裸 runner 上 glibc 安装失败,流程原样继续去装 llvm。

(b) llvm 的 post-install fixup 在 glibc 缺失时静默跳过,还把这个状态记成"成功" —— post_install.cppm:364-390:if (!glibcLibDir.empty() && exists(patchelfBin)) { … },glibc 找不到时整段跳过且一句告警都没有(gcc 路径至少有 :358-361 的 warning)。更糟的是随后 fixup_clang_cfg(:249-284)会把 bin/ 下每个 *.cfg 重写成省略 glibc -B/-L/--dynamic-linker/-isystem 的版本——正好覆盖掉 llvm.lua 本来会拒绝写、且会 fail-loud 的那份 cfg;然后幂等标记 .mcpp-fixup.jsonglibcLib: "" 记录为一次成功的 fixup,后续重装也不会重试。

(c) 装完不验活 —— lifecycle.cppm:578-585std::filesystem::exists(bin)。全仓唯一的 --version 调用在构建期 detect.cppm:56,它把 127 报成 unrecognized compiler outputmcpp doctor(doctor.cppm:239-313)会 readelf -dRUNPATH 缺目录,但从不查 PT_INTERP(全仓 PT_INTERP/interpreter 只出现在 pack.cppm/linkmodel.cppm),而 PT_INTERP 才是致命轴;它给出的提示文案还是"其提供方 xim 包可能被删除了",在"从未装过"的场景下正好误导。

5.3 建议

  1. 把 issue 重定向:标题/正文改成"llvm 安装链在 glibc 运行时缺失时静默降级并记为成功",把"llvm 包漏声明 glibc"的假设撤回(附上 llvm.lua:25-31 的证据),同时在 xlings/xim 侧另开一条查"声明了却没装上"的实际原因(bare runner 上 xlings install 的 dep 解析是否被跳过/失败)。
  2. mcpp 侧三处照修(与归因无关,无论 xim 侧结论如何都该做):
    • lifecycle.cppm:562-567 检查 dep 结果,失败即 hard error;
    • llvm_post_install_fixup 在 Linux 且 glibcLibDir 为空时告警或硬失败,且不写 fixup marker(对齐 gcc 路径与 llvm.lua 自身的 fail-loud 策略);
    • 安装末尾加 <frontend> --version 冒烟探针,127 直接报"glibc payload missing";doctor 补 PT_INTERP 存在性检查。
  3. 现有 workaround("先装 gcc")继续有效,但它掩盖问题,应在修好后从 opencv-m CI 撤掉。

这条 issue 的价值其实比作者以为的更高:它暴露的不是一个包的元数据错误,而是安装路径上一条完整的静默降级链——三个环节各自"宽容"地放过,合起来产出一个看起来装好了、实际不能 exec 的工具链。


6. #256 —— clang 20/22 模块 BMI 里的 replacement-operator 模板毒化名字查找 【上游 clang 回归,非 mcpp bug,P2;canary P1】

6.1 本机完整复现(不是转述)

用 issue 正文的 3 文件 repro,逐一跑本机四个编译器:

编译器 带 4 参 Matx*Matx 模板 去掉该模板(对照)
clang 20.1.7(mcpp 工具链) 崩溃 rc=1,PLEASE submit a bug report ok rc=0
clang 22.1.8(mcpp 工具链) 崩溃 rc=1,同上 ok rc=0
clang 18.1.3(Ubuntu 系统) ok rc=0
gcc 16.1.0(mcpp 工具链) ok rc=0

崩溃点与 issue 一致:use.cpp:2:46 解析 main 体内 p * 2 时前端崩,而 --precompile 生成 BMI 那步 rc=0。issue 的控制矩阵逐格复现,回归窗口 clang 18 → 20 成立。

6.2 判断

  • 不是 mcpp 的缺陷。mcpp 只是把标准 C++23 模块命令行交给 clang;--precompile 成功、消费端崩溃,完全在编译器内部。任何构建系统(CMake、Bazel、build2)在同样输入下都会看到同样的崩溃。
  • 是 mcpp 的暴露面。issue 指出的"模块包模式(镜像上游头里的 static inline 运算符模板 + 平凡真约束,做混合 TU subsumption)会直撞这个形状"是准确的架构观察——这个模式正是 mcpp 生态推荐的 header→module 封装配方,所以对本项目用户的命中率远高于一般 C++ 项目。
  • 三条 ask 全部合理,但优先级应重排:
    1. 上游报告优先(ask 2)。repro 只有 40 行、回归窗口干净(18 ok / 20+22 crash),是一份高质量的 LLVM issue。建议尽快提,并把编号回链本 issue。
    2. e2e canary(ask 3)升到 P1。理由:这是沉默的能力边界——没有 canary,未来任一次 clang bump 修好或再弄坏,mcpp 都不会知道;而 opencv-m 的经历表明发现成本是"一整轮 bisect + 一次误诊"。canary 很便宜:把 §6.1 的 3 个文件塞进 tests/e2e/,# requires: clang,断言"要么编过、要么给出可识别的诊断",崩溃即红。
    3. 文档(ask 1):写进模块包指南,一句可操作的规则——"运算符模板的所有模板参数应由第一个实参绑定;否则改为对整个推导操作数类型建模"。作者已经在 opencv-m 42aeb20 里给出了可直接复制的改写范式,值得原样收进文档。
  • 一个不建议做的方向:让 mcpp 检测子进程 139/SIGSEGV 并给专门提示。信噪比太低(前端崩溃原因千百种),canary + 文档已经覆盖了实际收益。

7. 横向观察

7.1 主旋律:静默降级

本批次六条里有四条的实质是引擎遇到没准备好的情况时,选择安静地少做一点事:

# 静默点 用户看到的
#257 clang 上不发 depfile 构建"成功",产物用了陈旧 BMI
#258 [target.*.build] 里的 flags 被丢弃 构建"成功",flags 从未生效
#259(a) dep 安装结果只进 debug 日志 安装"成功"
#259(b) glibc 缺失时跳过 patchelf 并记 marker 为成功 安装"成功",二进制不能 exec

而本仓在别的地方已经把"不许静默"做得很好:xpkg 未知键 → xpkgUnknownKeys + did-you-mean;scan_overrides 零命中 → 硬错;死 glob → 告警;#253 还专门为告警加了归属点名。也就是说,纪律存在,只是没有被提升为跨模块不变量

建议把它写成一条明文架构约束(放进批次总账 §5):任何"因为条件不满足而少做一步"的分支,必须要么报错,要么发出用户可见的告警;debug 日志不算。 并在 code review checklist 里加一条对 if (x.empty()) return; / 静默 continue 的审视。这条约束若在 0.0.97 就存在,#257 的 gate 会带一条 clang 上的 warning,#259 的两次静默都会响。

7.2 CI 矩阵的盲区形状

三条 bug 的可见性条件高度相似,都是"只在某一条腿上出现,而那条腿恰好没覆盖":

  • #261:windows × clang × 长路径(自有 CI 路径短 → 绿;作为依赖被消费 → 炸)
  • #257:非 GCC(e2e 118 明写 # requires: gcc)
  • #254:host ≠ target(cross e2e 存在,但不消费带 per-OS 段的 xpkg dep)
  • #256:clang × 模块 × 该模板形状(opencv-m 的 gtest 套件此前从未用 clang 构建过)

共同点:能力维度的笛卡尔积没有被系统地枚举。建议做一次一次性的覆盖矩阵盘点(工具链 × OS × host/target × 依赖形态 × 消费深度),把空格子显式记录为"已知未覆盖",而不是默认绿即安全。#261 尤其值得注意:它在包自己的 CI 上是全绿的,只有作为依赖被深路径消费时才现形——即"消费者视角"这一整维在 CI 里缺席。

7.3 文法平价原则的破口

#258 与 #254 都指向同一处结构性差异:xpkg 描述符靠文本体拼接获得能力(任何顶层键自动 per-OS 化),mcpp.toml 靠结构化逐键解析,于是每加一种条件维度都要手工补一遍。这解释了为什么不对称是偶然的而非有意的,也预示了它会继续复发(下一个 per-OS 键、下一个 cfg 维度)。

值得考虑的中期收敛:让 [target.<sel>] 的 build 段与顶层 [build]共用同一个解析器(把 ConditionalConfig 的 build 部分换成一个完整的 BuildConfig,解析后按谓词合并),而不是维护一份"条件段支持哪几个键"的手工子集。那样 #258 就不再是一个需要实现的特性,而是自动成立的性质;#227 的封闭文法 allowlist 也会自动覆盖条件段,§3.2 的静默吞键随之消失。


8. 建议的处置顺序

顺位 动作 规模 依据
1 #261 去 cmd /c,改 -o $out,两平台合一分支 ~8 行 构造性必现,已阻塞真实 CI
2 #257 拆 posixDepfile / mmd_filter 两谓词,depfile 放开到 Clang;删 e2e 118 的 # requires: gcc ~10 行 + 测试 正确性;实测证明当年的顾虑不成立
3 #259 mcpp 侧三处静默修复 + issue 重定向 静默降级链;归因需先纠正
4 #258 四处实施(含 §3.2 的静默吞键修复) 解锁 opencv-m windows 腿,消除 703 stub 文件
5 #256 上游报告 + e2e canary + 文档一节 mcpp 侧只需 canary 与文档
6 #261 P1:rspfile 推广到 windows 全部含 $local_includes 的规则 根治长度轴,不再逐规则救火
7 #254 host/target 轴收敛(建议同时上强类型) 中大 需独立设计 pass;先做类型再改行为

1、2 两条合起来不到 20 行,建议合成一个 PR 立刻发;3、4 各自一个 PR;7 单独走设计文档。


附:核实手段说明

  • 源码断言:全部在 HEAD(af25d18)上按 file:line 逐条回读,未采信 issue 正文的行号转述。
  • #256:本机跑通完整 4×2 控制矩阵(llvm 20.1.7 / 22.1.8 / 系统 clang 18.1.3 / gcc 16.1.0 × 有/无 4 参模板)。
  • #257:本机对比 clang 与 gcc 的 module-TU depfile 实际字节形状,推翻"clang 形状需要过滤"的原假设。
  • #261:本机验证 clang-scan-deps -o 在两个自带 LLVM 上均存在且产出合法 P1689 JSON。
  • #259:本机 file -L / readelf -d 核 PT_INTERP 与 RUNPATH;三份本地 xim-pkgindex checkout 交叉核对 llvm.lua 的 deps 声明与其 git 历史。
  • 未做:在 windows 上实测 8191 截断(无该平台);未实测裸 runner 上 glibc 安装为何失败(需 CI 环境)。