Android 构建语法教程:从 Android.mk、Android.bp 到 Bazel 的常用与进阶

Android 构建语法教程:从 Android.mk、Android.bp 到 Bazel 的常用与进阶
Photo by Mohamed Nohassi / Unsplash
https://juejin.cn/post/7341592148305231881

前言:理解三套构建语言的演进与设计哲学

在 Android 漫长的发展历程中,其构建系统为了应对日益增长的工程复杂性、提升构建效率与可靠性,经历了从 Android.mkAndroid.bp,再到逐步引入业界标准 Bazel 的演进。这三套构建语言不仅是工具的更迭,更体现了构建思维的深刻变迁。

  • Android.mk (GNU Make):作为元老,它基于强大的 GNU Make,提供了极高的灵活性。开发者可以直接利用 Make 的所有语法和函数,编写复杂的条件判断和自定义规则。然而,这种灵活性也带来了维护成本高、可读性差、难以解析和增量构建性能不佳等问题。.mk 文件本质上是 Make 的脚本,描述的是“如何做”(How)。
  • Android.bp (Soong/Blueprint):为了解决 .mk 的问题,Google 推出了 Soong 构建系统,并设计了声明式的 Android.bp 语法。.bp 文件类似 JSON,描述的是“是什么”(What),即定义模块及其属性。Soong 负责将这些声明解析成具体的构建规则(Ninja 文件),从而实现了更快的解析速度、更可靠的增量构建和更强的确定性。它剥夺了 .mk 的过程式控制能力,强制开发者关注模块定义而非构建过程。
  • Bazel (Starlark):随着 Android 工程规模的进一步扩大(AOSP),以及对跨平台、多语言和可复现构建的极致追求,Google 开始将内部久经考验的 Bazel 构建系统引入 Android。Bazel 使用类似 Python 的 Starlark 语言,其 BUILD 文件同样是声明式的。它通过严格的沙箱机制、精细的依赖分析和强大的远程缓存,提供了目前业界顶级的构建性能、可靠性和可扩展性。

理解这三者的关系至关重要:此内容本教程旨在帮助您系统掌握这三套构建语言,从“常用”的核心概念快速上手,到“进阶”的技巧融会贯通,并最终理解它们之间的“对照与迁移”思路,让您无论面对历史代码还是现代化的新项目,都能游刃有余。


一、Android.mk:灵活强大的“脚本式”构建

Android.mk 是基于 GNU Make 的构建描述文件。它的核心思想是定义一系列以 LOCAL_MY_ 开头的变量,然后通过 include $(BUILD_...) 来引入预设的 Make 规则,生成最终的构建产物。

常用语法与变量

1. 模块(Module)的生命周期

一个典型的 Android.mk 文件通常包含一个或多个模块。每个模块的定义都遵循“清理-定义-构建”的模式。

  • CLEAR_VARS:每个模块开始前,必须调用 include $(CLEAR_VARS)。这个脚本会清理掉绝大部分 LOCAL_* 变量(除了 LOCAL_PATH),避免模块间的变量污染。
  • 变量定义:设置 LOCAL_MODULE(模块名)、LOCAL_SRC_FILES(源文件)等变量,描述模块的属性。
  • BUILD_*:最后,根据模块类型调用 include $(BUILD_...),如 BUILD_SHARED_LIBRARYBUILD_STATIC_LIBRARYBUILD_EXECUTABLE。这个脚本会根据前面定义的 LOCAL_ 变量,生成所有必要的构建规则。
# ========================================
# 模块一:一个共享库
# ========================================
include $(CLEAR_VARS)

# 模块元数据
LOCAL_PATH := $(call my-dir)
LOCAL_MODULE := lib-hello-jni

# 源文件
LOCAL_SRC_FILES := \
    hello_jni.c \
    utils.c

# 依赖与编译选项
LOCAL_SHARED_LIBRARIES := liblog
LOCAL_CFLAGS := -DLOG_TAG=\"HelloJNI\" -Wall

include $(BUILD_SHARED_LIBRARY)

# ========================================
# 模块二:一个可执行文件
# ========================================
include $(CLEAR_VARS)

LOCAL_MODULE := my_executable
LOCAL_SRC_FILES := main.c
LOCAL_SHARED_LIBRARIES := lib-hello-jni # 依赖上面定义的库

include $(BUILD_EXECUTABLE)

2. 核心变量解析

