Skip to content

产品配置链

追踪 lunch 三元组、AndroidProducts.mk、产品继承、BoardConfig.mk 和 Soong JSON 导出的配置链,并定位继承、板级选择与变量导出失败。

基于android-17.0.0_r1
AndroidAOSPMakeKatiSoong

产品配置链 ​

本文面向已经读过 Android.mk迁移链、Soong构建链、Makefile与Kati 和 构建条件链 的读者。你需要知道 Make/Kati 会读取变量并生成构建输入,但不必预先记住 PRODUCT_* 与 BOARD_* 的完整字典。

本文回答一个具体问题:执行 lunch aosp_x86_64 trunk_staging userdebug 后,TARGET_PRODUCT、产品继承关系、TARGET_DEVICE、BoardConfig.mk 和 Soong 的产品变量如何依次生效?文章不把厂商设备树当作 AOSP 默认行为,也不展开 Ninja 执行、镜像打包或运行时 build.prop 消费。

读完后,你应能顺着固定源码定位:一次 lunch 失败是在参数解析、产品注册、产品继承、板级文件选择还是 Soong 导出阶段;也能解释为什么 PRODUCT_MAKEFILES 只负责发现产品,而 PRODUCT_NAME、TARGET_ARCH 和 PRODUCT_SOONG_NAMESPACES 要在后续节点中才成为有效构建状态。

1. 数据边界 ​

产品配置不是三个并列文件。lunch 产生选择,AndroidProducts.mk 把选择映射到产品文件,产品文件通过继承形成变量节点,板级配置再根据 TARGET_DEVICE 补充硬件变量,最终 soong_config.mk 只导出已经展开和校验过的结果。

状态所有者生效时机消费者
产品、release、variantenvsetup.sh shell 环境lunch 返回后后续 Make/Kati 与命令
产品路径与菜单product_config.mk产品配置开始时current_product_makefile、COMMON_LUNCH_CHOICES
继承后的 PRODUCT_*PRODUCTS.<makefile>.* 节点import-nodes 展开后strip-product-vars、Make 规则
板级变量当前 BoardConfig.mkboard config 阶段combo、Soong、镜像规则
JSON 配置soong_config.mk 输出文件WRITE_SOONG_VARIABLES=true 时Soong 解析器

2. Lunch入口 ​

2.1 参数拆分 ​

Android 17 的 lunch 同时接受新格式的三个参数和兼容格式的单个连字符字符串。它本身不读取产品 Makefile;它只拆分参数,然后把三项放进一次 build_build_var_cache 的环境中,让 Make/Kati 验证并回填规范值。

源码文件:build/envsetup.sh

相关函数/类型:535-580, android-17.0.0_r1

