☰
Android编译报错FAILED: ninja: ‘==xxx_intermediates‘ 的定位与修复
2026/10/6 4:58:24 网站建设 项目流程
今天在服务商配的编译机上整了一下午 Android 定制系统,全量编译走到一半突然炸了这么一条:

FAILED: ninja: 'out_sys/target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/' ninja: build stopped: subcommand failed.

第一眼看到这个报错,我后背就一凉。不是因为它有多难,而是因为 `==platform-lib-local` 这种字符串出现在构建路径里,十有八九是源码里面哪个模块名或者 `PRODUCT_PACKAGES` 引用写脏了。这种问题不像编译器报语法错误那么直白,光看报错根本不知道是哪个文件、哪一行引出来的,得顺着构建系统手动去翻。 这篇文章就把我这次排查的思路、定位命令、修复方式和后续的预防手段完整写出来。如果你也在编译 AOSP 或者其他基于 Soong/Ninja 的 Android 源码时遇到 `FAILED: ninja: 'xxx_intermediates/'` 这种诡异路径问题,可以直接照着一步步来,能省下不少折腾的时间。 ## 1. 报错现场与第一层解读 ### 1.1 完整的报错形式和编译环境 先把现场还原一下。我当时用的构建配置是 `lunch xxx-userdebug`,输出目录被指定为 `out_sys`(通过 `OUT_DIR=out_sys` 指定的),Android 源码版本是 Android 12 分支下的定制方案。执行 `m -j32` 之后,编译进程跑了大概十几分钟,突然在链接阶段报错退出。 终端里最后几行的完整输出大致是: ```bash [ 5% 684/12345] //frameworks/base/services/core/java:services.core.xml minimal xml rules FAILED: ninja: 'out_sys/target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/' ninja: build stopped: subcommand failed.

注意看这行报错的结构,FAILED: ninja: '...'其实是 Ninja 在告诉我们:整个构建图里存在一个目标out_sys/target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/,但是整个构建图中没有任何一条 rule 能够生成这个目标。这就好比你给一个文件写了一行依赖关系,但写了依赖却不告诉构建系统这文件怎么来,Ninja 自然直接罢工。

这里的out_sys/target/common/obj/JAVA_LIBRARIES/是 Java 类库的公共输出目录,属于 Android 构建系统里固定的产物路径模板。末尾的_intermediates是 Android 构建系统为模块生成中间产物时自动拼接的后缀。也就是说,这个目标本质上是一个名为==platform-lib-local的 JAVA 库模块的中间输出目录。

1.2 "=="出现在构建路径中意味着什么

熟悉 Android 模块命名规范的开发者都知道,模块名在 Soong/Kati 体系里只允许使用字母、数字、下划线、点号和中划线这类常规字符。=这个字符在 Makefile 里有赋值语义,在 Android.bp 里有键值对分隔语义,正常情况下永远不会作为模块名的一部分传到构建系统里。

当路径中出现两个连续的=,几乎可以断定是某个代码文件在拼接字符串时把==原样写进了模块名或者依赖引用里。常见的情况包括:

  • Android.mk里写LOCAL_MODULE := ==platform-lib-local。
  • Android.bp里的name: "==platform-lib-local"。
  • PRODUCT_PACKAGES里追加了一个字符串==platform-lib-local。
  • 某处用$(if)或$(eval)拼接时多带了一个=。

这些场景的共性都是:代码作者本意可能只是写判断逻辑、写注释、或者临时改一个模块名验证什么功能,结果把==这个在命令行或脚本里看起来无害的字符串传给了构建系统。

所以看到这种报错,第一反应不应该是清空整个out_sys目录重新全量编,那不仅慢,而且大概率解决不了问题。正确的思路是顺着模块名去源码里定位这个==platform-lib-local到底在哪里被引用、被定义。

2. 根因排查:把"=="从源码中揪出来

2.1 在整个源码树中搜索可疑模块名

排查这种字符串问题,第一板斧就是全源码 grep。我建议分两步走,第一步先搜完整的脏字符串,第二步再搜去掉==后的正常模块名,用来对比确认有没有同名文件。

# 搜索包含 ==platform-lib-local 的文件 grep -rn "==platform-lib-local" --include="*.mk" --include="*.bp" --include="*.soong" . # 搜索不含 == 的模块名引用 grep -rn "platform-lib-local" --include="*.mk" --include="*.bp" device/ vendor/ build/ 2>/dev/null

我这次搜第一遍就锁定了问题。结果出现在vendor/xxx/device/xxx.mk文件里,内容大致是这样:

PRODUCT_PACKAGES += ==platform-lib-local