变量名
功能说明
示例
LOCAL_PATH
定义当前 Android.mk 文件所在的目录路径。通常在文件开头用 $(call my-dir) 赋值。
LOCAL_PATH := $(call my-dir)
LOCAL_MODULE
定义模块的唯一名称。构建系统会根据这个名字和模块类型生成最终产物,如 lib-hello-jni.so
LOCAL_MODULE := lib-hello-jni
LOCAL_SRC_FILES
定义模块的源文件列表。路径是相对于 LOCAL_PATH 的。
LOCAL_SRC_FILES := main.c utils/helpers.cpp
LOCAL_CFLAGS / LOCAL_CPPFLAGS
分别用于 C 和 C++ 的编译选项。LOCAL_CFLAGS 会同时作用于 C 和 C++。
LOCAL_CFLAGS := -O2 -g -DNDEBUG
LOCAL_C_INCLUDES
指定头文件搜索路径。路径可以是相对于 LOCAL_PATH 的,也可以是绝对路径。
LOCAL_C_INCLUDES := $(LOCAL_PATH)/include external/libxml2/include
LOCAL_SHARED_LIBRARIES
指定模块运行时依赖的 共享库 模块名。
LOCAL_SHARED_LIBRARIES := liblog libutils libcutils
LOCAL_STATIC_LIBRARIES
指定模块链接时依赖的 静态库 模块名。
LOCAL_STATIC_LIBRARIES := libbase_static libjson_static
LOCAL_LDLIBS
指定链接时需要额外链接的系统库,通常以 -l 开头。主要用于链接非 Android 模块的系统原生库。
LOCAL_LDLIBS := -lz -lm

3. 构建预编译模块(Prebuilt)

当需要引入一个已经编译好的库(.so.a)时,可以使用预编译模块。

# 引入一个预编译的共享库
include $(CLEAR_VARS)
LOCAL_MODULE := lib-prebuilt-foo
# 源文件路径指向预编译库文件本身
LOCAL_SRC_FILES := libs/$(TARGET_ARCH_ABI)/libfoo.so
# 如果有头文件,也需要导出
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)/include
include $(PREBUILT_SHARED_LIBRARY)

# 另一个模块现在可以依赖它了
include $(CLEAR_VARS)
LOCAL_MODULE := my-app
LOCAL_SRC_FILES := main.c
LOCAL_SHARED_LIBRARIES := lib-prebuilt-foo # 像普通模块一样依赖
include $(BUILD_EXECUTABLE)
注意PREBUILT_SHARED_LIBRARY 表示引入一个共享库,PREBUILT_STATIC_LIBRARY 表示引入一个静态库。

4. glob 与辅助函数

  • $(call my-dir):获取当前 Android.mk 所在目录的路径。
  • $(wildcard ...)$(patsubst ...)GNU Make 的原生函数,常用于查找文件。例如,递归查找所有 .c 文件:
    •   虽然强大,但过度使用
    • wildcard
    • 会让构建脚本难以理解和维护,因此不推荐滥用。
# 查找所有 C 文件
MY_C_FILES := $(wildcard $(LOCAL_PATH)/*.c)
# 查找所有子目录下的 C 文件
MY_SUB_C_FILES := $(wildcard $(LOCAL_PATH)/*/*.c)
# 去掉路径前缀,得到相对于 LOCAL_PATH 的路径
LOCAL_SRC_FILES := $(patsubst $(LOCAL_PATH)/, %, $(MY_C_FILES) $(MY_SUB_C_FILES))

进阶技巧与常见陷阱

1. 条件判断与平台差异化

Android.mk 支持 ifeq, ifneq, ifdef, ifndefGNU Make 的条件语句,这使得处理平台差异化配置(如不同架构、不同 Android 版本)成为可能。

  • TARGET_ARCH:目标 CPU 架构,如 arm, arm64, x86, x86_64
  • TARGET_ARCH_ABI:带 ABI 信息的架构,如 armeabi-v7a, arm64-v8a

示例:为不同架构提供特定的源文件或编译选项。

include $(CLEAR_VARS)
LOCAL_MODULE := lib-arch-specific

# 通用源文件
LOCAL_SRC_FILES := common.c

# 根据架构添加特定源文件
ifeq ($(TARGET_ARCH), arm)
    LOCAL_SRC_FILES += arch/arm/optimized.S
    LOCAL_CFLAGS += -DARCH_ARM=1
else ifeq ($(TARGET_ARCH), arm64)
    LOCAL_SRC_FILES += arch/arm64/optimized.S
    LOCAL_CFLAGS += -DARCH_ARM64=1
endif

# 根据 Android 版本决定是否开启某个特性
ifneq ($(filter 19 21 22, $(PLATFORM_SDK_VERSION)),)
    LOCAL_CFLAGS += -DUSE_LEGACY_API=1
endif

include $(BUILD_SHARED_LIBRARY)

2. 生成代码与自定义规则

Android.mk 最强大的地方在于可以定义自己的构建规则,实现代码生成、文件处理等高级功能。这通常通过 $(shell) 命令或定义 BUILD_HOST_CUSTOM_RULE 实现。示例:使用 protoc.proto 文件生成 C++ 代码。

