产品配置链
本文面向已经读过 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、variant | envsetup.sh shell 环境 | lunch 返回后 | 后续 Make/Kati 与命令 |
| 产品路径与菜单 | product_config.mk | 产品配置开始时 | current_product_makefile、COMMON_LUNCH_CHOICES |
继承后的 PRODUCT_* | PRODUCTS.<makefile>.* 节点 | import-nodes 展开后 | strip-product-vars、Make 规则 |
| 板级变量 | 当前 BoardConfig.mk | board 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
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
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
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
$(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
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)")
endifcurrent_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
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
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)))
endefINHERIT_TAG 是图构建的中间状态。这样做允许 node_fns.mk 统一处理 DAG 去重、循环检测以及单值变量和列表变量的不同合并策略。
4.3 单值合并
_expand-inherited-values 对每个变量找到继承标记。列表变量会把父节点值插入标记位置;单值变量若当前节点已有真实值,则保留当前值,不再用父值覆盖。
源码文件:build/make/core/node_fns.mk
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
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))
endef5. 板级选择
5.1 设备定位
板级配置在产品变量已经提供 TARGET_DEVICE 后运行。board_config.mk 先使用显式的 TARGET_DEVICE_DIR,否则在 AOSP、generic、cuttlefish、device/ 和 vendor/ 路径中寻找同名 BoardConfig.mk。
源码文件:build/make/core/board_config.mk
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
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
_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
$(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
$(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
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 中复现这条链的只读检查:
# 查看当前产品如何注册与继承
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 失败”和“产品注册失败”区分开。
