17、自定义编译目标:创建新的编译目标、目标依赖链、目标属性配置

说实话,AOSP 的构建系统里,最让我觉得「有灵魂」的部分,就是自定义编译目标。

你想想看,默认的 make droid 能编译整个系统,但实际工作中,我们往往只需要编译某个模块、某个镜像,甚至是一个自定义的产物。我刚开始接触 AOSP 时,总觉得这东西很玄乎,直到有一次我需要为一个定制硬件单独生成一个 boot.img,才真正开始研究它。

嗯,今天我们就来聊聊,怎么创建自己的编译目标,怎么理清目标之间的依赖链,以及怎么配置目标属性。

17.1 什么是编译目标?

在 AOSP 里,编译目标说白了就是一个「任务」。它告诉构建系统:你要生成什么,以及怎么生成。

常见的编译目标有:

  • droid —— 全系统编译
  • bootimage —— 只生成 boot.img
  • systemimage —— 只生成 system.img
  • snod —— 快速打包镜像(不重新编译)

但这些都是系统预定义的。真正的高手,会自己定义目标。

核心概念: 编译目标 = 一个 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 vendorimage kernel system_server vendor_daemon 模块 A (libfoo) 模块 B (libbar) 模块 C (libbaz)

从上图可以看出,droid 依赖 bootimagesystemimage 等,而这些镜像又依赖各自的模块。这就是典型的依赖链。

在自定义目标时,你可以这样声明依赖:

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 目标,它依赖 bootimagesystemimage,然后额外打包一些调试工具进去。

# 文件位置: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) 的并行度。

嗯,自定义编译目标这块,其实没那么复杂。你只要记住:依赖链是骨架,属性配置是血肉。把这两块搞清楚了,剩下的就是写命令的事了。


公众号:蓝海资料掘金营,微信deep3321