目录
  1. 1. 一、Android 构建系统演进史
    1. 1.1. 1.1 演进路线图
    2. 1.2. 1.2 为什么放弃 Make
    3. 1.3. 1.3 为什么选择 Ninja
  2. 2. 二、AOSP 构建流程全览
    1. 2.1. 2.1 构建入口
    2. 2.2. 2.2 envsetup.sh 做了什么
    3. 2.3. 2.3 lunch 命令
    4. 2.4. 2.4 m 命令 → soong_ui
  3. 3. 三、Soong 构建系统
    1. 3.1. 3.1 Soong 架构
    2. 3.2. 3.2 Android.bp 文件
    3. 3.3. 3.3 Soong 模块类型
    4. 3.4. 3.4 Blueprint 框架
  4. 4. 四、Ninja 执行引擎
    1. 4.1. 4.1 Ninja 构建文件格式
    2. 4.2. 4.2 Ninja 的并行调度
    3. 4.3. 4.3 Ninja 文件的生成
  5. 5. 五、构建目标产品配置
    1. 5.1. 5.1 Product 配置体系
    2. 5.2. 5.2 lunch 选项解析
    3. 5.3. 5.3 构建变体 (Build Variants)
    4. 5.4. 5.4 BoardConfig.mk 关键配置
  6. 6. 六、构建输出物
    1. 6.1. 6.1 构建产物目录结构
    2. 6.2. 6.2 构建单个模块
  7. 7. 七、Soong 高级特性
    1. 7.1. 7.1 条件编译
    2. 7.2. 7.2 Defaults 模块
    3. 7.3. 7.3 生成器规则 (Genrule)
  8. 8. 八、Bazel 迁移
    1. 8.1. 8.1 为什么向 Bazel 迁移
    2. 8.2. 8.2 Bazel 与 Soong 的共存
  9. 9. 九、核心面试题
【深入内核篇】编译系统入门

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):
GNU Make (Android.mk + Makefile)
问题:递归 make 导致增量编译不稳定、配置文件过于灵活难以分析

Android 7.0 ~ 8.0 (2016 ~ 2017):
Make + Ninja 混合
Android.mk 被翻译为 .ninja 文件
Ninja 替代 make 作为执行引擎

Android 8.0 ~ 11.0 (2017 ~ 2020):
Soong (Android.bp) + Kati + Ninja
新的构建描述语言(Android.bp)替代 Makefile
Kati 将剩余的 Makefile 翻译为 .ninja
Soong 直接生成 .ninja 规则

Android 11+ ~ 未来:
Soong + Bazel 迁移
Google 正逐步将 构建系统迁移到 Bazel
目标是统一的跨语言构建系统

1.2 为什么放弃 Make

GNU Make 在 AOSP 中的主要问题:

  1. 增量构建不可靠:递归 make 中,子目录之间的隐式依赖难以正确追踪
  2. 解析太慢:完整的 AOSP 有数千个 Android.mk,每一个都需要被 make 解析
  3. 语法灵活性带来问题:Makefile 是图灵完备的,这意味着无法做静态分析(如列出所有模块)
  4. 并行度受限:make 的并行是粗粒度的(目录级别),无法充分利用多核 CPU

1.3 为什么选择 Ninja

Ninja 是一个极简构建系统(被设计为”构建系统的汇编语言”):

  • 仅支持描述依赖关系和构建规则,没有条件判断、函数等
  • 解析快、并行调度优秀
  • 被设计为由其他工具生成(而非手写)

二、AOSP 构建流程全览

2.1 构建入口

开发者最常用的构建命令序列:

# 1. 设置环境
source build/envsetup.sh

# 2. 选择构建目标
lunch aosp_arm64-userdebug

# 3. 构建
m

2.2 envsetup.sh 做了什么

