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 属性都是哪来的?


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

相关推荐
jason.zeng@150220718 分钟前
使用vscode+flutter开发android程序(入门)
android·vscode·flutter
事圆则缓22 分钟前
Kotlin学习导航:从知识树走到项目与面试
android·开发语言·kotlin
天空之城--1 小时前
近一周Android开发动态:AI编码、Compose与自适应实践
android
mmsx1 小时前
Android 混淆挡不住二次打包:一条完整的攻击链与突破口
android·混淆
聚美智数10 小时前
智能结构化识别-图片结构解析-图像结构化解析-照片结构化解析
android·java·数据库
mmsx15 小时前
Android 插件化架构:40 个 Gradle 模块的拆分、通信与共存
android·kotlin
mmsx15 小时前
Android 不改一行源码:用 AspectJ 做三类拦截
android·kotlin
TST-魚爷15 小时前
易视讯会议摄像头 + 全向麦克风实操讲解稿
android·电脑·钉钉·腾讯会议·zoom会议
嘟哩DuliDuli16 小时前
AI创作工作区的项目上下文与多模态任务边界
android·人工智能·安全·ai·软件工程