5、Android.bp 与 Soong:从 Make 到 Soong 的演进、Blueprint 文件语法、Android.bp 示例
聊到 Android 构建系统,就绕不开一个核心话题:Soong。
我记得刚入行那会儿,Android 还是清一色的 Android.mk 文件。那时候改个模块依赖,经常要等十几分钟的全量编译。后来 Google 推出了 Soong,说实话,我一开始是抵触的——又要学新语法,又要改构建脚本。但用了一段时间后,我只能说:真香。
这一章,我们就来聊聊 Soong 是什么,它怎么来的,以及最重要的——Android.bp 怎么写。
从 Make 到 Soong:为什么要变?
先说说历史。Android 早期用的是 GNU Make 构建系统。每个模块对应一个 Android.mk 文件,语法是 Makefile 那一套。
听起来挺正常的对吧?但问题在于:Make 太慢了。
为什么慢?因为 Make 是递归执行的。你想想看,一个大型项目有几千个模块,每个模块都要解析一遍 Makefile,还要处理各种变量展开、条件判断。我曾在项目中遇到过,单是解析构建脚本就要花掉 2 分钟。
而且 Make 的语法太灵活了。灵活到你可以写出各种奇奇怪怪的写法,导致构建系统很难做增量编译优化。
所以 Google 决定自己写一个构建系统——Soong。
Soong 的核心思路很简单:
- 用 Blueprint 文件(也就是
Android.bp)描述模块 - 用 Go 语言写的解析器,速度快得多
- 支持 Kati 做兼容层,把老的 Android.mk 转成 ninja 文件
说白了,Soong 就是 Make 的替代品,但更现代、更快、更可控。
核心变化:
- Android.mk → Android.bp
- Make 语法 → Blueprint 语法(类似 JSON)
- 递归解析 → 并行解析
- 不支持条件判断 → 用 Go 逻辑控制
Blueprint 文件语法:长什么样?
Blueprint 的语法其实很简单。它有点像 JSON,但更简洁。
一个基本的 Android.bp 文件长这样:
cc_binary {
name: "hello_world",
srcs: ["main.cpp"],
shared_libs: ["libcutils"],
cflags: ["-Wall"],
}
嗯,是不是很直观?
我来拆解一下:
- cc_binary:模块类型。表示这是一个 C++ 可执行文件。
- name:模块名字。必须唯一。
- srcs:源文件列表。
- shared_libs:依赖的共享库。
- cflags:编译选项。
常见的模块类型还有:
| 模块类型 | 说明 |
|---|---|
cc_library |
C++ 库(静态或动态) |
cc_binary |
C++ 可执行文件 |
java_library |
Java 库 |
android_app |
Android 应用 APK |
genrule |
自定义生成规则 |
每个模块类型都有自己支持的属性。比如 cc_library 支持 static_libs,而 android_app 支持 package_name。
小技巧: 我个人习惯在写 Android.bp 时,先用 m -j1 nothing 跑一遍,看看有没有语法错误。这样比等全量编译快得多。
Android.bp 示例:实战一下
光说不练假把式。我们来看一个实际项目中会用到的例子。
假设我们要编译一个 C++ 库,它依赖另一个静态库,并且需要链接系统库:
cc_library {
name: "libmylib",
srcs: [
"src/core.cpp",
"src/utils.cpp",
],
export_include_dirs: ["include"],
static_libs: ["libhelper"],
shared_libs: [
"liblog",
"libutils",
],
cflags: [
"-DLOG_TAG=\"mylib\"",
"-O2",
],
sdk_version: "current",
stl: "libc++",
}
这里有几个点需要注意:
- export_include_dirs:导出的头文件路径。其他模块依赖这个库时,会自动加上这个路径。
- static_libs:静态链接的库。编译时会把代码直接打包进来。
- shared_libs:动态链接的库。运行时需要加载。
- sdk_version:指定 SDK 版本。我建议尽量用
"current",除非你有特殊需求。
我曾经踩过的坑: 有一次我把 static_libs 和 shared_libs 搞混了。结果编译出来的库在设备上跑不起来,因为缺少动态库。排查了半天才发现是链接方式错了。所以写的时候一定要想清楚:这个依赖是编译时就要,还是运行时才要?
条件编译与变量
Blueprint 不支持 Make 那种 ifeq 条件判断。那怎么办?
答案是:用 Go 代码控制。
Soong 允许你在 Android.bp 中使用 soong_config 模块,或者直接在 Go 代码里写条件逻辑。
举个例子,如果你只想在 debug 版本中编译某些代码:
cc_library {
name: "libdebug_utils",
srcs: ["debug.cpp"],
enabled: false,
}
cc_library {
name: "librelease_utils",
srcs: ["release.cpp"],
enabled: true,
}
然后在 Go 代码中根据 eng 或 user 变体来决定启用哪个模块。
嗯,这种方式确实比 Make 的 ifeq 要麻烦一点。但好处是逻辑更清晰,不会出现嵌套 10 层的条件判断。
Soong 的构建流程
为了让你更直观地理解 Soong 是怎么工作的,我画了一张图:
流程其实很清晰:
- Soong 读取所有
Android.bp文件,以及通过 Kati 转换后的Android.mk文件。 - 用 Go 解析器生成模块依赖图。
- 输出
.ninja文件。 - Ninja 负责实际执行编译。
为什么要多一层 Ninja?因为 Ninja 是专门为增量编译设计的。它只编译有变化的文件,速度极快。
总结一下
这一章我们聊了:
- 为什么 Google 要从 Make 迁移到 Soong——说白了就是太慢了,而且不好维护。
- Blueprint 语法——其实就是一种结构化的模块描述方式。
- Android.bp 怎么写——从简单的 cc_binary 到带依赖的 cc_library。
- Soong 的构建流程——从 bp 文件到 ninja 再到编译。
我个人觉得,Soong 最大的好处不是快,而是可预测。同样的 Android.bp,在不同环境下编译结果完全一致。这在团队协作中太重要了。
下一章我们会深入 Soong 的模块类型,看看那些常用的模块到底有哪些坑。嗯,到时候我会分享一些我踩过的雷。