这行代码出现在某个PRODUCT_PACKAGES集合里,本意应该是想加入模块platform-lib-local,但在复制或者用脚本批量生成 product 配置的时候,前面多带了一个=。于是字符串==platform-lib-local变成了一个模块名,被 Soong 当成了一个真实存在的模块依赖。

如果你第一步没有搜到全字符串,可以放宽条件,只搜platform-lib-local,然后用肉眼逐条审,看有没有哪一行的模块名前后被拼了特殊字符。尤其是那种用模板拼接的 mk 文件,例如:

PRODUCT_PACKAGES += $(foreach lib,$(LOCAL_LIBS),=$(lib))

这种=$(lib)的写法就会在循环展开后产生==platform-lib-local这样的脏模块名。

2.2 顺着 Android.bp / Android.mk 追踪定义与引用

搜到PRODUCT_PACKAGES += ==platform-lib-local之后,还不能立刻下结论,因为也有可能真正问题出在这个模块的定义文件里。所以下一步要看这个模块到底存不存在,以及定义它的文件里name字段有没有被写脏。

在 AOSP 体系里,模块定义一般集中在packages/、frameworks/、vendor/、device/这几个大目录下,搜索模块名platform-lib-local时可以加一层过滤,只搜索定义和引用最密集的类型:

grep -rn "platform-lib-local" --include="*.bp" --include="*.mk" \ packages/ frameworks/ vendor/ device/ hardware/ 2>/dev/null

我当时搜索后发现,全源码里并没有一个合法的名为platform-lib-local的模块定义,只有PRODUCT_PACKAGES里那一个脏引用。这就更加确定问题了:引用的是个不存在的模块名,且模块名被拼上了==。

为了进一步确认构建系统是否还有其他地方也在引用这个字符串,还可以查产物输出目录里有没有实际生成过相关的中间文件:

find out_sys -type d -name "*platform-lib-local*" | head -20

如果有目录,说明之前在某个阶段 Soong 曾尝试处理这个模块名,或者构建图中残留过节点;如果没有,就是单纯的构建图引用错误。我这次查下来的结果是没有任何实际目录,进一步坐实了它只是字符串污染。

2.3 找出"=="的真正源头:条件赋值与脚本拼接

很多时候,问题并不像上文那么简单直接。我曾经遇到过另一种情况,源码里搜不到==platform-lib-local这种完整字符串,但 Ninja 报错里依然出现了类似的目标路径。这种情况的元凶通常是条件赋值或者函数宏拼接,让字符在预处理之后才变成==xxx。

举个例子,某个 mk 文件里写了:

PRODUCT_PACKAGES += $(if $(filter eng userdebug,$(TARGET_BUILD_VARIANT)),build-tools,)

这种写法的结果是正常的build-tools字符串。但如果写成下面这样,就会出问题:

PRODUCT_PACKAGES += $(if $(filter eng userdebug,$(TARGET_BUILD_VARIANT)),==build-tools,)

注意第二个参数==build-tools,如果前面的条件判断满足,那么展开后的结果就是==build-tools,从而把==build-tools当成一个模块名传给构建系统。同理,如果哪个文件里有$(eval LOCAL_MODULE := $(some_var))且some_var里面拼了几个等号,最终也会造成类似的脏路径。

所以排查时如果第一板斧搜不到完整字符串,第二步一定要盯住所有$(if ...)、$(eval ...)、$(foreach ...)以及:=赋值右侧以=开头的写法。这些地方最容易产生肉眼搜不到的脏字符串。

我当时还顺手用了另一个比较笨但有效的招:把整个源码目录里所有PRODUCT_PACKAGES相关的行捞出来,逐个看有没有以=开头追加的项。

grep -rn "PRODUCT_PACKAGES" --include="*.mk" . | grep "=="

这样能一次性看到所有可能掺入==的 PRODUCT_PACKAGES 引用。

3. 编译系统视角:Soong/Ninja 如何生成错误节点

3.1 模块名到输出路径的映射规则

要彻底理解这个报错,需要知道 Android 构建系统从源码到 Ninja 构建图的链路。Android 7.0 之后,构建系统由 Kati(Make 解析)和 Soong(Blueprint / Android.bp 解析)共同组成。Kati 负责把 Android.mk 转换成 Ninja 文件,Soong 负责把 Android.bp 转换成 Ninja 文件,最后由 Ninja 统一执行构建。

当一个模块被定义成一个 Java 库,例如:

java_library { name: "platform-lib-local", srcs: ["src/**/*.java"], }

那么它默认的输出目录就是:

out_sys/target/common/obj/JAVA_LIBRARIES/platform-lib-local_intermediates/

其中JAVA_LIBRARIES是模块的类型目录,platform-lib-local是模块名,_intermediates是固定后缀。整个路径由 Soong 在生成构建图时自动拼接。

当模块名里出现==时,路径就变成了==platform-lib-local_intermediates。从文件系统的角度讲,==并不是非法字符,Linux 下创建一个名为==platform-lib-local_intermediates的目录是允许的。问题在于 Ninja 的构建图规则里,必须有某个 rule 指明"如何生成这个路径下的文件",否则这个目标就是一个无源目标,Ninja 无法执行。

3.2 Ninja 报错"FAILED: ninja"的实际含义

Ninja 报错FAILED: ninja: 'some/path'并不是说某个命令执行失败了,而是说在整个构建图中,这个目标被某些节点引用为依赖,但构建图中根本不存在能生成它的 rule。

好比你在地图上标了一个目的地,但没画任何一条能通往目的地的路。导航软件自然只能提示"无法规划路径"。

在 Android 构建的语境中,通常是谁引用了这个目标呢?最常见的是PRODUCT_PACKAGES里的模块名。当 Kati 或 Soong 处理到PRODUCT_PACKAGES += ==platform-lib-local这行时,构建系统会认为产品需要打包一个名为==platform-lib-local的模块。于是它会在构建图里把这个模块对应的产物文件挂在依赖树上,等到 Ninja 执行时发现没有任何 rule 可以生成这个文件,于是整条构建中断。

如果你遇到的是更复杂的情况,不想靠猜,可以直接用 Ninja 自带的查询命令看依赖关系。

3.3 用 ninja -t 查询命令定位依赖来源

Ninja 提供了-t参数,可以查看目标之间的依赖关系。我们可以在out_sys目录下执行查询,看看这个诡异的节点到底被谁引用、有哪些依赖。

注意:如果路径里含有==,在 shell 里要用引号包住整个路径,防止被解释成特殊参数。

cd out_sys ninja -t query 'target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/'

这个命令会输出目标的上游依赖和下游依赖,如下游依赖是某个image或者system.img、vendor.img之类的打包目标,就说明有PRODUCT_PACKAGES在把它往系统镜像里塞。

如果-t query输出的信息不够直观,还可以用-t targets配合 grep 过滤出构建图中所有包含该关键字的节点:

cd out_sys ninja -t targets | grep "platform-lib-local" | head -20

我这次查到的结果是,构建图中出现了若干指向out_sys/target/product/xxx/system/framework/==platform-lib-local.jar的节点,这些节点全部都由PRODUCT_PACKAGES里的脏模块名引出来的。锁定这一步,修复就水到渠成了。

4. 修复步骤与清理技巧

4.1 修改脏引用或模块定义

根据上一步锁定的文件位置,修复其实很简单。我这次只需要把vendor/xxx/device/xxx.mk里的这一行:

PRODUCT_PACKAGES += ==platform-lib-local

修改成:

PRODUCT_PACKAGES += platform-lib-local

这里要注意一个重要问题:改完引用后,如果项目中确实存在名为platform-lib-local的模块,但同时它的name字段也被写成了==platform-lib-local,那么还需要去模块定义文件里同步改回来。

例如Android.bp文件里有:

java_library { name: "==platform-lib-local", srcs: ["src/**/*.java"], }

就要改成:

java_library { name: "platform-lib-local", srcs: ["src/**/*.java"], }

同时检查所有依赖它的static_libs、libs、PRODUCT_PACKAGES等字段有没有同步。

如果模块名本身合法,但只是引用处脏了,那只需要改引用处。修改之后不需要清理整个输出目录,直接重新执行 make,构建系统会重新生成 Ninja 构建图。因为你改动的文件属于构建配置类文件,Kati/Soong 会检测到依赖变化,自动触发重跑。

4.2 清理残留的异常中间目录

虽然改完配置后正常编译不会再生成==platform-lib-local_intermediates这个目录,但如果之前已经有部分脏产物落盘,最好还是手动清一下,避免后续出现奇怪的文件残留。

清理命令:

rm -rf out_sys/target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/

如果源码里还有路径名包含==的其他残留,也可以一次性把所有类似目录都找出来删掉:

# 先扫一遍看看有哪些 find out_sys -type d -name "==*_intermediates" | head -50 # 确认没问题后批量删除 find out_sys -type d -name "==*_intermediates" -exec rm -rf {} +

这里我不建议没确认就直接执行批量删除。先扫一遍、肉眼看清楚里面是什么,再动刀,不然误删了某些正常模块的中间产物会让后续编译变慢。

4.3 重新触发编译的正确姿势

修复完成后,重新编译建议分三步走:

source build/envsetup.sh lunch <你的产品名>-userdebug m -j32

