ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Ninja构建报错:multiple outputs aren‘t supported 的成因与解法

Ninja构建报错:multiple outputs aren‘t supported 的成因与解法 第一次撞上这个报错是在一个周五下午。项目用的是 CMake Ninja配置阶段一切正常cmake --build .刚跑起来不到两秒就崩了终端里孤零零甩了一行ninja: error: build.ninja:1180: multiple outputs arent (yet?) supported说真的这可能是 Ninja 那句报错里最有“求生欲”的文本了——(yet?)带问号官方自己都没把话说完。它的大致含义是构建脚本里有一条规则生成了多个输出文件但当前这种写法或者当前版本的 Ninja不支持这么干。这篇内容就是把遇到这个报错的人需要知道的东西一次性讲透它为什么会发生、在什么场景下高发、以及在 CMake、手写 build.ninja、CI 等不同环境里应该怎么处理最稳妥。1. 先认识这个报错Ninja 构建模型中的多输出规则1.1 Ninja 是什么为什么生成器都爱用它Ninja 是一个极简构建系统核心卖点就一个字快。它和 Make 最大的区别是设计哲学完全不同Make 自带一堆隐式规则、自动推导、内置函数试图让使用者直接手写构建规则而 Ninja 刻意不做复杂推导它假定build.ninja完全由更上层的生成器CMake、Meson、GN 等产出所以自己可以写得非常朴素启动速度、解析速度、并行调度效率都做到了极致。这种“上层生成器写规则Ninja 只负责执行”的模式在大型 C/C 项目里非常吃香尤其是 Chromium、LLVM 这类动辄几万源文件的项目。你在 Linux 上跑cmake -G Ninja本质上是让 CMake 根据你的CMakeLists.txt生成一份build.ninja然后 Ninja 按这份文件把编译任务拆成一个个并行 job 跑完。理解了这一点你就能明白这个报错为什么让人头疼Ninja 报错问题往往不一定在 Ninja 本身而在生成它的 CMakeLists或者你用某些工具导出的构建描述文件上。1.2 rule 的多输出语法合法但有限制Ninja 的构建规则由两部分组成rule定义“怎么把输入变成输出”build语句声明“哪个输入对应哪个输出”。一个build语句写多个输出在语法层面是完全合法的比如下面这段rule gen command python gen.py $in $out build generated.cpp generated.h: gen template.dat$out会被展开成generated.cpp generated.h生成脚本一次性吐出两个文件。这是非常典型的代码生成场景一个脚本根据一份模板同时生成源文件和头文件。但是Ninja 虽然允许这种多输出语法却有一条隐含红线rule 上不能同时挂某些“辅助机制”。这些机制包括depfile指定依赖文件路径Ninja 通过它读取精确的头文件依赖deps指定依赖模式gcc/msvc让 Ninja 直接从编译器输出解析依赖rspfile响应文件用于绕过命令行长度限制一旦多输出和这些机制组合出现Ninja 就会在解析阶段直接报错而不是等构建开始后才翻车。这正是build.ninja:1180: multiple outputs arent (yet?) supported这句话的由来。1.3 真正触发报错的开关depfile 与多输出的冲突为什么 depfile 和多输出水火不容这里得稍微掰开讲一下原理。depfile或deps是 Ninja 用来做“头文件级精确依赖”的机制。规则执行后编译器会额外产出一个.d文件里面写着“本次编译实际用到的所有头文件”。有了这份清单当你修改了一个头文件Ninja 就能精确判断哪些目标需要重建而不是简单粗暴地全部重编。问题在于这一整套机制隐含一个假设——一条规则只有一个主输出。当一条build语句同时输出a.h和a.cpp又在 rule 里写了depfile $out.d$out展开后变成了a.h a.cpp于是 depfile 路径就变成了a.h a.cpp.d——这显然不是编译器会产出的文件名也没法解析。Ninja 在解析阶段看到这种组合直接判定为“不支持”干脆拒绝往下走。报错文本里的(yet?)就是官方对这件事的表态他们知道这个缺口但设计上始终没找到优雅的解法。Ninja 的哲学本来就是“构建文件由生成器管理生成器应该避免写出这种歧义组合”所以宁可让它报错也不做模糊处理。2. 拿到报错别慌三步定位 build.ninja 的问题行2.1 还原现场先看报错行附近到底写了什么报错里的1180是build.ninja里的行号说明 Ninja 是在解析阶段失败的根本没走到构建那一步。第一件事就是把那行附近的内容拉出来看sed -n 1170,1190p build.ninja如果你嫌行号不好数也可以加个行号前缀awk NR1175 NR1185{print NR: $0} build.ninja窗口最好多带几行上下文因为错误可能在第 1180 行的build语句也可能在这条语句上面的rule定义里。把 rule 和 build 放在一起看问题基本就清楚了。我那次看到的场景长这样rule generate_headers command python gen.py $in $out depfile $out.d deps gcc build generated.h generated.cpp: generate_headers config.yaml一眼就能确诊rule 里挂着depfile和depsbuild 语句却给了两个输出。2.2 判断问题类型多输出、重复输出还是规则冲突从实际踩坑经历来看报这种错的现场可以分成几类你可以按下面的顺序快速定位自己的情况形态 Arule 带depfile/depsbuild 带多输出。这是最经典的来源基本就是 1.3 节说的冲突直接看 rule 定义就能确认。形态 BNinja 版本太旧不认“多输出”这种写法。老版本对构建语法支持不完整也会抛同样的错误文本。确认方法很简单跑ninja --version看版本号低于 1.9 的就要警惕了。形态 C构建描述文件不是 CMake 生成的而是某些脚本或工具导出的其中混入了不合法的多输出结构。手动写的build.ninja偶尔也会踩中但相对少见。定位的时候有个小技巧用grep -n output -A 5 -B 5 build.ninja直接搜“输出”相关关键字或者在编辑器里带行号打开文件快速跳转。Ninja 报错给的行号通常是很准的不用怀疑它在骗你。2.3 和两个容易混淆的报错做区分排查这个报错的时候很多人在网上搜索容易把另外两个相似报错混进来这里顺手帮大家理一下报错文本真实含义触发场景multiple outputs arent (yet?) supported单条规则输出多文件且与 depfile/deps 等机制组合被 Ninja 判定为不支持多输出 depfile、旧版本 Ninjamultiple rules generate ...多个规则声称生成同一个输出文件构建图冲突两个 build 语句写了同一个输出路径ninja: error: loading build.ninja: ...文件本身格式错误、路径缺失或编码问题手写文件抽风或 CMake 生成被中断这三个错误表面看起来都带 “build.ninja” 和行号但排查方向完全不同。第一个聚焦 rule 定义第二个聚焦输出路径重复第三个则是文件层级的读取出问题。先分清是哪一类能省下大量瞎折腾的时间。3. 四个解决方案从临时绕过到彻底修复3.1 先升级 Ninja 版本最便宜的一步遇到这种报错我建议的第一动作永远是看一眼 Ninja 版本ninja --version如果你用的版本偏老比如 Ubuntu 18.04 自带的 1.8.2、20.04 自带的 1.9.0升级到新版本很可能直接就解决了。Ninja 虽然整体设计没大变但对多输出 构建图边界情况的处理一直在补。Ubuntu / Debian系统自带的可能不是最新版建议从 GitHub releases 下载官方二进制或加工具链源。简单省事的话先试apt install ninja-build看版本够不够用。macOSbrew install ninja拿到的版本通常比较新。Windows官方直接给ninja-win.zip解压扔进 PATH 就行。如果用 Python 生态pip install ninja也可以装完记得确认 PATH 里哪个 ninja 生效。如果项目有 CI优先统一到一个已知没问题的版本别让开发机和新版、CI 和老版互相打架。升级完重新跑一次原来的构建命令不用重新生成Ninja 会直接重新解析build.ninja如果报错消失说明就是版本兼容问题。这种情况下就不用动 CMakeLists 了省心。3.2 临时改 build.ninja应急但不治本有时候项目正在赶工期不能立刻改 CMakeLists 再重新生成那就需要一个“先让编译过去”的应急手段。直接手改build.ninja是可行的但要记住一个前提build.ninja是生成物只要你重新跑一次 CMake所有手改都会消失。所以只适合现场急救不适合作为长期方案。应急改法之一是“拆多输出为多次调用”build generated.h: generate_headers config.yaml build generated.cpp: generate_headers config.yaml但注意这样写会让生成脚本被执行两次。如果脚本不是天然的幂等操作比如它会清空目录再生成一次执行清掉了另一次的输出就会踩出新坑。所以更稳妥的临时法是把次要输出“挂”到主输出上借助 phony 别名build generated.cpp: generate_headers config.yaml build generated.h: phony generated.cpp这样 Ninja 实际只会跑一次生成命令但generated.h也成为了一个可被其他规则依赖的合法目标。这个方案保留的“主输出”必须是脚本真实会生成的文件而 phony 目标本身不产生文件只是构建图里的一个别名节点。3.3 CMake 项目里根治拆分 custom command如果你用的是 CMake问题的根源通常出在add_custom_command上。最常见的翻车写法是这样add_custom_command( OUTPUT generated.h generated.cpp COMMAND python ${CMAKE_SOURCE_DIR}/tools/gen.py ${CMAKE_CURRENT_BINARY_DIR}/generated.h ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp DEPENDS config.yaml DEPFILE generated.d )OUTPUT给了两个文件又加了DEPFILECMake 生成的build.ninja里就会带上 depfile 相关的 rule正好踩中 Ninja 的限制。针对这种情况有几个根治方向方向一去掉 DEPFILE。如果生成脚本本身只依赖DEPENDS里列出的输入文件根本不需要 depfile 做额外的精确依赖。把DEPFILE那一行删掉让多输出保持最简单形态Ninja 就能接受了。这是最省事的方案。方向二把多输出拆成多个 custom command。一个命令只产出一个文件命令之间通过DEPENDS串联add_custom_command( OUTPUT generated.h COMMAND python gen.py --header generated.h DEPENDS config.yaml ) add_custom_command( OUTPUT generated.cpp COMMAND python gen.py --source generated.cpp DEPENDS generated.h )代价是生成脚本可能被调用两次如果生成过程比较重比如每次要解析大模板这个成本需要考虑。通常我会让脚本支持只生成一个文件或者自带缓存。方向三只留一个主输出其余进 BYPRODUCTS。BYPRODUCTS从 CMake 3.2 开始可用它表示“这个命令顺带会生成的文件”。CMake 在生成构建图时会把主OUTPUT作为主要驱动目标BYPRODUCTS作为附加输出参与依赖计算。但要注意主输出文件必须是命令真实会生成的如果脚本没有生成它Ninja 会认为目标缺失导致错误。所以如果脚本坚持一次写两个文件可以额外 touch 一个 stamp 文件当主输出add_custom_command( OUTPUT generated.stamp COMMAND python gen.py generated.h generated.cpp COMMAND ${CMAKE_COMMAND} -E touch generated.stamp BYPRODUCTS generated.h generated.cpp DEPENDS config.yaml )这样既保留了“一次生成两个文件”的高效又让 Ninja 只围绕一个主输出做构建判断绕开了多输出限制。3.4 手写 ninja 文件时的替代设计主输出 phony如果你维护的是手写的build.ninja比如嵌入式项目、游戏管线等场景设计生成规则时有几个原则可以提前规避这类问题原则一能不挂 depfile 就不挂。自定义生成脚本的输入依赖往往在build语句里显式写出来就够了。只有当你确实需要追踪“脚本内部再包含的头文件”时才有必要上 depfile而这种场景建议重新审视设计。原则二用 phony 做聚合层。让真实的规则只生成一个主文件所有实际需要“那一堆输出文件”的目标都依赖这个主文件或者依赖包了一层的 phonyrule generate command python gen.py generated.h generated.cpp build generated.stamp: generate command python gen.py generated.h generated.cpp touch generated.stamp build generated.h: phony generated.stamp build generated.cpp: phony generated.stamp写这套的时候关键是理解 Ninja 的依赖传播其他规则如果依赖generated.hNinja 会顺着 phony 链找到generated.stamp在generated.stamp缺失或过时时先执行生成命令然后认为generated.h也就绪了。原则三手动改build.ninja应急后一定要在项目里留文档。我见过太多人改了生成文件又不记录两周后同事重新生成又踩同一个坑然后一脸懵地来问你。任何非常规操作都值得写进 README 或 Makefile 注释里。4. 常见问题与避坑经验4.1 踩坑速查表把这次排查过程中常见的组合整理成一张表方便你按图索骥场景现象直接原因最优处理CMake add_custom_command多输出 DEPFILEdepfile 绑定多个输出Ninja 解析拒绝去掉 DEPFILE或拆命令老版本 Ninja同样的错误升级后消失语法支持欠缺升级到 1.10手写 build.ninjarule 里 $out 被展开成多个路径命令内部依赖 $out 做文件名推导显式传参不依赖 $out 的隐式展开两个 build 语句写同一输出multiple rules generate构建图冲突删除重复规则同名输出只留一处生成脚本有副作用拆命令后重复执行产物被清掉脚本不是幂等的采用 stamp 主输出方案4.2 调试 Ninja 构建图的两个实用技巧排查这类构建问题光看报错文本往往不够Ninja 自带的一些调试手段能帮你大幅缩小范围。技巧一用ninja -t targets看构建图全貌。-t是 Ninja 的 tool 模式ninja -t targets all会把当前build.ninja里所有目标列出来。如果你怀疑是某个目标被多个规则重复生成这个命令能帮你快速发现冲突。针对某个具体文件还可以用ninja -t query 目标路径查看它的生成规则和依赖关系。技巧二用ninja -d explain -n理解 Ninja 的判断逻辑。-d explain会让 Ninja 打印“为什么重新构建/为什么跳过”的决策过程-n是先不实际执行。组合起来就是干跑一遍看它在想什么。比如它可能会告诉你某个目标“missing”或“dirty”这样你就知道构建图哪里剪不断理还乱。另外强烈建议在调试时加-v参数让 Ninja 打印每个实际执行命令。很多多输出问题其实出在$out展开不符合预期上——因为你以为它只传了第一个输出实际上它把所有输出都拼在命令行里了。看到真实命令你会少很多猜测。4.3 一句实战心得在我后来维护项目时给自己定了一条硬规矩自定义生成命令只允许产出一个主产物其他文件一律当副产物如果脚本实在做不到就额外生成一个 stamp 文件当主产物。这个习惯让 CMake、Meson、Ninja 各生成器都少踩了很多坑。顺带一提这类构建问题看着吓人其实都是设计问题不是能力问题。Ninja 那句(yet?)说到底是在提醒你绕开它也许比等它更新更靠谱。如果你的项目同样被这个报错卡过希望这份排查清单能帮你把时间省下来早点回家。
返回列表