15、编译错误排查:常见编译错误、日志分析、Kati 错误定位

说实话,写 Android 系统最怕什么?不是需求改来改去,而是编译不过。

我见过太多新人,一看到终端里飘红就慌了,直接截图往群里丢。其实编译错误没那么可怕,关键是你得知道怎么看日志、怎么定位问题。今天我就把这几年的排查经验掰开揉碎讲给你听。

15.1 编译错误的“三大家族”

Android 编译错误,说白了就三类:语法错误、链接错误、还有 Kati 相关的构建错误。我习惯把它们分成“代码问题”、“依赖问题”和“构建系统问题”。

  • 语法错误:C++/Java/Kotlin 代码写错了,编译器直接报错。这种最好修,看行号就行。
  • 链接错误:函数声明了没实现,或者库没链接对。我遇到过最坑的一次,是 .bp 文件里少写了一个 shared_libs。
  • Kati 错误:这是 Android 特有的。Kati 负责把 Android.mk 转换成 ninja 文件,如果语法写错或者变量引用不对,就会挂。

核心原则:先看第一个错误,不要被后面几十个刷屏吓到。很多时候后面的错误都是第一个引发的连锁反应。

15.2 日志分析:你得知道看哪里

编译日志通常很长,但真正有用的信息就那么几块。我个人习惯用 grep 或者 less 来过滤。

15.2.1 关键关键词

  • error: —— 真正的错误
  • FAILED: —— ninja 构建失败的任务
  • *** —— make 时代的错误标记,现在 Kati 也会用
  • undefined reference —— 链接错误,常见于 C++

举个例子,你看到这样的输出:

FAILED: out/target/product/xxx/obj/SHARED_LIBRARIES/libfoo_intermediates/LINKED/libfoo.so
/bin/ld: error: undefined symbol: bar_func

嗯,这里就说明 libfoo.so 里调用了 bar_func 但没找到实现。要么是没链接对应的库,要么是那个库没编译。

15.2.2 日志文件在哪

编译日志默认在 out/soong.logout/build.log。我建议你养成习惯,每次编译失败后先看 build.log 的最后 200 行:

tail -200 out/build.log

为什么?因为真正的错误往往在最后面。前面那些 warning 和 info 大多是噪音。

小技巧:用 ninja -v 重新跑失败的任务,能看到完整的命令行和错误输出。我经常这么干。

15.3 Kati 错误定位:别被“伪错误”骗了

Kati 是 Android 构建系统里一个比较特殊的环节。它把 Android.mk 转换成 ninja 文件。如果 Kati 报错,通常是因为 .mk 文件写错了。

15.3.1 常见 Kati 错误类型

错误信息 含义 常见原因
*** missing separator. Makefile 语法错误 该用 Tab 的地方用了空格
*** commands commence before first target. 命令出现在目标之前 空行或缩进问题
*** unterminated variable reference. 变量引用没闭合 少写了 )}
kati: unknown option Kati 参数错误 Android.mk 里用了不支持的语法

我曾经遇到过一个案例:同事在 Android.mk 里写了一个 $(shell ...) 调用,里面用了 bash 的 <( ... ) 进程替换。Kati 不认这个,直接报错。折腾了半天才发现是 shell 语法太“高级”了。

15.3.2 如何定位 Kati 错误

Kati 的错误信息通常包含文件名和行号。比如:

build/make/core/base_rules.mk:238: *** missing separator.  Stop.

这时候直接打开 base_rules.mk 的第 238 行附近看看。我敢打赌,八成是有人把 Tab 换成了空格。

注意:Kati 错误有时候会指向 build/make/core/ 下的通用文件,但问题其实出在你的模块里。别被误导了。检查一下你的 Android.mk 里有没有不规范的写法。

15.4 实战:一个典型的排查流程

假设你执行 m 编译,终端输出一片红。别慌,按我的步骤来:

  1. 看第一个 FAILED:找到第一个 FAILED: 行,记住它后面的目标名。
  2. 看错误类型:是编译错误还是链接错误?如果是编译错误,直接去源码对应行修。
  3. 如果是链接错误:检查 Android.bpAndroid.mk 里的 shared_libsstatic_libs 有没有漏掉。
  4. 如果是 Kati 错误:打开报错文件,看行号附近。注意 Tab 和空格。
  5. 重新编译:修完后,用 m 目标名 单独编译那个模块,验证通过后再全编。

我记得有一次,一个同事编译整整挂了两个小时,最后发现是 Android.mk 里多了一个不可见字符。嗯,从那以后我养成了用 cat -A 检查 .mk 文件的习惯。

15.5 知识体系:一张图看懂编译错误排查

下面这张图是我自己总结的排查路径,你可以把它当成一个决策树来看:

编译失败 Kati 错误(.mk 语法) Ninja 错误(编译/链接) 检查 Android.mk 行号 Tab vs 空格 / 变量闭合 看第一个 FAILED 行 编译错误 去源码行修 链接错误 检查依赖库 修完后:m 目标名 单独验证 再全编,避免浪费时间

这张图从左到右,从上到下,基本覆盖了 90% 的编译错误场景。你只要跟着走一遍,大部分问题都能自己搞定。

15.6 避坑指南

最后分享几个我踩过的坑:

  • 不要相信“clean 一下就好了”:很多时候 clean 只是浪费时间。先看日志,找到根因。
  • 注意大小写:Android 构建系统对文件名大小写敏感。我见过有人把 foo.cpp 写成 Foo.cpp,结果链接报错。
  • Kati 缓存问题:有时候 Kati 会缓存旧的 .mk 解析结果。如果你改了 .mk 但没生效,试试 touch Android.mk 或者 rm -rf out/build_kati
  • 并行编译的坑:用 m -j32 时,错误信息可能会乱序。我建议第一次编译用 m -j1,虽然慢,但日志清晰。

嗯,编译错误排查其实就是一个“读日志 → 定位 → 修复”的循环。你多练几次,自然就熟练了。别怕,我也是一路从“看到红色就慌”走到现在的。


公众号:蓝海资料掘金营,微信deep3321