3. 构建系统演进
Android 的构建系统并不是一开始就为“百万级文件、多语言、多目标”而设计的。它经历了从 GNU Make 到 Soong,再到 Bazel 的演进。理解这条演进路线,能帮助我们在阅读 Android 源码树时,判断一个模块由谁编译、配置写在哪里、产物如何生成,以及为什么同一棵源码树里会同时存在 Android.mk、Android.bp 和 BUILD.bazel 三类构建描述文件。
一、演进背景与动机
早期 Android 使用 GNU Make 作为构建引擎,模块通过 Android.mk 描述。Make 的优点是灵活、通用、开发者熟悉;缺点是它是“文本宏展开 + 时间戳依赖”的模型,构建规则在解析阶段才真正确定,难以做全局依赖分析。随着 Android 代码量增长,Make 构建出现几个突出问题:
- 全量解析慢:每次构建都要解析大量
Android.mk,即使只编译一个模块也要付出解析成本。 - 依赖不精确:Make 依赖文件时间戳,头文件、生成文件、隐式依赖容易漏判,导致“改一行、编全树”。
- 难以并行与增量:规则之间缺少显式依赖图,增量构建和分布式构建难以安全实施。
- 多语言支持弱:Java、Kotlin、C/C++、Rust、Protobuf 等混合编译时,Make 规则难以统一表达。
Soong 的动机正是把“构建描述”从命令式 Make 脚本,升级为声明式的 Android.bp,并用 Go 编写的构建引擎生成 Ninja 文件。Ninja 负责高效执行,Soong 负责解析、分析和生成依赖图。这样既保留了 Ninja 的执行速度,又获得了可静态分析的模块模型。
Bazel 的动机则更进一步:Android 希望构建系统具备跨仓库、跨平台、可远程缓存与远程执行的能力。Bazel 的 BUILD.bazel 使用 Starlark 语言,强调 hermetic(封闭)构建、显式依赖、可复现和可扩展。它适合大型多语言仓库,也便于与 CI/CD、远程执行集群结合。
二、三代构建系统的核心差异
下面这张图展示了从 Make 到 Soong 再到 Bazel 的演进关系:每一代构建系统在“描述文件、解析引擎、执行引擎、依赖模型”四个维度上的变化。
从图中可以看到,三代构建系统的关键变化不在“编译命令”本身,而在依赖模型:Make 依赖文件时间戳,Soong 依赖模块依赖图,Bazel 依赖目标图与动作图。依赖模型越显式,增量构建和远程执行就越安全。
三、迁移趋势与共存策略
Android 源码树目前处于“Soong 为主、Make 兼容、Bazel 试点”的阶段。典型迁移路径是:
- 新模块优先写
Android.bp:使用cc_library、java_library、android_app等模块类型声明。 - 旧模块保留
Android.mk:Soong 提供androidmk工具辅助转换,但复杂宏仍需人工改写。 - Bazel 在部分子项目试点:通过
BUILD.bazel描述目标,配合远程缓存与远程执行验证收益。 - 统一产物与依赖:无论哪套系统,最终都生成可被上层镜像打包消费的产物。
下面是一个典型的 Android.bp 模块声明,可以看到它比 Android.mk 更接近“数据结构”而非“脚本”:
cc_library {
name: "libdemo",
srcs: ["demo.cpp"],
shared_libs: ["libbase"],
cflags: ["-Wall"],
visibility: ["//vendor:__subpackages__"],
}
对应的 Bazel 目标则更强调显式依赖与可复现:
cc_library(
name = "demo",
srcs = ["demo.cpp"],
deps = ["//libs/base"],
visibility = ["//visibility:public"],
)
四、案例与常见误区
案例:某团队在 Android 14 源码树上新增一个 C++ 库,最初直接复制旧模块的 Android.mk,结果每次修改头文件都会触发大量重编。改为 Android.bp 后,Soong 能精确识别 shared_libs 与 header_libs 依赖,增量构建时间从数分钟降到数十秒。进一步在 CI 中引入 Bazel 远程缓存后,多台构建机可复用同一份动作结果。
Android.bp 中未声明的依赖不会被隐式继承,漏写 shared_libs 会直接编译失败。常见误区二:认为 Bazel 会立刻取代 Soong。Android 体量巨大,迁移是长期过程,短期内三者共存是常态。
常见误区三:把
Android.mk 的宏逻辑直接翻译成 Android.bp。Soong 不支持 Make 宏,复杂逻辑应改用 Soong 的 soong_config_module_type 或生成式规则。
五、三代构建系统对比清单
| 维度 | Make | Soong | Bazel |
|---|---|---|---|
| 描述文件 | Android.mk | Android.bp | BUILD.bazel |
| 描述语言 | Make 宏 | Blueprint(类 JSON) | Starlark(Python 风格) |
| 执行引擎 | Make | Ninja | Bazel 自身 |
| 依赖模型 | 文件时间戳 | 模块依赖图 | 目标图 + 动作图 |
| 增量构建 | 粗粒度 | 精确 | 精确 + 远程缓存 |
| 远程执行 | 不支持 | 有限支持 | 原生支持 |
| 当前定位 | 兼容旧模块 | Android 主力构建 | 试点与未来方向 |
理解这条演进线后,再阅读 Android 源码树时,就能快速判断:看到 Android.mk 说明是历史模块,看到 Android.bp 说明是 Soong 主力模块,看到 BUILD.bazel 说明该目录已进入 Bazel 迁移范围。构建系统的选择,最终决定了模块的依赖表达方式、增量粒度和可复现能力。