27、AOSP构建系统源码分析:Soong源码结构、Blueprint源码解析、构建流程源码跟踪

说实话,AOSP的构建系统,很多人用了好几年,也只是停留在mmmmmasource build/envsetup.sh这些命令层面。一旦遇到编译报错,或者想加个自定义模块,就抓瞎了。

我个人习惯是,遇到问题先翻源码。Soong和Blueprint这套东西,说白了就是Google用Go语言重写的构建引擎。今天我就带你从源码层面,把Soong的骨架、Blueprint的解析逻辑,以及整个构建流程的源码链路,彻底捋一遍。

Soong的源码结构:它到底长什么样?

Soong的代码在AOSP根目录下的build/soong/里。你进去看一眼,会发现它其实是个Go项目。我刚开始看的时候,也被一堆包名搞晕过。后来我总结了一下,核心就这几个部分:

目录/文件 作用 我的一点体会
cmd/ Soong的主入口,比如soong_buildsoong_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_libraryandroid_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的解析流程

  1. 词法分析:Blueprint的解析器在parser/目录下。它会把Android.bp文件读进来,拆成一个个Token。比如cc_library{name":""mylib"
  2. 语法分析:根据Token生成抽象语法树(AST)。每个Android.bp文件会被解析成一个File对象,里面包含多个Module定义。
  3. 模块注册:Blueprint会调用Soong注册的模块工厂函数。比如你写了一个cc_library,Blueprint就会调用cc.RegisterLibraryModule()来创建一个模块实例。
  4. 依赖解析:Blueprint会遍历所有模块的depsshared_libs等属性,构建出一张依赖图。

避坑指南

我曾经在写Android.bp时,不小心写了个循环依赖:A依赖B,B依赖C,C又依赖A。结果Blueprint在解析依赖图时直接报错退出。后来我学乖了,在build/blueprint/depbuilder.go里加了个日志,打印出所有模块的依赖关系,一眼就发现了问题。

构建流程源码跟踪:从source build/envsetup.shninja

整个构建流程,说白了就是一条链:Shell脚本 → Soong UI → Blueprint解析 → Soong生成Ninja → Ninja执行。我带你一步步跟踪源码。

第一步:envsetup.sh干了什么?

这个脚本在build/envsetup.sh。它主要做了三件事:

  • 设置环境变量,比如ANDROID_BUILD_TOPANDROID_PRODUCT_OUT
  • 定义了一些Shell函数,比如lunchmmmmma
  • 调用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会:

  1. 调用Blueprint的解析器,扫描所有Android.bp文件。
  2. 为每个模块调用GenerateBuildActions方法,生成具体的构建动作(比如编译、链接、打包)。
  3. 把这些动作写入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文件的完整链路。

Android.bp 文件 Blueprint 解析器 模块依赖树 Soong 模块工厂 构建动作列表 Ninja 文件生成 Ninja 执行 输出产物 解析 生成 生成 写入

这张图你看懂了吗?其实整个流程并不复杂。Blueprint负责“读”和“理解”,Soong负责“想”和“安排”,Ninja负责“干”。

总结

源码分析这件事,说白了就是“大胆假设,小心求证”。我每次遇到构建问题,都会先看build/soong/build/blueprint/这两个目录。只要搞懂了Module接口和GenerateBuildActions方法,你就能掌控整个构建系统。

嗯,最后提醒一句:别怕看Go代码。AOSP的Go代码写得非常规范,注释也很全。你只要静下心来,跟着soong_buildmain.go走一遍,就能摸清整个脉络。

公众号:蓝海资料掘金营,微信deep3321