include $(CLEAR_VARS)
LOCAL_MODULE := lib-proto-generated

# 1. 定义一个规则来生成源文件
#    GEN_PROTO_SRC 是生成的目标文件
#    my_proto.proto 是依赖的输入文件
#    protoc ... 是执行的命令
GEN_PROTO_SRC := $(call local-generated-sources-dir)/my_proto.pb.cc
$(GEN_PROTO_SRC): PRIVATE_PROTO_FILE := $(LOCAL_PATH)/my_proto.proto
$(GEN_PROTO_SRC): $(LOCAL_PATH)/my_proto.proto
	@echo "Generating C++ from $<"
	$(hide) protoc --cpp_out=$(dir $@) --proto_path=$(dir $<) $<

# 2. 将生成的文件加入模块的源文件列表
LOCAL_GENERATED_SOURCES := $(GEN_PROTO_SRC)
LOCAL_SRC_FILES := main.cpp

# 3. 链接 protobuf 库
LOCAL_SHARED_LIBRARIES := libprotobuf-cpp-full

include $(BUILD_SHARED_LIBRARY)

这种方式非常灵活,但写法复杂,且容易出错,是 .bp 着力要替代的场景。

3. 常见陷阱与最佳实践

陷阱 1:忘记 include $(CLEAR_VARS)这是最常见的错误。忘记清理变量会导致前一个模块的 LOCAL_SRC_FILES, LOCAL_CFLAGS 等泄露到下一个模块,引发难以排查的编译或链接错误。陷阱 2:变量覆盖与 := vs =

  • := (立即求值):在定义时就确定变量的值。推荐始终使用 :=
  • = (延迟求值):在变量被使用时才展开。如果后续有其他变量改变,它的值也可能变。
  • += (追加):向变量追加内容。
  • ?= (条件赋值):如果变量未定义,则赋值。
    不理解这些赋值符的区别,很容易在复杂的 Android.mk 中导致非预期的行为。

陷阱 3:路径问题与 $(LOCAL_PATH)所有相对路径都应该基于 $(LOCAL_PATH)。滥用硬编码路径或 ../ 会导致构建在不同环境中失败。陷阱 4:隐式依赖GNU Make 依赖于明确的依赖关系图。如果一个文件依赖另一个生成的文件,但没有在规则中声明,增量构建时可能不会正确重新生成,导致使用的是过期的文件。


二、Android.bp:声明式的“蓝图”语言

Android.bp 是 Soong 构建系统使用的配置文件,它的语法类似 JSON,但更简洁。核心思想是声明模块(Module)和它的属性(Properties),而不是描述构建步骤。Soong 会解析这些 .bp 文件,并将其转换为 Ninja 构建文件,以实现高性能的构建。

常用语法与属性

1. 模块定义基础

一个 .bp 文件可以包含多个模块定义。每个模块由其类型、 {、一组 key: value 属性和 } 组成。

// 一个 C++ 共享库
cc_library_shared {
    name: "lib-hello-bp",
    srcs: [
        "hello_bp.cpp",
        "utils.cpp",
    ],
    cflags: [
        "-Wall",
        "-Werror",
        "-DLOG_TAG=\"HelloBP\"",
    ],
    shared_libs: [
        "liblog",
        "libutils",
    ],
    // 导出头文件给依赖它的模块
    export_include_dirs: ["include"],
}

// 一个使用上述库的可执行文件
cc_binary {
    name: "my_bp_executable",
    srcs: ["main.cpp"],
    shared_libs: [
        "lib-hello-bp", // 依赖上面的库
    ],
    stl: "c++_shared", // 明确指定 STL
}
核心区别.bp 文件是静态的、可解析的蓝图(Blueprint),没有任何流控制(if/else/for)。所有逻辑都通过属性和模块间的引用来表达。

2. 常用模块类型与属性

模块类型
功能说明
常用属性
cc_library
cc_library_shared
cc_library_static
C/C++ 静态库或共享库
name, srcs, cflags, include_dirs, shared_libs, static_libs, export_include_dirs
cc_binary
C/C++ 可执行文件
同上
android_app
Android 应用 (APK)
name, srcs, static_libs (for java), sdk_version, manifest, aaptflags
java_library
Java 库 (JAR)
name, srcs, static_libs, libs (prebuilt jars)
filegroup
定义一个文件集合,用于在模块间共享
name, srcs
genrule
通过 shell 命令生成文件
name, tool_files, cmd, out
defaults
定义一组可复用的属性集,供其他模块继承
name

3. defaults:复用属性

当多个模块有大量相同属性时,可以使用 defaults 模块来减少重复。

// 定义一个默认配置集
cc_defaults {
    name: "my_c_defaults",
    cflags: [
        "-O2",
        "-g",
    ],
    shared_libs: ["libbase"],
}

