26、构建系统扩展:自定义构建规则、构建插件开发、构建系统API

各位同学,今天我们来聊聊构建系统的扩展能力。说白了,AOSP 的构建系统不是死板的,它给你留了很多「后门」—— 嗯,专业点叫扩展点。我在做系统定制的时候,经常需要加一些奇奇怪怪的编译逻辑,比如给特定模块打上特殊标记,或者生成一些中间产物。这时候,光靠 Soong 内置的规则就不够用了。

为什么会这样?因为每个团队的硬件平台、软件需求都不一样。Google 不可能预判所有场景。所以,学会扩展构建系统,才是真正把 AOSP 玩转的关键一步。

26.1 自定义构建规则:从「用」到「造」

我们先从最基础的自定义构建规则说起。你平时用的 cc_binaryandroid_app 这些,其实都是 Soong 内置的模块类型。但如果你需要一种全新的模块类型呢?比如,我想把一堆图片打包成一个自定义的镜像文件。

这时候,你需要自己写一个 Blueprint 模块类型。我个人的习惯是,先在 build/soong/ 下新建一个目录,比如 my_custom_module,然后写一个 Go 文件。

核心思路:自定义构建规则 = 定义模块类型 + 实现构建逻辑 + 注册到 Soong

来看一个最简单的例子。假设我想定义一个 my_asset_bundle 模块,它把一堆文件打包成一个 zip:

// build/soong/my_custom_module/my_asset_bundle.go
package my_custom_module

import (
    "android/soong/android"
    "android/soong/cc"
)

func init() {
    // 注册模块类型
    android.RegisterModuleType("my_asset_bundle", MyAssetBundleFactory)
}

type MyAssetBundle struct {
    android.ModuleBase
    properties MyAssetBundleProperties
}

type MyAssetBundleProperties struct {
    // 输入文件列表
    Srcs []string
    // 输出文件名
    Stem *string
}

func MyAssetBundleFactory() android.Module {
    m := &MyAssetBundle{}
    m.AddProperties(&m.properties)
    return m
}

func (m *MyAssetBundle) GenerateAndroidBuildActions(ctx android.ModuleContext) {
    // 获取输入文件
    inputs := android.PathsForModuleSrc(ctx, m.properties.Srcs)
    // 确定输出文件
    outputFile := android.PathForModuleOut(ctx, *m.properties.Stem + ".zip")
    // 执行打包命令
    ctx.Build(pctx, android.BuildParams{
        Rule:   android.NewRuleBuilder(pctx, "zip").
                Command("zip -j $out $in").
                Build(),
        Inputs:  inputs,
        Outputs: []android.Path{outputFile},
    })
}

嗯,这里要注意:GenerateAndroidBuildActions 是核心方法,所有构建动作都在这里定义。我曾经犯过一个错,忘记调用 AddProperties,结果模块属性一直读不到,排查了半天。

小技巧:写自定义规则时,先用 m my_asset_bundle 测试编译,看能不能正常解析。如果报错,多半是注册或者属性定义的问题。

26.2 构建插件开发:给 Soong 装上「外挂」

自定义规则适合「造新轮子」,但如果你只是想给现有模块加点行为呢?比如,每次编译某个模块时,自动生成一个版本文件。这时候,插件就派上用场了。

Soong 的插件机制,说白了就是 Hook。你可以在模块的特定生命周期插入自己的逻辑。我记得有一次,客户要求所有 APK 编译时都要嵌入一个时间戳,我要是改每个模块的 Android.bp,那得改到猴年马月去。用插件,一行代码都不用改。

插件开发分三步:

  1. 实现插件接口:Soong 提供了 PluginInterface,你需要实现它的方法。
  2. 注册插件:在 init() 函数里调用 android.RegisterPlugin
  3. 在模块中启用:在 Android.bp 里加上 plugins: ["my_plugin"]

来看一个实际例子。这个插件会在所有 cc_binary 编译完成后,打印一条日志:

// build/soong/my_plugin/plugin.go
package my_plugin

import (
    "android/soong/android"
    "fmt"
)

type MyPlugin struct {
    android.PluginBase
}

