Skip to content

fix(link): 响应文件按行分隔,拆掉 link.exe 的单行 128 KiB 上限 (2026.8.5.3) - #360

Merged
speak-agent merged 1 commit into
mainfrom
fix/rsp-line-limit-and-cache
Aug 5, 2026
Merged

fix(link): 响应文件按行分隔,拆掉 link.exe 的单行 128 KiB 上限 (2026.8.5.3)#360
speak-agent merged 1 commit into
mainfrom
fix/rsp-line-limit-and-cache

Conversation

@speak-agent

Copy link
Copy Markdown
Member

症状

mcpp-index 的 opencv-module / opencv-module-dnn 在 windows 上,编译完 795s / 1166s 之后死在链接:

D:/.../clang++.exe @bin/opencv_module.exe.rsp -o bin/opencv_module.exe ...
C:\Users\RUNNER~1\AppData\Local\Temp\response-f6f0f2.txt :
    fatal error LNK1170: line in command file contains 135135 or more characters

根因:第二道上限,一直没人碰到

一条链接边和操作系统之间有两道上限,而「改用响应文件」只拆掉了第一道:

上限 状态
命令行 Windows CreateProcess 32 KiB;POSIX 下 ninja 用 sh -c "<整条命令>",整条命令是一个 argv 项 → MAX_ARG_STRLEN 128 KiB #344 / PR#345 已拆
响应文件的单行长度 link.exe 128 KiB 本 PR

rspfile_content = $in 让 ninja 把所有对象写成一行。对象一多,行就超过 128 KiB。

修复

-  rspfile_content = $in
+  rspfile_content = $in_newline

ninja 用换行分隔。之后没有任何上限再随对象数增长

全平台同一条规则形状,不需要按方言分支:GNU 与 LLVM 的响应文件解析把任何空白(含换行)当分隔符,而 link.exe / lib.exe 要的正是这种写法。

实测(25 个对象):

=== rsp 内容 ===
行数: 24  字节: 301
obj/main.o
obj/f9.o
obj/f8.o

测试

新增 e2e 190,两面都钉:

  • 结构面 —— 生成的规则必须是 $in_newline,且不得再出现裸 $in;
  • 可观测面 —— ninja -d keeprsp 保留下来的响应文件必须一行一个对象,最长行远低于 128 KiB。

只钉前者的话,ninja 哪天改了 $in_newline 的展开方式,测试仍会绿。结构断言已在 2026.8.5.2 上验证为红(那里是 rspfile_content = $in),不是空断言。

链接相关 e2e 全绿:28_target_static / 47_llvm_atomic_link / 86_llvm_hermetic_link / 07_static_library / 183_cross_no_relink / 190

同一族的第四次

#274#247#344,再加本条。前三次的教训写的是「一条命令有多长,不该有人放在心上」;这次补上的是它的孪生兄弟:一行有多长同样不该。构建系统不该有一个「靠崩溃才发现」的工程规模上限,而这次的代价尤其刺眼 —— 20 分钟的编译全做完了,才在最后一步倒下。

配套

mcpp-index 侧会另开 PR:pin 到本版,并去掉 MCPP_BUILD_CACHE: local。那个开关是 #344 的临时规避(#344 已在 2026.8.3.4 修复),它让每个 workspace member 各自重编全部依赖 —— opencv 系每个 10~20 分钟,全量运行 2 小时以上。

mcpp-index 的 opencv-module / opencv-module-dnn 在 windows 上编译完 795s / 1166s
之后死在链接:

    fatal error LNK1170: line in command file contains 135135 or more characters

一条链接边和操作系统之间有**两道**上限,而「改用响应文件」只拆掉了第一道:

  1. **命令行** —— Windows CreateProcess 32 KiB;POSIX 下 ninja 用
     `sh -c "<整条命令>"`,整条命令是**一个** argv 项,撞的是 MAX_ARG_STRLEN
     128 KiB(不是谁都会去查的 2 MiB ARG_MAX)。这道 #344 / PR#345 已经拆掉。
  2. **响应文件的单行长度** —— link.exe 上限 128 KiB。`rspfile_content = $in`
     让 ninja 把所有对象写成**一行**,于是对象一多就 LNK1170。

改成 `$in_newline`:ninja 用换行分隔。之后**没有任何上限再随对象数增长**。

全平台同一条规则形状,不需要按方言分支:GNU 与 LLVM 的响应文件解析把任何空白
(含换行)当分隔符,而 link.exe / lib.exe 要的正是这种写法。

── 测试 ─────────────────────────────────────────────────────────────────────
新增 e2e 190,**两面都钉**:
- 结构面:生成的规则必须是 `$in_newline`,且不得再出现裸 `$in`;
- 可观测面:`ninja -d keeprsp` 保留下来的响应文件必须一行一个对象,且最长行远
  低于 128 KiB。

只钉前者的话,ninja 哪天改了 `$in_newline` 的展开方式,测试仍然会绿。
结构断言已在 2026.8.5.2 上验证为红(它那里是 `rspfile_content = $in`)。

链接相关 e2e 全绿:28 / 47 / 86 / 07 / 183 / 190。

── 同一族的第四次 ──────────────────────────────────────────────────────────
#274#247#344,再加本条。前三次的教训写的是「一条命令有多长,不该有人放在
心上」;这次补上的是它的孪生兄弟:**一行有多长同样不该**。构建系统不该有一个
「靠崩溃才发现」的工程规模上限。
@speak-agent
speak-agent merged commit 9d4995e into main Aug 5, 2026
18 checks passed
@speak-agent
speak-agent deleted the fix/rsp-line-limit-and-cache branch August 5, 2026 14:26
Sunrisepeak pushed a commit that referenced this pull request Aug 5, 2026
`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 其余部分(命令长度架构)与此无关,照常。
Sunrisepeak pushed a commit that referenced this pull request Aug 5, 2026
真因是 **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)」,分不清是「版本不存在」
还是「构建失败」。这是我误判的助力之一,应当改掉。
speak-agent added a commit that referenced this pull request Aug 5, 2026
* fix(link): 命令长度上限从架构上消掉,不再补第八个洞 (2026.8.5.4)

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 也改掉。)

* revert: 内带 xlings 回退到 2026.8.5.1 —— 2026.8.5.2 有回归

`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 其余部分(命令长度架构)与此无关,照常。

* docs: #359 的架构设计 —— 两个缺口是同一个形状

在等 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 条依赖。

* revert: 撤回 xlings 回退 —— 我的归因是错的,与 xlings 无关

真因是 **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)」,分不清是「版本不存在」
还是「构建失败」。这是我误判的助力之一,应当改掉。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant