☰
安卓系统源码编译FAILED: ninja错误解析与JAVA_LIBRARIES路径排查指南
2026/10/5 3:25:27 网站建设 项目流程

1. 从一条编译报错说起:这个 FAILED 到底卡在哪

先别急着翻日志,我们直接把这条报错拆开看。很多人在安卓系统源码编译时遇到过类似的提示:

FAILED: ninja: 'out_sys/target/common/obj/JAVA_LIBRARIES/==platform-lib-local_intermediates/'

这条信息里面有三个非常关键的部分,一个是FAILED,一个是ninja,还有一个是JAVA_LIBRARIES。先说结论:这基本可以断定是系统级编译流程中,某个依赖模块没有被正确解析或者产物路径异常导致的。它不是简简单单的代码语法错误,也不是缺了某一个 SDK 版本,而是构建系统在组织整个编译任务时,发现目标产物根本不在预期位置。

做安卓系统开发的人都清楚,源码编译和普通 App 编译完全是两个量级。App 编译靠 Gradle,系统编译靠的是 Soong、Kati、Ninja 这套组合拳。Ninja 是这个链条里的“苦力”,它只负责执行任务,不管任务怎么编排;而out_sys/target/common/obj/JAVA_LIBRARIES/是产物输出目录,专门放系统编译过程中生成的 Java 库中间文件。这条报错本质上是在说:Ninja 认为某个 Java 库的中间产物应该存在于这个路径,但实际上找不到。

我把话说得更直白一些:你遇到这个问题的时候,大概率是刚改完某个模块的依赖关系,或者刚从别的分支同步完代码,又或者是 out 目录里残留了旧的构建信息。这类问题不会因为你重新跑一次m就自动好,因为 Ninja 已经生成了错误的构建描述文件,你得先搞清楚它为什么会生成这么一条路径。

这个标题在我的工作场景里出现过不止一次,几乎每次都是团队里有人动了Android.bp或者Android.mk里的模块注册方式,导致模块名出现了歧义。我后面会详细拆解。

顺便说一句,有的人看到==platform-lib-local_intermediates这一串会觉得很莫名其妙,因为正常路径应该是module_name_intermediates,中间不应该出现==。这个细节是突破口,后面会重点讲。

2. 构建链路剖析:Soong、Kati、Ninja 各自扮演的角色

要搞清楚这个报错为什么会出现,光看 Ninja 本身是不够的,你得理解安卓系统编译这条链路上每个环节的分工。我尽量用通俗的方式讲,不堆术语。

2.1 Ninja 只是执行者,不是决策者

Ninja 是一个非常底层的构建工具,设计目标就是“快”。它读一个.ninja文件,里面有成千上万的构建规则和依赖关系,然后按照这些规则去执行编译命令。但 Ninja 本身不做任何逻辑判断——它不会去检查你的模块名是否合法,也不会去分析依赖是否完整。它只知道:如果 A 文件不存在,那我就去执行生成 A 的命令;如果执行失败,我就报错。

所以你看到FAILED: ninja: 'xxx_intermediates/',其实是在告诉你:在 Ninja 看来,这个路径是一个需要先生成的目标文件或目录,但它找不到生成它的规则,或者生成规则执行后没有产出预期文件。换句话说,问题往往出在“生成这个路径的规则”上,而不是 Ninja 本身。

2.2 Kati 负责把 Makefile 翻译成 Ninja 能懂的东西

安卓系统原本用的是 Makefile 体系,后来为了提高增量编译速度,引入了 Kati。Kati 的作用是把Android.mk里的 Makefile 语法翻译成 Ninja 文件。如果Android.mk里有变量定义含糊不清,Kati 在解析时就会生成一些奇怪的路径。我遇到过的典型情况是,有人用$(LOCAL_MODULE)时没加括号,或者把模块名写成了带空格、带特殊符号的字符串,最终产生出来的路径就带着==这样不正常的符号。

2.3 Soong 负责处理 Android.bp,替代 Makefile 的新体系

Soong 是 Google 后来推出的新构建系统,处理的是Android.bp文件。Soong 的输出也是 Ninja 文件,但它内部的模块命名规则和 Kati 不完全一致。如果项目里同时有Android.mk和Android.bp,而且两边定义了相同名称的模块,就可能出现命名冲突。冲突的结果之一,就是 Soong 在生成产物路径时,给模块名加上了前缀或后缀来区分,但你看到的不是正常的分隔符,而是==。

我自己有一个习惯做法:收到这种报错后,第一件事不是看 Ninja,而是去看soong_ui.log或者ninja_log,因为这两份日志会记录构建系统到底执行了什么命令。很多时候,命令本身就能暴露出问题根源。

2.4 JAVA_LIBRARIES 这个路径到底代表什么

out_sys/target/common/obj/JAVA_LIBRARIES/是系统编译时存放 Java 库中间产物的标准目录。所有用java_library或类似模块类型定义的库,编译过程中都会在这个目录下生成一个以模块名命名的子目录,里面放着classes.jar、intermediates之类的文件。

这条报错的完整含义是:构建系统期望在这个路径下找到一个名为==platform-lib-local_intermediates的目录,但这个目录没有生成成功。可能原因有三类:

第一个原因,模块名定义有问题,导致 Soong/Kati 生成路径时用了错误的字符串拼接方式。第二个原因,模块本身没有被编译,可能是因为它被PRODUCT_PACKAGES排除,或者它所属的 product 配置没有包含该模块。第三个原因,增量编译状态下,旧的构建产物和新的构建描述文件不匹配,导致 Ninja 认为产物存在但实际上已经被清掉。

我在实际排查中,遇到最多的其实是第一类。==这个符号出现的位置,恰好就是模块名和_intermediates后缀的分界处,说明模块名本身可能是一个空字符串,或者包含了一个被解析成空值的变量。

2.5 什么是“platform-lib-local”

从名称来看,这个模块叫platform-lib-local,大概率是一个自定义的 Java 库模块,可能是团队内部封装的平台库,也可能是从某个公共代码仓库同步过来的。platform-lib这种命名方式,在系统开发里一般表示它依赖于系统 API,不能被普通的 App 模块引用。local这个后缀则可能是本地编译的变体标识。

对于系统源码编译来说,模块命名是有讲究的。如果模块名里面含有-这种连字符,Soong 在生成中间目录时会保留它,不会转成下划线,所以platform-lib-local_intermediates本身是正常路径。问题只出在它前面多了==。这说明模块名在 Soong 的内部表示里出现了某种“空壳”状态。

我建议所有做系统开发的朋友,看到这种报错时,第一反应不要是“我代码写错了”,而是“我构建描述写错了”。因为代码错误通常直接报在编译单元上,而这种路径类错误,几乎全部出在构建配置层面。

3. 一步步排查:从日志定位到最终解决

接下来我把自己的排查流程完整写下来,你跟着走基本能定位到问题。

3.1 第一步:确认报错出现的具体时机

先想清楚,这个报错是出现在全量编译还是增量编译过程中。两种场景的处理逻辑完全不同。

如果是全量编译刚开始就报这个错,说明构建描述文件在生成阶段就有问题。如果是编译到一半才报,说明某个模块的构建依赖没有被完整描述。如果是增量编译,特别是你很久没编译、刚 sync 完代码之后报的,那大概率是 out 目录里的旧状态污染了新构建。

我自己遇到的一个典型场景是:团队成员从服务器同步了代码,但本地的 out 目录还保留着上一次构建的产物,而那个产物对应的模块在新代码里已经被删除了。Soong 在生成新构建描述时,发现某个旧模块的依赖依然被其他模块引用,于是试图去构建一个已经不存在于源码树里的模块,路径就会变成畸形。

3.2 第二步:查看 soong_ui.log 和 ninja 日志

在源码根目录下执行:

tail -n 300 out/soong_ui.log

这个日志会记录 Soong 生成 Ninja 文件的完整过程。重点看报错之前的几十行,特别是和platform-lib-local相关的警告或错误信息。我见过很多次,Soong 在解析某个Android.bp文件时,已经给出了 warning,但因为是 warning 不是 error,很多人忽略了,结果最后 Ninja 直接 FAILED。

再看 Ninja 日志:

tail -n 300 out/build.log

或者直接搜索报错路径:

grep "platform-lib-local" out/build.log

如果能在 build.log 里看到具体的编译命令,那就非常有价值。比如命令里面可能出现了-classpath参数指向了某个 jar,而那个 jar 根本不存在。

3.3 第三步:定位模块定义的源头

用find在整个源码树中搜索模块名:

grep -r "platform-lib-local" --include="Android.bp" --include="Android.mk" .

这个命令会在所有Android.bp和Android.mk文件里查找模块名。找到定义之后,仔细看它的name字段、srcs字段、deps字段。特别注意有没有使用变量拼接模块名:

name: "platform-lib-" + variant,

