AOSP编译:敲下 `lunch xxx-userdebug`,你知道系统偷偷干了什么吗?

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 ...,中间到底经过了哪些代码?


二、完整时序图

sequenceDiagram participant Shell as 我们(bash) participant Lunch as lunch 函数 participant Meat as _lunch_meat participant Cache as build_build_var_cache participant Bash as soong_ui.bash participant Go as soong_ui Go 二进制 participant DumpVars as dumpMakeVars participant Kati as Kati 二进制 participant Config as config.mk participant ProductConfig as product_config.mk participant Apk as AndroidProducts.mk participant FirstMk as 第一个产品 .mk Shell->>Lunch: lunch aosp_cf_arm64_phone-trunk_staging-userdebug Lunch->>Lunch: 用 - 拆开参数 Lunch->>Meat: _lunch_meat product release variant Meat->>Meat: export TARGET_PRODUCT 等 Meat->>Cache: build_build_var_cache Cache->>Bash: soong_ui.bash --dumpvars-mode Bash->>Go: exec soong_ui Go->>DumpVars: dumpVars 解析参数 DumpVars->>Kati: KatiBin -f config.mk Kati->>Config: 读 config.mk 做一系列初始化 Config->>ProductConfig: 初始化完成后进入产品配置加载 ProductConfig->>Apk: 读 AndroidProducts.mk.list Apk-->>ProductConfig: 返回 PRODUCT_MAKEFILES 表 ProductConfig->>FirstMk: 查表找到 aosp_cf.mk,include 进来 FirstMk-->>ProductConfig: 所有 PRODUCT_XXX 变量累加 ProductConfig-->>Kati: 返回变量值 Kati-->>DumpVars: stdout KEY=VALUE DumpVars-->>Go: parse 成 map Go-->>Cache: 打印 KEY=VALUE Cache-->>Meat: eval 成 shell 变量 Meat-->>Shell: 导出环境变量

三、按时序图逐段解读源码

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
}

逐行解释:

  1. 第 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 一堆变量的值"
  2. 第 20~21 行:设置环境变量,把要查的变量名传给 Kati
  3. 第 24~27 行:真正执行命令,等 Kati 跑完
  4. 第 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 属性都是哪来的?


万水千山总是情,点个关注行不行

相关推荐
千里马学框架5 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台5 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone5 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc5 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo6 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077006 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼6 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone6 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen6 天前
属性动画原理与高级动画实现
android·kotlin·canvas