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 特有的宏函数。

核心原则:每个 Android.mk 可以定义多个模块(通过 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 注释规则

# 单行注释:以 # 开头,仅支持行首或行中独立位置
# 不支持多行注释块,如需多行说明,每行均需加 #
陷阱注意:Android.mk 中 不识别 C 风格的 /* ... */ 注释,误用会导致语法错误。所有注释必须使用 #。

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 展开。路径解析优先级如下:

  1. 如果路径以 / 开头,视为路径,直接使用。
  2. 否则,拼接 $(LOCAL_PATH)/ 前缀。
  3. 若文件不存在,编译系统会报错并提示 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_PACKAGEAPK.apkAndroid 应用包
BUILD_JAVA_LIBRARYJava 库.jarJava 类库(不含 framework)
BUILD_HOST_SHARED_LIBRARY主机共享库.so(Linux)/ .dylib(macOS)编译期使用的宿主工具依赖
BUILD_HOST_EXECUTABLE主机可执行文件(无后缀)编译期宿主机工具(如 aapt、zipalign)
BUILD_PREBUILT预编译二进制任意集成第三方 .so / .jar / 可执行文件

3.3.1 模块类型选择决策树

在实际项目中,选择哪种 BUILD_* 宏取决于模块的最终用途。以下决策流程可帮助快速判断:

  1. 模块是第三方提供的二进制文件? → 使用 BUILD_PREBUILT
  2. 模块需要在设备上独立运行? → 使用 BUILD_EXECUTABLE
  3. 模块需要被其他模块动态加载? → 使用 BUILD_SHARED_LIBRARY
  4. 模块仅在编译时静态链接? → 使用 BUILD_STATIC_LIBRARY
  5. 模块是 Java 应用或库? → 使用 BUILD_PACKAGE 或 BUILD_JAVA_LIBRARY
  6. 模块是编译期宿主工具? → 使用 BUILD_HOST_EXECUTABLE / BUILD_HOST_SHARED_LIBRARY
经验值:在 AOSP 原生代码中,约 60% 的模块是 BUILD_SHARED_LIBRARY,20% 是 BUILD_EXECUTABLE,10% 是 BUILD_STATIC_LIBRARY,剩余 10% 为其他类型。
Android.mk 编译流程与模块映射 Android.mk 入口 LOCAL_* 变量定义 模块名 · 源文件 · 依赖 · 标志 BUILD_* 模块类型选择 共享库 · 可执行 · APK · 预编译 编译系统解析与链接 .so / .a / .apk 💡 核心机制: 每个 BUILD_* 宏对应一组预定义的 Make 规则集,编译系统根据模块类型自动匹配编译、链接和打包流程。

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 / 15Android 平台版本号
BOARD_SYSTEMIMAGE_PARTITION_SIZE整数2147483648system 分区大小(字节)
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 referenceLOCAL_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)
优化收益参考:在大型 AOSP 项目(如车载 GSIG 版本)中,将 40+ 个零散 Android.mk 合并为 8 个聚合文件后,lunch 后的首次 make -j32 解析阶段耗时从 52 秒降至 18 秒,提升约 65%。

通过本章的学习,你应能独立编写 Android.mk 文件,正确选择模块类型,并利用全局变量实现跨平台条件编译。下一环节将进入 Soong 构建系统(Android.bp)的对比与迁移策略。