5、Android.bp 与 Soong:从 Make 到 Soong 的演进、Blueprint 文件语法、Android.bp 示例

聊到 Android 构建系统,就绕不开一个核心话题:Soong

我记得刚入行那会儿,Android 还是清一色的 Android.mk 文件。那时候改个模块依赖,经常要等十几分钟的全量编译。后来 Google 推出了 Soong,说实话,我一开始是抵触的——又要学新语法,又要改构建脚本。但用了一段时间后,我只能说:真香。

这一章,我们就来聊聊 Soong 是什么,它怎么来的,以及最重要的——Android.bp 怎么写。

从 Make 到 Soong:为什么要变?

先说说历史。Android 早期用的是 GNU Make 构建系统。每个模块对应一个 Android.mk 文件,语法是 Makefile 那一套。

听起来挺正常的对吧?但问题在于:Make 太慢了

为什么慢?因为 Make 是递归执行的。你想想看,一个大型项目有几千个模块,每个模块都要解析一遍 Makefile,还要处理各种变量展开、条件判断。我曾在项目中遇到过,单是解析构建脚本就要花掉 2 分钟。

而且 Make 的语法太灵活了。灵活到你可以写出各种奇奇怪怪的写法,导致构建系统很难做增量编译优化。

所以 Google 决定自己写一个构建系统——Soong

Soong 的核心思路很简单:

  • Blueprint 文件(也就是 Android.bp)描述模块
  • 用 Go 语言写的解析器,速度快得多
  • 支持 Kati 做兼容层,把老的 Android.mk 转成 ninja 文件

说白了,Soong 就是 Make 的替代品,但更现代、更快、更可控。

核心变化:

  • Android.mk → Android.bp
  • Make 语法 → Blueprint 语法(类似 JSON)
  • 递归解析 → 并行解析
  • 不支持条件判断 → 用 Go 逻辑控制

Blueprint 文件语法:长什么样?

Blueprint 的语法其实很简单。它有点像 JSON,但更简洁。

一个基本的 Android.bp 文件长这样:

cc_binary {
    name: "hello_world",
    srcs: ["main.cpp"],
    shared_libs: ["libcutils"],
    cflags: ["-Wall"],
}

嗯,是不是很直观?

我来拆解一下:

  • cc_binary:模块类型。表示这是一个 C++ 可执行文件。
  • name:模块名字。必须唯一。
  • srcs:源文件列表。
  • shared_libs:依赖的共享库。
  • cflags:编译选项。

常见的模块类型还有:

模块类型 说明
cc_library C++ 库(静态或动态)
cc_binary C++ 可执行文件
java_library Java 库
android_app Android 应用 APK
genrule 自定义生成规则

每个模块类型都有自己支持的属性。比如 cc_library 支持 static_libs,而 android_app 支持 package_name

小技巧: 我个人习惯在写 Android.bp 时,先用 m -j1 nothing 跑一遍,看看有没有语法错误。这样比等全量编译快得多。

Android.bp 示例:实战一下

光说不练假把式。我们来看一个实际项目中会用到的例子。

假设我们要编译一个 C++ 库,它依赖另一个静态库,并且需要链接系统库:

cc_library {
    name: "libmylib",
    srcs: [
        "src/core.cpp",
        "src/utils.cpp",
    ],
    export_include_dirs: ["include"],
    static_libs: ["libhelper"],
    shared_libs: [
        "liblog",
        "libutils",
    ],
    cflags: [
        "-DLOG_TAG=\"mylib\"",
        "-O2",
    ],
    sdk_version: "current",
    stl: "libc++",
}

这里有几个点需要注意:

  • export_include_dirs:导出的头文件路径。其他模块依赖这个库时,会自动加上这个路径。
  • static_libs:静态链接的库。编译时会把代码直接打包进来。
  • shared_libs:动态链接的库。运行时需要加载。
  • sdk_version:指定 SDK 版本。我建议尽量用 "current",除非你有特殊需求。

我曾经踩过的坑: 有一次我把 static_libsshared_libs 搞混了。结果编译出来的库在设备上跑不起来,因为缺少动态库。排查了半天才发现是链接方式错了。所以写的时候一定要想清楚:这个依赖是编译时就要,还是运行时才要?

条件编译与变量

Blueprint 不支持 Make 那种 ifeq 条件判断。那怎么办?

答案是:用 Go 代码控制

Soong 允许你在 Android.bp 中使用 soong_config 模块,或者直接在 Go 代码里写条件逻辑。

举个例子,如果你只想在 debug 版本中编译某些代码:

cc_library {
    name: "libdebug_utils",
    srcs: ["debug.cpp"],
    enabled: false,
}

cc_library {
    name: "librelease_utils",
    srcs: ["release.cpp"],
    enabled: true,
}

然后在 Go 代码中根据 enguser 变体来决定启用哪个模块。

嗯,这种方式确实比 Make 的 ifeq 要麻烦一点。但好处是逻辑更清晰,不会出现嵌套 10 层的条件判断。

Soong 的构建流程

为了让你更直观地理解 Soong 是怎么工作的,我画了一张图:

Soong 构建流程 Android.bp 文件 Android.mk 文件 Go 插件逻辑 Soong 解析器(Go) 生成 .ninja 构建文件 Ninja 执行编译 输入 输出

流程其实很清晰:

  1. Soong 读取所有 Android.bp 文件,以及通过 Kati 转换后的 Android.mk 文件。
  2. 用 Go 解析器生成模块依赖图。
  3. 输出 .ninja 文件。
  4. Ninja 负责实际执行编译。

为什么要多一层 Ninja?因为 Ninja 是专门为增量编译设计的。它只编译有变化的文件,速度极快。

总结一下

这一章我们聊了:

  • 为什么 Google 要从 Make 迁移到 Soong——说白了就是太慢了,而且不好维护。
  • Blueprint 语法——其实就是一种结构化的模块描述方式。
  • Android.bp 怎么写——从简单的 cc_binary 到带依赖的 cc_library。
  • Soong 的构建流程——从 bp 文件到 ninja 再到编译。

我个人觉得,Soong 最大的好处不是快,而是可预测。同样的 Android.bp,在不同环境下编译结果完全一致。这在团队协作中太重要了。

下一章我们会深入 Soong 的模块类型,看看那些常用的模块到底有哪些坑。嗯,到时候我会分享一些我踩过的雷。


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