3. 构建系统演进

Android 的构建系统并不是一开始就为“百万级文件、多语言、多目标”而设计的。它经历了从 GNU Make 到 Soong,再到 Bazel 的演进。理解这条演进路线,能帮助我们在阅读 Android 源码树时,判断一个模块由谁编译、配置写在哪里、产物如何生成,以及为什么同一棵源码树里会同时存在 Android.mk、Android.bp 和 BUILD.bazel 三类构建描述文件。

核心结论:Make 解决“能构建”,Soong 解决“构建得快且可分析”,Bazel 解决“跨语言、跨平台、可增量、可远程执行”。三者不是简单替代,而是在 Android 源码树中长期共存、逐步迁移。

一、演进背景与动机

早期 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 的演进关系:每一代构建系统在“描述文件、解析引擎、执行引擎、依赖模型”四个维度上的变化。

Android 构建系统演进主线 GNU Make 描述文件:Android.mk 解析:Make 宏展开 执行:Make 自身调度 依赖:文件时间戳 特点:灵活但难分析 典型问题 解析慢 · 增量粗 · 依赖漏 多语言规则难统一 Soong + Ninja 描述文件:Android.bp 解析:Soong(Go) 执行:Ninja 并行调度 依赖:显式模块依赖图 特点:声明式、可分析 典型优势 增量精确 · 并行高效 支持 Java/C++/Rust 等 Bazel 描述文件:BUILD.bazel 解析:Starlark 规则 执行:本地/远程执行 依赖:目标图 + 动作图 特点:封闭、可复现 典型优势 远程缓存 · 远程执行 跨仓库 · 跨平台 · 可扩展 迁移 演进 共存关系:Android.mk 仍可被 Soong 兼容解析;Android.bp 与 BUILD.bazel 在迁移期并行存在

从图中可以看到,三代构建系统的关键变化不在“编译命令”本身,而在依赖模型:Make 依赖文件时间戳,Soong 依赖模块依赖图,Bazel 依赖目标图与动作图。依赖模型越显式,增量构建和远程执行就越安全。

三、迁移趋势与共存策略

Android 源码树目前处于“Soong 为主、Make 兼容、Bazel 试点”的阶段。典型迁移路径是:

  1. 新模块优先写 Android.bp:使用 cc_library、java_library、android_app 等模块类型声明。
  2. 旧模块保留 Android.mk:Soong 提供 androidmk 工具辅助转换,但复杂宏仍需人工改写。
  3. Bazel 在部分子项目试点:通过 BUILD.bazel 描述目标,配合远程缓存与远程执行验证收益。
  4. 统一产物与依赖:无论哪套系统,最终都生成可被上层镜像打包消费的产物。

下面是一个典型的 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 远程缓存后,多台构建机可复用同一份动作结果。

常见误区一:认为 Soong 只是“换了个文件格式”。实际上 Soong 改变了依赖模型,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 迁移范围。构建系统的选择,最终决定了模块的依赖表达方式、增量粒度和可复现能力。