fix(link): 命令长度上限从架构上消掉,不再补第八个洞 (2026.8.5.4) - #361
Merged
Conversation
mcpp-index 的 opencv-module 在 windows 上 LNK1170 —— 这是同一族缺陷的**第七次**。 架构分析见 .agents/docs/2026-08-06-command-length-architecture.md。 前七次:#247(CreateProcess 32 KiB)、#261 两处(cmd.exe 8191)、#274(argv 50781 字符,失败是裸 127)、#344(POSIX MAX_ARG_STRLEN 128 KiB)、2026.8.5.3(link.exe 响应文件单行 128 KiB)、本次。共同形状: - **发现方式永远是崩溃**,且崩在构建最后一步(本次:编译完 356 秒之后); - **失败不可归因** —— 三种报错谁都不说是哪条边; - **触发者从来不是「写了很长的命令」**,而是无关改动:#344 修缓存正确性、#274 改 错误粒度、本次是**把 CI 的 pin 抬过 2026.8.3.4**。 每次修完都在注释里写「构建系统不该有靠崩溃才发现的规模上限」,然后换个地方再犯。 ── 为什么现在才出现 ────────────────────────────────────────────────────── #344 给每个依赖的对象加了一层包目录(缓存正确性要求),路径因此变长 —— 同一条边 在 linux 上从 56 840 涨到 161 687 字节。而 mcpp-index 的 CI 一直 pin 在 **2026.8.3.3**,正好是那之前一版,windows 腿从没用长路径链接过。抬 pin 才第一次撞到。 ── P1:让长度不再是变量 ───────────────────────────────────────────────── 2026.8.5.3 把 mcpp**自己**写的响应文件改成按行分隔。但 clang 作为 driver 时会 **再生成一个**响应文件转发给链接器,那个是单行的 —— 我们改不到它。所以 windows 上的 clang 链接改用 `-fuse-ld=lld`。 **不是 workaround**:lld 用 LLVM 的 tokenizer 解析响应文件,**没有单行上限**,消掉 的是一整类而不是把数字调大;路径也缩不短(那层包目录正是 #344 需要的);而且 **linux 与 macOS 早就在用 lld**,windows 是唯一还在用系统链接器、也是唯一有单行 上限的平台 —— 这是消除平台不一致。原生 cl.exe 保持 link.exe(那里响应文件是我们 自己写的,2026.8.5.3 已覆盖)。 ── P2:上限进表(新模块 cmdlimits.cppm)────────────────────────────────── 根因是命令构造层对「这条命令要穿过哪些通道、各自上限多少」一无所知,而这份知识 只在注释和 CHANGELOG 里 —— 是「同一决策 N 处推导」的镜像:**一个关键约束在零处 被表达**。 表里除字节数还记**症状**:这一族最贵的从来不是修,是**认出**。 `Argument list too long` / `LNK1170` / 裸 `127` 三种表现毫无共同点。新增通道必须 回答「你的上限是多少」,与 directives::kTable 里「Scope 是必填字段」同一手法。 ── P3:计划期拦截并指名道姓 ──────────────────────────────────────────── 生成完 build.ninja 统一扫描,超限时报出边名/通道/实测字节/上限/解法/文档路径, 且发生在**还没编译任何东西**的时候。 实施中纠正两处判断: - 校验点选「manifest 生成后统一扫描」而非「逐 emit site 插桩」—— 后者在新增一种边 时没人会想起来加,而那正是前七次的漏法; - **phony 必须排除**,否则会误报到 **#274 的修复本身**:它为解决 argv 超限,正是把 几千个目标收进一条 phony 聚合边,而 phony 根本没有 command。走 rspfile 的规则 同样豁免。判据是「rule 有 command 且不走 rspfile」。 ── 测试 ──────────────────────────────────────────────────────────────── 单测 test_cmdlimits 8 条:每个通道都在表里、数字是实测的那些(MAX_ARG_STRLEN 是 32 页,不是谁都会先想到的 2 MiB ARG_MAX)、每条都记了症状与解法、诊断含边名/字节/ 上限/解法/文档路径。 **没做超限 e2e**:P1 之后本地造不出自然超限的边,要造只能人为破坏 rspfile 规则, 那测的是被破坏的代码而不是真实路径。windows 的真实验证由 mcpp-index CI 承担。 全量单测 58/58;mcpp 自身 351 条边零误报。 ── 其他 ──────────────────────────────────────────────────────────────── 内带 xlings 升到 2026.8.5.2,.github/ 下 16 处 pin 由 check_version_pins.sh 校验同步。 (校验脚本当场抓到 7 处不同步,包括带 v 前缀的 5 处;并拦下了我一次误改 —— 全局 替换差点把它自己注释里的**历史记录** available: 2026.8.5.1 也改掉。)
`mcpp builds & runs xlings` 集成 CI 在 2026.8.5.2 上挂掉,**重跑可复现**:
error: xlings install_packages failed (exit 1) for '[email protected]'
归因:同一条 job 在 PR #360(xlings 仍是 2026.8.5.1)上是绿的;两个 xlings 版本
之间只有一个代码提交(xlings#481,把 runtime_deps 的版本匹配从字符串相等换成
semver 范围满足),而失败正好发生在依赖安装阶段。同一批里 cmdline / tinyhttps /
capi.lua 都装成功,所以不是全部包受影响。
已报 openxlings/xlings#486。等对方发新版再升 —— 用一个已知有回归的版本去满足
「用最新版」不是升级,是把回归带进来。
.xlings.json 的 mcpp bootstrap pin(2026.8.5.3)不受影响,不动。
本 PR 其余部分(命令长度架构)与此无关,照常。
在等 xlings 修回归的间隙做 #359 的架构分析。 **关键发现:模型早就存在,新东西没接进去。** mcpp 有一套完整的「依赖提供什么 × 提供给谁」模型 —— `UsageRequirements` × {privateBuild, publicUsage, linkUsage}, include dirs / defines / ldflags / modules 全走它。而 #355 引入的两种新提供物 (host 工具、host 模块)**没有进这个模型**,各自硬编码成「只给发出请求的那条边」: prepare.cppm:4113 toolEnvByConsumer[edge.consumerPackageIndex] prepare.cppm:4016 只遍历 m->dependencies(只认 root 的直接依赖) 所以「库代用户拉起整条 codegen 工具链」在架构上不可能:工具被构建了,但环境变量 记在库的账上,消费者看不见。实测确认过,不是推断。 **根因不是少了一次传播,而是:新增一种提供物时,没有任何地方逼你回答「它怎么 传播」。** 这是「同一决策 N 处推导」的镜像 —— 一个必答问题在**零处**被表达。 缺口 B 同构:build.mcpp 的输入只有「文件内容哈希」与「环境变量」两种形态, `hash_file` 读的是内容,于是「我的输出取决于这个目录里有哪些文件」无法表达 —— 新增 .proto 静默不生成。同样是「新增一种输入时,没地方回答它的指纹怎么取」。 设计主张:两个都收敛成「表 + 必答字段」,与 directives::kTable 同一范式,而不是 各打一个补丁。三条语义写死:传播的是可见性不是自动执行、必须显式声明不能默认 传播(否则是供应链问题)、目录指纹只取成员集合不取内容(否则一次重跑放大成全量 重编)。 两条必须一起做:只做 A 仍要逐个列 proto,只做 B 仍要写 4 条依赖。
真因是 **mcpp-index 的描述符**:pkgs/x/xpkg.lua 里一个格式错误的 0.0.49 条目把 0.0.47 / 0.0.48 一起吞掉了,那两个版本根本不可解析。已由 mcpplibs/mcpp-index#160 修复。 时间线是决定性的:我最后一次失败在 **17:41:31**,#160 合并在 **17:51:19** —— 失败早于修复 10 分钟。 我的归因链有两处错误,记下来: 1. 先怪 xlings 2026.8.5.2,依据是「#360(.5.1)绿、#361(.5.2)红,且两版之间 只有一个代码提交」。**相关性是真的,因果是假的** —— 那段时间 mcpp-index 的 xpkg.lua 也刚好坏了。 2. 回退 xlings 后**仍然失败**,这本该立刻推翻结论,我却先去怀疑自己新加的命令 长度校验(本地复现证明它没误报)。 内带 xlings 恢复到 2026.8.5.2;已在 openxlings/xlings#486 更正并说明。 另记一条待办:mcpp 调 xlings 用 `install_packages ... 2>/dev/null`,把对方的 报错吞了,失败只剩一行「install_packages failed (exit 1)」,分不清是「版本不存在」 还是「构建失败」。这是我误判的助力之一,应当改掉。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
mcpp-index 的
opencv-module在 windows 上LNK1170—— 同一族缺陷的第七次。设计文档:
.agents/docs/2026-08-06-command-length-architecture.md七次
$in,数千对象cmd /c包裹-I列表MAX_ARG_STRLEN128 KiBArgument list too long,不说哪条边LNK1170,不说哪个 target共同形状:发现方式永远是崩溃,且崩在最后一步;失败不可归因;触发者从来不是「写了很长的命令」 —— #344 是修缓存正确性,#274 是改错误粒度,本次是把 CI 的 pin 抬过 2026.8.3.4。
每次修完都在注释里写「构建系统不该有靠崩溃才发现的规模上限」,然后换个地方再犯。
为什么现在才出现
#344 给每个依赖的对象加了一层包目录(缓存正确性要求),路径因此变长 —— 同一条边在 linux 上从 56 840 涨到 161 687 字节。而 mcpp-index 的 CI 一直 pin 在 2026.8.3.3,正好是那之前一版,它的 windows 腿从没用长路径链接过。抬 pin 才第一次撞到。
根因:一个关键约束在零处被表达
命令实际穿过的是一条链:
每层上限互不相同,而构造层不知道自己会穿过哪几层。这是本仓库反复付学费的「同一决策在 N 处推导」的镜像。
P1 — 让长度不再是变量
2026.8.5.3 把 mcpp 自己写的响应文件改成按行分隔。但 clang 作为 driver 会再生成一个转发给链接器,那个是单行的 —— 我们改不到它。所以 windows 上的 clang 链接改用
-fuse-ld=lld。为什么这不是 workaround(这一条是本 PR 的关键判断):
kLinkDriverFlags)。windows 是唯一还在用系统链接器、也是唯一有单行上限的平台。这是消除平台不一致,不是新增特例。原生 cl.exe 保持 link.exe:那条路径上响应文件是我们自己写的,2026.8.5.3 已覆盖。
P2 — 上限进表(新模块
cmdlimits.cppm)把「执行通道 → 上限」变成数据。表里除字节数还记症状,因为这一族最贵的从来不是修,是认出:
Argument list too long、LNK1170、裸127三种表现毫无共同点,下次第四种时表能直接把人指过来。新增执行通道必须回答「你的上限是多少」 —— 与
directives::kTable里「Scope 是必填字段」同一手法:把容易忘的问题变成结构上绕不过去的字段。P3 — 计划期拦截并指名道姓
生成完
build.ninja统一扫描,超限时报出边名 / 通道 / 实测字节 / 上限 / 解法 / 文档路径,且发生在还没编译任何东西的时候 —— 而不是 356 秒之后由别人的程序报一个没有上下文的错。实施中纠正了两处判断:
phony必须排除,否则会误报到 feat: mcpp test capability batch — isolation, parallel, filter, list, timeout, JSON (0.0.104) #274 的修复本身:它为解决 argv 超限,正是把几千个目标收进一条 phony 聚合边 —— 而 phony 根本没有 command。不排除的话,新校验会把解法报成问题。走 rspfile 的规则同样豁免。判据是「rule 有 command 且不走 rspfile」。测试
test_cmdlimits8 条:每个通道都在表里、数字是实测的那些(MAX_ARG_STRLEN是 32 页,不是谁都会先想到的 2 MiBARG_MAX—— [bug] 全局 build cache:obj 布局随消费方包组合变化(#233 消歧),而 cache key 不含消费方 → 同一 key 下第二个消费者必挂 'missing and no known rule' #344 为此花了一个版本)、每条都记了症状与解法、诊断含边名/字节/上限/解法/文档路径。其他
内带 xlings 升到 2026.8.5.2,
.github/下 16 处 pin 由check_version_pins.sh校验同步。