27、AOSP构建系统源码分析:Soong源码结构、Blueprint源码解析、构建流程源码跟踪
说实话,AOSP的构建系统,很多人用了好几年,也只是停留在mmm、mma、source build/envsetup.sh这些命令层面。一旦遇到编译报错,或者想加个自定义模块,就抓瞎了。
我个人习惯是,遇到问题先翻源码。Soong和Blueprint这套东西,说白了就是Google用Go语言重写的构建引擎。今天我就带你从源码层面,把Soong的骨架、Blueprint的解析逻辑,以及整个构建流程的源码链路,彻底捋一遍。
Soong的源码结构:它到底长什么样?
Soong的代码在AOSP根目录下的build/soong/里。你进去看一眼,会发现它其实是个Go项目。我刚开始看的时候,也被一堆包名搞晕过。后来我总结了一下,核心就这几个部分:
| 目录/文件 | 作用 | 我的一点体会 |
|---|---|---|
cmd/ |
Soong的主入口,比如soong_build、soong_ui |
编译时最先跑的就是这里 |
android/ |
核心模块类型定义,比如Module接口 |
所有Android.bp里的模块,最终都会实现这个接口 |
bp2build/ |
把Android.bp转换成Bazel用的BUILD文件 |
这是Google内部迁移到Bazel的桥接层,目前外部用得少 |
ui/ |
构建UI逻辑,比如打印进度、处理--make参数 |
你看到的ninja进度条,就是这里控制的 |
scripts/ |
一些辅助脚本,比如soong_zip |
打包用的,偶尔需要调试 |
嗯,这里要注意:Soong本身并不直接解析Android.bp文件。它把解析工作交给了Blueprint。Soong只负责定义“模块是什么”以及“怎么构建”。
核心接口:android.Module
在build/soong/android/module.go里,定义了Module接口。所有你在Android.bp里写的cc_library、android_app,最终都会生成一个实现了这个接口的结构体。
// 简化后的Module接口
type Module interface {
Name() string
GenerateBuildActions(ctx ModuleContext)
// ... 还有其他方法
}
我当年调试一个自定义模块时,就是在这里加了日志,才搞明白GenerateBuildActions被调用的时机。
Blueprint源码解析:Android.bp是怎么变成Ninja文件的?
Blueprint的代码在build/blueprint/下。它的核心任务就一个:把Android.bp解析成一颗“模块依赖树”,然后交给Soong去生成ninja文件。
为什么会这样设计?你想想看,如果Soong既要做解析又要做构建,代码会变得非常臃肿。Blueprint把“解析”和“构建”解耦了。
Blueprint的解析流程
- 词法分析:Blueprint的解析器在
parser/目录下。它会把Android.bp文件读进来,拆成一个个Token。比如cc_library、{、name、":"、"mylib"。 - 语法分析:根据Token生成抽象语法树(AST)。每个
Android.bp文件会被解析成一个File对象,里面包含多个Module定义。 - 模块注册:Blueprint会调用Soong注册的模块工厂函数。比如你写了一个
cc_library,Blueprint就会调用cc.RegisterLibraryModule()来创建一个模块实例。 - 依赖解析:Blueprint会遍历所有模块的
deps、shared_libs等属性,构建出一张依赖图。
避坑指南
我曾经在写Android.bp时,不小心写了个循环依赖:A依赖B,B依赖C,C又依赖A。结果Blueprint在解析依赖图时直接报错退出。后来我学乖了,在build/blueprint/depbuilder.go里加了个日志,打印出所有模块的依赖关系,一眼就发现了问题。
构建流程源码跟踪:从source build/envsetup.sh到ninja
整个构建流程,说白了就是一条链:Shell脚本 → Soong UI → Blueprint解析 → Soong生成Ninja → Ninja执行。我带你一步步跟踪源码。
第一步:envsetup.sh干了什么?
这个脚本在build/envsetup.sh。它主要做了三件事:
- 设置环境变量,比如
ANDROID_BUILD_TOP、ANDROID_PRODUCT_OUT。 - 定义了一些Shell函数,比如
lunch、mmm、mma。 - 调用
lunch,选择目标设备。
嗯,这里要注意:lunch其实会调用build/soong/soong_ui.bash,这才是真正的入口。
第二步:Soong UI启动
soong_ui.bash会启动build/soong/cmd/soong_ui/main.go。这个Go程序会:
- 读取
AndroidProducts.mk,确定要编译的产品。 - 调用
build/soong/cmd/soong_build/main.go,开始真正的构建。
第三步:Soong Build生成Ninja文件
这是最核心的一步。soong_build会:
- 调用Blueprint的解析器,扫描所有
Android.bp文件。 - 为每个模块调用
GenerateBuildActions方法,生成具体的构建动作(比如编译、链接、打包)。 - 把这些动作写入
out/soong/build.ninja文件。
关键代码位置
在build/soong/cmd/soong_build/main.go里,有一个run()函数。你可以在这里打断点,观察整个构建流程的启动过程。
func run(ctx *blueprint.Context, config android.Config) {
// 1. 注册所有模块类型
android.RegisterModuleTypes(ctx)
// 2. 解析Android.bp文件
ctx.ParseFileList(config.BpFiles())
// 3. 生成构建动作
ctx.ResolveDependencies()
// 4. 写入Ninja文件
ctx.WriteBuildFile(config.NinjaFile())
}
第四步:Ninja执行
最后,soong_ui会调用ninja,读取build.ninja文件,并行执行所有编译任务。你看到的编译进度条,就是Ninja在干活。
注意
Ninja文件是纯文本的,你可以直接打开out/soong/build.ninja看看。里面记录了每个模块的编译命令、输入文件、输出文件。我经常在这里手动修改命令来调试编译问题,但记得改完后要重新运行m,因为Soong会重新生成Ninja文件,覆盖你的修改。
核心逻辑流程图
下面这张图,是我根据源码梳理出来的。它展示了从Android.bp到最终.so、.apk文件的完整链路。
这张图你看懂了吗?其实整个流程并不复杂。Blueprint负责“读”和“理解”,Soong负责“想”和“安排”,Ninja负责“干”。
总结
源码分析这件事,说白了就是“大胆假设,小心求证”。我每次遇到构建问题,都会先看build/soong/和build/blueprint/这两个目录。只要搞懂了Module接口和GenerateBuildActions方法,你就能掌控整个构建系统。
嗯,最后提醒一句:别怕看Go代码。AOSP的Go代码写得非常规范,注释也很全。你只要静下心来,跟着soong_build的main.go走一遍,就能摸清整个脉络。