bash
function lunch()
{
    if [[ $# -eq 0 ]]; then
        echo "No target specified. See lunch --help" 1>&2
        return 1
    fi
    local product release variant
    local legacy=$(echo $1 | grep "-")
    if [[ $# -eq 1 && -n $legacy ]]; then
        IFS="-" read -r product release variant <<< "$1"
        if [[ -z "$product" ]] || [[ -z "$release" ]] || [[ -z "$variant" ]]; then
            echo "Invalid lunch combo: $1" 1>&2
            return 1
        fi
    fi
    if [[ -z $legacy ]]; then
        product=$1
        release=${2:-trunk_staging}
        variant=${3:-eng}
    fi
    _lunch_meat $product $release $variant
    _lunch_store_leftovers $product $release $variant
}

这里的 owner 是 shell 局部变量。_lunch_meat 先把它们作为环境变量传入 build_build_var_cache,验证成功后才 export TARGET_PRODUCT 和 TARGET_BUILD_VARIANT。所以在 _lunch_meat 返回失败时,不能假设 shell 已经拥有一套半有效配置。

2.2 规范回填 ​

源码文件:build/envsetup.sh

bash
function _lunch_meat()
{
    local product=$1
    local release=$2
    local variant=$3
    TARGET_PRODUCT=$product \
    TARGET_RELEASE=$release \
    TARGET_BUILD_VARIANT=$variant \
    TARGET_BUILD_APPS= \
    build_build_var_cache
    if [ $? -ne 0 ]; then
        return 1
    fi
    export TARGET_PRODUCT=$(_get_build_var_cached TARGET_PRODUCT)
    export TARGET_BUILD_VARIANT=$(_get_build_var_cached TARGET_BUILD_VARIANT)
    export TARGET_RELEASE=$release
    export TARGET_BUILD_TYPE=release
    export TARGET_BUILD_APPS=
    set_stuff_for_environment
}

TARGET_RELEASE 在这里保留用户选择的 release 字符串,而 TARGET_PRODUCT 与 variant 从 Make/Kati 计算结果回读。这种差异很重要:环境变量是上一阶段的输入,也是下一阶段的输出快照,不是产品定义本身。

2.3 错误证据 ​

官方 build/make/tests/lunch_tests.sh 用 aosp_arm64、非法 variant、缺失 product 和多余连字符构造输入,断言成功时的 TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_PLATFORM_VERSION,失败时三者都为空。它证明的是 lunch 的参数与回填边界,不证明某个设备镜像能成功编译。

3. 产品注册 ​

3.1 文件发现 ​

product_config.mk 从 .module_paths/AndroidProducts.mk.list 与通用 build/target/product/AndroidProducts.mk 读取注册文件。每个注册文件只应依赖 LOCAL_DIR 等局部输入;它的职责是提供 PRODUCT_MAKEFILES、COMMON_LUNCH_CHOICES 和可选的 STARLARK_OPT_IN_PRODUCTS。

源码文件:build/make/core/product_config.mk

makefile
android_products_makefiles := $(file <$(OUT_DIR)/.module_paths/AndroidProducts.mk.list) \
  $(SRC_TARGET_DIR)/product/AndroidProducts.mk

_product-spec=$(strip $(if $(findstring :,$(1)),\
    $(1),$(basename $(notdir $(1))):$(1)))

define _read-ap-file
  $(eval PRODUCT_MAKEFILES :=) \
  $(eval COMMON_LUNCH_CHOICES :=) \
  $(eval LOCAL_DIR := $(patsubst %/,%,$(dir $(f)))) \
  $(eval include $(f)) \
  $(foreach p,$(PRODUCT_MAKEFILES),\
    $(eval ap_product_paths += $(call _product-spec,$(p)))) \
  $(eval ap_common_lunch_choices := $(COMMON_LUNCH_CHOICES))
endef

_product-spec 将两种写法统一为 product:path;因此后续并不是通过文件名猜产品,而是使用规范化后的产品名。LOCAL_DIR 是注册文件唯一可靠的目录上下文,不能在 AndroidProducts.mk 中读取尚未确定的 TARGET_PRODUCT。

3.2 菜单校验 ​

源码文件:build/make/core/product_config.mk

makefile
$(eval _products := $(call _first,$(ap_product_paths),:)) \
$(eval _bad := $(filter-out $(_products),\
    $(call _first,$(ap_common_lunch_choices),-))) \
$(if $(_bad),$(error COMMON_LUNCH_CHOICES contains products(s) not defined in this file: $(_bad))) \
$(eval _bad := $(filter-out %-eng %-userdebug %-user,$(ap_common_lunch_choices))) \
$(if $(_bad),$(error invalid variant in COMMON_LUNCH_CHOICES: $(_bad)))

验证先检查菜单中的 product 是否在同一注册文件声明,再检查 variant 是否属于 eng、userdebug 或 user。这解释了一个常见误区:COMMON_LUNCH_CHOICES 是可展示和校验的组合列表,不是产品继承图;真正的产品文件路径来自 PRODUCT_MAKEFILES。

3.3 当前文件 ​

所有注册文件聚合后,源码排序、去重并通过 TARGET_PRODUCT 选择当前文件:

源码文件:build/make/core/product_config.mk

makefile
product_paths :=
common_lunch_choices :=
$(foreach f,$(android_products_makefiles), \
    $(call _read-ap-file,$(f)) \
    $(eval product_paths += $(ap_product_paths)) \
    $(eval common_lunch_choices += $(ap_common_lunch_choices)))

product_paths := $(sort $(product_paths))
all_named_products := $(sort $(call _first,$(product_paths),:))
current_product_makefile := \
  $(call _second,$(filter $(TARGET_PRODUCT):%,$(product_paths)),:)
COMMON_LUNCH_CHOICES := $(sort $(common_lunch_choices))

ifeq (,$(current_product_makefile))
  $(error Cannot locate config makefile for product "$(TARGET_PRODUCT)")
endif

current_product_makefile 是进入产品图的入口。若产品名重复,源码会报 Product name must be unique;若没有匹配文件,则在继承任何产品变量之前失败。

4. 继承展开 ​

4.1 节点缓存 ​

产品继承使用 node_fns.mk 的节点模型。_import-node 清空当前变量、include 一个 makefile、把变量复制到 PRODUCTS.<path>.*,然后记录该节点声明的 inherited 列表。此时变量还没有被压平成最终产品。

源码文件:build/make/core/node_fns.mk

makefile
define _import-node
  $(eval _include_stack := $(2) $$(_include_stack))
  $(call clear-var-list,$(3))
  $(eval LOCAL_PATH := $(patsubst %/,%,$(dir $(2))))
  $(eval include $(2))
  $(call copy-var-list,$(1).$(2),$(3))
  $(call clear-var-list,$(3))
  $(eval $(1).$(2).inherited := \
      $(call get-inherited-nodes,$(1).$(2),$(3)))
  $(call _import-nodes-inner,$(1),$($(1).$(2).inherited),$(3),$(4))
  $(call _expand-inherited-values,$(1),$(2),$(3),$(4))
  $(eval $(1).$(2).inherited :=)
  $(eval _include_stack := $(wordlist 2,9999,$(_include_stack)))
endef

这里的状态 owner 是带路径的 PRODUCTS 节点,不是全局 PRODUCT_* 变量。清空与复制让同一个 makefile 可以被多个产品图共享,而不会把上一个产品的临时变量泄露到下一个产品。

4.2 继承标记 ​

产品文件调用 inherit-product 时,源码并不立即把父文件的每个值拼进当前全局变量,而是把 @inherit:<path> 标记写入对应节点的变量,并登记 .INHERITS_FROM。

源码文件:build/make/core/product.mk

makefile
define inherit-product
  $(eval _inherit_product_wildcard := $(wildcard $(1))) \
  $(if $(_inherit_product_wildcard),,$(error $(1) does not exist.)) \
  $(foreach part,$(_inherit_product_wildcard), \
    $(eval np := $(strip $(part))) \
    $(foreach v,$(_product_var_list), \
      $(eval $(v) := $($(v)) $(INHERIT_TAG)$(np))) \
    $(eval current_mk := $(strip $(word 1,$(_include_stack)))) \
    $(eval inherit_var := PRODUCTS.$(current_mk).INHERITS_FROM) \
    $(eval $(inherit_var) := $(sort $($(inherit_var)) $(np))) \
    $(call dump-inherit,$(current_mk),$(1)))
endef

INHERIT_TAG 是图构建的中间状态。这样做允许 node_fns.mk 统一处理 DAG 去重、循环检测以及单值变量和列表变量的不同合并策略。

4.3 单值合并 ​

_expand-inherited-values 对每个变量找到继承标记。列表变量会把父节点值插入标记位置;单值变量若当前节点已有真实值,则保留当前值,不再用父值覆盖。

源码文件:build/make/core/node_fns.mk

makefile
define _expand-inherited-values
  $(foreach v,$(3), \
    $(eval _eiv_tv := $(1).$(2).$(v)) \
    $(eval _eiv_i := $(sort $(patsubst $(INHERIT_TAG)%,%,\
      $(filter $(INHERIT_TAG)%,$($(_eiv_tv)))))) \
    $(eval _eiv_sv := $(filter $(v),$(4))) \
    $(foreach i,$(_eiv_i), \
      $(eval _eiv_ev := \
        $(if $(and $(_eiv_sv),$(filter-out $(INHERIT_TAG)%,$($(_eiv_tv)))),,\
          $($(1).$(i).$(v)))) \
      $(eval $(_eiv_tv) := \
        $(strip $(patsubst $(INHERIT_TAG)$(i),$(_eiv_ev),$($(_eiv_tv))))) \
      $(eval $(1).$(i).$(v) :=)))
endef

所以“后写的 PRODUCT_NAME 一定覆盖前写的值”并不是普遍规则:PRODUCT_NAME 属于单值变量,继承展开时当前节点已有值会保留;PRODUCT_PACKAGES 等列表变量才会按继承标记位置合并,之后还可能排序去重。

4.4 最终剥离 ​

产品图完成后,strip-product-vars 将当前 INTERNAL_PRODUCT 节点的值放回易读的全局变量,并把旧式 PRODUCTS.<node>.* 访问标记为 obsolete。此时才是 Make 规则消费产品变量的稳定时机。

源码文件:build/make/core/product.mk

makefile
define strip-product-vars
$(foreach v,$(_product_var_list) \
    PRODUCT_ENFORCE_PACKAGES_EXIST \
    PRODUCT_ENFORCE_PACKAGES_EXIST_ALLOW_LIST, \
  $(eval $(v) := $(strip $(PRODUCTS.$(INTERNAL_PRODUCT).$(v)))) \
  $(eval get-product-var = $$(if $$(filter $$(1),$$(INTERNAL_PRODUCT)),\
    $$($$(2)),$$(PRODUCTS.$$(strip $$(1)).$$(2)))) \
  $(KATI_obsolete_var PRODUCTS.$(INTERNAL_PRODUCT).$(v),Use $(v) instead))
endef

5. 板级选择 ​

5.1 设备定位 ​

板级配置在产品变量已经提供 TARGET_DEVICE 后运行。board_config.mk 先使用显式的 TARGET_DEVICE_DIR,否则在 AOSP、generic、cuttlefish、device/ 和 vendor/ 路径中寻找同名 BoardConfig.mk。

源码文件:build/make/core/board_config.mk

makefile
ifdef TARGET_DEVICE_DIR
  ifneq ($(origin TARGET_DEVICE_DIR),command line)
    $(error TARGET_DEVICE_DIR may not be set manually)
  endif
  board_config_mk := $(TARGET_DEVICE_DIR)/BoardConfig.mk
else
  board_config_mk := \
    $(sort $(wildcard \
      $(SRC_TARGET_DIR)/board/$(TARGET_DEVICE)/BoardConfig.mk \
      device/generic/goldfish/board/$(TARGET_DEVICE)/BoardConfig.mk \
      device/google/cuttlefish/board/$(TARGET_DEVICE)/BoardConfig.mk \
      $(shell test -d device && find -L device -maxdepth 4 \
        -path '*/$(TARGET_DEVICE)/BoardConfig.mk') \
      $(shell test -d vendor && find -L vendor -maxdepth 4 \
        -path '*/$(TARGET_DEVICE)/BoardConfig.mk')))
  ifeq ($(board_config_mk),)
    $(error No config file found for TARGET_DEVICE $(TARGET_DEVICE))
  endif
  ifneq ($(words $(board_config_mk)),1)
    $(error Multiple board config files for TARGET_DEVICE $(TARGET_DEVICE): $(board_config_mk))
  endif
endif

这段选择的消费者是后续 include $(board_config_mk)。路径搜索得到零个或多个结果都会在 include 前失败;不能把“找到了某个同名文件”理解为已经完成板级配置。

5.2 变量校验 ​

源码文件:build/make/core/board_config.mk

makefile
include $(board_config_mk)

ifeq ($(TARGET_ARCH)$(TARGET_ARCH_SUITE),)
  $(error Target architectures not defined by board config: $(board_config_mk))
endif
ifeq ($(TARGET_CPU_ABI)$(TARGET_ARCH_SUITE),)
  $(error TARGET_CPU_ABI not defined by board config: $(board_config_mk))
endif

$(foreach var,$(_board_strip_readonly_list),\
  $(eval $(var) := $$(strip $$($(var)))))
$(foreach var,$(_board_true_false_vars), \
  $(if $(filter-out true false,$($(var))), \
    $(error Valid values of $(var) are "true", "false", and "".\
      Not "$($(var))")))

TARGET_ARCH、TARGET_CPU_ABI 是 board 阶段的硬门槛;布尔变量则统一限制为 true、false 或空。它们的错误 owner 是 BoardConfig.mk,不是 Soong 模块。

5.3 只读时机 ​

产品变量和板级变量都存在 Kati 只读屏障。产品继承完成后,早期只读变量锁定产品身份;板级清理后,_board_strip_readonly_list 中的值被 strip 并锁定,防止后续规则悄悄改变输入。

源码文件:build/make/core/product.mk

makefile
_readonly_late_variables := \
  DEVICE_PACKAGE_OVERLAYS \
  PRODUCT_COPY_FILES \
  PRODUCT_DEX_PREOPT_BOOT_FLAGS

_readonly_early_variables := \
  $(filter-out $(_readonly_late_variables),$(_product_var_list))

define readonly-variables
$(foreach v,$(1), \
  $(eval $(v) ?=) \
  $(eval .KATI_READONLY := $(v)))
endef

只读不是“变量不能再被任何函数访问”,而是 Kati 在后续赋值时报告违规。调试产品配置时,应先确定某个值是在节点展开前、strip-product-vars 后还是 board 清理后被锁定。

6. Soong导出 ​

6.1 JSON入口 ​

soong_config.mk 不是重新解析产品图的地方。它读取已经由 Make/Kati 计算出的变量,并在 WRITE_SOONG_VARIABLES=true 时写出 soong.variables。例如产品、设备、架构和 ABI 都从 TARGET_* 读取,调试属性则从 variant 计算。

源码文件:build/make/core/soong_config.mk

makefile
$(call add_json_bool, Debuggable, $(filter userdebug eng,$(TARGET_BUILD_VARIANT)))
$(call add_json_bool, Eng,        $(filter eng,$(TARGET_BUILD_VARIANT)))
$(call add_json_str,  BuildType,  $(TARGET_BUILD_TYPE))

$(call add_json_str,  DeviceName,    $(TARGET_DEVICE))
$(call add_json_str,  DeviceProduct, $(TARGET_PRODUCT))
$(call add_json_str,  DeviceArch,    $(TARGET_ARCH))
$(call add_json_str,  DeviceArchVariant, $(TARGET_ARCH_VARIANT))
$(call add_json_str,  DeviceCpuVariant, $(TARGET_CPU_VARIANT))
$(call add_json_list, DeviceAbi, $(TARGET_CPU_ABI) $(TARGET_CPU_ABI2))

这里的 consumer 是 Soong 的配置加载器。它消费的是 JSON 字段名,而不是 Make 变量名;例如 Debuggable 已经是布尔值,Soong 不需要再次解释 userdebug eng。

6.2 Soong命名空间 ​

产品文件可以声明 PRODUCT_SOONG_NAMESPACES,但写入 JSON 时仍经过统一列表导出:

源码文件:build/make/core/soong_config.mk

makefile
$(call add_json_list, NamespacesToExport, $(PRODUCT_SOONG_NAMESPACES))
$(call add_json_list, BoardVendorSepolicyDirs, \
  $(BOARD_VENDOR_SEPOLICY_DIRS) $(BOARD_SEPOLICY_DIRS))
$(call add_json_str, BoardPlatform, $(TARGET_BOARD_PLATFORM))

命名空间影响 Soong 对模块路径的可见性;它不会自动改变产品继承关系,也不会替代 Android.bp 的 visibility。同样,sepolicy 目录导出只传递路径集合,不代表策略编译已经成功。

6.3 Soong配置变量 ​

Make 侧的 soong_config_set_bool、soong_config_set_int 和 soong_config_set_string_list 会同时设置值与类型。类型随后进入 VendorVarTypes,供 Soong soong_config_module_type 或 select 使用。

源码文件:build/make/core/config.mk

makefile
define soong_config_set_bool
$(call soong_config_define_internal,$1,$2) \
$(eval SOONG_CONFIG_$(strip $1)_$(strip $2):=$(filter true,$3))
$(eval SOONG_CONFIG_TYPE_$(strip $1)_$(strip $2):=bool)
endef

define soong_config_set_string_list
$(call soong_config_define_internal,$1,$2) \
$(eval SOONG_CONFIG_$(strip $1)_$(strip $2):=$(strip $3))
$(eval SOONG_CONFIG_TYPE_$(strip $1)_$(strip $2):=string_list)
endef

若把 bool 当普通字符串导出,Soong 的条件类型就会改变;若 int 输入不是整数,soong_config_set_int 会在 Make 侧报错。类型是配置状态的一部分,不是注释。

7. 调用时序 ​

前面的函数分属不同文件,最容易漏掉的是生效顺序。下面的时序把 shell、Make/Kati、产品节点、board 和 Soong 的 owner 分开:

8. 失败分层 ​

恢复动作必须回到失败阶段的 owner。修正 BoardConfig.mk 的 TARGET_ARCH 不会解决 COMMON_LUNCH_CHOICES 中的非法 product;修改 PRODUCT_SOONG_NAMESPACES 也不会让一个缺失的 BoardConfig.mk 被找到。

9. 源码验证 ​

可以在 AOSP checkout 中复现这条链的只读检查:

bash
# 查看当前产品如何注册与继承
rg -n "PRODUCT_MAKEFILES|COMMON_LUNCH_CHOICES|inherit-product" \
  build/target/product device/google/cuttlefish

# 追踪节点图和单值/列表展开
rg -n "INHERIT_TAG|_expand-inherited-values|import-nodes|strip-product-vars" \
  build/make/core/node_fns.mk build/make/core/product.mk

# 追踪板级唯一选择与硬门槛
rg -n "board_config_mk|Multiple board config|TARGET_ARCH|TARGET_CPU_ABI" \
  build/make/core/board_config.mk

# lunch 后查看已经导出的状态
source build/envsetup.sh
lunch aosp_x86_64 trunk_staging userdebug
get_build_var TARGET_PRODUCT
get_build_var TARGET_DEVICE
get_build_var TARGET_ARCH
get_build_var PRODUCT_SOONG_NAMESPACES

源码测试的覆盖边界要分开看:build/make/tests/lunch_tests.sh 证明选择与错误回填;node_fns.mk 的 RBC fixtures(例如 single_value_inheritance、prefixed_sort_order)证明产品继承转换的节点和顺序;它们不证明所有真实 vendor 产品的变量语义,也不证明镜像最终可启动。

10. 边界收束 ​

Android 17 的产品配置是一条有明确生效屏障的数据链:shell 只产生选择;product_config.mk 负责产品注册、规范化和当前文件定位;node_fns.mk 与 product.mk 将继承关系保存为节点并按变量类型展开;board_config.mk 根据 TARGET_DEVICE 唯一选择板级输入并校验硬件变量;soong_config.mk 最后把结果转换为 Soong 能消费的 JSON。

因此排查配置时不应从“哪个 mk 文件看起来像答案”开始,而应先问:当前值由哪个 owner 持有?它在节点展开前还是剥离后生效?下一个 consumer 读取的是 Make 变量、板级变量还是 JSON 字段?沿这三个问题走,才能把“lunch 成功但 Soong 失败”和“产品注册失败”区分开。