这种写法不是不能有,但如果variant变量是空字符串,模块名就变成了platform-lib-,再加上 Soong 的命名规则,就可能生成==platform-lib-local这样的路径。我之前踩过这个坑,最后查出来是一个环境变量在编译脚本里没有被正确赋值。

3.4 第四步:检查模块是否被正确包含进编译目标

找到模块定义后,还要确认它是否在当前编译目标中。如果是full编译,看PRODUCT_PACKAGES里有没有这个模块。如果是自定义 target,看AndroidProducts.mk里的 product 配置。

最直接的验证方式是执行:

m platform-lib-local

单独编译这个模块。如果单独编译能成功,那说明问题不在于模块本身,而在于整个构建图的依赖关系。如果单独编译也报同样的错,那就可以基本确定问题出在模块定义上。

3.5 第五步:检查 out 目录的残留状态

这一步很多人容易忽略。安卓系统编译最怕的就是 out 目录“夹生”。所谓夹生,就是新旧产物混在一起,Ninja 基于旧的.ninja文件找到了一个已经不存在的目标,或者找到了一个内容已经不合法的目标。

处理方式有两个,由轻到重:

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

这个命令只删除出问题模块的中间产物,让它重新生成。如果还不行,就需要清理更广的范围:

rm -rf out_sys/target/common/obj/JAVA_LIBRARIES/

清掉所有 Java 库中间产物,让它们全部重新编译。这样做的代价是会增加编译时间,但能排除大部分杂症。我在实际工作中,只有非常确定问题出在特定模块时,才会用第一个方案;不确定的情况下直接走第二个方案,反而省时间。

3.6 第六步:终极手段,单模块编译验证

如果上面几步都做了还是报错,那就要用“单模块编译”来缩小范围。先确认这个模块到底能不能单独构建:

source build/envsetup.sh lunch <你的产品配置> m platform-lib-local

如果单模块编译能成功,说明问题一定出在全局构建图的依赖关系上。此时你需要注意整个依赖链中是否有循环依赖,或者某一个依赖模块被两次定义。

如果是循环依赖,Ninja 在生成构建图时会死循环或者生成畸形路径。检查方式是对报错路径里的每个模块名都执行一遍grep -r,看是不是有模块互相依赖了。我记得有次排查,最后发现是两个java_library模块互相引用了对方的srcs里面的类,但依赖关系上又互相加了deps,Soong 直接生成了一条损坏的路径。

3.7 实操心得:这几种处理方式要分清优先级

如果你现在正在被这个问题卡着,我建议你按这个顺序操作,不要跳步:

第一步,先做“最轻量尝试”,即单模块编译。这一步能最快判断问题是全局的还是局部的。第二步,如果单模块编译失败,马上检查模块定义,重点看模块名来源是否是变量拼接,以及变量是否为空。第三步,如果单模块成功、全量失败,那就清理中间产物目录后重编。第四步,如果清理后仍失败,检查全局依赖关系,特别是是否有新增模块没有被注册进产品配置。

我特别想强调一个经验:很多人一上来就清理整个 out 目录,这是效率最低的做法。因为你清完之后,面对的是一锅粥,根本不知道问题出在哪。正确的做法是带着怀疑去定位,先看日志,再单模块测试,最后才考虑清理。

4. 深入源码:JAVA_LIBRARIES 中间产物的生成机制

要真正理解这个报错,你还是得懂一点JAVA_LIBRARIES目录背后的机制。它不是简简单单的一个文件夹,而是有严格的命名规范和产物管理逻辑的。

4.1 namespaced 模块 vs 非 namespaced 模块

在 Soong 中,模块的中间目录命名受一个叫namespaced的属性影响。如果模块没有设置namespaced: true,它的中间目录会直接使用模块名,路径就是out_sys/target/common/obj/JAVA_LIBRARIES/模块名_intermediates/。如果设置了namespaced: true,中间目录会加上包名前缀,比如out_sys/target/common/obj/JAVA_LIBRARIES/包名.模块名_intermediates/。

你可能会问,这和==有什么关系?关系在于:当 Soong 内部处理模块时,如果模块名是通过字符串拼接生成的,并且拼接的某个部分为空,Soong 内部可能会用==来填充缺失的命名空间部分。这属于实现细节,但实际现象就是路径中会出现==。

4.2 中间产物目录里到底有什么

