Android 构建系统是整个 AOSP 工程的基础设施。从最初的 GNU Make 体系,到 2015 年后逐步引入的 Soong 和 Ninja,再到如今向 Bazel 的迁移,Android 的构建系统经历了一次次彻底的革新。本文将基于 Android 11 (API 30) 的 AOSP 源码,从构建系统演进的历史脉络出发,深入解析 Soong 和 Ninja 的工作原理、Android.bp 的编写规则以及构建流程的每个关键环节。
一、Android 构建系统演进史
1.1 演进路线图
Android 1.0 ~ 6.0 (2008 ~ 2015): |
1.2 为什么放弃 Make
GNU Make 在 AOSP 中的主要问题:
- 增量构建不可靠:递归 make 中,子目录之间的隐式依赖难以正确追踪
- 解析太慢:完整的 AOSP 有数千个 Android.mk,每一个都需要被 make 解析
- 语法灵活性带来问题:Makefile 是图灵完备的,这意味着无法做静态分析(如列出所有模块)
- 并行度受限:make 的并行是粗粒度的(目录级别),无法充分利用多核 CPU
1.3 为什么选择 Ninja
Ninja 是一个极简构建系统(被设计为”构建系统的汇编语言”):
- 仅支持描述依赖关系和构建规则,没有条件判断、函数等
- 解析快、并行调度优秀
- 被设计为由其他工具生成(而非手写)
二、AOSP 构建流程全览
2.1 构建入口
开发者最常用的构建命令序列:
# 1. 设置环境 |
2.2 envsetup.sh 做了什么
# build/envsetup.sh 核心功能 |
2.3 lunch 命令
# lunch 的核心逻辑(简化版) |
2.4 m 命令 → soong_ui
# m 命令的实际执行流 |
soong_ui 是构建系统的最顶层入口,它负责:
- 设置构建环境
- 调用 Kati 将 Makefile 翻译为 Ninja
- 调用 Soong 处理 Android.bp 并生成 Ninja 规则
- 调用 Ninja 执行实际构建
- 处理构建输出(生成 .img 文件等)
三、Soong 构建系统
3.1 Soong 架构
soong_ui (Go) |
3.2 Android.bp 文件
Android.bp 是 JSON-like 的声明式构建描述文件:
// 示例:一个 C++ 二进制模块 |
3.3 Soong 模块类型
| 模块类型 | 用途 | 输出路径 |
|---|---|---|
cc_binary |
C/C++ 可执行文件 | /system/bin/ |
cc_library |
C/C++ 动态库 (.so) | /system/lib/ 或 /system/lib64/ |
cc_library_static |
C/C++ 静态库 (.a) | 链接时使用(不安装) |
cc_test |
C/C++ 测试可执行文件 | /data/nativetest/ |
java_library |
Java 库 (.jar) | /system/framework/ |
java_library_static |
Java 静态库 | 编译时使用(不安装) |
android_app |
Android APK | /system/app/ 或 /system/priv-app/ |
android_test |
Android 测试 APK | /data/app/ |
prebuilt_etc |
预构建配置文件 | /system/etc/ |
prebuilt_binary |
预构建可执行文件 | /system/bin/ |
filegroup |
文件集合(不构建,仅组织文件) | N/A |
genrule |
通用生成规则 | 自定义 |
sh_binary |
shell 脚本 | /system/bin/ |
rust_binary |
Rust 可执行文件(Android 12+) | /system/bin/ |
rust_library |
Rust 库(Android 12+) | /system/lib/ |
3.4 Blueprint 框架
Soong 构建在 Blueprint 之上,Blueprint 提供了 Android.bp 文件的解析基础设施:
// Blueprint 的核心概念(build/blueprint/) |
Soong 在 Blueprint 之上实现了 Android 特定的模块类型:
// build/soong/cc/binary.go |
四、Ninja 执行引擎
4.1 Ninja 构建文件格式
# Ninja 文件示例(由 Soong/Kati 生成) |
4.2 Ninja 的并行调度
Ninja 的核心调度算法:
- 解析 .ninja 文件,构建 DAG(有向无环图)
- 识别所有就绪(所有依赖已完成)的构建目标
- 按负载分配任务到可用 CPU 核心
- 每个任务完成后检查是否有新就绪的目标
- 重复直到所有目标完成
Ninja 相比于 Make 的并行优势:
- 细粒度任务(单个编译步骤 vs 整个目录)
- 统一的 DAG 视图(不会出现递归 make 的依赖断裂)
- I/O 调度优化(自动检测磁盘队列深度)
4.3 Ninja 文件的生成
# 内部流程(soong_ui 调用链) |
最终生成的 Ninja 文件结构:
out/ |
五、构建目标产品配置
5.1 Product 配置体系
构建一个具体的 Android 系统镜像涉及三层配置:
Product (产品): aosp_arm64 |
5.2 lunch 选项解析
lunch 命令展示的选项来自:
device/<vendor>/<device>/AndroidProducts.mk— 定义了 PRODUCT_MAKEFILESbuild/make/target/product/— AOSP 通用产品定义
以 aosp_arm64-userdebug 为例:
aosp_arm64:产品名,对应build/make/target/product/aosp_arm64.mkuserdebug:构建变体
5.3 构建变体 (Build Variants)
| Variant | ro.debuggable | ro.secure | ADB root | 优化级别 |
|---|---|---|---|---|
| user | false | true | No | 全优化 |
| userdebug | true | true | 可授权 | 中等优化 |
| eng | true | false | Yes | 低/无优化(调试) |
5.4 BoardConfig.mk 关键配置
# device/generic/arm64/BoardConfig.mk (示例) |
六、构建输出物
6.1 构建产物目录结构
out/target/product/<product_name>/ |
6.2 构建单个模块
# 构建当前目录及其子目录中的模块 |
七、Soong 高级特性
7.1 条件编译
Android.bp 通过 Go 模板语言支持条件编译:
// 使用 boolean 变量 |
7.2 Defaults 模块
Defaults 模块允许复用公共配置:
// 定义公共配置 |
7.3 生成器规则 (Genrule)
genrule { |
八、Bazel 迁移
8.1 为什么向 Bazel 迁移
Bazel 是 Google 的内部构建系统 Blaze 的开源版本,Android 正逐步迁移到 Bazel:
Bazel 的优势:
- 真正的增量构建(基于内容哈希的 action cache)
- 远程构建缓存和执行(RBE — Remote Build Execution)
- 可重现构建(hermetic builds)
- 跨语言和多仓库构建的天然支持
迁移路径:
- Android 11+:部分模块支持用 BUILD 文件替代 Android.bp
- 早期迁移:AndroidX、部分 APEX 模块
- 长期目标:整个 AOSP 构建由 Bazel 管理
8.2 Bazel 与 Soong 的共存
在过渡期间,Bazel 和 Soong 共存。Bazel 可以调用 Soong 模块作为依赖,反之亦然:
Bazel BUILD 文件 → 调用 soong_module() → 引用 Soong/Android.bp 中定义的模块 |
九、核心面试题
Q1:Android.mk 和 Android.bp 可以互相转换吗?为什么要推动从 .mk 向 .bp 的迁移?
答:(1) Android.mk 文件由 GNU Make 解析,Make 是图灵完备的,导致无法在构建前做静态分析(无法准确列出所有模块依赖关系)。Android.bp 是声明式的,解析器(Blueprint/Soong)可以做完整的依赖图分析。(2) Make 的增量构建依赖文件时间戳,这在分布式构建中不可靠;.bp+Ninja 使用内容哈希可以做到精确的增量构建。(3) 自动化工具 androidmk 可以将简单的 Android.mk 转换为 Android.bp,但包含复杂 Make 条件逻辑的 .mk 文件需要手动转换。
Q2:m、mm、mma、mmm、mmma 命令之间有什么区别?什么时候用哪个?
答:m 是 make 的顶层入口,构建所有内容。mm 构建当前目录(及子目录)下的所有模块,但不构建依赖(假设依赖已经构建好了)。mma 与 mm 类似但会构建模块所需的依赖项。mmm <path> 和 mmma <path> 与上面类似,但作用于指定路径而非当前目录。建议日常开发使用 mma,它能正确处理依赖变化。如果只想快速编译一个改动的小模块且确定依赖没变,可以用 mm 节省时间。
Q3:Kati 和 Soong 都生成 .ninja 文件,它们生成的内容是如何合并的?为什么需要两个生成器?
答:Soong 处理 Android.bp 文件,Kati 处理剩余的 Android.mk / Makefile 文件。Soong 生成 out/soong/build.ninja,Kati 生成 out/build-<product>.ninja。soong_ui 会生成 out/combined-<product>.ninja 作为两者的组合入口(通过 Ninja 的 subninja 指令 include 两者)。需要两个生成器的原因是:迁移是渐进的,有大量历史遗留的 .mk 文件(尤其是 device/ 和 vendor/ 下的文件),无法一次性全部转换。Soong 最终会完全取代 Kati,但目前两者需要共存。
Q4:如果 Android 源码构建失败,如何快速定位是哪个模块出了问题?
答:(1) 查看错误输出中靠近最后的编译命令和错误信息。(2) 使用 m <module_name> 单独编译失败的模块来隔离问题。(3) 查看 out/error.log(如果存在)。(4) 检查 out/.ninja_log 查看最后执行的构建步骤。(5) 如果错误与具体文件相关,检查该文件所在目录的 Android.bp 是否正确定义了依赖。(6) 使用 showcommands 参数查看完整编译命令:make showcommands <module_name>。(7) 对于 OTA 或 image 打包相关的错误,检查 BoardConfig.mk 中的分区大小是否足够。
Q5:Android 构建系统的”hermetic build”(封闭构建)是什么概念?Soong 是如何实现它的?
答:Hermetic build(封闭构建)是指构建结果只依赖于明确声明的输入,不受构建机器环境(如已安装的工具版本、环境变量)的影响。Soong 通过以下方式实现:(1) 使用预构建的编译工具链(prebuilts/clang/、prebuilts/build-tools/),而非系统安装的版本;(2) 通过 PATH 严格限制为 AOSP 提供的工具;(3) 在构建容器或 chroot 中执行,隔离宿主环境影响;(4) Soong 的沙箱机制可以检测未声明的依赖(如隐式的系统头文件引用)。Hermetic build 是实现可重现构建(reproducible builds)和远程构建缓存(RBE)的基础。
AOSP 核心路径参考:
build/soong/— Soong 构建系统核心(Go 语言实现)build/soong/cc/— C/C++ 模块类型实现build/soong/java/— Java 模块类型实现build/blueprint/— Blueprint 构建系统框架build/make/— 传统 Make 构建系统及 Kati 翻译build/make/core/— Make 构建系统核心 .mk 文件build/make/target/product/— 通用产品定义build/envsetup.sh— 构建环境设置脚本build/soong/soong_ui.bash— soong_ui 入口脚本prebuilts/build-tools/— 预构建的构建工具(kati、ninja 等)prebuilts/clang/— 预构建的 Clang 工具链external/ninja/— Ninja 源码