func (p *MyPlugin) OnModuleBuilt(ctx android.PluginContext, module android.Module) {
    // 只对 cc_binary 生效
    if _, ok := module.(*cc.Module); ok {
        fmt.Printf("[MyPlugin] Module %s built successfully!\n", module.Name())
    }
}

func init() {
    android.RegisterPlugin("my_plugin", func() android.Plugin {
        return &MyPlugin{}
    })
}

避坑指南:我曾经在插件里直接修改了模块的属性,结果导致增量编译缓存失效。记住,插件应该只读,不要改模块的状态。如果你非要改,请用 PluginContext.AddPostDeps 来添加依赖,而不是直接操作模块。

26.3 构建系统 API:Soong 给你留的「后门」

Soong 本身提供了一套 API,让你能跟构建系统深度交互。这些 API 分布在 android 包下,我挑几个常用的说说。

API 名称 作用 使用场景
ModuleContext 模块上下文,提供文件路径、构建动作等 自定义规则中获取输入输出
PluginContext 插件上下文,提供模块信息、依赖关系 插件开发中遍历模块
android.PathForModuleSrc 获取模块源文件路径 定位输入文件
android.PathForModuleOut 获取模块输出目录路径 指定输出位置
android.NewRuleBuilder 创建自定义构建规则 执行 shell 命令
android.RegisterModuleType 注册新的模块类型 自定义构建规则

你想想看,这些 API 其实就是 Soong 的「骨架」。掌握了它们,你就能在构建系统里做任何事。我个人最常用的是 NewRuleBuilder,因为它能直接执行 shell 命令,灵活性极高。

举个例子,我想在编译时自动生成一个头文件,里面包含 Git 的 commit ID:

func (m *MyModule) GenerateBuildActions(ctx android.ModuleContext) {
    outputFile := android.PathForModuleOut(ctx, "version.h")
    ctx.Build(pctx, android.BuildParams{
        Rule: android.NewRuleBuilder(pctx, "gen_version").
            Command("echo '#define GIT_COMMIT \"$(git rev-parse HEAD)\"' > $out").
            Build(),
        Outputs: []android.Path{outputFile},
    })
}

提示:API 的文档其实不多,最好的学习方式是看 Soong 源码。打开 build/soong/android/ 目录,里面全是宝藏。我当初就是靠读源码学会的。

26.4 知识体系总览

为了让你更直观地理解这三者的关系,我画了一张图。你可以看到,自定义规则、插件、API 是三个层次,从底层到上层,覆盖了构建系统扩展的方方面面。

构建系统扩展知识体系 自定义构建规则 定义新模块类型(如 my_asset_bundle) 实现 GenerateAndroidBuildActions 方法 构建插件开发 实现 PluginInterface,注册 Hook 在模块生命周期中插入自定义逻辑 构建系统 API ModuleContext / PluginContext / NewRuleBuilder 等 从底层到上层,逐步扩展构建系统能力

这张图你看懂了吗?最底层是 API,它是所有扩展的基础。中间层是插件,它让你能「插入」到现有流程中。最上层是自定义规则,它让你能「创造」全新的构建逻辑。三者层层递进,缺一不可。

26.5 实战经验总结

最后,我分享几个实战中踩过的坑,希望能帮你少走弯路。

  • 命名冲突:自定义模块类型名不要跟内置的重复,比如别叫 cc_binary。我建议加个前缀,比如 my_
  • 增量编译:自定义规则里如果用了外部命令(比如 git),记得把命令的输出作为输入依赖,否则增量编译会失效。
  • 调试困难:Soong 的日志默认不打印。你可以用 ctx.ModuleErrorf 或者 fmt.Printf 输出到终端,但记得调试完删掉。
  • 版本兼容:不同 AOSP 版本的 API 可能有变化。我遇到过在 Android 12 上能跑的插件,到 Android 13 就报错了。升级时一定要测试。

一句话总结:自定义规则让你「造轮子」,插件让你「改轮子」,API 让你「懂轮子」。三者结合,你就能随心所欲地控制 AOSP 的构建过程。

好了,这一章的内容就到这里。记住,构建系统扩展不是一蹴而就的,多读源码、多动手实验,你也能成为 Soong 专家。


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