一个典型的java_library模块编译完成后,它的_intermediates目录下通常会有这些内容:

  • classes.jar:编译后的 class 文件打包。
  • classes-extra.jar:额外的 class 文件。
  • javac相关文件:记录 javac 参数和输出。
  • aar相关文件:如果是 Android 库,还会有资源相关产物。

Ninja 在执行构建任务时,会检查这些文件的生成规则。如果规则里声明的输出文件路径与实际产物路径不一致,Ninja 就会报FAILED。所以当你看到FAILED: ninja: 'out_sys/.../==platform-lib-local_intermediates/'时,本质上就是 Ninja 在构建图中发现了一个不可能存在的输出。

我个人觉得,把这条报错翻译成人话就是:构建系统在问“这个中间产物目录到底是谁负责生成的?”,结果大家互相推诿,最后没人认领,于是 Ninja 罢工了。

4.3 变体(variant)概念带来的路径变化

安卓系统编译里还有一个核心概念叫“变体”。同一个模块,在不同的编译场景下会有不同的变体名称,比如android_common、android_common_apex30、host等。变体不同,中间目录也不同。

platform-lib-local_intermediates这个名字里的local,很可能就是某个变体的标识。而当变体值为空或者未定义时,Soong 内部会生成一个拼接符号,在日志里显示成==。这也能解释为什么有时候同样的源码,在不同分支下编译结果不同——因为不同分支的变体配置不一样。

我在一次实际排障中,发现项目的Android.bp里通过target: { android: { name: ... } }来覆盖模块名,结果那个name字段里使用了soong_config_module_type这种动态配置变量。在某个产品配置下变量为空,模块名塌缩,路径就变成了畸形。

如果你在用动态配置变量,我强烈建议你在变量下方加一行测试代码,用//注释临时写死一个字符串,确认路径恢复正常,那样就能确认是变量的问题。

5. 常见问题速查与避坑技巧

我把这类报错里最常踩的坑汇总成了表格,方便你在排查时快速对照。

症状可能原因排查方向解决办法
路径中出现==模块名通过变量拼接,变量为空检查 Android.bp / Android.mk 中的变量定义给变量加默认值,或者改写成直接量
单独编译成功,全量编译失败模块没被产品配置包含,或依赖链断裂检查 PRODUCT_PACKAGES、产品 mk 文件在对应 product 配置中补充分模块
报错出现在增量编译后out 目录状态与源码不同步检查 out 目录中对应模块产物删除该模块中间目录后重编
报错伴随循环依赖提示两个模块互相依赖查看模块间的 deps 关系移除循环依赖,或新增一个公共模块下沉依赖
报错在清理 out 后消失旧产物污染无以后做增量编译前先看日志再清
报错只存在于特定分支分支间的构建配置不一致diff 两个分支的 Android.bp合并分支时仔细核对构建配置

然后我再补充几个避坑经验:

第一,不要在Android.bp里动态拼接模块名。这不是说绝对不能用,但你要想清楚所有可能为空的分支。很多资深工程师都在这上面栽过跟头,因为本地编译环境变量很多时候是默认值,但 CI 环境或同事的环境没有设置那个变量,构建就挂了。

第二,修改模块依赖后,建议至少执行一次m nothing或者m -j全量构建,不要只依赖增量编译。增量编译在构建描述文件没有变化时表现良好,但一旦修改了依赖关系,Soong 可能不会及时重算整个构建图,导致 Ninja 依然按旧规则执行。

第三,如果你的项目里有自定义的custom_build_rules,一定要检查它们是否注册到了soong_build的全局规则中。这类问题最隐蔽,因为报错不会直接指向你的规则文件,而是指向路径。

第四,切分支之后,如果暴力清理了 out 目录,建议先rm -rf out再重新执行source build/envsetup.sh和lunch。有些人图省事只删部分目录,结果.config、.ninja_log这些隐藏状态文件还是旧的,反而更容易踩到坑。

第五,产品配置里的PRODUCT_PACKAGES一定要记得检查。这个变量控制整个系统包里包含哪些模块。如果你的模块没有写进去,系统编译时就不会生成它的产物,但其他模块又依赖它,Ninja 到执行时就会发现问题。

我再多聊一个小技巧:如果报错路径里带==,你可以直接在out_sys/target/common/obj/JAVA_LIBRARIES/目录下执行ls -la,看实际存在的模块目录有哪些,对比一下正常模块的命名格式。正常格式一般是模块名_intermediates。一旦你看到路径里出现了==,几乎可以断定模块命名逻辑出了问题。

6. 一次完整排障案例:从报错到解决的全过程

