23、构建系统与定制:Android.bp 与 Soong、产品配置、分区定制

说到 Android Automotive 的构建系统,很多人第一反应就是「不就是 make 一下吗?」。嗯,如果你真这么想,那在车机项目里大概率会踩坑。我最早接触 Android 构建时,还是 Android.mk 的时代,那时候改个模块依赖都能折腾半天。后来 Google 推出了 Soong 和 Android.bp,说实话,刚开始我是拒绝的——又得学一套新语法。但用了一段时间后,我不得不承认:真香。

今天我们就来聊聊 Automotive 构建系统的核心。说白了,就是三件事:怎么写构建文件、怎么配产品、怎么管分区

23.1 Android.bp 与 Soong:新一代构建核心

Soong 是 Android 从 Nougat 开始引入的构建系统,用来替代老旧的 Makefile。它的输入文件是 Android.bp,采用类似 JSON 的声明式语法。你可能会问:为什么要换?

我个人的理解是:Makefile 太灵活了,灵活到容易写出「幽灵依赖」。而 Android.bp 强制你声明清楚每个模块的源文件、依赖、编译选项,构建系统可以更好地做增量编译和并行优化。

23.1.1 基本语法

来看一个最简单的例子。假设我们要编译一个原生服务:

cc_binary {
    name: "my_car_service",
    srcs: ["main.cpp", "service.cpp"],
    shared_libs: [
        "libbinder",
        "libutils",
        "libhardware",
    ],
    cflags: ["-Wall", "-Werror"],
    init_rc: "my_car_service.rc",
}

这里 cc_binary 是模块类型,表示编译一个 C++ 可执行文件。类似的还有 cc_libraryjava_libraryandroid_app 等等。每个模块都有一个唯一的 name,其他模块可以通过这个名字来引用它。

小技巧: 我习惯把模块名和目录名保持一致,这样找代码时不用猜。比如目录是 packages/services/Car/service/,模块名就叫 car_service。团队里其他人一看就懂。

23.1.2 模块依赖与可见性

Android 11 之后,Soong 引入了 visibility 机制。说白了,就是控制「谁能用我这个模块」。以前在 Makefile 里,任何模块都可以随意链接你的库,很容易造成循环依赖或者接口滥用。

cc_library {
    name: "libcar_common",
    srcs: ["common.cpp"],
    visibility: [
        "//packages/services/Car:__subpackages__",
        "//vendor/xxx:__subpackages__",
    ],
}

上面这个配置表示:只有 packages/services/Carvendor/xxx 目录下的模块才能依赖 libcar_common。其他模块想用?对不起,编译直接报错。

注意: 我曾经在一个项目里没设 visibility,结果系统服务、HAL、甚至第三方应用都来依赖同一个内部库。后来改接口时,改一个地方崩一片。从那以后,我每个新模块都会先想清楚:这个模块到底该给谁用?

23.1.3 Soong 的扩展机制

Soong 还支持用 Go 语言写扩展,叫做 soong_build 插件。不过说实话,大部分场景用不到。如果你只是做车机定制,掌握 Android.bp 的基本语法就够用了。只有当你需要自定义编译规则(比如生成代码、处理特殊资源)时,才需要碰 Go 扩展。

23.2 产品配置:从 AOSP 到你的车机

产品配置,说白了就是告诉构建系统:你要编译一个什么样的系统?是给哪款车用的?有哪些硬件?预装哪些应用?

在 AOSP 里,产品配置通常放在 device/vendor/ 目录下。一个典型的产品配置包含以下几个文件:

  • BoardConfig.mk:定义硬件相关参数,比如芯片架构、分区大小、内核路径。
  • device.mk:定义产品相关的模块列表,比如要编译哪些包、预装哪些 APK。
  • product.mk:定义产品名称、品牌、设备代号等。
  • AndroidProducts.mk:把上面的配置组合起来,形成一个完整的产品。

举个例子,假设我们要为某款车机定义产品:

# device/mycompany/my_car/device.mk
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
$(call inherit-product, packages/services/Car/car_product/build/car.mk)

PRODUCT_NAME := my_car
PRODUCT_DEVICE := my_car
PRODUCT_BRAND := MyCompany
PRODUCT_MODEL := MyCarHeadUnit

# 预装车机应用
PRODUCT_PACKAGES += \
    MyCarLauncher \
    MyCarSettings \
    MyCarMedia

# 添加车机特有权限
PRODUCT_COPY_FILES += \
    frameworks/native/data/etc/android.hardware.type.automotive.xml:$(TARGET_COPY_OUT_VENDOR)/etc/permissions/android.hardware.type.automotive.xml

这里的关键是 inherit-product,它允许你继承其他产品的配置。比如 car.mk 是 Google 提供的 Automotive 基础配置,里面包含了车机需要的系统服务、权限、默认应用等。你只需要在此基础上添加自己的东西就行。

