4、构建系统基础概念:Make、Ninja、Soong、Blueprint、Kati的职责与关系
AOSP(Android Open Source Project)的构建系统是Android开发中复杂度最高的基础设施之一。许多开发者初次接触AOSP时,面对make、m、mm、soong_ui、blueprint等命令和术语,往往感到困惑。本章将系统梳理AOSP构建系统中五个核心组件——Make、Ninja、Soong、Blueprint、Kati——各自的职责边界、协作关系以及它们在一次完整构建中的执行顺序。理解这些基础概念,是后续学习模块化构建、增量编译优化、自定义产品配置的基石。
上图展示了AOSP构建系统的整体架构。从顶层到底层,构建请求依次经过Soong_UI(入口)、Kati与Blueprint(并行解析)、Soong(核心生成)、Ninja(最终执行)四个阶段。下面逐一剖析每个组件的职责。
4.1 核心原理:五组件各司其职
4.1.1 Make:历史包袱与兼容层
Make是Unix世界最古老的构建工具之一(1976年由Stuart Feldman发明)。AOSP早期版本(Android 7.0之前)完全基于Make构建,通过Android.mk文件描述模块编译规则。Make的优点是灵活、生态成熟,但缺点是:
- 解析速度慢:AOSP包含数千个
Android.mk,Make需要递归解析所有文件,冷启动耗时可达数分钟。 - 全局变量污染:所有模块共享全局变量,模块间隐式依赖难以追踪。
- 并行效率低:Make的依赖图构建算法(基于文件时间戳)在大规模项目中容易产生错误或重复执行。
因此,从Android 7.0开始,Google引入了Kati作为Make的“翻译器”,但保留了Make语法兼容性。如今,Make本身已不直接参与构建执行,而是通过Kati间接工作。
4.1.2 Kati:Make到Ninja的翻译器
Kati是一个用C++编写的工具,其核心功能是:读取所有Android.mk文件,解析Make语法,生成等价的build.ninja片段。它解决了Make解析慢的问题,因为Kati会缓存解析结果(.kati_stamp文件),增量构建时只需重新解析变更的Android.mk。
Kati的关键设计决策:
- 支持大部分Make语法(包括
ifeq、foreach、call等),但不支持所有GNU Make扩展。 - 将Make的“递归展开”模型转换为Ninja的“扁平依赖图”模型。
- 输出文件为
build.ninja的一部分,通常位于out/build-<product>.ninja。
4.1.3 Blueprint:Android.bp的解析器
Blueprint是Google为AOSP设计的声明式构建配置解析器。它读取Android.bp文件(一种类似JSON的格式),将其转换为Soong可理解的模块定义中间表示(IR)。Blueprint本身不执行构建,只负责“读配置”。
与Make的“过程式”写法不同,Blueprint采用“声明式”风格:
// 示例:Android.bp
cc_binary {
name: "hello_world",
srcs: ["main.cpp"],
shared_libs: ["liblog"],
cflags: ["-Wall"],
}
Blueprint的优势在于:语法简洁、无全局变量、模块间依赖显式声明(通过deps字段),便于静态分析和并行解析。
4.1.4 Soong:构建核心引擎
Soong是AOSP构建系统的“大脑”,负责:
- 接收Blueprint的IR:将
Android.bp中的模块定义转换为具体的构建规则。 - 合并Kati的输出:将
Android.mk生成的Ninja片段与Blueprint生成的规则统一。 - 生成最终
build.ninja:包含所有模块的编译、链接、打包规则。 - 管理全局配置:如
BoardConfig.mk、ProductConfig.mk等。
Soong使用Go语言编写,支持并发解析和缓存,是AOSP构建性能优化的关键组件。
4.1.5 Ninja:最终执行器
Ninja是一个极简的构建执行工具,设计目标是“快”。它不解析任何配置文件,只读取Soong生成的build.ninja,然后根据文件时间戳和依赖关系并行执行命令。Ninja的特点:
- 无语法糖:只支持
rule、build、default等基本指令。 - 并行度极高:默认使用所有CPU核心,通过
-j参数控制。 - 增量构建精准:基于文件内容哈希(而非时间戳)判断是否需要重建。
4.2 判断方法与操作流程:一次完整构建的执行链路
当开发者执行source build/envsetup.sh && lunch <product> && make -j16时,实际发生以下流程:
| 步骤 | 组件 | 输入 | 输出 | 耗时(冷启动) |
|---|---|---|---|---|
| 1 | Soong_UI | 命令行参数、环境变量 | 构建配置(产品、变体) | <1s |
| 2 | Kati | 所有Android.mk | build.ninja片段(Make部分) | 30-90s |
| 3 | Blueprint | 所有Android.bp | 模块IR | 10-20s |
| 4 | Soong | Kati输出 + Blueprint IR | 最终build.ninja | 5-10s |
| 5 | Ninja | build.ninja | 编译产物(.o、.img等) | 数分钟至数小时 |
关键判断点:
- 如果修改了
Android.bp,只需重新运行Blueprint+Soong(步骤3-4),Kati可跳过。 - 如果修改了
Android.mk,则必须重新运行Kati(步骤2)。 - 如果只修改了C++源码,则直接进入Ninja执行阶段(步骤5),前四步全部跳过。
4.3 案例拆解:一次增量编译的排障实战
场景:某团队在AOSP中添加了一个新的系统服务模块,使用Android.bp定义。首次编译成功,但修改源码后执行make systemimage,发现改动未生效。
初始现象:out/target/product/<device>/system/framework/下的jar包时间戳未更新。
分析过程:
- 检查
build.ninja中该模块的规则是否存在:grep "my_service" out/build-<product>.ninja—— 规则存在。 - 检查Ninja是否认为模块需要重建:
ninja -t query out/target/product/<device>/system/framework/my_service.jar—— 输出显示“no work to do”。 - 怀疑是Blueprint缓存问题:删除
out/soong/build.ninja和out/soong/Android.bp缓存,重新执行make。 - 问题依旧。最终发现
Android.bp中srcs字段写的是["src/*.cpp"],但新增的源文件放在src2/目录下,未被通配符匹配。
解决方案:修改Android.bp,将srcs改为["src/*.cpp", "src2/*.cpp"],重新执行make。
复盘经验:
- Blueprint的
srcs通配符不会自动递归子目录,需显式声明。 - 当修改
Android.bp后,务必确认Soong重新生成了build.ninja(可通过ninja -t targets验证)。 - 增量构建失效时,优先检查依赖声明,而非盲目清理输出目录。
4.4 常见误区与总结复盘
误区1:认为Make已完全废弃。实际上,AOSP中仍有大量Android.mk文件(尤其是vendor和硬件相关模块),Kati必须持续维护Make兼容性。
误区2:混淆Blueprint与Soong。Blueprint只负责“读配置”,Soong负责“生成规则”。修改Android.bp后,必须重新运行Soong(通常由make自动触发)。
误区3:手动编辑build.ninja。该文件是自动生成的,手动修改会在下次构建时被覆盖。正确做法是修改Android.bp或Android.mk。
误区4:忽略Kati缓存。Kati的缓存(out/.kati目录)可显著加速增量构建。若缓存损坏,可删除后重新生成,但不要手动修改缓存文件。
总结:AOSP构建系统是一个“分层翻译”架构——Make语法(Kati)和Blueprint声明(Blueprint)被翻译为统一的Ninja规则(Soong),最终由Ninja高效执行。理解这五者的关系,是优化构建速度、排查编译问题的前提。建议开发者熟练掌握以下命令:
# 查看构建配置
m -j16 showcommands
# 查看Ninja依赖图
ninja -t graph out/target/product/<device>/system/bin/app_process
# 清理Soong缓存(不清理输出)
rm -rf out/soong
# 强制重新生成build.ninja
m -j16 nothing