编译脚本定制:自定义编译命令、扩展编译系统、添加新功能

Android 构建系统不是死的。说白了,它是一套可以让你随意折腾的框架。我见过很多团队,拿到 AOSP 源码后第一件事就是改构建脚本——加个自定义命令、集成内部工具、或者干脆重写部分逻辑。今天我们就聊聊怎么在 Android 构建系统里「加料」。

为什么需要定制编译脚本?

你可能觉得 Google 给的编译命令够用了。嗯,大部分场景确实够。但实际项目中,总有些特殊需求:

  • 编译完成后自动把镜像推到测试机
  • 集成第三方静态检查工具
  • 生成自定义的版本号或签名逻辑
  • 并行编译多个产品变体

我在做车载系统时,就遇到过需要同时编译三个不同屏幕尺寸的镜像。默认的 make 命令一次只能搞一个,手动跑三遍太傻了。于是我们加了个自定义命令,一行搞定。

理解构建系统的扩展点

Android 构建系统基于 Make 和 Soong(Blueprint)。扩展它,你得知道往哪插。核心扩展点有三个:

扩展点 位置 作用
BoardConfig.mk device/<vendor>/<product>/ 定义板级变量、编译选项
Android.mk / Android.bp 各模块目录 添加模块、自定义编译规则
vendorsetup.sh device/<vendor>/<product>/ 添加 lunch 选项、环境变量

我个人习惯把自定义命令放在 device/<vendor>/common/ 下,这样所有产品都能复用。

实战:添加一个自定义编译命令

假设我们要加一个 mymake 命令,编译完成后自动把镜像拷贝到指定目录。怎么做?

第一步,在 device/<vendor>/common/ 下创建 custom_commands.mk

# 自定义编译命令
.PHONY: mymake
mymake: droid
    @echo "编译完成,开始拷贝镜像..."
    cp $(OUT)/target/product/$(TARGET_DEVICE)/*.img /mnt/shared/images/
    @echo "镜像已拷贝到 /mnt/shared/images/"

第二步,在产品的 BoardConfig.mk 末尾引入:

include device/<vendor>/common/custom_commands.mk

然后你就可以在源码根目录下执行 source build/envsetup.sh && lunch 后,直接敲 mymake 了。

小技巧: 如果你想让自定义命令支持参数,可以用 $(filter)$(if) 函数。我曾经写过一个带版本号参数的命令,方便 CI 系统调用。

扩展 Soong 构建系统

Soong 是 Android 7.0 之后引入的构建系统,用 Go 语言写的。扩展它需要写 Go 代码,但别怕,没那么复杂。

举个例子,我想在编译某个模块时,自动生成一个头文件。在 Android.bp 里可以这样:

cc_binary {
    name: "my_daemon",
    srcs: ["main.cpp"],
    generated_headers: ["my_header_gen"],
}

genrule {
    name: "my_header_gen",
    cmd: "python3 $(location gen_header.py) $(out)",
    out: ["my_header.h"],
    tools: ["gen_header.py"],
}

这里 genrule 就是 Soong 提供的扩展点。你可以用它执行任意脚本,生成文件、检查代码、甚至发通知。

注意: Soong 的 genrulecmd 不能写太复杂的逻辑。我踩过坑——写了个 200 行的 shell 命令,结果调试到崩溃。建议把复杂逻辑写成独立脚本,用 tools 引用。

添加新功能:集成代码检查工具

很多团队希望在编译时自动跑静态检查。比如用 cppcheck 检查 C++ 代码。我们可以通过 Android.mkLOCAL_POST_LINK_CMD 来实现:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_MODULE := my_service
LOCAL_SRC_FILES := my_service.cpp

# 编译后自动运行 cppcheck
LOCAL_POST_LINK_CMD := cppcheck --enable=all $(LOCAL_SRC_FILES)

include $(BUILD_EXECUTABLE)

这样每次编译 my_service 时,都会自动跑一遍 cppcheck。如果检查出问题,编译会失败。

但说实话,我不建议在每次编译时都跑全量检查。太慢了。更好的做法是加一个独立的目标:

.PHONY: check_all
check_all:
    find $(TOP)/system/ -name "*.cpp" -exec cppcheck {} \;

然后在 CI 里单独调用 make check_all。这样开发时编译快,提交前再跑完整检查。

知识体系总览

下面这张图帮你理清整个定制流程:

编译脚本定制知识体系 BoardConfig.mk 板级变量、编译选项 Android.mk / .bp 模块定义、自定义规则 vendorsetup.sh lunch选项、环境变量 自定义编译命令 .PHONY 目标 include 引入 扩展 Soong genrule 生成文件 Go 插件 添加新功能 集成检查工具 自定义构建步骤 定制后的构建系统 更灵活、更自动化、更符合团队需求 核心原则:不改核心,只做扩展

避坑指南

定制编译脚本看着简单,但坑不少。我总结几个常见的:

  • 不要修改 build/core/ 下的文件——那是构建系统的核心,升级 AOSP 时会被覆盖。我曾经手贱改过,结果升级时一堆冲突,后悔死了。
  • 变量名别冲突——Android 构建系统用了大量全局变量。你自定义的变量最好加个前缀,比如 MY_CUSTOM_
  • 注意并行编译——Make 的 -j 参数会让多个目标并行执行。如果你的自定义命令依赖某个模块,记得加 .NOTPARALLEL 或显式声明依赖。
  • 调试时用 $(info)——在 .mk 文件里加 $(info 变量名=$(变量名)),编译时就能看到变量值。比猜快多了。

核心原则:定制编译脚本时,始终记住「不改核心,只做扩展」。这样升级 AOSP 时,你的定制代码还能继续用。

好了,关于编译脚本定制就聊这么多。你可以在自己的项目里试试,加个自定义命令,或者集成个检查工具。动手做一遍,比看十遍文章都管用。


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