状态:已实施(2026.8.5.4) 触发:mcpp-index 的
opencv-module在 windows 上LNK1170,这是同一族缺陷的第七次 涉及:src/build/ninja_backend.cppm、src/build/flags.cppm、新模块src/build/cmdlimits.cppm
同一族缺陷,七次:
| # | 版本 | 谁超了 | 撞的是什么上限 | 当时的修法 |
|---|---|---|---|---|
| 1 | #247 | 链接命令内联 $in,数千对象 |
Windows CreateProcess 32 KiB |
windows 的链接规则改走 rspfile |
| 2 | #261 | scan 规则经 shell 重定向 → 被 cmd /c 包裹 |
cmd.exe 8191 |
改用 clang-scan-deps -o,去掉包裹 |
| 3 | #261 | 编译/扫描内联无界 -I 列表 |
同上 | windows 的这些规则也改走 rspfile |
| 4 | #274 | mcpp test 的显式 ninja 目标集(FFmpeg 2281 个单元 → argv 50 781 字符) |
cmd.exe 8191,失败是裸 127 |
目标集改成 phony 聚合边 |
| 5 | #344 | 对象路径变长(每依赖多一层包目录),同一条边 56 840 → 161 687 字节 | POSIX MAX_ARG_STRLEN 128 KiB(ninja 用 sh -c,整条命令是一个 argv 项) |
链接/归档全平台改走 rspfile |
| 6 | 2026.8.5.3 | rspfile 把所有对象写在一行 | link.exe 响应文件单行 128 KiB |
rspfile_content = $in_newline |
| 7 | 本次 | clang driver 读完我们的 rspfile,又生成一个单行的给 link.exe | 同上 | ← 本文档 |
七次的共同形状:
- 发现方式永远是崩溃,而且崩在构建的最后一步(第 7 次:编译 356 秒之后)。
- 失败不可归因:
posix_spawn: Argument list too long不说哪条边;LNK1170不说哪个 target;cmd /c那次是裸127,ninja 和 mcpp 都没机会打印任何东西。 - 触发者从来不是"写了很长的命令",而是一个看似无关的改动:#344 是修缓存正确性,#274 是改错误报告粒度,本次是把 CI 的 pin 从 2026.8.3.3 抬到 2026.8.5.x。
每次修完,都在注释里写下"构建系统不该有一个靠崩溃才发现的规模上限"——然后下一次换个地方再犯。
所以问题不在任何一个上限,而在于:命令构造层对「这条命令要经过哪些通道、每个通道的上限是多少」一无所知,而这份知识只存在于注释和 CHANGELOG 里。
ninja_backend 负责拼命令,但一条命令实际要穿过的通道是:
mcpp 拼出的规则文本
→ ninja 展开(可能内联,可能写 rspfile)
→ 进程创建(CreateProcess / posix_spawn / sh -c)
→ 工具自身(driver 可能再写一个 rspfile 转发给 linker)
→ 最终工具(link.exe / lld / ar)
每一层都有自己的上限,而且互不相同。构造层不知道自己产出的东西会经过哪几层,于是「加一层包目录」这种改动无法被任何机制提醒。
这与本仓库反复付过学费的「同一决策在 N 处推导」是同一类问题的镜像:一个关键约束在零处被表达。
mcpp 已经有成熟的表驱动范式:
CommandDialect—— 一个 flag 怎么拼(gnu / msvc)BmiTraits—— BMI 的形态与引用方式directives::kTable—— build.mcpp 的指令(一行一条指令,解析/缓存/落盘全由该行驱动)
唯独「执行通道 → 上限」没有表。它散落在七处注释里,每处只讲自己那次。没有任何地方能回答「windows 上一条链接命令的可用预算是多少」。
上限是在 ninja 执行边 或 link.exe 解析文件 时才撞上的。那时:
- 已经花掉了全部编译时间;
- 报错的是别人的程序,信息里没有 mcpp 的上下文(哪个 target、哪个包、多少个对象);
- mcpp 没有介入的机会。
而 mcpp 在生成 build.ninja 时就完全知道每条边的输入个数与路径长度。校验点选错了。
三条原则,对应三条根因。
凡是可能随项目规模无界增长的载荷(对象列表、include 列表、库列表),必须满足:
- 走响应文件,不进命令行;
- 响应文件按行分隔;
- 下游工具对响应文件没有单行上限。
第 3 条是本次新增的认识,也是前六次都没覆盖到的:我们控制不了 driver 再生成的那个文件。唯一的解法是让最终工具不带这个限制。
因此:windows 上的 clang 链接改用 -fuse-ld=lld。
这不是"换个工具绕过去"。理由有三:
- lld 通过 LLVM 的 tokenizer 解析响应文件,没有单行上限——是消掉一整类,不是把某个数字调大;
- 路径本身缩不短:per-package 那层目录正是 #344 需要的,其余是源码树自己的结构;
- linux 与 macOS 早就在用 lld(
kLinkDriverFlags)。windows 是唯一还在用系统链接器的平台,也是唯一有单行上限的。这是消除平台不一致,不是新增特例。原生 cl.exe(
isMsvcDialect)保持 link.exe:那条路径上响应文件是 mcpp 自己写的,2026.8.5.3 已经修好。
新模块 src/build/cmdlimits.cppm,把执行通道与其上限写成一张表:
enum class Channel {
NinjaArgv, // ninja 直接创建进程
PosixShell, // ninja 的 `sh -c "<整条命令>"`:整条是一个 argv 项
CmdWrapper, // `cmd /c`(#261 起已在全仓绝迹,留在表里以防复活)
RspContent, // 响应文件总量
RspLine, // 响应文件单行
};
struct Limit {
Channel channel;
std::size_t bytes;
std::string_view where; // 谁施加的
std::string_view symptom; // 撞上时用户会看到什么
std::string_view remedy; // 怎么消掉
};表里同时记录症状——因为这一族缺陷最贵的部分从来不是修,而是认出它。Argument list too long、LNK1170、裸 127 这三种表现毫无共同点,下一次遇到第四种时,表能把人直接指到这里。
新增一个执行通道时,必须在表里回答"你的上限是多少",否则加不进来——与 directives::kTable 里「Scope 是必填字段」同一个手法:把一个容易忘的问题变成结构上绕不过去的字段。
ninja_backend 发射每条边时,已经持有该边的全部输入。因此:
- 估算该边在每个它会穿过的通道上的字节数;
- 与表比对;
- 超限时:能自动降级就降级(例如内联 → rspfile),不能降级就报错,并给出 target 名、通道、实测字节数、上限、以及表里的 remedy。
关键是报错时机:在 mcpp build 刚开始、还没编译任何东西的时候,而不是 356 秒之后。
| 步 | 内容 | 状态 |
|---|---|---|
| 1 | -fuse-ld=lld 用于 windows clang 链接(P1) |
✅ flags.cppm |
| 2 | 新模块 cmdlimits.cppm:通道表 + 预算/判定/诊断(P2) |
✅ |
| 3 | ninja_backend 生成 build.ninja 后统一校验(P3) |
✅ |
| 4 | 单测锁住表与诊断 | ✅ tests/unit/test_cmdlimits.cpp(8 条) |
(a) 校验点不在「发射每条边」,而在「manifest 生成之后统一扫描」。 逐点插桩要改每个 emit site,而新增一种边时没人会想起来加——这正是前七次的漏法。改为扫描已生成的 manifest:新边当天就被覆盖。
(b) phony 必须排除,否则会误报到 #274 的修复本身。 一条 build 行长 ≠ 命令长:phony 根本没有 command。而 #274 为解决 argv 超限,正是把几千个目标收进一条 phony 聚合边——不排除的话,新校验会把那条边报成超限,把解法当成问题。走 rspfile 的规则同样豁免(命令里只有 @$out.rsp)。
判据因此是:该边的 rule 有 command,且不走 rspfile → 它的输入会进命令行 → 校验。
- 步 1:mcpp-index 的
opencv-module/opencv-module-dnn在 windows 上通过。这是当前唯一已知能触发的真实场景——本地无法复现(需要 windows + 那个规模的依赖图)。 - 步 2–3:单测
test_cmdlimits(8 条)锁住表与诊断——每个通道都在表里、数字是实测的那些、每条都记了症状与解法、诊断里含边名/实测字节/上限/解法/文档路径。没有做超限的 e2e:P1 之后本地已经造不出自然超限的边(要造只能人为破坏 rspfile 规则,那测的是被破坏的代码而非真实路径)。 - 回归:全量单测 58/58;链接相关 e2e(28 / 47 / 86 / 07 / 148 / 190)绿;mcpp 自身 351 条边零误报。
- 不缩短对象路径。per-package 那层是 #344 的正确性要求,缩回去就是拿正确性换长度。
- 不给"最大项目规模"设一个文档化的数字。P1 的目标是让这个数字不存在;凡是还存在的,进表并在计划期校验。
- 不改
cmd /c。#261 起它已在全仓 ninja 规则中绝迹,表里保留一行只是为了它某天复活时有人认得出。 - 本次不动原生 cl.exe 路径。那里的响应文件是 mcpp 自己写的,2026.8.5.3 已覆盖。