Android 构建语法教程:从 Android.mk、Android.bp 到 Bazel 的常用与进阶
https://juejin.cn/post/7341592148305231881
前言:理解三套构建语言的演进与设计哲学
在 Android 漫长的发展历程中,其构建系统为了应对日益增长的工程复杂性、提升构建效率与可靠性,经历了从 Android.mk 到 Android.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_LIBRARY、BUILD_STATIC_LIBRARY或BUILD_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, ifndef 等 GNU 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_librarycc_library_sharedcc_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. filegroup 与 genrule:文件集合与代码生成
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/release、sanitize 类型、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_LIBRARIES,static_libs 对应 LOCAL_STATIC_LIBRARIES。陷阱 2:声明式思维的限制.bp 中没有循环和复杂的字符串处理。所有逻辑必须通过属性的组合和引用来表达。如果你发现自己在 .bp 中“想写一个 for 循环”,说明你的思路还没从 .mk 转换过来。这时应该考虑使用 defaults、genrule 或者重新组织模块。陷阱 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"]):用于匹配符合模式的文件列表,比.mk的wildcard更规范、更安全,因为它不允许递归匹配(**),防止意外包含不应有的文件。
进阶技巧与常见陷阱
1. select():配置化构建的核心
select() 是 Bazel 中实现条件编译的“瑞士军刀”,功能远超 .bp 的 arch 属性。它根据当前的配置(如 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
当 genrule 的 cmd 过于复杂时,可以将其封装成一个可复用的宏。宏是用 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_library 或 java_library 设置 visibility = ["//..."],会导致“target not visible”错误。陷阱 3:重复依赖与钻石依赖Bazel 要求依赖图中每个库的版本是唯一的。如果你的项目通过不同路径依赖了 libA v1.0 和 libA v2.0(钻石依赖问题),Bazel 会报错。这强制开发者保持依赖树的清洁。陷阱 4:select 的滥用虽然 select 很强大,但过度使用会让 BUILD 文件变得难以阅读。对于复杂的逻辑,更好的方式是将其抽象成宏或自定义规则。
四、核心概念对照表
为了帮助您快速在三套语法间切换,下表总结了核心构建概念在 Android.mk、Android.bp 和 Bazel 中的表达方式。
概念 / 功能点 | Android.mk (GNU Make) | Android.bp (Soong) | Bazel (Starlark) |
|---|---|---|---|
文件 | Android.mk | Android.bp | BUILD 或 BUILD.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.bp 和 Bazel。
示例一:一个简单的 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 环境中执行
mm或mmm 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 会根据依赖关系自动决定是静态链接还是动态链接。srcs和name类似。- 头文件通过
hdrs属性声明,这使得 Bazel 可以更精确地分析依赖。includes属性用于设置-I路径。 - 必须设置
visibility,否则其他包无法依赖它。
- 构建命令:
bazel build //jni:libhello-bazel。 - 产物:
bazel-bin/jni/libhello-bazel.so。 - 易错点:忘记设置
visibility;hdrs和includes的区别;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收回了权力,强迫你“只说你要什么”,从而换取了速度和可靠性。Bazel在bp的哲学上更进一步,提供了无与伦比的跨平台能力、缓存效率和生态扩展性。
希望这篇教程能为你揭开 Android 构建系统的面纱,让你在日常开发和系统定制中更加得心应手。无论是维护旧代码,还是拥抱新技术,理解这三者的核心思想与差异,都将是你宝贵的财富。