// 模块一继承默认配置
cc_library {
    name: "lib-foo",
    defaults: ["my_c_defaults"], // 继承
    srcs: ["foo.c"],
    cflags: [
        "-DFOO_SPECIFIC", // 追加自己的 cflags
    ],
}

// 模块二也继承
cc_library {
    name: "lib-bar",
    defaults: ["my_c_defaults"],
    srcs: ["bar.c"],
    shared_libs: [
      "libutils", // 追加自己的依赖
    ],
}

4. filegroupgenrule:文件集合与代码生成

  • filegroup:最简单的用法,就是给一组文件起个名字。
  • genrule:是 .bp 中执行 shell 命令生成文件的“逃生舱”,功能上对标 .mk 的自定义规则。

示例:使用 protoc 生成 Java 代码。

// 1. 定义一个 filegroup,包含所有 proto 文件
filegroup {
    name: "my-proto-files",
    srcs: ["src/main/proto/**/*.proto"],
}

// 2. 定义一个 genrule,用于执行 protoc
genrule {
    name: "my-proto-java-gen",
    tool_files: [
        // 声明 genrule 依赖的工具
        "protoc-3.9.1",
    ],
    // cmd 是要执行的 shell 命令
    // $(location ...) 获取文件的路径
    // $(genDir) 是 Soong 提供的输出目录
    cmd: "$(location protoc-3.9.1) --java_out=$(genDir) -I$(dir $(location :my-proto-files)) $(locations :my-proto-files)",
    // out 声明输出的文件列表
    out: [
        "com/example/my_proto.java",
    ],
}

// 3. 定义一个 java_library,使用生成的文件
java_library {
    name: "lib-my-proto",
    // 通过 :module-name (冒号语法) 引用 genrule 的输出
    srcs: [":my-proto-java-gen"],
    libs: ["libprotobuf-java-lite"],
}

5. 路径、包与可见性

  • 路径.bp 中的所有路径都相对于当前 .bp 文件所在的目录(即包的根目录)。不允许使用 ../ 跨越包边界。
  • 包(Package):一个包含 Android.bp 文件的目录就是一个包。
  • 可见性(Visibility):默认情况下,一个包中定义的模块只能被该包及其子包中的 .bp 文件引用。可以通过 visibility 属性来控制。
    •   
    • //
    • 表示从源码树根目录开始的路径。这套机制比
    • .mk
    • 的全局命名空间要严格得多。
cc_library {
    name: "lib-internal-use",
    // ...
    // 只有同一目录和 //vendor/product/feature1 下的 bp 文件可以引用我
    visibility: [
        "//vendor/product/feature1",
        ":__subpackages__", // :__subpackages__ 表示所有子包
    ],
}

进阶技巧与常见陷阱

1. 架构与产品配置 (arch, target, product_variables)

Soong 提供了结构化的方式来处理不同配置下的差异化属性,取代了 .mk 中杂乱的 ifeq示例:为 arm64 架构使用特定的源文件和编译选项。

cc_library {
    name: "lib-arch-bp-specific",
    srcs: ["common.c"],
    cflags: ["-DCOMMON"],
    arch: {
        arm: {
            srcs: ["arch/arm.c"],
            cflags: ["-DARCH_ARM=1"],
        },
        arm64: {
            name: "lib-arch-bp-specific-arm64", // 可以为特定架构重命名
            srcs: ["arch/arm64.c"],
            cflags: ["-DARCH_ARM64=1"],
        },
        x86: {
            enabled: false, // 甚至可以为某个架构禁用此模块
        },
    },
}

类似的结构还有 target (区分 android, host, host_cross)、multilib (区分 32/64 位) 等。product_variables 用于根据产品配置(在 .mk 中通过 PRODUCT_... 定义)来选择不同属性。

cc_library {
    name: "lib-feature-toggle",
    srcs: ["default_feature.c"],
    product_variables: {
        // 如果产品的 makefile 中定义了 a_feature_enabled = true
        a_feature_enabled: {
            srcs: ["advanced_feature.c"],
            cflags: ["-DUSE_ADVANCED_FEATURE"],
        },
        // 如果定义了 b_feature_enabled = true
        b_feature_enabled: {
            cflags: ["-DUSE_B_FEATURE"],
        },
    },
}

2. Variants:构建变体

Soong 有一个强大的“变体(Variant)”概念,可以为同一个模块根据不同维度(如 debug/releasesanitize 类型、sdk_version)生成多个不同的构建产物。

cc_binary {
    name: "my_app_with_variants",
    srcs: ["main.c"],
    sanitize: {
        // 为这个模块开启地址消毒剂 (asan)
        address: true,
        // asan 开启时,使用特定的源文件
        srcs: ["asan_helper.c"],
    },
    // 为不同版本的 SDK 提供不同实现
    min_sdk_version: "29",
    sdk_version: "current",
}