# build/envsetup.sh 核心功能
source build/envsetup.sh
# 定义了大量 shell 函数:
# lunch — 选择 target product 和 build variant
# m — 顶层构建包装器(调用 soong_ui)
# mm — 构建当前目录下的模块
# mma — 构建当前目录下的模块及其依赖
# mmm — 构建指定目录下的模块
# croot — 回到 aosp 根目录
# cgrep — 在所有 C/C++ 文件中 grep
# jgrep — 在所有 Java 文件中 grep
# godir — 跳转到包含指定文件的目录
# add_lunch_combo — 添加 lunch 选项

2.3 lunch 命令

# lunch 的核心逻辑(简化版)
function lunch() {
# 1. 解析参数:product_name-build_variant
# 如:aosp_arm64-userdebug
local product=$1

# 2. 设置环境变量
export TARGET_PRODUCT=aosp_arm64
export TARGET_BUILD_VARIANT=userdebug
export TARGET_BUILD_TYPE=release
export TARGET_ARCH=arm64
export TARGET_ARCH_VARIANT=armv8-a
export TARGET_CPU_VARIANT=generic
export TARGET_2ND_ARCH=arm
export TARGET_2ND_ARCH_VARIANT=armv7-a-neon
export TARGET_2ND_CPU_VARIANT=generic

# 3. 设置 PATH(包含 host tools)
# soong_ui 所在的路径
set_stuff_for_environment
}

2.4 m 命令 → soong_ui

# m 命令的实际执行流
function m() {
# 调用 soong_ui.bash 并传递参数
# soong_ui 是 Go 语言编写的构建入口程序
build/soong/soong_ui.bash --make-mode "$@"
}

soong_ui 是构建系统的最顶层入口,它负责:

  1. 设置构建环境
  2. 调用 Kati 将 Makefile 翻译为 Ninja
  3. 调用 Soong 处理 Android.bp 并生成 Ninja 规则
  4. 调用 Ninja 执行实际构建
  5. 处理构建输出(生成 .img 文件等)

三、Soong 构建系统

3.1 Soong 架构

soong_ui (Go)
├── 环境设置
├── 调用 Kati(Make → Ninja)
├── 调用 Soong(Android.bp → Ninja)
│ └── Blueprint (构建系统框架)
│ ├── 解析 Android.bp
│ ├── 依赖分析
│ └── 生成 .ninja
└── 调用 Ninja(执行构建)

3.2 Android.bp 文件

Android.bp 是 JSON-like 的声明式构建描述文件:

// 示例:一个 C++ 二进制模块
cc_binary {
name: "my_native_daemon",
srcs: [
"main.cpp",
"config_parser.cpp",
],
shared_libs: [
"libcutils",
"libutils",
"libbinder",
],
static_libs: [
"libbase",
],
cflags: [
"-Wall",
"-Werror",
"-DLOG_TAG=\"MyDaemon\"",
],
init_rc: ["my_daemon.rc"],
// 安装到 /system/bin/
vendor: false,
owner: "platform",
}

// 示例:一个 Java 库模块
java_library {
name: "framework-minus-apex",
srcs: [
"core/java/**/*.java",
],
libs: [
"android-hidden-api",
],
static_libs: [
"app-compat-annotations",
],
}

// 示例:预构建模块
prebuilt_etc {
name: "my_config.xml",
src: "my_config.xml",
sub_dir: "myapp",
}

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/)
// 1. Context:构建上下文(类似 Soong 的 singleton 对象)
// 2. Module:构建模块的基本单元(每个 Android.bp 中的定义对应一个 Module)
// 3. ModuleContext:模块的构建上下文(包含依赖信息)
// 4. Singleton:在模块构建之后运行的全局操作(如生成 image 文件)

// dependencies 是在 Blueprint 层的核心操作
// 每个 Module 通过 DepsMutator 声明对其他 Module 的依赖

Soong 在 Blueprint 之上实现了 Android 特定的模块类型:

// build/soong/cc/binary.go
// cc_binary 的 Go 实现
type binaryDecorator struct {
// ...
}

