lunch 一个产品名背后,到底读了哪些 .mk?做了些什么事情?
专栏:AOSP 构建系统全解析 源码版本:Android 17 作者:知昂七昂
一、先从一个问题开始
上一篇我们讲完了 source build/envsetup.sh。现在我们敲下:
bash
lunch aosp_cf_arm64_phone-trunk_staging-userdebug
从这一行回车,到终端打印出 PLATFORM_VERSION=... TARGET_PRODUCT=aosp_cf_arm64_phone ...,中间到底经过了哪些代码?
二、完整时序图
三、按时序图逐段解读源码
3.1 第一步:lunch 函数本体
bash
# build/envsetup.sh:535
function lunch()
{
# 我们传进去 aosp_cf_arm64_phone-trunk_staging-userdebug
if [ "$#" -gt 1 ]; then
echo "usage: lunch [target]"
return 1
fi
local product release variant
if [ "$#" -eq 1 ]; then
# 用 - 把字符串拆开
local legacy=$(echo $1 | grep "-")
if [ -n "$legacy" ]; then
# 拆成三个:product=aosp_cf_arm64_phone, release=trunk_staging, variant=userdebug
IFS="-" read -r product release variant <<< "$1"
else
product=$1
fi
fi
# lunch 本身只是个参数解析壳,真正干活的是 _lunch_meat
_lunch_meat $product $release $variant
}
3.2 第二步:_lunch_meat 做了什么
bash
# build/envsetup.sh:449
function _lunch_meat()
{
local product=$1
local release=$2
local variant=$3
# 把三个参数导出到环境,传给后面的构建系统
export TARGET_PRODUCT=$product
export TARGET_RELEASE=$release
export TARGET_BUILD_VARIANT=$variant
# 这一步是关键:真正去调 Go 程序要变量值
build_build_var_cache
# 把构建系统返回的值再导出来,覆盖之前的默认值
export TARGET_PRODUCT=$(_get_build_var_cached TARGET_PRODUCT)
export TARGET_BUILD_VARIANT=$(_get_build_var_cached TARGET_BUILD_VARIANT)
}
3.3 第三步:build_build_var_cache 怎么调到 Go
bash
# build/envsetup.sh:56
function build_build_var_cache()
{
local T=$(gettop)
# 从 envsetup.sh 里 grep 出所有要缓存的变量名
cached_vars=(`cat $T/build/envsetup.sh | ...`)
# 就是这一行,我们从 shell 跳进了 Go 程序
# 传 --dumpvars-mode 表示"我只查变量值,不真的编译"
build_dicts_script=`build/soong/soong_ui.bash --dumpvars-mode \
--vars="${cached_vars[*]}" \
--var-prefix=var_cache_`
# 把 Go 程序打印出来的 KEY=VALUE 用 eval 变成 shell 变量
eval "$build_dicts_script"
}
3.4 第四步:soong_ui.bash 怎么调到 Go
上一篇我们详细讲过:soong_ui.bash 会 source microfactory.bash,用 soong_build_go 函数把 build/soong/cmd/soong_ui 这个 Go main 包编译成 out/soong_ui 二进制,然后 exec 执行它。
这部分上一篇讲透了,这里不重复。我们直接进到 Go 代码里。
3.5 第五步:Go 代码入口
go
// build/soong/cmd/soong_ui/main.go:410
func dumpVars(ctx build.Context, config build.Config, args []string) {
// 解析 --vars="TARGET_PRODUCT ..." 命令行参数
flags := flag.NewFlagSet("dumpvars", flag.ExitOnError)
varsStr := flags.String("vars", "", "Space-separated list of variables to dump")
flags.Parse(args)
// 把字符串拆成 ["TARGET_PRODUCT", "TARGET_BUILD_VARIANT", ...]
vars := strings.Fields(*varsStr)
allVars := append([]string{}, vars...)
// 关键入口:Go 自己不读 .mk 文件,调 DumpMakeVars 让 Kati 去读
varData, err := build.DumpMakeVars(ctx, config, nil, allVars)
if err != nil {
ctx.Fatal(err)
}
// 把 map 里的值打印成 KEY='VALUE' 格式,stdout 出来
for _, name := range vars {
fmt.Printf("%s%s='%s'\n", *varPrefix, name, varData[name])
}
}
3.6 第六步:Go 是怎么真正启动 Kati 的?
上一节我们看到 main.go:410 里调了 build.DumpMakeVars。那这个函数内部是怎么启动 Kati 的?
3.6.1 第一层:DumpMakeVars
go
// build/soong/ui/build/dumpvars.go:60
func DumpMakeVars(ctx Context, config Config, goals, vars []string) (map[string]string, error) {
// 先把不需要调 Make 的变量摘出来(比如 OUT_DIR,Go 自己就知道)
makeVars := make([]string, 0, len(vars))
for _, v := range vars {
if _, ok := soongUiVars[v]; !ok {
makeVars = append(makeVars, v)
}
}
// 剩下的变量,交给 dumpMakeVars 去问 Kati
ret, err := dumpMakeVars(ctx, config, goals, makeVars, tmpDir, DUMPVARS_USER)
if err != nil {
return nil, err
}
return ret, nil
}
关键:第 16 行,调了 dumpMakeVars。
3.6.2 第二层:dumpMakeVars 真正启动 Kati
go
// build/soong/ui/build/dumpvars.go:105
func dumpMakeVars(ctx Context, config Config, goals, vars []string, tmpDir string, which dumpvarsType) (map[string]string, error) {
// 就是这一行!真正执行 Kati 命令
cmd := Command(ctx, config, e, "dumpvars",
config.KatiBin(), // Kati 二进制的路径
"-f", "build/make/core/config.mk", // 关键:-f 指定从这个文件开始读
"--color_warnings",
"--kati_stats",
"dump-many-vars", // make 目标:dump 一堆变量
"MAKECMDGOALS="+strings.Join(goals, " "))
// 设置环境变量
cmd.Environment.Set("CALLED_FROM_SETUP", "true")
cmd.Environment.Set("DUMP_MANY_VARS", strings.Join(vars, " "))
// 启动命令,等执行完
if err := cmd.Start(); err != nil {
return nil, err
}
if err := cmd.Wait(); err != nil {
return nil, err
}
// 解析 Kati 输出的 stdout,变成 map
ret := make(map[string]string, len(vars))
for _, line := range strings.Split(output.String(), "\n") {
if key, value, ok := decodeKeyValue(line); ok {
ret[key] = value
}
}
return ret, nil
}
逐行解释:
- 第 10~16 行:就是这里启动了 Kati
config.KatiBin()是 Kati 二进制的路径(比如out/host/linux-x86/bin/kati)-f build/make/core/config.mk是告诉 Kati:"从这个 Makefile 开始读"dump-many-vars是一个 make 目标,表示"我要 dump 一堆变量的值"
- 第 20~21 行:设置环境变量,把要查的变量名传给 Kati
- 第 24~27 行:真正执行命令,等 Kati 跑完
- 第 30~35 行:解析 Kati 输出的 stdout,变成
map[string]string返回
3.6.3 整个调用链
css
main.go:410 dumpVars()
└─ 调 build.DumpMakeVars()
└─ dumpvars.go:60 DumpMakeVars()
└─ 调 dumpMakeVars()
└─ dumpvars.go:105 dumpMakeVars()
└─ 执行命令:KatiBin -f build/make/core/config.mk dump-many-vars
└─ Kati 进程启动,开始读 config.mk
注意:Kati 第一个读的就是 config.mk,这是 Go 代码里写死的(第 12 行 -f 参数)。
3.7 过渡:config.mk 做了哪些初始化?
Kati 从 build/make/core/config.mk 开始读(这是 Go 命令行参数 -f 写死的)。
我们看一下 config.mk 里到底 include 了哪些文件:
makefile
# build/make/core/config.mk:23
include $(BUILD_SYSTEM_COMMON)/core.mk
# build/make/core/config.mk:30
include $(BUILD_SYSTEM)/distdir.mk
# build/make/core/config.mk:204
include $(BUILD_SYSTEM_COMMON)/math.mk
# build/make/core/config.mk:206
include $(BUILD_SYSTEM_COMMON)/strings.mk
# build/make/core/config.mk:208
include $(BUILD_SYSTEM_COMMON)/json.mk
# build/make/core/config.mk:211
include $(BUILD_SYSTEM)/pathmap.mk
# build/make/core/config.mk:214
include $(BUILD_SYSTEM)/project_definitions.mk
# build/make/core/config.mk:247
include $(BUILD_SYSTEM)/deprecation.mk
# build/make/core/config.mk:428
include $(BUILD_SYSTEM)/envsetup.mk
# build/make/core/config.mk:573
include $(BUILD_SYSTEM)/combo/javac.mk
# build/make/core/config.mk:576
include $(BUILD_SYSTEM)/ccache.mk
# build/make/core/config.mk:577
include $(BUILD_SYSTEM)/rbe.mk
# build/make/core/config.mk:1308
include $(BUILD_SYSTEM)/sysprop_config.mk
# build/make/core/config.mk:1312
include $(BUILD_SYSTEM)/android_soong_config_vars.mk
# build/make/core/config.mk:1325
include $(BUILD_SYSTEM)/ninja_config.mk
# build/make/core/config.mk:1326
include $(BUILD_SYSTEM)/soong_config.mk
# build/make/core/config.mk:1332
include $(BUILD_SYSTEM)/dumpvar.mk
这些文件做了一系列初始化:设置编译器路径、设置环境变量、定义通用函数、配置 Soong 等等。
初始化完成后,我们接下来读的就是产品配置加载的核心文件:build/make/core/product_config.mk。
3.8 产品配置加载:product_config.mk 怎么找到第一个产品 .mk
3.8.1 所有 AndroidProducts.mk 的列表从哪来?
makefile
# build/make/core/product_config.mk:148-152
# 所有 AndroidProducts.mk 的路径列表
# 这个 list 文件在 out/.module_paths/ 目录下,构建系统启动时生成
android_products_makefiles := $(file <$(OUT_DIR)/.module_paths/AndroidProducts.mk.list) \
$(SRC_TARGET_DIR)/product/AndroidProducts.mk
3.8.2 怎么读每个 AndroidProducts.mk 里的 PRODUCT_MAKEFILES?
makefile
# build/make/core/product_config.mk:179-196
define _read-ap-file
# 先清空变量,避免上一个文件的残留
$(eval PRODUCT_MAKEFILES :=) \
$(eval COMMON_LUNCH_CHOICES :=) \
$(eval ap_product_paths :=) \
# 设置 LOCAL_DIR 为这个 AndroidProducts.mk 所在的目录
$(eval LOCAL_DIR := $(patsubst %/,%,$(dir $(f)))) \
# include 这个 AndroidProducts.mk 文件
$(eval include $(f)) \
# 从文件里读出来的 PRODUCT_MAKEFILES,转换成 <产品名>:<路径> 的格式
$(foreach p, $(PRODUCT_MAKEFILES),$(eval ap_product_paths += $(call _product-spec,$(p)))) \
endef
3.8.3 遍历所有 AndroidProducts.mk,收集成一张大表
makefile
# build/make/core/product_config.mk:202-207
product_paths :=
# 遍历 list 文件里的每一个 AndroidProducts.mk
$(foreach f,$(android_products_makefiles), \
$(call _read-ap-file,$(f)) \ # 读这个文件
$(eval product_paths += $(ap_product_paths)) \ # 追加到大表
)
最后 product_paths 就是一张完整的大表:
bash
aosp_cf_arm64_phone:device/google/cuttlefish/vsoc_arm64/phone/aosp_cf.mk
aosp_cf_x86_64_phone:device/google/cuttlefish/vsoc_x86_64/phone/aosp_cf.mk
aosp_cf_arm64_auto:device/google/cuttlefish/vsoc_arm64_only/auto/aosp_cf.mk
...
3.8.4 根据 TARGET_PRODUCT 查表
makefile
# build/make/core/product_config.mk:212
# 从大表里过滤出 aosp_cf_arm64_phone: 开头的那一行
# 取冒号后面的部分,就是 .mk 文件路径
current_product_makefile := $(call _second,$(filter $(TARGET_PRODUCT):%,$(product_paths)),:)
最后 current_product_makefile 就等于 device/google/cuttlefish/vsoc_arm64/phone/aosp_cf.mk。
3.8.5 include 第一个产品 .mk
makefile
# build/make/core/product_config.mk:234
# 把第一个产品 .mk include 进来,然后一层层展开里面的 inherit-product
$(call import-products, $(current_product_makefile))
3.9 第九步:第一个产品 .mk 里写了什么
makefile
# device/google/cuttlefish/vsoc_arm64/phone/aosp_cf.mk
# 1. 继承通用 64 位系统配置(system 分区)
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/generic_system.mk)
# 2. 继承 system_ext 分区配置
$(call inherit-product, $(SRC_TARGET_DIR)/product/handheld_system_ext.mk)
# 3. 继承 product 分区配置
$(call inherit-product, $(SRC_TARGET_DIR)/product/aosp_product.mk)
# 4. 继承 vendor 分区配置(硬件相关)
$(call inherit-product, device/google/cuttlefish/shared/phone/device_vendor.mk)
# 5. 产品基本信息
PRODUCT_NAME := aosp_cf_arm64_phone
PRODUCT_DEVICE := vsoc_arm64
PRODUCT_MANUFACTURER := Google
PRODUCT_MODEL := Cuttlefish arm64 phone
关键就是那一堆 $(call inherit-product, xxx.mk)。
3.10 第十步:inherit-product 到底干了什么(build/make/core/product.mk:532)
makefile
# build/make/core/product.mk:532
define inherit-product
# 1. 先检查这个 .mk 文件存不存在,不存在直接报错
$(eval _inherit_product_wildcard := $(wildcard $(1)))
$(if $(_inherit_product_wildcard),,$(error $(1) does not exist.))
# 2. 遍历每一个匹配到的文件(支持通配符)
$(foreach part,$(_inherit_product_wildcard),
$(if $(findstring ../,$(part)),
$(eval np := $(call normalize-paths,$(part))),
$(eval np := $(strip $(part))))
# 3. 关键!不是立刻 include,而是把这个 .mk 路径加入所有 PRODUCT_XXX 变量的后面
# 等所有 inherit-product 都调完了,再统一递归展开
$(foreach v,$(_product_var_list),
$(eval $(v) := $($(v)) $(INHERIT_TAG)$(np)))
# 4. 记录继承关系,后面可以画继承图
$(eval current_mk := $(strip $(word 1,$(_include_stack)))
$(eval inherit_var := PRODUCTS.$(current_mk).INHERITS_FROM)
$(eval $(inherit_var) := $(sort $($(inherit_var)) $(np)))
)
endef
为什么要这么设计?
因为一个产品可以继承多个 .mk,被继承的 .mk 自己又可能继承别的 .mk,这是一棵树。必须等整棵树收集完了,才能正确合并变量。
还有一个变体:
makefile
# build/make/core/product.mk:579
# 这个 .mk 存在才继承,不存在就跳过不报错
# 厂商经常用这个加可选配置
define inherit-product-if-exists
$(if $(wildcard $(1)),$(call inherit-product,$(1)),)
endef
四、完整调用链总结
scss
bash
└─ lunch aosp_cf_arm64_phone-trunk_staging-userdebug
└─ _lunch_meat (envsetup.sh:449)
└─ build_build_var_cache (envsetup.sh:56)
└─ soong_ui.bash --dumpvars-mode
└─ exec soong_ui (Go 二进制)
└─ dumpVars (main.go:410)
└─ build.DumpMakeVars
└─ dumpMakeVars (dumpvars.go:105)
└─ Kati -f build/make/core/config.mk
└─ config.mk 做一系列初始化
└─ 初始化完成后进入产品配置加载
├─ 读 AndroidProducts.mk.list
├─ 遍历所有 AndroidProducts.mk
├─ 查表找到 aosp_cf.mk
├─ import-products include aosp_cf.mk
└─ 一层层 inherit-product 展开
└─ 所有 PRODUCT_XXX 变量累加
五、面试可能会问
Q1:lunch 函数本身做了什么? A:只是参数解析,把 xxx-userdebug 拆开,然后调 _lunch_meat。
Q2:从 shell 到 Go 是哪一行代码跳过去的? A:build_build_var_cache 里调 soong_ui.bash --dumpvars-mode,这一行 exec 出 Go 二进制。
Q3:Go 这边是怎么调起 Kati 的?Kati 第一个读的是哪个文件? A:dumpMakeVars 里执行 KatiBin -f build/make/core/config.mk,第一个读的是 config.mk。
Q4:所有 AndroidProducts.mk 的列表是从哪来的? A:product_config.mk 从 out/.module_paths/AndroidProducts.mk.list 这个文件读出来的,这个 list 文件是构建系统启动时生成的。
Q5:inherit-product 是立刻 include 这个文件吗? A:不是。它先把路径加入待加载列表,等所有 inherit-product 都调完了,再统一递归展开整棵树。
六、下一篇预告
这篇我们知道了产品配置是怎么被一层层拼起来的。下一篇我们就扒:build.prop 是怎么生成的?那些 ro.xxx 属性都是哪来的?
万水千山总是情,点个关注行不行