如果上一步的修复涉及PRODUCT_PACKAGES这种全局变量,而且你担心旧的 Ninja 构建图没有被自动更新,可以强制删除生成规则文件让 Soong/Kati 重新生成:

rm -rf out_sys/build-*.ninja rm -rf out_sys/soong rm -rf out_sys/.module_paths

然后再执行m -j32。这一步会强制让构建系统重新解析所有Android.bp和Android.mk,基本上可以杜绝"改完还是报同样错"的尴尬。

需要注意,直接删除out_sys/soong会重新生成大量模块描述文件,编译前期会多花几分钟。相比全量删除out_sys来说,已经非常温柔了,至少不需要重新跑完整套 C/C++ 编译。

5. 同类问题的预防与快速排查清单

5.1 模块命名规范与强制检查清单

这次问题虽然定位快,但本质上也是低级命名规范问题。为了不反复踩坑,我在项目里制定了一个简单的检查清单,每次提交代码前或者出问题时先跑一遍。

下面的命令可以直接复制成脚本放在 CI 或者本地 pre-commit 钩子里:

#!/bin/bash # 检查 PRODUCT_PACKAGES / PRODUCT_PACKAGES_DEBUG 中是否有以 = 开头的脏模块名 grep -rn "PRODUCT_PACKAGES.*==" --include="*.mk" device/ vendor/ build/ 2>/dev/null # 检查 Android.bp 中 name 字段是否以 = 开头 grep -rn 'name: "=' --include="*.bp" packages/ frameworks/ vendor/ device/ hardware/ 2>/dev/null # 检查 Android.mk 中 LOCAL_MODULE 是否以 = 开头 grep -rn "LOCAL_MODULE.* := *=" --include="*.mk" packages/ frameworks/ vendor/ device/ hardware/ 2>/dev/null

如果上面三条命令都没有输出,说明模块命名层面基本健康。

另外建议在每一个新模块引入构建系统时,遵守只使用[A-Za-z0-9_.-]的约定。==、//、..这类字符串一旦混进模块名,轻则像这次一样 Ninja 报错,重则可能引起路径穿越或者覆盖其他模块的产物,排错成本远大于命名时多看一眼的成本。

5.2 在 CI 中提前拦截

如果团队里很多人都在改 vendor 和 device 下的 product 配置,纯靠自觉不太现实。我比较推荐在 CI 的编译前检查阶段加上上面那段脚本。CI 一旦发现PRODUCT_PACKAGES里有以=开头的字符串,直接让流水线失败并输出错误文件路径。这样能在编译还没开始之前就把问题挡在门外。

对于已经发现的问题,修复后建议顺手做一次"脏配置全扫描",把所有可能的不规范字符都找出来,而不是只改当前报错的这一个。我这次修复完就扫描了一遍,结果还真在vendor/xxx/common/xxx.mk里又找到一个类似的==lib-local字符串,尽管它这次还没引爆 Ninja 报错,但留着就是个定时炸弹。

5.3 实用 debug 命令速查

最后把我这次排错用到的命令整理成一个速查表,遇到 Ninja 报错时按顺序执行,基本都能快速定位到是哪个文件在搞事。

目的命令
搜源码里完整的脏字符串grep -rn "==platform-lib-local" --include="*.mk" --include="*.bp" .
搜去脏后的模块名引用grep -rn "platform-lib-local" --include="*.mk" --include="*.bp" vendor/ device/ build/
看构建图中目标列表cd out_sys && ninja -t targets | grep platform-lib-local
查看目标依赖关系cd out_sys && ninja -t query 'target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/'
清理残留目录find out_sys -type d -name "==*_intermediates" -exec rm -rf {} +
强制重建构建图rm -rf out_sys/build-*.ninja out_sys/soong out_sys/.module_paths

这些命令不仅适用于JAVA_LIBRARIES,也适用于APPS、STATIC_LIBRARIES、SHARED_LIBRARIES等所有模块类型。只要报错路径里出现特殊字符,先不要急着清理全量输出,按表格里的顺序查一遍,通常十几分钟内就能定位到脏字符串所在的文件。

我在实际遇到这类 Ninja 报错时,最反感的做法就是一上来就rm -rf out_sys。全量编译一次动辄两三个小时,如果根因是某个 mk 文件里多写了一个=,清理全量输出毫无意义。先花几分钟用 grep 和ninja -t query定位到具体文件,改完配置再重编,才是最省时间的路径。

希望这次踩坑记录能帮你在下次看到FAILED: ninja: '...intermediates/'时,少走一点弯路,直接按住这个思路把脏字符串揪出来改掉。如果你在 Android 源码编译时还遇到过其他奇奇怪怪的模块名报错,也可以按这个思路试试看。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询