8、Soong构建系统深入:Soong编译流程、模块工厂、全局配置(BoardConfig.mk与Soong配置的交互)
好,我们继续往下走。前面几章我们把 Make 和 Soong 的宏观关系理清了,也聊了 Blueprint 和 Ninja 的基本玩法。这一章,咱们得真正钻进 Soong 的肚子里,看看它到底是怎么把 .bp 文件变成编译指令的。
说实话,我刚接触 Soong 那会儿,最头疼的就是搞不清它内部的工作流。明明写了个模块,编译时却报找不到依赖,或者全局配置没生效。后来我花了整整一周,把 Soong 的源码啃了一遍,才算是摸清了门道。今天我把这些经验提炼出来,希望能帮你少走弯路。
8.1 Soong 的编译流程:从 .bp 到 Ninja 文件
Soong 的编译流程,说白了就是三步走:解析 → 加载 → 生成。咱们一步步拆开看。
8.1.1 解析阶段:Blueprint 的活儿
Soong 启动后,首先会调用 Blueprint 的解析器。它会扫描整个源码树,找到所有 Android.bp 文件,然后解析成一颗巨大的抽象语法树(AST)。
嗯,这里要注意:Blueprint 只负责解析语法,它并不关心模块的具体逻辑。它把每个模块定义当成一个 JSON 对象来处理,提取出模块名、类型、属性这些基本信息。
// 一个典型的 Android.bp 文件
cc_library {
name: "libfoo",
srcs: ["foo.cpp"],
shared_libs: ["libbar"],
cflags: ["-Wall"],
}
Blueprint 解析完后,会生成一个 ModuleList,里面包含了所有模块的元数据。这个列表会传给 Soong 的加载器。
8.1.2 加载阶段:Soong 模块工厂登场
加载阶段才是 Soong 真正干活的地方。Soong 会遍历 Blueprint 传过来的模块列表,对每个模块调用对应的模块工厂函数。
模块工厂是什么?说白了,就是一个注册表。Soong 内部维护了一张映射表,把模块类型(比如 cc_library、android_app)和对应的 Go 结构体绑定起来。
核心概念:模块工厂
每个模块类型在 Soong 中都有一个对应的工厂函数。这个函数负责创建模块实例、初始化默认属性、注册编译规则。
举个例子,cc_library 的工厂函数大致长这样:
// 伪代码,展示 Soong 内部逻辑
func init() {
RegisterModuleType("cc_library", CcLibraryFactory)
}
func CcLibraryFactory() Module {
m := &ccLibraryModule{}
m.AddProperties(&ccLibraryProperties{})
return m
}
我在项目中遇到过一个问题:自定义了一个模块类型,但编译时总是报 "unknown module type"。后来发现是忘了在 init() 函数里注册工厂。这个坑我踩过一次,之后就再也没忘过。
8.1.3 生成阶段:输出 Ninja 文件
加载完成后,Soong 会进入生成阶段。它会遍历所有已加载的模块,调用每个模块的 GenerateBuildActions 方法,生成具体的编译规则。
这些规则最终会被写入 build.ninja 文件。Ninja 拿到这个文件后,就开始真正执行编译了。
个人经验:如果你想调试 Soong 的生成过程,可以在编译时加上 SOONG_DEBUG=true 环境变量。这样 Soong 会把中间生成的 Ninja 文件保留下来,方便你分析。
8.2 模块工厂:Soong 的模块化设计
模块工厂是 Soong 最核心的设计之一。它让 Soong 具备了极强的扩展性——你想加一种新的模块类型,只需要写一个工厂函数,注册进去就行了。
8.2.1 模块工厂的工作机制
每个模块工厂函数都返回一个 Module 接口。这个接口定义了模块必须实现的方法:
Name():返回模块名GenerateBuildActions(ctx):生成编译规则Dependencies(ctx):声明依赖关系
Soong 在加载阶段会调用工厂函数创建模块实例,然后调用 Dependencies() 解析依赖,最后调用 GenerateBuildActions() 生成规则。
注意:模块工厂函数只负责创建实例,不负责初始化属性。属性的初始化是在 AddProperties() 方法里完成的。如果你在工厂函数里直接操作属性,很可能会出问题。
8.2.2 自定义模块类型
我曾经在项目中需要一种特殊的编译规则——把多个静态库打包成一个动态库。Soong 默认没有这个模块类型,于是我写了一个自定义模块:
// 自定义模块工厂
func init() {
RegisterModuleType("combined_library", CombinedLibraryFactory)
}
type combinedLibrary struct {
properties struct {
Name string
StaticLibs []string
}
}
func CombinedLibraryFactory() Module {
m := &combinedLibrary{}
m.AddProperties(&m.properties)
return m
}
func (m *combinedLibrary) GenerateBuildActions(ctx ModuleContext) {
// 生成打包规则
ctx.Build(pctx, AndroidBuildParams{
Rule: "CombineStaticLibs",
Inputs: m.properties.StaticLibs,
Output: m.properties.Name + ".so",
})
}
这个例子虽然简单,但足以说明模块工厂的灵活性。你完全可以按照自己的需求,定制任何编译规则。
8.3 全局配置:BoardConfig.mk 与 Soong 的交互
全局配置这块,是很多开发者容易搞混的地方。BoardConfig.mk 是 Make 时代的产物,而 Soong 有自己的配置体系。它们之间怎么交互?我来给你讲清楚。
8.3.1 BoardConfig.mk 的遗留问题
在 Make 时代,BoardConfig.mk 是定义板级配置的核心文件。比如 CPU 架构、内存大小、分区布局,都在这里定义。
到了 Soong 时代,Google 希望逐步淘汰 Make,但完全抛弃 BoardConfig.mk 又不现实——因为很多硬件相关的配置,还是得靠 Make 来定义。
于是,Soong 采用了一种桥接机制:BoardConfig.mk 中定义的变量,会被转换成 Soong 能识别的格式。
8.3.2 Soong 配置的桥接机制
Soong 在编译开始时,会先解析 BoardConfig.mk,提取出关键的配置变量。然后把这些变量写入一个叫 soong.config 的临时文件。
这个文件的内容大致长这样:
{
"Arch": "arm64",
"BoardName": "my_board",
"ProductName": "my_product",
"TARGET_BOARD_PLATFORM": "mt6765"
}
Soong 的 Go 代码会读取这个 JSON 文件,把配置注入到全局的 Config 对象中。之后,所有模块都可以通过 ctx.Config() 来访问这些配置。
关键点:BoardConfig.mk 中的变量,只有被 Soong 显式声明过的,才会被桥接过来。不是所有 Make 变量都能自动生效。
8.3.3 在 .bp 文件中使用全局配置
一旦配置被桥接到 Soong,你就可以在 .bp 文件中使用条件编译了:
cc_library {
name: "libfoo",
srcs: ["foo.cpp"],
// 根据架构选择不同的源文件
arch: {
arm64: {
srcs: ["foo_arm64.cpp"],
},
x86: {
srcs: ["foo_x86.cpp"],
},
},
// 根据板级配置选择编译选项
target: {
board: {
my_board: {
cflags: ["-DMY_BOARD"],
},
},
},
}
我个人习惯把板级相关的配置放在 target.board 下面,这样代码结构清晰,也方便后续维护。
8.3.4 避坑指南:配置冲突
我曾经遇到过一个很头疼的问题:BoardConfig.mk 中定义了一个变量 TARGET_USES_64_BIT := true,但在 Soong 中始终读不到这个值。
后来排查发现,Soong 只桥接了它认识的变量。如果你想让自定义的 Make 变量被 Soong 识别,需要在 build/soong/cc/config/global.go 中显式声明。
避坑指南:如果你在 BoardConfig.mk 中定义了自定义变量,记得在 Soong 的配置文件中注册一下。否则,你可能会花一整天时间 debug,最后发现是变量没传过来。
8.4 知识体系总览
为了让你更直观地理解 Soong 的编译流程和配置交互,我画了一张图:
这张图把 Soong 的三大阶段和配置交互都串起来了。你可以把它当作一个参考地图,遇到问题时回来看看,思路会清晰很多。
8.5 小结
这一章我们深入了 Soong 的内部机制。从编译流程到模块工厂,再到全局配置的交互,每个环节都有它的设计逻辑。
我个人觉得,理解 Soong 的关键在于把握两点:一是它的模块化设计,二是它的桥接机制。前者决定了 Soong 的扩展性,后者决定了它与 Make 的兼容性。
嗯,内容不少,但都是干货。你如果能把这一章吃透,后面再遇到 Soong 相关的问题,基本都能自己搞定了。
最后提醒一句:如果你在项目中遇到 Soong 的诡异问题,不妨先检查一下模块工厂是否注册正确,或者 BoardConfig.mk 的变量是否被桥接。这两个地方,是我见过最容易出问题的点。