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_libraryandroid_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 编译流程与配置交互 解析阶段 Blueprint 解析 .bp 文件 加载阶段 模块工厂创建实例 生成阶段 输出 Ninja 文件 模块工厂注册表 cc_library → CcFactory android_app → AppFactory 自定义 → CustomFactory ... 更多类型 BoardConfig.mk 与 Soong 配置交互 BoardConfig.mk 定义板级变量 桥接机制 soong.config 临时文件 Soong Config 全局配置对象

这张图把 Soong 的三大阶段和配置交互都串起来了。你可以把它当作一个参考地图,遇到问题时回来看看,思路会清晰很多。

8.5 小结

这一章我们深入了 Soong 的内部机制。从编译流程到模块工厂,再到全局配置的交互,每个环节都有它的设计逻辑。

我个人觉得,理解 Soong 的关键在于把握两点:一是它的模块化设计,二是它的桥接机制。前者决定了 Soong 的扩展性,后者决定了它与 Make 的兼容性。

嗯,内容不少,但都是干货。你如果能把这一章吃透,后面再遇到 Soong 相关的问题,基本都能自己搞定了。

最后提醒一句:如果你在项目中遇到 Soong 的诡异问题,不妨先检查一下模块工厂是否注册正确,或者 BoardConfig.mk 的变量是否被桥接。这两个地方,是我见过最容易出问题的点。

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