news 2026/9/28 5:51:02

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ninja构建报错:multiple outputs aren‘t supported 的成因与解法

第一次撞上这个报错是在一个周五下午。项目用的是 CMake + Ninja,配置阶段一切正常,cmake --build .刚跑起来不到两秒就崩了,终端里孤零零甩了一行:

ninja: error: build.ninja:1180: multiple outputs aren't (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 aren't (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 'NR>=1175 && NR<=1185{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和deps,build 语句却给了两个输出。

2.2 判断问题类型:多输出、重复输出还是规则冲突

从实际踩坑经历来看,报这种错的现场可以分成几类,你可以按下面的顺序快速定位自己的情况:

  • 形态 A:rule 带depfile/deps,build 带多输出。这是最经典的来源,基本就是 1.3 节说的冲突,直接看 rule 定义就能确认。
  • 形态 B:Ninja 版本太旧,不认“多输出”这种写法。老版本对构建语法支持不完整,也会抛同样的错误文本。确认方法很简单,跑ninja --version看版本号,低于 1.9 的就要警惕了。
  • 形态 C:构建描述文件不是 CMake 生成的,而是某些脚本或工具导出的,其中混入了不合法的多输出结构。手动写的build.ninja偶尔也会踩中,但相对少见。

定位的时候有个小技巧:用grep -n "output" -A 5 -B 5 build.ninja直接搜“输出”相关关键字,或者在编辑器里带行号打开文件快速跳转。Ninja 报错给的行号通常是很准的,不用怀疑它在骗你。

2.3 和两个容易混淆的报错做区分

排查这个报错的时候,很多人在网上搜索,容易把另外两个相似报错混进来,这里顺手帮大家理一下:

报错文本真实含义触发场景
multiple outputs aren't (yet?) supported单条规则输出多文件,且与 depfile/deps 等机制组合,被 Ninja 判定为不支持多输出 + depfile、旧版本 Ninja
multiple 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,看版本够不够用。
  • macOS:brew 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给了两个文件,又加了DEPFILE,CMake 生成的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 做聚合层。让真实的规则只生成一个主文件,所有实际需要“那一堆输出文件”的目标都依赖这个主文件,或者依赖包了一层的 phony:

rule 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.h,Ninja 会顺着 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?)说到底是在提醒你:绕开它也许比等它更新更靠谱。如果你的项目同样被这个报错卡过,希望这份排查清单能帮你把时间省下来,早点回家。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 5:50:47

立创EDA正则表达式批量修改PCB丝印大小实战指南

画完PCB直接下单打样的朋友&#xff0c;应该都经历过这一幕&#xff1a;板子寄回来&#xff0c;电阻电容的位号丝印小到要拿放大镜才能勉强看清&#xff0c;贴着板边甚至要斜着看。我最早在嘉立创EDA里画板时&#xff0c;也在这上面栽过跟头。板上一堆R1、R2、C1、C2&#xff0…

作者头像 李华
网站建设 2026/9/28 5:50:42

Maven手动安装jar依赖:install-file命令详解与排错指南

先说个挺常见的场景&#xff1a;你从某个渠道拿到一个第三方功能包的jar文件&#xff0c;往项目lib目录里一扔&#xff0c;然后在pom.xml里加了对应依赖&#xff0c;IDEA 一刷新&#xff0c;理论上应该能用了吧&#xff1f;结果编译报错&#xff0c;依赖依然爆红&#xff0c;控…

作者头像 李华
网站建设 2026/9/28 5:50:22

Java Web+MySQL教研室管理系统:部署、源码解析与二次开发指南

简介&#xff1a;基于Java Web与MySQL的教研室管理系统&#xff0c;是一份面向高校或教育机构教研管理场景的完整项目源码包&#xff0c;涵盖教师信息、课程安排、学生档案、教学资源分配等核心模块&#xff0c;适合正在学习Java Web开发、需要参考完整前后端交互与数据库设计的…

作者头像 李华
网站建设 2026/9/28 5:48:39

WebSocket协议全解析:从HTTP轮询到心跳重连实战

做前端这几年&#xff0c;“轮询”这俩字只要出现在业务里&#xff0c;我眉头就会先皱一下。不是轮询不能用&#xff0c;而是它太像复读机了&#xff1a;为了拿到一条新消息&#xff0c;客户端得每隔几秒问一遍服务端&#xff0c;服务端每次都空手而归&#xff0c;一来一回全是…

作者头像 李华
网站建设 2026/9/28 5:48:03

用Python线性规划优化饲料配方,降低养殖成本实战

饲料成本占养殖总成本的六到七成&#xff0c;这个数字在猪场、鸡场、牛场里都是铁律。要说哪一项成本最值得抠&#xff0c;饲料配比绝对排在第一位。以前老师傅配饲料全靠经验和手感&#xff0c;玉米多加一瓢、豆粕少抓一把&#xff0c;营养够了就行&#xff0c;但价格高的时候…

作者头像 李华