我拿一个真实的排障过程作为例子讲,隐藏掉具体项目名称,但步骤是完整的。

那一次,团队里的一个同事在新增了一个系统级 Java 库之后,编译到中途就挂了,报的就是这条路径错误。同事很郁闷,因为他确认自己的Android.bp写得没问题,模块名也检查了好几遍。

我先让他执行m platform-lib-local,结果单模块编译居然也是这个报错。这就不对劲了,说明问题一定在模块定义本身,而不是依赖关系。

接着我用grep -r "platform-lib-local" --include="Android.bp"找到了定义文件,发现模块名是这样写的:

java_library { name: "platform-lib-" + "local", ... }

当时看到这种写法就觉得有问题了。虽然 Soong 支持字符串拼接,但他把 module type 直接当成了变量字符串来写,name字段里出现了+拼接。于是我去查 Soong 的解析规则,发现这种写法乍一看没问题,但如果模块定义被某个soong_namespace包裹,并且该命名空间下的默认模块名前缀是空,路径就会出现异常。

后来我们把name字段直接改成了"platform-lib-local",并且把host_supported: true之类的属性检查了一遍,编译就通过了。

这个案例告诉我们一件事:报错是最终的表面现象,真正的凶手可能在很上游的配置里。你越是觉得“我这也对那也对”,越要检查那些不起眼的拼接、变量、命名空间设置。

还有一次,问题出在Android.mk和Android.bp的混用。同一个模块,在 mk 文件里用LOCAL_MODULE := platform-lib-local定义了,在 bp 文件里又用java_library定义了一次,名字完全一样。Kati 和 Soong 同时解析,生成的 Ninja 规则互相覆盖。最终 Ninja 里出现了两条互相冲突的规则,输出路径就变成了畸形。

解决方案也很简单,二选一即可:要么删掉 mk 文件里的定义,统一用 bp;要么把 bp 里的模块改名,避免冲突。我通常更推荐前者,因为 Soong 是趋势,新的模块都应该写在 bp 文件里。

7. 给不同基础读者的操作建议

我知道很多刚接触系统编译的朋友,看到这种报错会非常慌,觉得自己哪里都错了。其实不然,这类问题在系统开发里非常常见,处理起来也有套路。我把这类问题拆成两个级别,不同基础的人可以按不同路径来。

刚入门的读者,建议你按照“单模块编译 → 搜索模块定义 → 检查变量 → 清理中间产物 → 全量重编”这个顺序来。每一步做完都重新编译验证一遍,不要跳步。不管最终能不能解决,这个排查过程本身就是在帮你理解整个安卓构建链路。

有一定经验但没专门搞过构建系统的读者,建议你重点看 Soong 日志和 ninja 日志。很多时候答案就在日志里,只是你没有被训练出来看日志的习惯。比如soong_ui.log里会记录它收集了哪些Android.bp,如果你发现模块定义文件根本不在列表中,那就是命名空间或者Android.bp路径的问题。

至于资深的构建系统维护者,我想说的是,这类问题往往能暴露你项目中构建配置的“坏味道”。比如过度使用动态变量拼接模块名、bp 和 mk 混用、模块依赖循环等问题,都是在平时积累下来的。每次出现这种报错,其实都是代码库在向你发出重构的信号。

我个人的建议是,在项目里建立一个“构建配置检查清单”,每次提交涉及构建配置的修改时,都按这个清单过一遍。清单里面应该包含:模块名是否唯一、命名空间是否清晰、依赖是否有环、产品配置是否已注册、是否有 bp/mk 混用。养成这个习惯之后,你在构建系统上踩的坑会少很多。

8. 最后再分享一点实践中的体会

安卓系统编译是个复杂的系统,FAILED: ninja这种报错只是冰山一角。我做了这么多年系统开发,感觉这类问题最消耗人的不是解决它本身,而是定位它的过程。明明报错就一行,但背后牵扯到构建工具链、模块系统、产品配置,甚至环境变量,任何一个环节出了偏差,最后都会汇聚成这一条信息。

我自己现在遇到这种报错,心态已经平稳很多,原因就是摸索出了一套固定的排查流程,把“不确定性”变成了“确定性”。每次按流程走,基本都能在两小时内定位到根因。所以也希望读完这篇文章的你,不要惧怕这类报错,而是把它当成一次深度学习安卓构建系统的机会。下次再看到JAVA_LIBRARIES目录下出现奇怪的路径,你就知道该往哪里看了。

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

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

立即咨询