17、自定义编译目标:创建新的编译目标、目标依赖链、目标属性配置
说实话,AOSP 的构建系统里,最让我觉得「有灵魂」的部分,就是自定义编译目标。
你想想看,默认的 make droid 能编译整个系统,但实际工作中,我们往往只需要编译某个模块、某个镜像,甚至是一个自定义的产物。我刚开始接触 AOSP 时,总觉得这东西很玄乎,直到有一次我需要为一个定制硬件单独生成一个 boot.img,才真正开始研究它。
嗯,今天我们就来聊聊,怎么创建自己的编译目标,怎么理清目标之间的依赖链,以及怎么配置目标属性。
17.1 什么是编译目标?
在 AOSP 里,编译目标说白了就是一个「任务」。它告诉构建系统:你要生成什么,以及怎么生成。
常见的编译目标有:
droid—— 全系统编译bootimage—— 只生成 boot.imgsystemimage—— 只生成 system.imgsnod—— 快速打包镜像(不重新编译)
但这些都是系统预定义的。真正的高手,会自己定义目标。
核心概念: 编译目标 = 一个 Make 规则(rule)或 Ninja 目标(target),它关联了一组依赖和一组操作。
17.2 创建新的编译目标
我个人习惯在 build/make/core/ 下或者直接在项目的 Android.mk 里定义新目标。但更推荐的做法,是在你的产品配置目录下单独维护。
举个例子,假设我想定义一个叫 my_custom_image 的目标,用来生成一个自定义的镜像文件。
# 在 device/xxx/yyy/ 下创建 custom_target.mk
.PHONY: my_custom_image
my_custom_image: $(PRODUCT_OUT)/my_custom.img
$(PRODUCT_OUT)/my_custom.img: $(INSTALLED_BOOTIMAGE_TARGET) $(INSTALLED_SYSTEMIMAGE_TARGET)
$(hide) echo "Creating custom image..."
$(hide) dd if=/dev/zero of=$@ bs=1M count=64
$(hide) $(call build-image, $@, $(PRODUCT_OUT)/system)
$(hide) echo "Done: $@"
然后在 AndroidProducts.mk 里 include 这个文件:
include device/xxx/yyy/custom_target.mk
这样,你就能直接 make my_custom_image 了。
小技巧: 我建议用 .PHONY 声明伪目标,避免和文件名冲突。曾经我踩过坑,目标名和文件重名,结果 make 说「文件已存在,不执行」。
17.3 目标依赖链
构建系统的核心,就是依赖链。没有依赖,目标就是个孤岛。
在 AOSP 里,依赖链通常是这样组织的:
从上图可以看出,droid 依赖 bootimage、systemimage 等,而这些镜像又依赖各自的模块。这就是典型的依赖链。
在自定义目标时,你可以这样声明依赖:
my_custom_target: $(INSTALLED_BOOTIMAGE_TARGET) $(INSTALLED_SYSTEMIMAGE_TARGET)
# 执行自定义操作
这里 $(INSTALLED_BOOTIMAGE_TARGET) 是系统预定义的变量,指向最终的 boot.img 文件路径。通过这种依赖关系,你的自定义目标会自动等待 boot.img 和 system.img 编译完成后再执行。
注意: 依赖链不能有环。我曾经在项目中不小心写了个循环依赖,结果 make 直接报错退出。排查了半天才发现是 A 依赖 B,B 又依赖 A。
17.4 目标属性配置
每个编译目标都可以配置属性。在 AOSP 里,属性通常通过变量来设置。
常见的属性包括:
| 属性名 | 说明 | 示例 |
|---|---|---|
LOCAL_MODULE |
模块名称 | LOCAL_MODULE := libfoo |
LOCAL_SRC_FILES |
源文件列表 | LOCAL_SRC_FILES := foo.c bar.c |
LOCAL_MODULE_TAGS |
模块标签(optional, debug, eng 等) | LOCAL_MODULE_TAGS := optional |
LOCAL_REQUIRED_MODULES |
强制依赖的模块 | LOCAL_REQUIRED_MODULES := libcutils |
LOCAL_INIT_RC |
init rc 文件 | LOCAL_INIT_RC := my_daemon.rc |
对于自定义的编译目标(非模块),属性配置方式略有不同。你可以通过 Make 变量来控制:
# 自定义目标的属性
MY_TARGET_OUT := $(PRODUCT_OUT)/my_target
MY_TARGET_DEPS := $(INSTALLED_BOOTIMAGE_TARGET) $(INSTALLED_SYSTEMIMAGE_TARGET)
MY_TARGET_POST_PROCESS := true
.PHONY: my_target
my_target: $(MY_TARGET_DEPS)
@echo "Building my_target..."
@mkdir -p $(MY_TARGET_OUT)
@cp $(PRODUCT_OUT)/boot.img $(MY_TARGET_OUT)/
@if [ "$(MY_TARGET_POST_PROCESS)" = "true" ]; then \
echo "Running post process..."; \
fi
个人经验: 我习惯把自定义目标的属性集中放在一个 target_config.mk 文件里,然后用 include 引入。这样不同产品可以复用同一套目标定义,只需要覆盖属性变量即可。
17.5 实战:创建一个带依赖的自定义镜像目标
好了,理论说完了。我们直接上手一个实际案例。
假设我们要创建一个 my_debug_image 目标,它依赖 bootimage 和 systemimage,然后额外打包一些调试工具进去。
# 文件位置:device/xxx/yyy/debug_target.mk
# 定义输出路径
MY_DEBUG_IMAGE := $(PRODUCT_OUT)/my_debug.img
# 声明依赖
.PHONY: my_debug_image
my_debug_image: $(INSTALLED_BOOTIMAGE_TARGET) $(INSTALLED_SYSTEMIMAGE_TARGET)
@echo "=== Building my_debug_image ==="
@echo "Step 1: Create empty image"
@dd if=/dev/zero of=$(MY_DEBUG_IMAGE) bs=1M count=128 2>/dev/null
@echo "Step 2: Copy system image"
@dd if=$(PRODUCT_OUT)/system.img of=$(MY_DEBUG_IMAGE) conv=notrunc 2>/dev/null
@echo "Step 3: Append debug tools"
@$(call append-debug-tools, $(MY_DEBUG_IMAGE))
@echo "=== Done: $(MY_DEBUG_IMAGE) ==="
# 辅助函数
define append-debug-tools
@echo "Adding strace, gdb, etc."
@# 实际操作用 mkbootfs 等工具
endef
然后在产品配置中启用:
# AndroidProducts.mk
include device/xxx/yyy/debug_target.mk
编译时直接运行:
source build/envsetup.sh
lunch xxx-eng
make my_debug_image
关键点: 自定义目标的核心在于「依赖链」和「属性配置」。依赖链决定了编译顺序,属性配置决定了行为。两者缺一不可。
17.6 避坑指南
最后,分享几个我踩过的坑:
- 目标名冲突: 我曾经定义了一个叫
clean的目标,结果覆盖了系统的clean规则。建议加前缀,比如my_clean。 - 忘记 .PHONY: 如果目标名和文件名相同,make 会认为文件已存在而不执行。用
.PHONY声明伪目标可以避免。 - 依赖变量未定义: 使用
$(INSTALLED_BOOTIMAGE_TARGET)前,确保已经include了对应的 mk 文件,否则变量为空,依赖不生效。 - 并行编译问题: 自定义目标如果涉及文件操作,建议用
$(hide)隐藏命令输出,同时注意$(MAKE)的并行度。
嗯,自定义编译目标这块,其实没那么复杂。你只要记住:依赖链是骨架,属性配置是血肉。把这两块搞清楚了,剩下的就是写命令的事了。