4. Soong构建系统入门

在 Android 源代码树中,Soong 是当前主流的构建系统,它用 Android.bp 文件替代了旧版 Android.mk,把构建描述从 Make 的“命令式脚本”转变为“声明式配置”。理解 Soong 的关键,是理解 Blueprint 文件如何被解析、模块如何定义、属性如何组合,以及变量如何影响最终构建图。

一、核心原理:Blueprint 是 Soong 的配置语言

Soong 的输入是 Android.bp,它采用 Blueprint 语法。Blueprint 不是 Make,也不是 JSON,而是一种类似声明式配置的 DSL。每个 Android.bp 文件由若干模块定义组成,每个模块定义以模块类型开头,后跟一对花括号,内部是属性键值对。

Soong 解析时,会先读取所有 Android.bp 文件,把模块定义转换成内存中的模块对象,再根据模块类型和属性生成构建规则,最终交给 Ninja 执行。因此,Android.bp 只描述“要构建什么”,不直接描述“如何执行编译命令”。

Blueprint 文件的基本结构:
模块类型 { 属性名: 属性值, ... }
例如:cc_library { name: "libfoo", srcs: ["foo.cpp"] }

二、Android.bp 语法与模块定义

一个典型的 Android.bp 文件可以包含多个模块。模块类型决定了这个模块是库、可执行文件、资源包还是测试程序。常见模块类型包括 cc_library、cc_binary、java_library、android_app、filegroup 等。

cc_library {
    name: "libmylib",
    srcs: [
        "src/foo.cpp",
        "src/bar.cpp",
    ],
    shared_libs: [
        "liblog",
        "libbase",
    ],
    cflags: [
        "-Wall",
        "-Werror",
    ],
    visibility: [
        "//vendor/mycompany:__subpackages__",
    ],
}

模块定义中的 name 是模块的唯一标识,其他模块通过这个名字引用它。属性值可以是字符串、布尔值、整数、列表或映射。列表用方括号表示,元素之间用逗号分隔;字符串必须用双引号包裹。

Blueprint 支持注释,使用 // 表示单行注释,使用 /* ... */ 表示多行注释。注释不会影响构建结果,但能显著提升可读性。

三、属性与变量:让配置更灵活

Soong 允许在 Android.bp 中定义变量,变量以 name = value 的形式出现,可以在后续模块中引用。变量常用于存放公共源文件列表、编译选项或依赖库列表,避免重复书写。

common_srcs = [
    "src/common.cpp",
    "src/utils.cpp",
]

common_cflags = [
    "-Wall",
    "-Wextra",
]

cc_library {
    name: "libcommon",
    srcs: common_srcs,
    cflags: common_cflags,
}

cc_binary {
    name: "my_tool",
    srcs: common_srcs + ["src/main.cpp"],
    cflags: common_cflags,
}

变量支持列表拼接,例如 common_srcs + ["src/main.cpp"]。这种写法在大型项目中非常常见,可以让多个模块共享同一组源文件或编译参数。

除了自定义变量,Soong 还提供了一些内置变量和函数,例如 soong_config_module_type、select 等。其中 select 用于根据条件选择不同属性值,常用于区分不同架构、不同产品配置或不同编译变体。

cc_library {
    name: "libarch",
    srcs: ["src/common.cpp"],
    arch: {
        arm: {
            srcs: ["src/arm.cpp"],
        },
        arm64: {
            srcs: ["src/arm64.cpp"],
        },
    },
}

上面的 arch 属性是 Soong 中常见的条件属性,它根据目标架构选择不同的源文件。类似地,还有 product_variables、vendor、proprietary 等属性,用于处理产品差异和分区差异。

四、模块依赖与可见性

模块之间通过属性建立依赖关系。例如 shared_libs、static_libs、header_libs 用于 C/C++ 模块;libs、static_libs 用于 Java 模块;deps 用于通用依赖。Soong 会根据这些属性构建依赖图,并确保依赖模块先于当前模块构建。

可见性属性 visibility 控制模块可以被哪些其他模块引用。默认情况下,模块只对同一目录或子目录可见。如果希望跨目录引用,需要显式声明可见性,例如 //vendor/mycompany:__subpackages__ 表示对 vendor/mycompany 及其子目录可见。

属性 作用 常见模块类型
name 模块唯一名称 所有模块
srcs 源文件列表 cc_library、java_library 等
shared_libs 动态库依赖 cc_library、cc_binary
static_libs 静态库依赖 cc_library、cc_binary
cflags C/C++ 编译选项 cc_library、cc_binary
visibility 模块可见范围 所有模块

五、案例:从零编写一个 Android.bp

假设我们要在 vendor/demo/hello 目录下构建一个可执行程序 hello,它依赖一个本地静态库 libhello。可以这样写:

cc_library_static {
    name: "libhello",
    srcs: ["hello.cpp"],
    cflags: ["-Wall"],
    visibility: [":__subpackages__"],
}

cc_binary {
    name: "hello",
    srcs: ["main.cpp"],
    static_libs: ["libhello"],
    cflags: ["-Wall"],
}

执行 m hello 或 mm 后,Soong 会解析这两个模块,生成 Ninja 规则,最终产出 hello 可执行文件。如果依赖关系或可见性配置错误,构建会直接报错,而不是像 Make 那样在链接阶段才暴露问题。

建议在新增模块后运行 m nothing 或 soong_ui --make-mode nothing,让 Soong 只解析 Android.bp 而不执行完整编译,可以快速发现语法错误和依赖问题。

六、常见误区与复盘

  • 把 Android.bp 当 Makefile 写:Blueprint 不支持 shell 命令、条件分支和函数调用,不能用 if、foreach 等 Make 语法。
  • 忽略 visibility:跨目录引用模块时,必须显式声明可见性,否则会报“module not visible”错误。
  • 变量作用域混淆:变量只在当前 Android.bp 文件内有效,不能跨文件直接引用。
  • 依赖顺序错误:Soong 会自动处理依赖顺序,但循环依赖会导致解析失败,需要拆分模块或提取公共库。
  • 属性名拼写错误:Soong 对属性名大小写敏感,拼写错误会直接报 unknown property。

掌握 Blueprint 文件、模块定义、属性和变量,是进入 Soong 构建系统的第一步。后续章节会进一步介绍 Soong 的模块类型体系、条件编译、产品配置以及如何与 Ninja 协同工作。

Soong 构建系统解析流程与 Android.bp 结构 Android.bp cc_library { name: "libfoo", srcs: ["foo.cpp"], shared_libs: ["liblog"], cflags: ["-Wall"], } Soong 解析器 1. 读取所有 Android.bp 2. 解析模块与属性 3. 构建依赖图 4. 生成 Ninja 规则 Ninja 构建执行 编译 foo.cpp 链接 libfoo.so 生成中间产物 输出到 out/ 目录 最终产物:libfoo.so 模块定义、属性与变量关系 模块类型 属性 变量 依赖与可见性 cc_library / cc_binary name / srcs / cflags common_srcs / common_cflags shared_libs / visibility Android.bp 声明“构建什么”,Soong 解析后生成 Ninja 规则,最终由 Ninja 执行编译