Soong 会根据当前的构建目标(比如 userdebug 开启了 asan)自动选择正确的属性组合。

3. soong_config_module_type:模块化配置

当产品配置非常复杂时,product_variables 会变得臃肿。soong_config_module_type 提供了一种更模块化的方式,将配置项预先定义好,然后在 .bp 中使用。步骤 1:在 .mk 文件中定义 Soong 配置

# vendor/product/config.mk
SOONG_CONFIG_NAMESPACES += myGlobalConfig
SOONG_CONFIG_myGlobalConfig += \
    feature_a \
    feature_b

SOONG_CONFIG_myGlobalConfig_feature_a := $(PRODUCT_FEATURE_A_ENABLED)
SOONG_CONFIG_myGlobalConfig_feature_b := $(PRODUCT_FEATURE_B_LEVEL)

步骤 2:在 .bp 中定义 soong_config_module_type

// build/soong/configs/my_config.bp
soong_config_module_type {
    name: "my_feature_cc_library",
    module_type: "cc_library",
    config_namespace: "myGlobalConfig",
    variables: [
        "feature_a",
        "feature_b",
    ],
    properties: [
        "cflags",
        "srcs",
    ],
}

步骤 3:在模块中使用

// my_module.bp
my_feature_cc_library {
    name: "lib-soong-config-demo",
    srcs: ["default.c"],
    soong_config_variables: {
        feature_a: {
            // 当 feature_a 为 true 时
            "true": {
                srcs: ["feature_a_impl.c"],
            },
        },
        feature_b: {
            // 当 feature_b 为 "level1" 或 "level2"
            "level1": { cflags: ["-DLEVEL=1"] },
            "level2": { cflags: ["-DLEVEL=2"] },
            // 其他值
            "*":      { cflags: ["-DLEVEL=0"] },
        },
    },
}

4. 常见陷阱与最佳实践

陷阱 1:属性名与 .mk 不一致很多从 .mk 迁移过来的开发者会习惯性地写 LOCAL_....bp 的属性名是独立的,例如 shared_libs 对应 LOCAL_SHARED_LIBRARIESstatic_libs 对应 LOCAL_STATIC_LIBRARIES陷阱 2:声明式思维的限制.bp 中没有循环和复杂的字符串处理。所有逻辑必须通过属性的组合和引用来表达。如果你发现自己在 .bp 中“想写一个 for 循环”,说明你的思路还没从 .mk 转换过来。这时应该考虑使用 defaultsgenrule 或者重新组织模块。陷阱 3:路径基于包根目录.bp 中所有 srcs 里的路径都是相对于当前 Android.bp 文件的。例如,srcs: ["a/b.c"] 意味着 a 目录和 Android.bp 在同一级。这和 .mk 中可以自由拼接 $(LOCAL_PATH) 有很大不同。陷阱 4:name 的唯一性所有模块的 name 必须在整个源码树中唯一,否则 Soong 会报错。这有助于避免 .mk 中常见的模块名冲突问题。


三、Bazel:现代化的多语言构建系统

Bazel 是 Google 开源的一套可复现、高性能的构建系统,设计之初就为了应对超大规模的单体仓库(Monorepo)。它使用类似 Python 的 Starlark 语言来编写 BUILD 文件。与 Soong 类似,BUILD 文件也是声明式的,但其规则(Rules)和宏(Macros)系统提供了比 Soong 更强大的可扩展性。

常用语法与规则

1. 工作区(Workspace)与 BUILD 文件

  • WORKSPACE 文件:位于项目根目录,用于定义项目的外部依赖(如其他 Bazel 项目、第三方库等)。
  • BUILD 文件:类似于 Android.bp,用于定义包(Package)内的构建目标(Target)。一个包含 BUILD 文件的目录就是一个包。

2. 核心概念:规则(Rule)与目标(Target)

每个 BUILD 文件包含一系列规则的调用。一个规则调用会生成一个或多个构建目标。

# C++ 库规则
cc_library(
    name = "hello_lib",
    srcs = ["hello_lib.cc"],
    hdrs = ["hello_lib.h"],
    visibility = ["//main:__pkg__"], # 只对 main 包可见
)

# C++ 可执行文件规则
cc_binary(
    name = "hello_world",
    srcs = ["hello_world.cc"],
    deps = [
        ":hello_lib", # 依赖同一个包下的 hello_lib 目标
    ],
)
  • name:目标的名称,在包内必须唯一。
  • srcs:源文件列表。
  • deps:依赖列表,指向其他目标。: 开头表示同一包下的目标,// 开头表示从工作区根目录开始的绝对路径。
  • visibility:控制谁可以依赖此目标。"//visibility:public" 表示公开,"//visibility:private" 表示私有(默认)。