func (binary *binaryDecorator) GenerateAndroidBuildActions(ctx ModuleContext) {
// 生成编译、链接、strip 等构建动作
// 输出:out/soong/.intermediates/<path>/<name>/<name>.so
// 安装路径:out/target/product/<product>/system/bin/<name>
}

四、Ninja 执行引擎

4.1 Ninja 构建文件格式

# Ninja 文件示例(由 Soong/Kati 生成)
# 变量定义
cc = prebuilts/clang/host/linux-x86/clang-r383902/bin/clang
ar = prebuilts/clang/host/linux-x86/clang-r383902/bin/llvm-ar
out = out/target/product/generic_arm64

# 构建规则
rule cc
command = $cc $cflags -c $in -o $out
description = CC $out

rule ar
command = $ar cr $out $in
description = AR $out

rule link
command = $cc $ldflags $in -o $out
description = LINK $out

# 构建目标(build edge)
build $out/obj/main.o: cc main.cpp
cflags = -Wall -O2

build $out/obj/utils.o: cc utils.cpp
cflags = -Wall -O2

build $out/libutils.a: ar $out/obj/main.o $out/obj/utils.o

build $out/my_binary: link $out/obj/main.o $out/libutils.a
ldflags = -lpthread

4.2 Ninja 的并行调度

Ninja 的核心调度算法:

  1. 解析 .ninja 文件,构建 DAG(有向无环图)
  2. 识别所有就绪(所有依赖已完成)的构建目标
  3. 按负载分配任务到可用 CPU 核心
  4. 每个任务完成后检查是否有新就绪的目标
  5. 重复直到所有目标完成

Ninja 相比于 Make 的并行优势:

  • 细粒度任务(单个编译步骤 vs 整个目录)
  • 统一的 DAG 视图(不会出现递归 make 的依赖断裂)
  • I/O 调度优化(自动检测磁盘队列深度)

4.3 Ninja 文件的生成

# 内部流程(soong_ui 调用链)
# 1. kati 将 .mk 文件翻译为 build-<product>.ninja
# build/make/core/main.mk → kati → out/build-<product>.ninja
prebuilts/build-tools/linux-x86/bin/ckati \
-f build/make/core/main.mk \
--ninja \
--ninja_dir=out \
--ninja_suffix=.ninja

# 2. soong 处理所有 Android.bp 生成 out/soong/build.ninja
out/soong_ui --make-mode

# 3. 生成组合 ninja 文件 out/combined-<product>.ninja

最终生成的 Ninja 文件结构:

out/
├── combined-<product>.ninja # 主入口(include 其他)
├── build-<product>.ninja # 来自 Makefile
├── soong/
│ └── build.ninja # 来自 Soong/Android.bp
└── .ninja_log # 构建日志(用于增量构建)

五、构建目标产品配置

5.1 Product 配置体系

构建一个具体的 Android 系统镜像涉及三层配置:

Product (产品): aosp_arm64
├── Device (设备): generic_arm64
│ └── BoardConfig.mk — 板级配置
├── Board (板卡): (定义内核、分区大小等)
└── Architecture (架构): arm64 + arm

5.2 lunch 选项解析

lunch 命令展示的选项来自:

  • device/<vendor>/<device>/AndroidProducts.mk — 定义了 PRODUCT_MAKEFILES
  • build/make/target/product/ — AOSP 通用产品定义

aosp_arm64-userdebug 为例:

  • aosp_arm64:产品名,对应 build/make/target/product/aosp_arm64.mk
  • userdebug:构建变体

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 (示例)
# 架构
TARGET_ARCH := arm64
TARGET_ARCH_VARIANT := armv8-a
TARGET_CPU_VARIANT := generic
TARGET_2ND_ARCH := arm
TARGET_2ND_ARCH_VARIANT := armv7-a-neon

# 分区大小
BOARD_SYSTEMIMAGE_PARTITION_SIZE := 1610612736 # 1.5GB
BOARD_USERDATAIMAGE_PARTITION_SIZE := 576716800 # 550MB

