Ninja构建系统
m、mm 和 mmm 只负责选择初始目标。真正决定动作依赖、并行顺序和增量重用的,是 Soong/Kati 生成的 Ninja 兼容构建图,以及 Soong UI 选择的执行器。
Android 17 的流程可以分成五步:
- lunch 提供 Product、Release、Variant;
- Soong 解析
Android.bp; - Kati 解析仍保留的
Android.mk; - Soong UI 组合各 Ninja 子图;
- Ninja、Siso、N2 或 Ninjago 执行动作图。
m-mm-mmm编译命令 已说明命令如何选择目标。本文从目标进入 Soong UI 后开始,重点说明生成文件、依赖图、增量判断和诊断入口。
本文面向已经会选择构建目标、但还不能解释“为什么重编”或“为什么没有重编”的读者。文章边界 是 Android 17 的 Soong/Kati 生成、combined Ninja 和执行器消费关系,不展开 Blueprint 模块解析 或远程构建服务内部。源码主线从 Build、createCombinedBuildNinjaFile、runNinjaForBuild 到具体 action;读者应能定位输入、输出、depfile、环境过滤和 executor 的所有者,并知道增量结论 何时只能停留在构建图层。
本文以前置的 m-mm-mmm编译命令 为入口;读者若要观察图生成后的 产物如何进入设备,可继续阅读 模块化编译。
1. 生成执行
1.1 Soong/Kati
| 组件 | 主要输入 | 主要输出 |
|---|---|---|
| Product config | lunch 三元组、产品配置、release flags | 完整构建变量 |
| Soong | Android.bp、module type、defaults | Soong Ninja 文件 |
| Kati | Android.mk 与 Make 兼容配置 | Kati Ninja 文件 |
| Soong UI | Soong/Kati 输出 | Combined Ninja 文件 |
| Executor | Combined DAG | 对象、库、APK、APEX 和镜像 |
Ninja 不理解 Java、C++、AIDL 或 APK。生成器已经把这些语义转换成“输入、输出、命令和依赖”。
1.2 构建入口
Config.SoongNinjaFile 根据当前 product 选择文件名:
源码文件:build/soong/ui/build/config.go
相关函数/类型:SoongNinjaFile
func (c *configImpl) SoongNinjaFile() string {
targetProduct, err := c.TargetProductOrErr()
if err != nil {
return filepath.Join(c.SoongOutDir(), "build.ninja")
}
// 说明:正常产品构建把product写入文件名,
// 不同产品不会共享同一份Soong动作图。
return filepath.Join(
c.SoongOutDir(),
"build."+targetProduct+c.CoverageSuffix()+".ninja",
)
}Soong 还会产生 phony、dist 和 nodist 辅助文件。它们都是生成物,不应手工编辑。
1.3 KatiNinja文件
Kati 为 Make 兼容部分生成主构建和 packaging Ninja 文件。文件名带 Kati suffix,用于区分不同 产品与配置组合。
只要产品仍依赖 Android.mk,最终图就可能同时包含 Soong 与 Kati 输出。迁移到 Android.bp 不表示 整个源码树已经完全移除 Make。
2. Ninja文件
2.1 Combined职责
Soong UI 创建一份 combined 文件,统一设置 builddir 与资源 pool,并通过 subninja 引入:
- Kati build Ninja;
- Kati package Ninja;
- Soong Ninja。
执行器只需要使用 combined 文件作为入口,就能看到完整构建 DAG。
2.2 Combined文件
源码文件:build/soong/ui/build/config.go
相关函数/类型:CombinedNinjaFile
func (c *configImpl) CombinedNinjaFile() string {
if c.katiSuffix == "" {
return filepath.Join(c.OutDir(), "combined.ninja")
}
// 说明:Kati suffix把产品相关配置编码进文件名。
return filepath.Join(
c.OutDir(),
"combined"+c.KatiSuffix()+".ninja",
)
}诊断时不要猜文件名。应从构建日志、Soong config 或 showcommands 确认当前 combined 文件。
3. Ninja语义
3.1 基本元素
下面是概念示例,不是 AOSP 生成文件原文:
rule compile_cpp
command = clang -c INPUT -o OUTPUT
depfile = OUTPUT.d
deps = gcc
build out/obj/example.o: compile_cpp src/example.cpp
build example: phony out/obj/example.o
default example| 元素 | 作用 |
|---|---|
rule | 定义动作模板 |
build | 定义输入、输出和依赖 |
depfile | 补充编译器发现的头文件依赖 |
pool | 限制高成本动作并发 |
phony | 聚合逻辑目标 |
default | 未指定目标时的入口 |
subninja | 引入其他 Ninja 文件 |
3.2 DAG调度
无依赖关系的节点可以并行;依赖链上的动作必须等待前置输出完成。
4. Android17执行器
4.1 执行器选择
当前 config 将默认执行器设为 Siso,并允许 SOONG_NINJA 选择 Ninja、N2、Siso 或 Ninjago:
源码文件:build/soong/ui/build/config.go
var NINJA_DEFAULT ninjaCommandType = NINJA_SISO
switch os.Getenv("SOONG_NINJA") {
case "ninja":
ret.ninjaCommand = NINJA_NINJA
case "n2":
ret.ninjaCommand = NINJA_N2
case "siso":
ret.ninjaCommand = NINJA_SISO
case "ninjago":
ret.ninjaCommand = NINJA_NINJAGO
default:
ret.ninjaCommand = NINJA_DEFAULT
}
// 说明:当前Darwin环境在默认Siso时回退到Ninja。
if runtime.GOOS == "darwin" &&
ret.ninjaCommand == NINJA_SISO {
ret.ninjaCommand = NINJA_NINJA
}因此,“Android 使用 Ninja”更准确地表示 Android 使用 Ninja 兼容动作图。实际执行器由平台和配置 决定。
4.2 规则边界
Soong UI 会把 duplicate build、missing depfile 和 missing output 等问题提升为错误。模块规则应完整 声明输入和输出,不能依赖某个执行器恰好忽略错误。
5. Soong构建
5.1 基础命令参数
源码文件:build/soong/ui/build/ninja.go
相关函数/类型:runNinja
func runNinja(ctx Context, config Config, ninjaArgs []string) {
// ...
executable = config.NinjaBin()
args = []string{
"-d", "keepdepfile",
"-d", "keeprsp",
"-d", "stats",
"--frontend_file", fifo,
"-w", "dupbuild=err",
"-w", "missingdepfile=err",
"-j", strconv.Itoa(parallel),
}
args = append(args, ninjaArgs...)
if config.keepGoing != 1 {
args = append(args, "-k", strconv.Itoa(config.keepGoing))
}
// 说明:combined文件作为完整图入口。
args = append(args, "-f", config.CombinedNinjaFile())
cmd := Command(ctx, config, e, "ninja", executable, args...)
// ...
}不同 executor 的启动参数不同,但都从 config 取得并行度、目标和 combined 图。
5.2 额外Ninja参数
Soong UI 同时读取 NINJA_ARGS 和 NINJA_EXTRA_ARGS:
NINJA_ARGS="-d explain" m SystemUI
NINJA_ARGS="-n" m SystemUI-d explain 解释 dirty 原因;-n 只模拟动作。不要把诊断变量长期写入 shell profile。
6. 增量构建依据
6.1 输入输出
生成器必须把真实输入写入 Ninja 图。C/C++ 动作通常通过 depfile 补充头文件依赖。遗漏依赖可能导致 修改后不重编;不稳定依赖则会导致每次重编。
6.2 命令行变化
编译器、宏、flags 和输出路径属于动作身份。会影响输出的配置必须进入命令或显式输入。
6.3 环境变量
Ninja 不会因为任意环境变量变化自动重建。Soong UI 因此限制执行环境,并把可见变量写入 ninja.environment。真正影响输出的变量应由生成器复制到命令或依赖文件。
6.4 日志
.ninja_log 与 deps log 用于增量和诊断。它们是派生状态,不是源码事实,也不应在不了解影响时随意 删除。
7. 诊断工具
7.1 常用subtools
| 命令 | 作用 |
|---|---|
-t targets | 列出目标 |
-t commands | 列出重建目标所需命令 |
-t query | 查询直接输入和输出 |
-t inputs | 递归列出输入 |
-t deps | 查看 deps log |
-t graph | 输出 Graphviz DAG |
-t path / paths | 查询依赖路径 |
-t compdb | 导出 compilation database |
-t ninja_files | 列出递归载入文件 |
通常通过 m 加 NINJA_ARGS 使用诊断选项,避免绕过 Soong UI 环境。直接调用 Ninja 时必须指定正确 combined 文件。
7.2 showcommands
showcommands 使用 Ninja -t commands 输出目标命令。需要重新生成构建图时,可通过 NINJA_ARGS="-t commands ..." m 进入正常 Soong UI 流程。
8. 常见问题
8.1 重复构建
使用 -d explain 查看 dirty 原因。常见原因是生成文件时间戳变化、命令变化、depfile 不稳定、环境 状态变化或输出缺失。
8.2 问题定位
检查 query、inputs 和 deps。真实输入没有进入图时,应修复 Android.bp/Android.mk 或生成器,而不是 靠删除 out 目录维持正确性。
8.3 Ninja编辑风险
生成物会被覆盖。修改应落在模块描述、产品配置或生成器。
8.4 日志显示Siso
Siso 是执行器选择,不表示 Ninja 图消失。诊断参数是否支持要查看对应 executor。
9. 增量构建图
遇到“为什么重编”时,从 -d explain 的第一条 dirty 原因向输入方向回溯;遇到“为什么没重编”时, 从目标的 query、inputs 和 depfile 向真实源码输入核对。两类问题方向相反,但都要落到同一组图事实: 显式输入、隐式输入、命令身份、输出和 consumer。
Soong UI 的配置与环境测试约束了 executor 参数、受控环境和辅助目标的生成逻辑;产品构建则在这些 契约上继续加入实际源码、host tools、lunch 配置与模块依赖。诊断时应先确认当前 combined 文件和 executor,再决定问题属于模块描述、生成器还是执行 action。
一个健康的增量图应同时满足:真实输入变化会使正确 action 变 dirty,无关变化不会扩大重编范围, 缺失输出会被重新生成,命令或关键配置变化会进入动作身份。依靠手工删除 out 才能维持这些行为, 说明图的输入输出契约仍有缺口。
10. 构建图测试
第一组验证在固定目标上修改一个真实 Android.bp 输入,运行 ninja -d explain,断言包含该输入的 action 变 dirty,而与它无依赖的目标不被牵连;这证明 depfile/DAG 的局部传播,不证明产品全量构建 正确。第二组只改变一个被 Soong UI 允许传递的关键环境变量,比较 combined Ninja 中 action command 和 dirty 原因,断言命令身份变化会触发重建;这证明环境过滤与动作身份的边界,不证明任意环境变量 都会生效。两组结果都应保存目标、输入、输出和 executor 名称,避免把 Siso 或 Ninja 的差异误认为 图生成差异。