3. 常见规则与属性

规则
功能说明
常用属性
cc_library / cc_binary / cc_test
C++ 库、可执行文件、测试
name, srcs, hdrs, deps, copts (编译选项), linkopts (链接选项)
java_library / java_binary / java_test
Java 库、可执行文件、测试
name, srcs, deps, resources
android_library / android_binary
Android 库 (AAR)、应用 (APK)
name, srcs, deps, manifest, resource_files
filegroup
文件集合
name, srcs
genrule
通用代码生成规则
name, srcs, outs, cmd, tools
glob
文件查找函数
include, exclude

示例:一个包含 Java 库和 Android 应用的 BUILD 文件。

# 加载 Android 规则
load("@rules_android//android:rules.bzl", "android_library", "android_binary")

# 一个 Java 工具库
java_library(
    name = "utils",
    srcs = glob(["*.java"]),
)

# 一个 Android 库,依赖上面的 Java 库
android_library(
    name = "my_android_lib",
    srcs = ["MyActivity.java"],
    manifest = "AndroidManifest.xml",
    resource_files = glob(["res/**"]),
    deps = [":utils"],
)

# 最终的 Android 应用
android_binary(
    name = "app",
    manifest = "AndroidManifest.xml",
    deps = [":my_android_lib"],
)

4. load 语句与 glob 函数

  • load("path/to/file.bzl", "symbol1", "symbol2"):用于从 .bzl 文件中导入宏、规则等。Bazel 的核心规则是内建的,但许多生态(如 Android、Go、Python)的规则需要通过 load 导入。
  • glob(["*.java"], exclude=["Test.java"]):用于匹配符合模式的文件列表,比 .mkwildcard 更规范、更安全,因为它不允许递归匹配(**),防止意外包含不应有的文件。

进阶技巧与常见陷阱

1. select():配置化构建的核心

select() 是 Bazel 中实现条件编译的“瑞士军刀”,功能远超 .bparch 属性。它根据当前的配置(如 CPU、编译器选项、自定义标志)从一个字典中选择一个值。示例:为不同平台和编译模式选择不同的源文件和编译选项。

cc_library(
    name = "platform_lib",
    srcs = ["common.cc"] + select({
        "//:android_arm": ["arm_specific.cc"],
        "//:android_x86": ["x86_specific.cc"],
        "//conditions:default": [], # 默认情况
    }),
    copts = select({
        "//:debug_build": ["-g"],
        "//:release_build": ["-O2"],
        "//conditions:default": ["-O0"],
    }),
)

这里的 //:android_arm 是一个 config_setting,在项目高层级文件中定义。

# 在根 BUILD 文件或专门的 build_configs.bzl 中定义
config_setting(
    name = "android_arm",
    values = {
        "cpu": "armv7",
        "crosstool_top": "//external:android/crosstool",
    },
)

2. 自定义宏与 genrule

genrulecmd 过于复杂时,可以将其封装成一个可复用的宏。宏是用 Starlark 语言编写的函数,可以像规则一样被调用。示例:将 protobuf 生成逻辑封装成一个宏。

# 在 my_rules.bzl 文件中
def proto_cc_library(name, proto_file):
    # 使用 genrule 生成 .pb.cc 和 .pb.h 文件
    native.genrule(
        name = name + "_proto_gen",
        srcs = [proto_file],
        outs = [
            proto_file + ".pb.cc",
            proto_file + ".pb.h",
        ],
        cmd = "protoc --cpp_out=$(@D) $<",
        tools = ["@protobuf//:protoc"],
    )
    # 再定义一个 cc_library 来编译生成的文件
    native.cc_library(
        name = name,
        srcs = [":" + name + "_proto_gen"], # 引用 genrule 的输出
        deps = ["@protobuf//:protobuf_lite"],
    )

# 在 BUILD 文件中使用
load(":my_rules.bzl", "proto_cc_library")

proto_cc_library(
    name = "my_message_cc_proto",
    proto_file = "my_message.proto",
)
native. 用于调用 Bazel 的内建规则。这个宏极大地简化了 BUILD 文件的复杂度。

3. 平台、约束与工具链(Toolchains)

Bazel 通过平台(Platform)、约束(Constraint)和工具链(Toolchain)的组合,来解决复杂的跨平台编译问题。

  • 约束(Constraint):定义了环境的某个维度,如 cpu: x86, os: linux
  • 平台(Platform):是一系列约束的集合,如 my_linux_platform = {cpu: x86, os: linux}
  • 工具链(Toolchain):声明了在特定平台上提供特定工具(如编译器、链接器)的能力。

当构建时,Bazel 会根据目标平台选择最匹配的工具链,从而实现真正的解耦和跨平台构建。