核心思路: 产品配置的本质是「组合」和「覆盖」。你不需要从零开始,而是站在 AOSP 的肩膀上,只定制差异部分。

23.3 分区定制:system、vendor、product 与 odm

分区是 Android 构建里最容易让人迷糊的地方。尤其是 Automotive,分区设计比手机更复杂。我们先理清几个关键分区的作用:

分区 用途 谁可以修改
system Android 框架、系统应用、核心库 SoC 厂商 / Google
vendor 硬件抽象层(HAL)、驱动、厂商特有服务 硬件厂商
product 产品定制内容,如预装应用、壁纸、配置 OEM / 车厂
odm 原始设计制造商定制,如传感器校准数据 ODM

为什么要分这么多分区?说白了,是为了解耦。Google 只更新 system,硬件厂商只更新 vendor,车厂只更新 product。大家互不干扰,OTA 升级时也可以只推送自己负责的分区。

23.3.1 分区定制实战

假设我们要在 product 分区里添加一个车机专属的配置文件。首先,在 device/mycompany/my_car/ 下创建目录结构:

device/mycompany/my_car/
├── product/
│   └── etc/
│       └── car_config.xml
└── device.mk

然后在 device.mk 里添加:

PRODUCT_COPY_FILES += \
    device/mycompany/my_car/product/etc/car_config.xml:$(TARGET_COPY_OUT_PRODUCT)/etc/car_config.xml

这样编译后,car_config.xml 就会出现在 /product/etc/ 目录下。应用可以通过 getprop 或者直接读取文件来获取配置。

避坑指南: 我曾经把配置文件放到了 system 分区,结果 OTA 升级时被覆盖了。后来才意识到,车厂定制的数据应该放在 productdata 分区,而不是 system。记住:system 是 Google 的地盘,别乱动。

23.3.2 动态分区与超级镜像

Android 10 之后引入了动态分区。什么意思呢?以前每个分区的大小是固定的,比如 system 分 2GB,vendor 分 1GB。如果 system 不够用,vendor 却有大量空闲,那就浪费了。

动态分区允许所有分区共享一个「超级镜像」(super.img),空间按需分配。在 Automotive 上,这个特性特别有用——因为车机的硬件配置五花八门,分区大小很难提前定死。

配置动态分区需要在 BoardConfig.mk 里设置:

TARGET_USERIMAGES_USE_EXT4 := true
BOARD_SUPER_PARTITION_SIZE := 4294967296  # 4GB
BOARD_SUPER_PARTITION_GROUPS := my_group
BOARD_MY_GROUP_SIZE := 4294967296
BOARD_MY_GROUP_PARTITION_LIST := system vendor product odm

这样,systemvendorproductodm 四个分区都放在一个 4GB 的超级分区里,空间自动调节。

23.4 知识体系总览

下面这张图总结了本章的核心内容,你可以把它当作一个快速参考:

Android Automotive 构建系统核心架构 Android.bp / Soong 模块定义 & 依赖管理 产品配置 BoardConfig / device.mk 分区定制 system / vendor / product 模块类型 cc_binary / cc_library java_library / android_app visibility 可见性控制 配置要点 inherit-product 继承 PRODUCT_PACKAGES 预装 PRODUCT_COPY_FILES 复制 分区策略 动态分区 super.img TARGET_COPY_OUT_PRODUCT OTA 升级兼容 核心原则:声明式构建 + 分层解耦 + 按需定制 Android.bp 声明模块 → 产品配置组合模块 → 分区策略决定输出位置

23.5 一些实战建议

最后,分享几个我在 Automotive 项目里积累的经验:

  • 善用 soong_log:编译时加 SOONG_LOG=debug 可以打印 Soong 的解析过程,排查依赖问题特别好用。
  • 模块命名要规范:我见过有人把车机服务命名为 car_service,也有人叫 my_car_service,还有人叫 service_car。同一个项目里三种命名风格,找模块时简直崩溃。建议团队统一前缀,比如 automotive_ 开头。
  • 分区大小别算太死:车机的存储芯片型号可能会变,留 20% 的余量比较安全。我吃过亏,第一次流片时 product 分区只留了 100MB,结果客户要加 3 个预装应用,直接爆了。
  • 用好 PRODUCT_ENFORCE_PRODUCT_PARTITION_INTERFACE:这个开关可以强制所有产品定制内容只能放在 product 分区,防止不小心污染 systemvendor。建议默认开启。

嗯,构建系统这块内容确实有点干,但它是车机定制的基石。你想想看,如果连模块怎么编译、文件放哪个分区都没搞清楚,后面做功能定制时一定会手忙脚乱。希望今天的分享能帮你理清思路。


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