# 内核
TARGET_NO_KERNEL := true # 使用预构建内核

# 文件系统
TARGET_USERIMAGES_USE_EXT4 := true
BOARD_FLASH_BLOCK_SIZE := 4096

# SELinux
BOARD_SEPOLICY_DIRS += device/generic/arm64/sepolicy

六、构建输出物

6.1 构建产物目录结构

out/target/product/<product_name>/
├── system.img # 系统镜像(主)
├── vendor.img # 厂商镜像
├── product.img # 产品镜像
├── system_ext.img # system 扩展镜像
├── userdata.img # 用户数据镜像
├── boot.img # 启动镜像(kernel + ramdisk)
├── ramdisk.img # RAM disk
├── recovery.img # 恢复镜像
├── super.img # 动态分区超级镜像(Android 10+)
├── vbmeta.img # Verified Boot 元数据
├── obj/
│ ├── EXECUTABLES/ # 可执行文件中间产物
│ ├── SHARED_LIBRARIES/# .so 中间产物
│ ├── STATIC_LIBRARIES/# .a 中间产物
│ ├── APPS/ # APK 中间产物
│ ├── JAVA_LIBRARIES/ # .jar 中间产物
│ └── PACKAGING/ # 打包中间文件
├── system/ # system 分区文件树
│ ├── bin/
│ ├── lib/
│ ├── lib64/
│ ├── etc/
│ └── framework/
├── root/ # ramdisk 文件树
└── data/ # data 分区文件树

6.2 构建单个模块

# 构建当前目录及其子目录中的模块
mma

# 构建指定目录下的模块
mmma <path>

# 构建指定模块(需要知道模块名)
make <module_name>

# 构建并推送到设备
make <module_name>
adb sync
# 或者
make <module_name>
adb push <output_path> /system/...

# 构建 boot.img 并快速刷入
m bootimage && adb reboot bootloader && fastboot flash boot out/.../boot.img

七、Soong 高级特性

7.1 条件编译

Android.bp 通过 Go 模板语言支持条件编译:

// 使用 boolean 变量
cc_library {
name: "libfoo",
srcs: ["foo.cpp"],
// 动态启用 neon 优化
arch: {
arm: {
srcs: ["foo_neon.cpp"],
cflags: ["-mfpu=neon"],
},
},
// 根据目标操作系统过滤
target: {
android: {
srcs: ["foo_android.cpp"],
},
linux_glibc: {
srcs: ["foo_linux.cpp"],
},
},
}

7.2 Defaults 模块

Defaults 模块允许复用公共配置:

// 定义公共配置
cc_defaults {
name: "my_defaults",
cflags: [
"-Wall",
"-Werror",
"-DANDROID",
],
shared_libs: [
"liblog",
"libcutils",
],
header_libs: [
"libbase_headers",
],
}

// 使用 defaults
cc_binary {
name: "my_binary1",
defaults: ["my_defaults"],
srcs: ["binary1.cpp"],
}

cc_binary {
name: "my_binary2",
defaults: ["my_defaults"],
srcs: ["binary2.cpp"],
}

7.3 生成器规则 (Genrule)

genrule {
name: "generate_version_header",
srcs: ["version.txt"],
out: ["version.h"],
cmd: "echo 'static const char* VERSION = \"$(cat $(in))\"';' > $(out)",
}

cc_binary {
name: "my_app",
srcs: ["main.cpp"],
generated_headers: ["generate_version_header"],
}

八、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 中定义的模块
Soong Android.bp → 依赖 Bazel 模块 → 通过 bazel_module 引用

九、核心面试题

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:mmmmmammmmmma 命令之间有什么区别?什么时候用哪个?

答:m 是 make 的顶层入口,构建所有内容。mm 构建当前目录(及子目录)下的所有模块,但不构建依赖(假设依赖已经构建好了)。mmamm 类似但会构建模块所需的依赖项。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 源码
打赏
  • 微信
  • 支付宝

评论