4. 常见陷阱与最佳实践

陷阱 1:路径的相对性与绝对性

  • //a/b:c 是一个绝对标签,指向工作区根目录下 a/b 包中的 c 目标。
  • :c 是一个相对标签,指向当前包中的 c 目标。
  • srcs 中的文件路径是相对于当前包的。
    混淆这几种路径是初学者的常见错误。

陷阱 2:严格的可见性Bazel 默认可见性是私有的 (//visibility:private)。如果忘记给需要被外部依赖的 cc_libraryjava_library 设置 visibility = ["//..."],会导致“target not visible”错误。陷阱 3:重复依赖与钻石依赖Bazel 要求依赖图中每个库的版本是唯一的。如果你的项目通过不同路径依赖了 libA v1.0libA v2.0(钻石依赖问题),Bazel 会报错。这强制开发者保持依赖树的清洁。陷阱 4:select 的滥用虽然 select 很强大,但过度使用会让 BUILD 文件变得难以阅读。对于复杂的逻辑,更好的方式是将其抽象成宏或自定义规则。


四、核心概念对照表

为了帮助您快速在三套语法间切换,下表总结了核心构建概念在 Android.mkAndroid.bpBazel 中的表达方式。

概念 / 功能点
Android.mk (GNU Make)
Android.bp (Soong)
Bazel (Starlark)
文件
Android.mk
Android.bp
BUILDBUILD.bazel
模块/目标定义
include $(CLEAR_VARS) LOCAL_MODULE := name include $(BUILD_...)
cc_library { name: "name", ... }
cc_library(name = "name", ...)
源文件
LOCAL_SRC_FILES := a.c b.c
srcs: ["a.c", "b.c"]
srcs = ["a.c", "b.c"]glob(["*.c"])
头文件目录
LOCAL_C_INCLUDES := path LOCAL_EXPORT_C_INCLUDES
include_dirs: ["path"] export_include_dirs: ["path"]
hdrs = glob(["*.h"]) includes = ["path"]
编译选项
LOCAL_CFLAGS += -DNAME
cflags: ["-DNAME"]
copts = ["-DNAME"]
共享库依赖
LOCAL_SHARED_LIBRARIES := libfoo
shared_libs: ["libfoo"]
deps = [":libfoo"] (如果 libfoo 是 cc_library)
静态库依赖
LOCAL_STATIC_LIBRARIES := libbar
static_libs: ["libbar"]
deps = [":libbar"]
预编译库
include $(PREBUILT_SHARED_LIBRARY)
cc_prebuilt_library_shared { ... }
cc_import 或自定义宏
代码生成
自定义 Make 规则 $(shell command)
genrule { ... }
genrule(...) 或自定义 Starlark 宏
条件逻辑
ifeq ($(TARGET_ARCH), arm) ... endif
arch: { arm: { ... } } product_variables: { ... }
select({ "//:arm_config": ..., ... })
属性复用
定义变量,多处引用
defaults: ["my_defaults"]
定义 Starlark 变量或宏
可见性
全局命名空间 (无)
visibility: [":subpackages"]
visibility = ["//path:pkg"]

五、迁移示例:从 .mk 到 .bp 再到 Bazel

理论结合实践是最好的学习方式。下面我们通过两个最小可行示例,展示如何将一个项目从 Android.mk 逐步迁移到 Android.bpBazel

示例一:一个简单的 C++ 库

假设我们有一个 C++ 库 libhello, 它有一个源文件 hello.cpp 和一个头文件 hello.h

第 1 步:Android.mk 版本

# in jni/Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := libhello-mk
LOCAL_SRC_FILES := hello.cpp
LOCAL_EXPORT_C_INCLUDES := $(LOCAL_PATH)
include $(BUILD_SHARED_LIBRARY)
  • 构建命令:在 AOSP 环境中执行 mmmmm jni/
  • 产物out/target/product/.../libhello-mk.so

第 2 步:迁移到 Android.bp

在同一目录下创建一个 Android.bp 文件。

// in jni/Android.bp
cc_library_shared {
    name: "libhello-bp",
    srcs: ["hello.cpp"],
    // export_include_dirs 告诉依赖方头文件在哪里
    export_include_dirs: ["."], // "." 代表当前目录
    cflags: ["-fPIC"], // 编译共享库推荐开启
}
  • 思路
    • include $(BUILD_SHARED_LIBRARY) 变成了 cc_library_shared { ... }
    • LOCAL_MODULE 变成了 name
    • LOCAL_SRC_FILES 变成了 srcs 列表。
    • LOCAL_EXPORT_C_INCLUDES 变成了 export_include_dirs
  • 构建命令:在 AOSP 环境中执行 m libhello-bp
  • 易错点:忘记 .bp 中的 srcs 是一个列表 [];路径是相对于 .bp 文件的,所以 $(LOCAL_PATH) 变成了 .

第 3 步:迁移到 Bazel

在项目根目录创建 WORKSPACE 文件,然后在 jni 目录创建 BUILD 文件。

# in WORKSPACE (usually empty for simple cases)
workspace(name = "hello_bazel_project")
# in jni/BUILD
cc_library(
    name = "libhello-bazel",
    srcs = ["hello.cpp"],
    hdrs = ["hello.h"],
    # includes 属性告诉编译器在当前目录下寻找 #include "hello.h"
    includes = ["."],
    visibility = ["//visibility:public"],
)
  • 思路
    • cc_library_shared 变成了 cc_library。Bazel 会根据依赖关系自动决定是静态链接还是动态链接。
    • srcsname 类似。
    • 头文件通过 hdrs 属性声明,这使得 Bazel 可以更精确地分析依赖。includes 属性用于设置 -I 路径。
    • 必须设置 visibility,否则其他包无法依赖它。
  • 构建命令bazel build //jni:libhello-bazel
  • 产物bazel-bin/jni/libhello-bazel.so
  • 易错点:忘记设置 visibilityhdrsincludes 的区别;Bazel 的标签语法 //path/to:target

示例二:一个简单的 Android 库

假设我们有一个 Android 库,包含一个 Activity 和一些资源。

第 1 步:Android.mk (通常与 Gradle 结合)

在纯 AOSP 系统应用开发之外,.mk 很少直接用于构建 Android 库。更常见的是 Android.mk 用于集成一个由 Gradle 构建好的 aar

# aosp/vendor/app/Android.mk
LOCAL_PATH := $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := MyAndroidLibFromGradle
LOCAL_SRC_FILES := prebuilt/my-android-lib.aar
# 声明这是一个预编译的 Android 库
LOCAL_MODULE_CLASS := JAVA_LIBRARIES
LOCAL_MODULE_SUFFIX := .aar
include $(BUILD_PREBUILT)

第 2 步:迁移到 Android.bp

.bp 提供了直接从源码构建 Android 库的规则。

// aosp/vendor/app/Android.bp
android_library {
    name: "MyAndroidLibBp",
    srcs: [
        "java/com/example/MyActivity.java",
    ],
    // 静态依赖一个 java 库
    static_libs: ["androidx.appcompat.appcompat"],
    // 指定 manifest 文件
    manifest: "AndroidManifest.xml",
    sdk_version: "current",
}

或者,如果仍然是引入预编译的 aar

android_library_import {
    name: "MyAndroidLibBpPrebuilt",
    aars: ["prebuilt/my-android-lib.aar"],
}

第 3 步:迁移到 Bazel (使用 rules_android)

需要先在 WORKSPACE 中配置 rules_android

# in WORKSPACE
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

http_archive(
    name = "rules_android",
    sha256 = "...", # a valid sha256 checksum
    urls = ["https://github.com/bazelbuild/rules_android/releases/download/v0.11.1/rules_android-v0.11.1.tar.gz"],
)

load("@rules_android//android:android_rules.bzl", "android_sdk_repository")

android_sdk_repository(
    name = "androidsdk", # This is the name of your android_sdk
)

然后在 app 目录创建 BUILD 文件。

# in app/BUILD
load("@rules_android//android:rules.bzl", "android_library")

android_library(
    name = "my_android_lib_bazel",
    srcs = glob(["java/**/*.java"]),
    manifest = "AndroidManifest.xml",
    resource_files = glob(["res/**"]),
    custom_package = "com.example.myapp",
    visibility = ["//visibility:public"],
    deps = [
        "@maven//:androidx_appcompat_appcompat", # 依赖从 Maven 拉取的库
    ],
)
  • 思路
    • android_library 规则由 rules_android 提供,需要 load
    • resource_files 用于声明所有资源文件。
    • deps 指向外部依赖,如从 Maven 下载的 appcompat
  • 易错点WORKSPACE 配置复杂;glob 的使用;外部依赖(@maven//)的管理。

结语

Android.mk 的过程式脚本,到 Android.bp 的声明式蓝图,再到 Bazel 的现代化工程体系,Android 构建系统的演进之路清晰地展示了软件工程对声明式、可复现、高性能构建的追求。

  • mk 给了你“无限的权力”,但需要你为每个细节负责。
  • bp 收回了权力,强迫你“只说你要什么”,从而换取了速度和可靠性。
  • Bazelbp 的哲学上更进一步,提供了无与伦比的跨平台能力、缓存效率和生态扩展性。

希望这篇教程能为你揭开 Android 构建系统的面纱,让你在日常开发和系统定制中更加得心应手。无论是维护旧代码,还是拥抱新技术,理解这三者的核心思想与差异,都将是你宝贵的财富。