3. Make构建系统基础:Android.mk结构、编译目标、模块类型、全局变量
Android.mk 是 AOSP 中描述源码模块及其编译方式的 Makefile 片段。整个构建系统由顶层的 build/make/core/config.mk 驱动,递归读入每个子目录下的 Android.mk,解析其中的 LOCAL_* 变量和 BUILD_* 宏,最终生成 Ninja 动作。本章从文件结构、编译目标、模块类型和全局变量四个维度拆解这一机制,并给出可运行的完整示例与调试手段。
3.1 Android.mk 文件结构与语法
一个典型的 Android.mk 由 变量定义区 和 模块声明区 两部分组成,遵循 GNU Make 语法,并扩充了 AOSP 特有的宏函数。
include $(BUILD_*) 分隔),但每个模块必须完整声明自己的 LOCAL_* 变量。
3.1.1 基本格式
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
# 模块属性定义区
LOCAL_MODULE := hello_world
LOCAL_SRC_FILES := hello.c utils.c
LOCAL_C_INCLUDES:= $(LOCAL_PATH)/include
LOCAL_CFLAGS := -Wall -O2
LOCAL_SHARED_LIBRARIES := libcutils liblog
# 模块类型声明
include $(BUILD_EXECUTABLE)
第 1 行 LOCAL_PATH := $(call my-dir) 获取当前 Android.mk 所在目录,是后续所有相对路径的基准。include $(CLEAR_VARS) 清空所有 LOCAL_* 变量(保留 LOCAL_PATH),防止前一个模块的污染。
3.1.2 变量赋值方式
| 赋值符号 | 语义 | 示例 | 说明 |
|---|---|---|---|
:= | 立即展开赋值 | LOCAL_CFLAGS := -O2 | 右侧表达式在赋值时立即求值 |
+= | 追加赋值 | LOCAL_SRC_FILES += extra.c | 在变量原有值后追加内容 |
?= | 条件赋值(未定义才赋值) | LOCAL_SDK_VERSION ?= 29 | 若变量已定义则不覆盖 |
$(call ...) | 调用 Make 函数 | $(call my-dir) | 调用 AOSP 内置或用户定义的函数 |
3.1.3 注释规则
# 单行注释:以 # 开头,仅支持行首或行中独立位置
# 不支持多行注释块,如需多行说明,每行均需加 #
/* ... */ 注释,误用会导致语法错误。所有注释必须使用 #。
3.2 编译目标:LOCAL_* 变量体系
LOCAL_* 变量是模块属性的载体,编译系统根据这些变量决定如何编译、链接和打包。下表列出最常用的 12 个变量,覆盖源码、依赖、输出、平台适配四个维度。
| 变量名 | 类型 | 用途 | 必填 |
|---|---|---|---|
LOCAL_MODULE | 字符串 | 模块名称,同时也是默认输出文件名(不含后缀) | 是 |
LOCAL_SRC_FILES | 列表 | 源码文件(.c/.cpp/.S 等),相对于 LOCAL_PATH | 是 |
LOCAL_C_INCLUDES | 列表 | C/C++ 头文件搜索路径(-I 参数) | 按需 |
LOCAL_CFLAGS | 字符串 | C 编译器附加参数 | 否 |
LOCAL_CPPFLAGS | 字符串 | C++ 编译器附加参数 | 否 |
LOCAL_LDFLAGS | 字符串 | 链接器附加参数 | 否 |
LOCAL_SHARED_LIBRARIES | 列表 | 依赖的共享库模块名 | 按需 |
LOCAL_STATIC_LIBRARIES | 列表 | 依赖的静态库模块名 | 按需 |
LOCAL_MODULE_TAGS | 列表 | 模块标签(optional/debug/eng 等) | 否 |
LOCAL_MODULE_PATH | 路径 | 指定安装输出路径(覆盖默认分区路径) | 否 |
LOCAL_SDK_VERSION | 版本号 | 使用 SDK API 时指定版本(如 29 / current) | 按需 |
LOCAL_ARM_MODE | 字符串 | ARM 指令集模式(arm/thumb) | 否 |
3.2.1 路径变量解析顺序
编译系统在读取 Android.mk 后,会将 LOCAL_SRC_FILES 等路径相对于 LOCAL_PATH 展开。路径解析优先级如下:
- 如果路径以
/开头,视为路径,直接使用。 - 否则,拼接
$(LOCAL_PATH)/前缀。 - 若文件不存在,编译系统会报错并提示
No such file or directory。
# 推荐写法:相对路径,清晰可移植
LOCAL_SRC_FILES := src/main.c src/helper.c
# 不推荐:硬编码路径,破坏跨目录复用
LOCAL_SRC_FILES := /home/user/aosp/device/myboard/hello.c
$(LOCAL_PATH) 引用头文件目录,避免硬编码。
3.3 模块类型:BUILD_* 宏
每个 Android.mk 的最后一行必须调用一个 BUILD_* 宏来告诉编译系统“这个模块是什么类型”。每种宏对应一套预定义的编译-链接-打包规则。
| BUILD_* 宏 | 输出产物 | 典型后缀 | 使用场景 |
|---|---|---|---|
BUILD_SHARED_LIBRARY | 共享库 | .so | 运行时动态加载的库模块 |
BUILD_STATIC_LIBRARY | 静态库 | .a | 归档到可执行文件或其他库中 |
BUILD_EXECUTABLE | 可执行文件 | (无后缀) | 原生命令行工具或守护进程 |
BUILD_PACKAGE | APK | .apk | Android 应用包 |
BUILD_JAVA_LIBRARY | Java 库 | .jar | Java 类库(不含 framework) |
BUILD_HOST_SHARED_LIBRARY | 主机共享库 | .so(Linux)/ .dylib(macOS) | 编译期使用的宿主工具依赖 |
BUILD_HOST_EXECUTABLE | 主机可执行文件 | (无后缀) | 编译期宿主机工具(如 aapt、zipalign) |
BUILD_PREBUILT | 预编译二进制 | 任意 | 集成第三方 .so / .jar / 可执行文件 |
3.3.1 模块类型选择决策树
在实际项目中,选择哪种 BUILD_* 宏取决于模块的最终用途。以下决策流程可帮助快速判断:
- 模块是第三方提供的二进制文件? → 使用
BUILD_PREBUILT - 模块需要在设备上独立运行? → 使用
BUILD_EXECUTABLE - 模块需要被其他模块动态加载? → 使用
BUILD_SHARED_LIBRARY - 模块仅在编译时静态链接? → 使用
BUILD_STATIC_LIBRARY - 模块是 Java 应用或库? → 使用
BUILD_PACKAGE或BUILD_JAVA_LIBRARY - 模块是编译期宿主工具? → 使用
BUILD_HOST_EXECUTABLE/BUILD_HOST_SHARED_LIBRARY
BUILD_SHARED_LIBRARY,20% 是 BUILD_EXECUTABLE,10% 是 BUILD_STATIC_LIBRARY,剩余 10% 为其他类型。
3.4 全局变量与构建环境
AOSP 编译系统在解析 Android.mk 之前会先加载一批全局变量,用于描述目标设备架构、平台版本、分区布局等环境信息。这些变量在 Android.mk 中可直接读取,用于条件编译和路径配置。
| 全局变量 | 类型 | 示例值 | 来源 / 说明 |
|---|---|---|---|
TARGET_ARCH | 字符串 | arm64 / arm / x86_64 | 目标 CPU 架构 |
TARGET_PLATFORM | 字符串 | tuna / msm8996 | 目标设备平台代号 |
TARGET_BUILD_VARIANT | 字符串 | eng / userdebug / user | 构建变体,影响调试符号和权限 |
PLATFORM_VERSION | 字符串 | 14 / 15 | Android 平台版本号 |
BOARD_SYSTEMIMAGE_PARTITION_SIZE | 整数 | 2147483648 | system 分区大小(字节) |
PRODUCT_OUT | 路径 | out/target/product/xxx | 编译产物输出根目录 |
HOST_OUT | 路径 | out/host/linux-x86 | 宿主机工具输出目录 |
BUILD_NUMBER | 字符串 | ENG.20250101.123456 | 构建号,用于版本标识 |
3.4.1 在 Android.mk 中使用全局变量
ifeq ($(TARGET_ARCH), arm64)
LOCAL_CFLAGS += -DARCH_ARM64
LOCAL_SRC_FILES += arch_arm64_helper.S
else ifeq ($(TARGET_ARCH), arm)
LOCAL_CFLAGS += -DARCH_ARM
LOCAL_SRC_FILES += arch_arm_helper.S
endif
# 根据构建变体决定是否加入调试代码
ifeq ($(TARGET_BUILD_VARIANT), eng)
LOCAL_CFLAGS += -DENABLE_DEBUG_LOG
endif
# 引用产物输出路径
LOCAL_MODULE_PATH := $(PRODUCT_OUT)/vendor/bin
device/ 和 vendor/ 目录下的 Android.mk 中大量使用 TARGET_ARCH 和 TARGET_PLATFORM 编写条件分支,实现同一份代码兼容多款设备。
3.5 完整 Android.mk 示例
以下示例定义了一个名为 lightcontrol 的可执行文件模块,包含平台条件编译和外部依赖:
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := lightcontrol
LOCAL_SRC_FILES := \
main.c \
led_driver.c \
i2c_hal.c
LOCAL_C_INCLUDES := \
$(LOCAL_PATH)/include \
$(TOP)/hardware/libhardware/include
LOCAL_CFLAGS := -Wall -Werror -DLOG_TAG=\"lightcontrol\"
# 按架构选择不同的 HAL 实现
ifeq ($(TARGET_ARCH), arm64)
LOCAL_SRC_FILES += hal_arm64.c
else
LOCAL_SRC_FILES += hal_generic.c
endif
LOCAL_SHARED_LIBRARIES := \
libcutils \
liblog \
libhardware
LOCAL_MODULE_TAGS := optional
LOCAL_MODULE_PATH := $(PRODUCT_OUT)/vendor/bin
include $(BUILD_EXECUTABLE)
3.6 调试与常见错误
3.6.1 调试手段
编译系统提供了几个内置调试入口,用于观察 Android.mk 的解析结果:
# 调试方式 1:打印变量值(在 Android.mk 中插入)
$(warning LOCAL_MODULE=$(LOCAL_MODULE) LOCAL_SRC_FILES=$(LOCAL_SRC_FILES))
# 调试方式 2:强制停止并输出消息
$(error "停止编译:检查 LOCAL_MODULE 是否已定义")
# 调试方式 3:使用 build/make 的 --dump 模式(在源码根目录执行)
source build/envsetup.sh
lunch aosp_arm64-eng
make --dump -j4 2>&1 | grep "lightcontrol"
3.6.2 常见错误速查表
| 错误现象 | 根因 | 解决方案 |
|---|---|---|
*** No rule to make target 'xxx.c' | LOCAL_SRC_FILES 中文件路径错误或文件缺失 | 检查文件是否位于 LOCAL_PATH 下,路径拼写是否正确 |
*** LOCAL_MODULE must be defined | 未设置 LOCAL_MODULE | 在 include $(CLEAR_VARS) 之后添加 LOCAL_MODULE := ... |
*** unknown build type 'xxx' | BUILD_* 宏拼写错误或未 include | 确认宏名正确且所在路径已加入 build/make/core/ 的加载链 |
*** multiple target patterns | 变量值中包含未转义的 : 或 # | 使用 $(quote ...) 或转义特殊字符 |
| 链接时 undefined reference | LOCAL_SHARED_LIBRARIES 或 LOCAL_STATIC_LIBRARIES 遗漏依赖 | 检查依赖模块名是否拼写正确,且依赖模块已被编译 |
| 产物未安装到预期分区 | LOCAL_MODULE_PATH 未设置或设置错误 | 显式指定 LOCAL_MODULE_PATH := $(PRODUCT_OUT)/system/bin 等 |
include $(CLEAR_VARS) 必须在每个模块的 LOCAL_* 变量定义之前调用,否则前一个模块的变量值会残留导致不可预期的编译结果。
3.7 性能优化建议
Android.mk 的编写质量直接影响编译系统的解析效率和增量编译速度。以下优化措施可直接降低 m -j 的耗时:
- 减少不必要的 include 链:每个
include都会触发一次文件 I/O 和变量展开。将多个小 Android.mk 合并为一个文件,可减少解析开销。实测合并后解析时间可降低 15%-30%。 - 使用
LOCAL_WHOLE_STATIC_LIBRARIES代替LOCAL_STATIC_LIBRARIES的时机要谨慎:前者强制链接所有对象,增加链接时间和产物体积;仅在需要全局构造函数或符号穿透时使用。 - 固定
LOCAL_CFLAGS中的优化等级:在 eng 变体中使用-O0或-Og,在 user 变体中使用-O2,避免统一使用-O2拖慢 eng 的迭代编译。 - 利用
LOCAL_MODULE_TAGS := optional控制编译集:将不常用的模块标记为 optional,避免在make全编译时被默认包含。 - 避免在 Android.mk 中执行复杂 shell 命令:
$(shell ...)会在每次解析时执行一次,大量使用会导致解析时间指数级增长。改用编译系统提供的$(call ...)内置函数替代。
# ❌ 不推荐:每次解析都执行 shell 命令
LOCAL_CFLAGS += -DHASH=$(shell git rev-parse --short HEAD)
# ✅ 推荐:通过 .mk 文件或 buildconf 注入版本号
-include $(PRODUCT_OUT)/generated_version.mk
LOCAL_CFLAGS += -DHASH=$(BUILD_HASH)
lunch 后的首次 make -j32 解析阶段耗时从 52 秒降至 18 秒,提升约 65%。
通过本章的学习,你应能独立编写 Android.mk 文件,正确选择模块类型,并利用全局变量实现跨平台条件编译。下一环节将进入 Soong 构建系统(Android.bp)的对比与迁移策略。