01-01:AOSP envsetup.sh 与 lunch 源码剖析(Android 16 / aosp-main)

4 阅读17分钟

AOSP envsetup.sh 与 lunch 源码剖析(Android 16 / aosp-main)

概述

Android 编译系统在真正展开模块图之前,必须先知道三个东西:当前工作树根在哪里、要产出哪一套产品镜像、以及这套产品启用哪一档 release 标志和哪一种调试模式(user\userdebug等)。这些信息不是写在某个临时配置文件里给单次构建读取那么简单,而是通过环境变量进入当前交互 shell,再由后续每一次 m、mmm、以及 soong_ui 子进程继承。

谁负责把函数装进 shell?是 source build/envsetup.sh。 谁负责把产品三角写进环境?是 lunch。 两步顺序不能反:没有前者,lunch 函数根本不存在;没有后者,全局 PATH 虽已可用,但 OUT、TARGET_PRODUCT 仍未对齐,后续构建会在产品配置阶段失败或落到错误输出目录。

全文以平台 COMMON_LUNCH_CHOICES 中的第一条组合为贯穿样例:

aosp_arm64-trunk_staging-eng

即:TARGET_PRODUCT=aosp_arm64,TARGET_RELEASE=trunk_staging,TARGET_BUILD_VARIANT=eng。源码逐段分析,开篇先按该目标走一遍完整时序;其后各小节再拆开同一条链路上的分支与失败模式。阅读时请始终对照:“此刻若输入的是 lunch aosp_arm64-trunk_staging-eng,变量与 makefile 会落在哪里”。

覆盖范围如下:

  1. envsetup.sh 如何定位树根、注入与 lunch 无关的全局 PATH、按规则加载可选 vendorsetup.sh,以及为何大量历史 shell 函数要被 unset;
  2. lunch 如何解析新旧两种参数形态,如何拒绝无参、过多参数、残缺旧格式,并最终把字符串解释成 TARGET_PRODUCT / TARGET_RELEASE / TARGET_BUILD_VARIANT;
  3. _lunch_meat 如何通过 build_build_var_cache 拉起 soong_ui --dumpvars-mode,在构建系统内部完成产品 makefile 定位与 release 合法性校验,再回写环境并调用 set_lunch_paths;
  4. 以 aosp_arm64 为样板,产品如何从 AndroidProducts.mk 注册表落到 aosp_arm64.mk,PRODUCT_DEVICE=generic_arm64 如何决定 PRODUCT_OUT,trunk_staging 如何进入 release 校验;
  5. list_products / list_releases / list_variants 与补全函数如何服务“不再提供交互菜单”的新交互;tapas / banchan 与 lunch 共享落点却不同语义的边界。

本篇是“能读源码”到“能对准构建目标”的转折点。通过序章你将知道仓库从哪里来、如何检索;本篇解决“检索到的代码属于哪次构建配置”。若跳过 lunch 直接谈 Soong 或镜像打包,大家很容易在错误的 OUT 目录里找产物,或把某个产品特有的 makefile 覆盖误当成平台通用逻辑。因此本篇用一个真实 lunch 目标把 shell、产品注册表、release、输出路径串成一条可复现链路,而不是只罗列函数名。

前置知识

阅读本篇前应具备以下背景,否则容易把“环境装配”和“真正编译”混为一谈。

第一,已完成序章内容:知道完整 AOSP 工作树由 Manifest 拼装,顶层的 build/envsetup.sh 往往是 linkfile 指到 build/make/envsetup.sh 的结果,而不是一份孤立脚本。

第二,熟悉 shell 语义。source(或点号)在当前进程内执行脚本,因此函数定义、export 的变量会留在你正在用的终端里;子 shell 里跑同样脚本则改不到父环境。这正是 lunch 必须是 shell 函数、不能简单做成外部可执行文件了事的原因之一。

第三,理解三个正交维度。Product 回答“构建哪套产品配置”,例如 aosp_arm64;Release 回答“启用哪套 release 标志组合”,例如本树默认常见的 trunk_staging;Variant 回答“调试与性能取舍落在哪一档”,取值只有 user、userdebug、eng。三者同时成立,才构成一次可复现的 lunch 选择。

第四,知道 Make 或 Kati 侧会读取环境中的 TARGET_*,再去展开 AndroidProducts.mk 与具体产品 makefile。shell 层负责“选中并导出”,构建系统层负责“校验并展开”。两边通过 soong_ui 的 dump 接口相遇。可以把 dump 理解成一次“只求配置、不求编译”的探测:它会把产品注册与 release 规则跑到足够远,以便回答变量值是否合法,但并不是完整的镜像构建。

第五,接受一个操作习惯上的约束:讨论 lunch 时,默认是在 bash 或 zsh 中工作。validate_current_shell 对其他 shell 只警告不阻断,但补全与部分数组语法行为可能偏离预期。本篇示例命令均按 bash 语义描述。

第六,记住贯穿样例的三个字段含义,避免后文反复解释:aosp_arm64 是平台侧常见的 64 位 AOSP/GSI 向产品名;trunk_staging 是本树 lunch 新格式默认 release,对应 build/release/release_configs/trunk_staging.textproto;eng 是开发态 variant。三者合在一起,才是一次可复现的目标,而不是“随便 lunch 一个 arm64”。

核心数据结构

1. 环境变量三角与衍生变量

lunch 成功后,当前 shell 里最关键的是下面这组变量。它们不是文档约定的口头概念,而是 _lunch_meat 与 set_lunch_paths 真实 export 的对象。

变量含义lunch 默认(新格式缺省)
TARGET_PRODUCT产品名,对应某份产品 makefile无默认,必须给出
TARGET_RELEASErelease 配置名trunk_staging
TARGET_BUILD_VARIANT变体eng
TARGET_BUILD_TYPE固定写成字符串 release由 _lunch_meat 赋值
ANDROID_BUILD_TOP源码树根set_stuff_for_environment
ANDROID_PRODUCT_OUT / OUT产品输出目录set_lunch_paths 从构建系统取
ANDROID_HOST_OUT主机侧输出根同上
ANDROID_SOONG_HOST_OUTSoong 主机输出根同上
JAVA_HOME / ANDROID_JAVA_HOME构建使用的 JDK同上
BUILD_ENV_SEQUENCE_NUMBER环境序列号常量当前源码中为 13

其中 TARGET_BUILD_TYPE=release 容易误读。源码注释写明这是字符串 "release",不是把某个 shell 变量 release 的值再展开一次。它与 TARGET_RELEASE(release 配置名)不是同一个概念。

2. 产品注册表

产品不是写死在 lunch 函数的数组里。构建系统在配置阶段读取所有 AndroidProducts.mk,汇总成一张“产品名 → makefile 路径”的表,再按 TARGET_PRODUCT 取出当前行。

来源有两类:

  • 模块扫描生成的列表文件:$(OUT_DIR)/.module_paths/AndroidProducts.mk.list;
  • 平台固定入口:build/make/target/product/AndroidProducts.mk。

每一份 AndroidProducts.mk 可设置:

  • PRODUCT_MAKEFILES:每一项是 产品名:路径,或仅给路径(此时产品名默认为文件名去掉 .mk);
  • COMMON_LUNCH_CHOICES:希望出现在“常见组合”语义里的字符串列表;
  • STARLARK_OPT_IN_PRODUCTS:默认走 Starlark 产品配置的产品名集合。

本树平台侧可见的常见组合已经是三段式,而不再是早年文档里常见的两段式:

# AOSP aosp-main
# 文件:build/make/target/product/AndroidProducts.mk
COMMON_LUNCH_CHOICES := \
    aosp_arm64-trunk_staging-eng \
    aosp_arm-trunk_staging-eng \
    aosp_x86_64-trunk_staging-eng \
    aosp_x86-trunk_staging-eng \

以 aosp_arm64 为例,产品 makefile 内会给出稳定的产品身份字段:

# AOSP aosp-main
# 文件:build/make/target/product/aosp_arm64.mk
PRODUCT_NAME := aosp_arm64
PRODUCT_DEVICE := generic_arm64
PRODUCT_BRAND := Android

lunch 选择的是 PRODUCT_NAME 这一侧的名字;设备目录、品牌等由该 makefile 及其 inherit-product 链继续展开。本篇不深入 inherit 图,只强调:名字对不上 AndroidProducts.mk 注册表,lunch 就会报错失败。

3. 构建变量缓存

build_build_var_cache 不会对每个变量单独拉起一次配置。它先扫描 envsetup.sh 文本,收集所有 _get_build_var_cached 与 _get_abs_build_var_cached 的参数名,再一次性调用 soong_ui.bash --dumpvars-mode,让构建系统吐出一段可 eval 的赋值脚本,生成 var_cache_* 与 abs_var_cache_*,并置 BUILD_VAR_CACHE_READY=true。_lunch_meat 末尾调用 destroy_build_var_cache,避免后续偶然读到过期缓存。

这套机制的设计意图很明确:lunch 之后立刻需要很多路径类变量来改 PATH 与 OUT,若逐个 dump,延迟不可接受;但若永久保留缓存,又可能在切换产品后读到旧值。因此采用“一次建齐、用完即毁”。

4. PATH 双层模型

层函数是否依赖 lunch典型内容
全局set_global_paths否build/soong/bin、development/scripts、emulator 等
lunchset_lunch_paths是host out、JAVA 工具链、PRODUCT_OUT、asuite 等

两层都采用同一套路:把“自己上次追加进 PATH 的那一段字符串”记在专用变量里,下次先从 PATH 中抠掉旧段,再追加新段。这样反复 source build/envsetup.sh 或反复 lunch 不会把 PATH 无限拉长,也不会残留指向上一次产品 out 的错误前缀。

5. Release 配置对象

本树 build/release/release_configs/ 下存在 trunk_staging.textproto、bp1a.textproto、ap4a.textproto 等文件。trunk_staging 在 lunch 新格式中是默认 release;bp1a 一类配置可通过 inherits 继承其他配置。lunch 本身不解析 textproto,它只把名字塞进 TARGET_RELEASE,真正的合法性与标志展开在 release_config.mk 与相关工具链中完成。对阅读 lunch 而言,记住两点即可:名字必须属于当前产品的 ALL_RELEASE_CONFIGS_FOR_PRODUCT;未设置与设置错误是两种不同失败模式。

也可以把 release 理解成“挂在产品之上的第二层旋钮”。同一 aosp_arm64 在不同 release 下可能启用不同的 aconfig / build flag 组合,从而改变后续模块编译开关,但产品 makefile 身份不变。因此排障时既要问“产品是什么”,也要问“release 是什么”;只报产品名而不报 release,在现代 AOSP 里信息量已经不够。

调用链路

graph TD
  A[source envsetup.sh] --> B[_gettop_once]
  B --> C[source shell_utils.sh]
  C --> D[set_global_paths]
  D --> E[source_vendorsetup]
  E --> F[addcompletions]
  F --> G[lunch aosp_arm64-trunk_staging-eng]
  G --> H[_lunch_meat]
  H --> I[build_build_var_cache]
  I --> J[soong_ui dumpvars]
  J --> K{成功?}
  K -->|否| L[return 1 提示]
  K -->|是| M[export TARGET星号]
  M --> N[set_stuff_for_environment]
  N --> O[set_lunch_paths]
  O --> P[printconfig]
  P --> Q[destroy_build_var_cache]

纯文本调用链路(以贯穿样例代入):

. build/envsetup.sh
  ├─ _gettop_once / gettop          # 钉住 ~/aosp
  ├─ source build/make/shell_utils.sh
  ├─ set_global_paths               # cgrep/list_products 等可用;尚未选产品
  ├─ source_vendorsetup             # 纯净树常为空操作
  └─ addcompletions

lunch aosp_arm64-trunk_staging-eng   # 或: lunch aosp_arm64 trunk_staging eng
  ├─ 旧格式拆段 → product=aosp_arm64 release=trunk_staging variant=eng
  └─ _lunch_meat aosp_arm64 trunk_staging eng
        ├─ TARGET_PRODUCT/RELEASE/VARIANT 前缀调用 build_build_var_cache
        │     └─ soong_ui.bash --dumpvars-mode ...
        │           ├─ product_config.mk
        │           │     └─ 命中 aosp_arm64 → build/.../aosp_arm64.mk
        │           ├─ 展开 PRODUCT_DEVICE=generic_arm64
        │           ├─ PRODUCT_OUT := out/target/product/generic_arm64
        │           └─ release_config.mk 校验 trunk_staging ∈ 可用集合
        ├─ export TARGET_PRODUCT=aosp_arm64 等
        ├─ set_lunch_paths → OUT=$ANDROID_PRODUCT_OUT=.../generic_arm64
        ├─ printconfig
        └─ destroy_build_var_cache

样例专用数据流(产品注册 → 输出目录):

graph LR
  A[COMMON_LUNCH_CHOICES] --> B[aosp_arm64]
  B --> C[aosp_arm64.mk]
  C --> D[PRODUCT_DEVICE generic_arm64]
  D --> E[PRODUCT_OUT]
  E --> F[shell OUT]
build/make/target/product/AndroidProducts.mk
  COMMON_LUNCH_CHOICES 含 aosp_arm64-trunk_staging-eng
  PRODUCT_MAKEFILES 含 $(LOCAL_DIR)/aosp_arm64.mk
        ↓ product_config.mk 按 TARGET_PRODUCT=aosp_arm64 过滤
build/make/target/product/aosp_arm64.mk
  PRODUCT_NAME := aosp_arm64
  PRODUCT_DEVICE := generic_arm64
        ↓ envsetup.mk
PRODUCT_OUT := $(OUT_DIR)/target/product/generic_arm64
        ↓ set_lunch_paths
export OUT=$ANDROID_PRODUCT_OUT  # 绝对路径形式的同一目录

源码逐段分析

0. 贯穿示例:lunch aosp_arm64-trunk_staging-eng

本节按一次真实操作的时间顺序走完主成功路径。后文各节是同一链路的拆解与分支补全;若只想先建立直觉,读完本节再回头看分支表即可。

步骤 0:操作命令

在树根执行:

cd ~/aosp
source build/envsetup.sh
lunch aosp_arm64-trunk_staging-eng
# 等价新格式:
# lunch aosp_arm64 trunk_staging eng
# 若只写 lunch aosp_arm64,release/variant 会默认成 trunk_staging / eng

选这个目标有三个源码理由:它出现在平台 COMMON_LUNCH_CHOICES 第一条;产品 makefile 与 Board 配置都在 build/make 树内,不依赖某台具体手机的【厂商扩展】设备树;trunk_staging 与 eng 分别对应新格式默认 release 与默认 variant,和“只写产品名”时的行为一致。

步骤 1:source 只装配工具面,不选产品

source build/envsetup.sh 走完 _gettop_once → set_global_paths → source_vendorsetup → addcompletions 后,当前 shell 应具备:

  • gettop 打印树根(例如 /root/aosp 或你的 ~/aosp 解析路径);
  • PATH 前端出现 build/soong/bin,因而 list_products、cgrep 可执行;
  • 尚未稳定拥有对准 aosp_arm64 的 TARGET_PRODUCT / OUT(若曾 lunch 过其它产品,旧值可能仍在,但本轮尚未校验刷新)。

对应源码落点见下文「加载 envsetup」「全局 PATH」。此时若直接 echo $OUT 就去找镜像,属于误区五。

步骤 2:lunch 把字符串拆成三角

对 lunch aosp_arm64-trunk_staging-eng:

  1. $# == 1 且参数含 - → 走旧格式;
  2. IFS="-" read 得到 product=aosp_arm64、release=trunk_staging、variant=eng;
  3. 三段均非空 → 调用 _lunch_meat aosp_arm64 trunk_staging eng。

若写成 lunch aosp_arm64 trunk_staging eng,则 legacy 为空,走新格式位置赋值,三个字段相同。若误写成 lunch aosp_arm64 eng,第二位置被当成 release 名 eng,通常在 release_config.mk 失败——这是样例上最值得做的对照实验。

步骤 3:dump 前把候选三角喂给构建系统

_lunch_meat 并不先相信用户字符串,而是:

TARGET_PRODUCT=aosp_arm64 \
TARGET_RELEASE=trunk_staging \
TARGET_BUILD_VARIANT=eng \
build_build_var_cache

build_build_var_cache 扫描 envsetup.sh 收集待 dump 变量名,再执行:

build/soong/soong_ui.bash --dumpvars-mode --vars=... --abs-vars=...

对本篇,把这次 dump 理解成:“带着 aosp_arm64 + trunk_staging + eng 跑一轮产品/release 配置探测”。失败则 lunch 失败;成功则得到可 eval 的 var_cache_* / abs_var_cache_*。

步骤 4:产品注册表命中 aosp_arm64.mk

product_config.mk 汇总各 AndroidProducts.mk。平台入口里,完整构建路径(TARGET_BUILD_APPS 为空,即 lunch 主路径)包含:

# AOSP aosp-main
# 文件:build/make/target/product/AndroidProducts.mk
PRODUCT_MAKEFILES := \
    $(LOCAL_DIR)/aosp_arm64.mk \
    # ... 其它产品
COMMON_LUNCH_CHOICES := \
    aosp_arm64-trunk_staging-eng \
    aosp_arm-trunk_staging-eng \
    aosp_x86_64-trunk_staging-eng \
    aosp_x86-trunk_staging-eng \

路径项被规范成 aosp_arm64:……/aosp_arm64.mk 这类 产品名:makefile 后:

# AOSP aosp-main
# 文件:build/make/core/product_config.mk
current_product_makefile := $(call _second,$(filter $(TARGET_PRODUCT):%,$(product_paths)),:)
ifeq (,$(current_product_makefile))
  $(error Cannot locate config makefile for product "$(TARGET_PRODUCT)")
endif

当 TARGET_PRODUCT=aosp_arm64 时,current_product_makefile 指向 build/make/target/product/aosp_arm64.mk。若你 lunch 了未注册名(例如拼写 aosp_arm6),此处 error,dump 非 0,_lunch_meat 返回 1。

步骤 5:读入产品身份:PRODUCT_NAME 与 PRODUCT_DEVICE
# AOSP aosp-main
# 文件:build/make/target/product/aosp_arm64.mk
$(call inherit-product, $(SRC_TARGET_DIR)/product/core_64_bit.mk)
$(call inherit-product, $(SRC_TARGET_DIR)/product/generic_system.mk)
# ... system_ext / product / gsi_release 等 inherit ...
$(call inherit-product, $(SRC_TARGET_DIR)/board/generic_arm64/device.mk)

PRODUCT_NAME := aosp_arm64
PRODUCT_DEVICE := generic_arm64
PRODUCT_BRAND := Android
PRODUCT_MODEL := AOSP on ARM64

对 lunch 链路,此刻最关键的两个结论:

  1. 产品名是 aosp_arm64(与 TARGET_PRODUCT 对齐的是 PRODUCT_NAME 这一侧);
  2. 设备名是 generic_arm64,它会进入输出目录公式,而不是把 out 目录简单命名成产品名。

Board 侧 build/make/target/board/generic_arm64/BoardConfig.mk 声明 TARGET_ARCH := arm64 等,解释“为什么这个 lunch 目标面向 64 位 ARM”。inherit 链上的包集合属于产品内容展开,本篇不逐文件开列;只要承认:选中 aosp_arm64 后,配置阶段会继续 include 这条链,dump 才能算出完整的 PRODUCT_OUT、host 路径等变量。

步骤 6:release 校验 trunk_staging

shell 已把 TARGET_RELEASE=trunk_staging 传入 dump。release_config.mk 要求该名字属于当前产品的 ALL_RELEASE_CONFIGS_FOR_PRODUCT;本树存在:

# AOSP aosp-main
# 文件:build/release/release_configs/trunk_staging.textproto
name: "trunk_staging"
aconfig_value_sets: "aconfig_value_set-platform_build_release-trunk_staging"
release_config_type: RELEASE_CONFIG

对样例目标,合法路径是“名字存在且允许用于 lunch”。非法对照:lunch aosp_arm64 eng 会把 eng 送进 release 槽,触发 No release config found for TARGET_RELEASE: eng(可用 release 列表会出现在同一错误里)。list_releases aosp_arm64 可在 lunch 前预览合法集合。

步骤 7:PRODUCT_OUT 如何变成 shell 里的 OUT

产品与设备身份就绪后,envsetup.mk 用设备名拼输出根:

# AOSP aosp-main
# 文件:build/make/core/envsetup.mk
TARGET_OUT_ROOT := $(OUT_DIR)/target
TARGET_PRODUCT_OUT_ROOT := $(TARGET_OUT_ROOT)/product
PRODUCT_OUT := $(TARGET_PRODUCT_OUT_ROOT)/$(TARGET_DEVICE)

默认 OUT_DIR 为 out 时,样例对应:

PRODUCT_OUT = out/target/product/generic_arm64

注意分母是 generic_arm64(PRODUCT_DEVICE),不是 aosp_arm64(PRODUCT_NAME)。这是样例上最容易空转的一点:有人 lunch 成功后去 out/target/product/aosp_arm64 找镜像,目录不对。

dump 成功返回后,_lunch_meat 导出:

变量样例期望值
TARGET_PRODUCTaosp_arm64(以构建系统回读为准)
TARGET_RELEASEtrunk_staging
TARGET_BUILD_VARIANTeng
TARGET_BUILD_TYPE字面量 release
ANDROID_PRODUCT_OUT / OUT…/out/target/product/generic_arm64 的绝对路径
ANDROID_BUILD_TOP树根绝对路径

set_lunch_paths 中的关键赋值:

# AOSP aosp-main
# 文件:build/envsetup.sh
unset ANDROID_PRODUCT_OUT
export ANDROID_PRODUCT_OUT=$(_get_abs_build_var_cached PRODUCT_OUT)
export OUT=$ANDROID_PRODUCT_OUT

同时把 SOONG_HOST_OUT_EXECUTABLES、HOST_OUT_EXECUTABLES、JDK 工具链等绝对路径段前置进 PATH。至此,样例 lunch 的“环境对齐”才算完成。

步骤 8:成功后应立刻核对的命令
gettop
echo "$TARGET_PRODUCT $TARGET_RELEASE $TARGET_BUILD_VARIANT"
echo "$OUT"
get_build_var TARGET_PRODUCT
get_build_var TARGET_DEVICE
get_build_var PRODUCT_OUT
printconfig   # 除非设置了 ANDROID_QUIET_BUILD

期望关系:TARGET_PRODUCT 为 aosp_arm64,TARGET_DEVICE 为 generic_arm64,OUT / PRODUCT_OUT 指向同一 …/generic_arm64 目录。第一次在干净树上 lunch 时,还可能看到 spam_for_lunch 打印的 variant 说明(缺少 out/soong/build.aosp_arm64.ninja 时);这不代表失败。

步骤 9:同一产品上的三条故意失败(建议亲手做)
命令预期失败层典型信息锚点
lunch aosp_arm64-engshell 旧格式拆段Invalid lunch combo(缺 release 段)
lunch aosp_arm64 engrelease 校验No release config found for TARGET_RELEASE: eng
lunch aosp_arm6-trunk_staging-eng产品注册表Cannot locate config makefile for product "aosp_arm6"

三条都失败时,set_lunch_paths 不应被视为已切换到新目标。若之前已成功 lunch 过,失败返回后 shell 里可能仍残留上一次成功的 TARGET_* / OUT——以本次 _lunch_meat 是否走完 export 为准,不要看见旧变量就以为新命令成功了。

步骤 10:和后文各节的对应关系
本节目的后文深入位置
树根与全局 PATH§1、§2
vendorsetup§3
参数拆分§4
_lunch_meat / 缓存 / dump§5、§6
产品注册表§7
release§8
OUT / PATH§9
列表命令与 tapas 边界§10、§11

读完贯穿示例后,再进入 §1 时,请带着问题:“若这里失败,aosp_arm64-trunk_staging-eng 会在哪一步断掉?”

1. 加载 envsetup:先钉住树根

# AOSP aosp-main
# 文件:build/envsetup.sh
function _gettop_once
{
    local TOPFILE=build/make/core/envsetup.mk
    if [ -n "$TOP" -a -f "$TOP/$TOPFILE" ] ; then
        (cd "$TOP"; PWD= /bin/pwd)
    else
        if [ -f $TOPFILE ] ; then
            PWD= /bin/pwd
        else
            local HERE=$PWD
            local T=
            while [ \( ! \( -f $TOPFILE \) \) -a \( "$PWD" != "/" \) ]; do
                \cd ..
                T=`PWD= /bin/pwd -P`
            done
            \cd "$HERE"
            if [ -f "$T/$TOPFILE" ]; then
                echo "$T"
            fi
        fi
    fi
}
T=$(_gettop_once)
if [ ! "$T" ]; then
    echo "Couldn't locate the top of the tree. Always source build/envsetup.sh from the root of the tree." >&2
    return 1
fi
IMPORTING_ENVSETUP=true source $T/build/make/shell_utils.sh

这段逻辑的分支可以完整枚举。

若环境变量 TOP 非空,且 $TOP/build/make/core/envsetup.mk 存在,则相信 TOP,并用子 shell 的 pwd 去掉符号链接干扰。若 TOP 不可用,但当前工作目录下已经能直接看到锚点文件,说明你正站在树根,直接 pwd。若两者都不成立,则保存 HERE,向上逐级 cd ..,每层用物理路径记录候选 T,直到找到锚点或到达文件系统根。无论向上爬的结果如何,最后都 cd 回 HERE,避免 source 过程把用户的工作目录偷偷改掉。若最终 T 仍空,脚本 return 1,后续所有函数定义都不会执行。这就是“在错误目录 source,发现没有 lunch 命令”的直接原因。

IMPORTING_ENVSETUP=true 再去 source shell_utils.sh,是为了让工具脚本里那些“独立运行时必须 require_top”的断言,在被 envsetup 引入时不要误伤。shell_utils.sh 中的正式 gettop 与这里算法同构,供 list_products 等外部脚本复用。也可以反过来说:正因为很多辅助命令已经拆成 build/soong/bin 下的独立脚本,它们不能再假设“自己一定运行在已经 source 过的函数环境里”,于是必须重新 source shell_utils.sh 并 require_top。envsetup 与独立脚本共用同一套找树根算法,避免出现“交互 shell 认为树根在 A,脚本认为树根在 B”的分裂。

实际操作上,建议始终在仓库根目录执行 source build/envsetup.sh,而不是依赖向上爬目录的容错能力。向上爬能救急,但在多层嵌套挂载、符号链接工作区、或者误把 TOP 导出成旧树路径时,容错反而掩盖问题。把“树根定位成功”当成可观察事件:source 后立刻 gettop,确认打印路径就是你正在阅读的那棵 ~/aosp。

文件末尾的收尾顺序同样有含义:

# AOSP aosp-main
# 文件:build/envsetup.sh
validate_current_shell
set_global_paths
source_vendorsetup
addcompletions

先警告非 bash/zsh 风险,再挂全局 PATH,此时 cgrep、list_products、hmm 等已可执行;然后才加载厂商 vendorsetup.sh,避免厂商脚本需要调用树内工具时找不到命令;最后挂补全。注意:到此为止仍然没有选择产品,也没有调用 set_lunch_paths。

2. 全局 PATH:与 lunch 解耦

# AOSP aosp-main
# 文件:build/envsetup.sh
function set_global_paths()
{
    local T=$(gettop)
    if [ ! "$T" ]; then
        echo "Couldn't locate the top of the tree.  Try setting TOP."
        return
    fi
    if [ -n "$ANDROID_GLOBAL_BUILD_PATHS" ] ; then
        export PATH=${PATH/$ANDROID_GLOBAL_BUILD_PATHS/}
    fi
    ANDROID_GLOBAL_BUILD_PATHS=$T/build/soong/bin
    ANDROID_GLOBAL_BUILD_PATHS+=:$T/build/bazel/bin
    ANDROID_GLOBAL_BUILD_PATHS+=:$T/development/scripts
    ANDROID_GLOBAL_BUILD_PATHS+=:$T/prebuilts/devtools/tools
    # Linux 追加 dtc/libufdt;按 OS 可选追加 emulator
    export PATH=$ANDROID_GLOBAL_BUILD_PATHS:$PATH
}

gettop 失败时只 return,不 exit,避免把用户的交互 shell 直接杀死。Linux 与 Darwin 对 emulator 预编译目录走 case;未知操作系统则清空 emulator 路径,不追加。build/soong/bin 被放在最前,保证后面讲到的 list_products、以及序章中的 cgrep 族,都优先命中树内脚本,而不是宿主机偶然同名的命令。

3. vendorsetup:可选扩展点及其失败模式

# AOSP aosp-main
# 文件:build/envsetup.sh
function source_vendorsetup() {
    unset VENDOR_PYTHONPATH
    local T="$(gettop)"
    allowed=
    for f in $(cd "$T" && find -L device vendor product -maxdepth 4 -name 'allowed-vendorsetup_sh-files' 2>/dev/null | sort); do
        if [ -n "$allowed" ]; then
            echo "More than one 'allowed_vendorsetup_sh-files' file found, not including any vendorsetup.sh files:"
            echo "  $allowed"
            echo "  $f"
            return
        fi
        allowed="$T/$f"
    done
    allowed_files=
    [ -n "$allowed" ] && allowed_files=$(cat "$allowed")
    for dir in device vendor product; do
        for f in $(cd "$T" && test -d $dir && \
            find -L $dir -maxdepth 4 -name 'vendorsetup.sh' 2>/dev/null | sort); do
            if [[ -z "$allowed" || "$allowed_files" =~ $f ]]; then
                echo "including $f"; . "$T/$f"
            else
                echo "ignoring $f, not in $allowed"
            fi
        done
    done
    setup_cog_env_if_needed
}

这里至少有四条完整分支。

第一,若找到两份及以上白名单文件,函数打印冲突路径并立刻 return,所有 vendorsetup.sh 都不加载。这是“宁可缺失,不可歧义”的策略。第二,若恰好一份白名单,则只有名单中出现的相对路径会被 source,其余打印 ignoring。第三,若没有任何白名单,则对 device、vendor、product 三个目录做深度上限为 4 的查找,命中即 source,并打印 including。第四,某顶级目录不存在时,test -d 失败,该目录被跳过,不视为错误。

【厂商扩展】的内容通常就落在这些被 source 的脚本里,例如额外产品提示、公司内部 mirror 相关环境变量、私有 lunch 包装函数等。AOSP 只提供加载框架与白名单裁剪机制,不定义厂商脚本内部语义。

4. lunch:参数形态与全部分支

# AOSP aosp-main
# 文件:build/envsetup.sh
function lunch()
{
    if [[ $# -eq 1 && $1 = "--help" ]]; then
        _lunch_usage
        return 0
    fi
    if [[ $# -eq 0 ]]; then
        echo "No target specified. See lunch --help" 1>&2
        return 1
    fi
    if [[ $# -gt 3 ]]; then
        echo "Too many parameters given. 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
            echo "Valid combos must be of the form <product>-<release>-<variant> when using" 1>&2
            echo "the legacy format.  Run 'lunch --help' for usage." 1>&2
            return 1
        fi
    fi

    if [[ -z $legacy ]]; then
        product=$1
        release=$2
        if [[ -z $release ]]; then
            release=trunk_staging
        fi
        variant=$3
        if [[ -z $variant ]]; then
            variant=eng
        fi
    fi

    _lunch_meat $product $release $variant
}

把分支整理成表,便于对照源码;右列用贯穿样例代入:

条件行为样例
单参数且为 --help打印用法,返回 0lunch --help
参数个数为 0报错并返回 1;不再弹出交互菜单lunch
参数个数大于 3报错并返回 1lunch a b c d
恰 1 个参数且其中含 -按旧格式拆成三段;任一段为空则失败lunch aosp_arm64-trunk_staging-eng 成功;lunch aosp_arm64-eng 失败
其他走新格式;release 缺省 trunk_staging,variant 缺省 englunch aosp_arm64 与 lunch aosp_arm64 trunk_staging eng 等价于样例三角

旧格式判定条件是“总参数个数为 1,并且第一个参数能被 grep "-" 命中”。因此贯穿样例 lunch aosp_arm64-trunk_staging-eng 走旧格式;lunch aosp_arm64-trunk_staging 会拆出空的 variant,触发 Invalid lunch combo。新格式下,lunch aosp_arm64 等价于产品 aosp_arm64、release trunk_staging、variant eng。

这里有一个必须用源码位置语义讲清的坑:新格式是按位置赋值的。lunch aosp_arm64 eng 并不会把 eng 当成 variant,因为第二个位置是 release。此时构建系统会去查找名为 eng 的 release 配置,通常失败。正确写法要么给满三段,要么只写产品名让后两段走默认,要么显式写出 lunch aosp_arm64 trunk_staging eng。与 §0 步骤 9 的对照表一致。

_lunch_usage 的文案进一步确认产品策略变化:交互菜单和硬编码精选列表已经移除;若要浏览可选项,应分别执行 list_products、list_releases、list_variants。

5. _lunch_meat:校验、导出、装配

# AOSP aosp-main
# 文件: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 \
    build_build_var_cache
    if [ $? -ne 0 ]
    then
        if [[ "$product" =~ .*_(eng|user|userdebug) ]]
        then
            echo "Did you mean -${product/*_/}? (dash instead of underscore)"
        fi
        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

    [[ -n "${ANDROID_QUIET_BUILD:-}" ]] || echo

    set_stuff_for_environment
    [[ -n "${ANDROID_QUIET_BUILD:-}" ]] || printconfig

    if [[ -z "${ANDROID_QUIET_BUILD}" ]]; then
        local spam_for_lunch=$(gettop)/build/make/tools/envsetup/spam_for_lunch
        if [[ -x $spam_for_lunch ]]; then
            $spam_for_lunch
        fi
    fi

    destroy_build_var_cache

    if [[ -n "${CHECK_MU_CONFIG:-}" ]]; then
      check_mu_config
    fi
}

成功路径可以概括成六步。第一步,把候选三角放进环境变量前缀中调用 build_build_var_cache,让 dump 过程能看到它们。第二步,若失败,除了返回 1,还用正则检测产品名是否误把 variant 用下划线粘在末尾,例如 foo_eng;若是,提示改用 -eng。第三步,成功则导出变量:TARGET_PRODUCT 与 TARGET_BUILD_VARIANT 以构建系统回读值为准,TARGET_RELEASE 使用用户传入值,TARGET_BUILD_TYPE 固定为字面量 release。第四步,调用 set_stuff_for_environment,内部先 set_lunch_paths,再设置序列号与 ANDROID_BUILD_TOP。第五步,除非设置了 ANDROID_QUIET_BUILD,否则打印空行、printconfig,并在 spam_for_lunch 可执行时运行它。第六步,销毁变量缓存;若设置了 CHECK_MU_CONFIG,再调用 check_mu_config。

关于最后一步:在本树 build/ 目录内检索,未见 check_mu_config 的函数定义。它只在环境变量被设置时才会调用,因此对常规 AOSP 使用是可选钩子。若有人导出了该变量却未提供函数,就会在 lunch 末尾暴露为命令未找到。这不影响主路径理解,但说明 lunch 末尾预留了外部扩展点。

spam_for_lunch 自身也很克制:它先判断 $(getoutdir)/soong/build.${TARGET_PRODUCT}.ninja 是否存在;若不存在,才打印一大段关于 user / userdebug / eng 取舍的说明。也就是说,这种提示偏向“干净树第一次 lunch”,而不是每次切换产品都刷屏。

6. build_build_var_cache:一次 dump 多变量

# AOSP aosp-main
# 文件:build/envsetup.sh
function build_build_var_cache()
{
    local T=$(gettop)
    cached_vars=(`cat $T/build/envsetup.sh | tr '()' '  ' | awk '{for(i=1;i<=NF;i++) if($i~/_get_build_var_cached/) print $(i+1)}' | sort -u | tr '\n' ' '`)
    cached_abs_vars=(`cat $T/build/envsetup.sh | tr '()' '  ' | awk '{for(i=1;i<=NF;i++) if($i~/_get_abs_build_var_cached/) print $(i+1)}' | sort -u | tr '\n' ' '`)
    build_dicts_script=`\builtin cd $T; build/soong/soong_ui.bash --dumpvars-mode \
                        --vars="${cached_vars[*]}" \
                        --abs-vars="${cached_abs_vars[*]}" \
                        --var-prefix=var_cache_ \
                        --abs-var-prefix=abs_var_cache`_
    local ret=$?
    if [ $ret -ne 0 ]
    then
        unset build_dicts_script
        return $ret
    fi
    eval "$build_dicts_script"
    ret=$?
    unset build_dicts_script
    if [ $ret -ne 0 ]
    then
        return $ret
    fi
    BUILD_VAR_CACHE_READY="true"
}

失败分支有两处:soong_ui 返回非 0,或 eval 返回非 0。任一失败都不会把 BUILD_VAR_CACHE_READY 置真。成功后,_get_build_var_cached 优先读缓存;若缓存未就绪,才退回单次 --dumpvar-mode。gettop 失败时该读取函数返回 1,并在 stderr 提示设置 TOP。

变量名集合来自扫描 envsetup.sh 自身文本。好处是少维护一份手工列表;坏处是,如果将来有人通过间接函数名去调用缓存读取,扫描可能漏项。这是源码里真实存在的耦合方式,不是臆测。

7. 产品如何被找到:product_config.mk

# AOSP aosp-main
# 文件:build/make/core/product_config.mk
android_products_makefiles := $(file <$(OUT_DIR)/.module_paths/AndroidProducts.mk.list) \
  $(SRC_TARGET_DIR)/product/AndroidProducts.mk

对每一个文件,_read-ap-file 会清空相关变量、include 该文件,把 PRODUCT_MAKEFILES 规范成 product:path,并做三类校验:COMMON_LUNCH_CHOICES 里拆出的产品名必须属于本文件声明的产品;每个 choice 必须以 -eng、-userdebug 或 -user 结尾;STARLARK_OPT_IN_PRODUCTS 也不得引用未声明产品。随后所有文件的结果累积、去重,得到 all_named_products、COMMON_LUNCH_CHOICES、以及当前 TARGET_PRODUCT 对应的 current_product_makefile。

两个最常见的硬失败如下。

# AOSP aosp-main
# 文件:build/make/core/product_config.mk
ifeq (,$(current_product_makefile))
  $(error Cannot locate config makefile for product "$(TARGET_PRODUCT)")
endif

以及产品名冲突时的 Product name must be unique。它们都会让 soong_ui dump 失败,从而让 _lunch_meat 返回 1。

平台 AndroidProducts.mk 还根据 TARGET_BUILD_APPS 是否非空切换两套 PRODUCT_MAKEFILES。unbundled 场景使用更短的 generic 列表。lunch 主路径会避免把 apps 模式带进去;print_lunch_menu 与 list_products 都显式清空 TARGET_BUILD_APPS。相反,tapas 与 banchan 会设置该变量,这是它们与 lunch 的重要分界。

回到贯穿样例:完整构建分支的 PRODUCT_MAKEFILES 含 aosp_arm64.mk,COMMON_LUNCH_CHOICES 含 aosp_arm64-trunk_staging-eng。因此 lunch aosp_arm64-… 与“在常见组合里看见它”是同一张注册表上的两件事——前者靠 TARGET_PRODUCT 过滤 makefile,后者只是给补全/菜单残留用的推荐字符串列表。

8. Release:缺省、空串、非法名

# AOSP aosp-main
# 文件:build/make/core/release_config.mk
ifeq ($(TARGET_RELEASE),)
    TARGET_RELEASE ?= was_unset
    ifeq ($(TARGET_RELEASE),was_unset)
        $(error No release config set for target; please set TARGET_RELEASE, or if building on the command line use 'lunch <target>-<release>-<build_type>', where release is one of: $(ALL_RELEASE_CONFIGS_FOR_PRODUCT))
    endif
    TARGET_RELEASE = trunk_staging
endif

以及最终 pass 中的成员校验:

# AOSP aosp-main
# 文件:build/make/core/release_config.mk
ifeq ($(filter $(ALL_RELEASE_CONFIGS_FOR_PRODUCT), $(TARGET_RELEASE)),)
    $(error No release config found for TARGET_RELEASE: $(TARGET_RELEASE). Available releases are: $(ALL_RELEASE_CONFIGS_FOR_PRODUCT))
endif

这里要区分三种状态。变量完全未设置时,?= 会写成哨兵值 was_unset,然后走 error,并打印当前产品可用的 release 列表。变量被显式设成空字符串时,不属于“未设置”,于是落到 TARGET_RELEASE = trunk_staging;注释承认 full build 走到空串更像 bug,但仍然给出合法默认以避免结果漂移。变量有值却不在 ALL_RELEASE_CONFIGS_FOR_PRODUCT 中时,走第二段 error。

另外,release 配置还可能被标记为不允许用于 lunch。源码中存在 _disallow_lunch_use 相关错误:Release config ${TARGET_RELEASE} is disallowed for build。这说明即使名字存在,也不意味着一定能从 lunch 入口选用。

lunch 新格式默认填 trunk_staging,与 list_products 脚本注释中“该配置处处存在、用于避免未指定 release 错误”的策略一致。本树也能看到 build/release/release_configs/trunk_staging.textproto。

9. set_lunch_paths:把产品相关路径写进 shell

# AOSP aosp-main
# 文件:build/envsetup.sh
function set_stuff_for_environment()
{
    set_lunch_paths
    set_sequence_number
    export ANDROID_BUILD_TOP=$(gettop)
}

set_lunch_paths 是 lunch 成功后对环境“动刀”最多的函数。它先处理历史兼容:若只有旧变量 ANDROID_BUILD_PATHS,就迁移到 ANDROID_LUNCH_BUILD_PATHS。若存在 ANDROID_PRE_BUILD_PATHS,从 PATH 中移除并清空。然后移除上一次 lunch 追加的路径段,再按当前缓存重新组装:SOONG_HOST_OUT_EXECUTABLES、HOST_OUT_EXECUTABLES、LLVM binutils、asuite 三类预编译、ANDROID_JAVA_TOOLCHAIN。同时设置 ASAN_SYMBOLIZER_PATH、JAVA_HOME、ANDROID_JAVA_HOME,并重拼 PYTHONPATH(包含 development/python-packages 下若干包路径,以及可能的 VENDOR_PYTHONPATH)。最后导出 ANDROID_PRODUCT_OUT、OUT、ANDROID_HOST_OUT、ANDROID_SOONG_HOST_OUT 以及主机与目标测试用例目录,并把新的 lunch PATH 段前置到 PATH。

因为这些值全部来自刚刚建立的 dump 缓存,所以顺序被锁死:先 build_build_var_cache 成功,再 set_lunch_paths。若有人手工改乱顺序,PATH 与 OUT 就会与真实产品配置不一致。

对 aosp_arm64-trunk_staging-eng,读缓存后的直观结果是:OUT/ANDROID_PRODUCT_OUT 指向 …/out/target/product/generic_arm64,host 工具路径来自本次 dump 算出的 HOST_OUT / SOONG_HOST_OUT,而不是上一次 lunch 其它产品残留的目录。切换到例如 aosp_x86_64-trunk_staging-eng 再 lunch 一次时,同一函数会先抠掉旧的 ANDROID_LUNCH_BUILD_PATHS,再挂上新产品对应段——这就是“可重复 lunch”的源码保证。

10. 列表命令、菜单残留与补全

list_products 固定使用 TARGET_RELEASE=trunk_staging,并清空 TARGET_BUILD_APPS,然后 dump all_named_products,再用 sed 把空格列表打成换行。这样既避免“未指定 release”错误,也不让 unbundled 模式缩小产品集合。

list_releases 要求命令行给出产品名,或环境中已有 TARGET_PRODUCT;否则打印 Usage 并以状态 1 退出。它 dump 的是 ALL_RELEASE_CONFIGS_FOR_PRODUCT。list_variants 最简单,直接输出三行固定字符串,不访问构建系统。

print_lunch_menu 仍然存在:它清空产品相关变量后去取 COMMON_LUNCH_CHOICES,失败则 Warning,成功则编号打印。但当前 lunch 主路径在无参时直接报错,不再调用它。阅读源码时不要看到菜单函数还在,就断言交互菜单还活着;要以 lunch 函数的实际分支为准。

补全有两套痕迹。_lunch 缓存 COMMON_LUNCH_CHOICES,给旧式单参数组合补全。_lunch_completion 按词位分别补产品、release、variant,其中前两档会调用 list_products 与 list_releases,第三档写死三种 variant;它挂在 lunch2 上。add_lunch_combo 已废弃,调用时只打印过时信息,明确要求改用 AndroidProducts.mk 中的 COMMON_LUNCH_CHOICES。

11. tapas 与 banchan:共享落点,不同入口语义

tapas 面向未捆绑应用构建。它从参数里用正则滤出 arch、variant、density、devkeys、以及剩余应用名;help 则转去 tapasHelp.sh。多 arch、多 variant、多 density、多 keys 都会各自报错返回。默认产品映射是 aosp_arm,再按 arch 改成 aosp_x86 / aosp_arm64 / aosp_x86_64;若出现 devkeys,会把产品名从 aosp_ 改写成 aosp_devkeys_ 前缀形式。缺省 variant 为 eng,缺省 apps 为 all,缺省 density 为 alldpi,release 目前写死 trunk_staging(源码 TODO 提到尚未扩展 release 参数)。然后直接 export 一串 TARGET_* 与 TARGET_BUILD_APPS,再走与 lunch 相同的 cache / set_stuff_for_environment / printconfig / destroy 收尾。

banchan 面向未捆绑 APEX 模块。无模块名直接 error;默认 arch 语义落到 module_arm64 等产品;同样设置 TARGET_BUILD_APPS。源码注释写明:当前虽借用 TARGET_BUILD_APPS,但用例与 tapas 不同,未来可能分裂。

对本篇主题而言,结论只有一句:二者不是 lunch 的语法糖,而是另一类产品选择入口;它们会改变 TARGET_BUILD_APPS 语义,从而影响 AndroidProducts.mk 里可见的产品集合。

若把三条入口放在同一张决策表里,差异会更清楚。lunch 面向完整产品镜像或常规平台构建,要求产品名来自完整注册表,release 与 variant 显式或默认给出,且通常保持 TARGET_BUILD_APPS 为空。tapas 面向 APK 级未捆绑构建,产品被收敛到少量 generic 架构产品,应用列表进入 TARGET_BUILD_APPS。banchan 面向 APEX 模块,产品名映射到 module_* 系列,模块列表同样进入 TARGET_BUILD_APPS,并且拒绝“什么模块都不给”的空调用。三条路径最终都调用 set_stuff_for_environment,所以从“环境变量落点”看很像;从“产品注册表看到什么”看却完全不同。阅读后续编译日志时,先确认自己是从哪条入口进来的,可以少走很多弯路。

还需要强调一次 shell 状态的可重复性。由于 lunch、tapas、banchan 都在当前 shell 中 export,同终端里后一次调用会覆盖前一次的产品三角与 PATH 段。这是特性而不是缺陷:它允许开发者在同一会话切换目标。但若脚本或文档假设“source 一次、lunch 一次后环境永恒不变”,在多次切换后就会得到过期结论。set_lunch_paths 每次先抠掉旧的 ANDROID_LUNCH_BUILD_PATHS,正是为这种重复调用服务的。

边界场景与异常分支

把前面打散的失败模式收束成一张排障清单。

  1. 未在树根 source,或 TOP 指错:_gettop_once 失败,lunch 未定义。
  2. lunch 无参数:直接失败,无菜单。
  3. lunch 参数超过三个:直接失败。
  4. 旧格式缺段:Invalid lunch combo。
  5. 新格式第二参数误写 variant:会把该字符串当作 release,随后在 release 校验阶段失败。
  6. 产品名未注册:Cannot locate config makefile for product。
  7. 产品名冲突:Product name must be unique。
  8. COMMON_LUNCH_CHOICES 引用未声明产品或非法后缀:读取 AndroidProducts 时即 error。
  9. 未设置 TARGET_RELEASE 走内部 make 路径:No release config set for target。
  10. release 名非法:No release config found for TARGET_RELEASE。
  11. release 存在但不允许 lunch:disallowed for build。
  12. lunch xxx_eng 这类下划线误写:失败后额外提示改用短横线。
  13. 多份 vendorsetup 白名单:全部不加载。
  14. 未 lunch 就跑依赖产品 out 的命令:例如 showcommands 发现 TARGET_PRODUCT 为空并提示。
  15. ANDROID_QUIET_BUILD:抑制配置打印与 spam,不改变校验成败。
  16. CHECK_MU_CONFIG:仅当设置时调用;本树无对应实现时可能变成额外错误源。

设计取舍(Trade-off)

第一,删除交互菜单,换取显式目标与可扩展产品池。_lunch_usage 与无参失败共同证明了这一点。收益是 lunch 不再依赖一份难维护的精选列表,也不再暗示“菜单里没有的产品就不该用”;代价是新人失去“回车选编号”的路径,必须学会 list_* 或记住组合串。从工程维护角度看,产品数量随设备树与主线配置膨胀后,交互菜单本身就会变成错误来源:菜单滞后于真实 AndroidProducts.mk 时,开发者会以为某些产品不存在。

第二,引入 release 作为一等参数,并让新格式默认 trunk_staging。收益是标志组合可按产品切换且可校验,避免所有人暗默认同一套编译标志;代价是参数位置更容易用错,两段式历史记忆会持续制造误用。默认值降低了日常输入成本,却也降低了“我到底选了哪个 release”的可见性,所以 printconfig 与 get_build_var TARGET_RELEASE 在排障里变得更重要。

第三,批量 dumpvars 缓存。收益是 lunch 后立刻改 PATH 与 OUT 时延迟可控;代价是与“扫描 envsetup 文本收集变量名”强耦合,并要求严格的 destroy 纪律。若只 destroy 一半变量,下一次 lunch 前残留的 BUILD_VAR_CACHE_READY 可能让读取函数误以为缓存仍有效。源码选择“成功才置位、结束即销毁”,就是在性能与正确性之间划出最小窗口。

第四,PATH 拆成全局与 lunch 两层。收益是未选产品时也能用检索脚本,选产品后 host 工具与输出目录正确切换;代价是两套几乎相同的“抠 PATH 再追加”代码,注释都在警告不要随手改。把所有路径都塞进 set_global_paths 看似简单,却会让未 lunch 的 shell 提前指向某个过期 PRODUCT_OUT;把所有路径都推迟到 lunch,又会让序章所需的 cgrep 在 source 后仍不可用。

第五,vendorsetup 白名单与“多白名单则全都不加载”。收益是在合规场景可控制扩展面;代价是错误配置会导致厂商环境静默缺失,只能靠 including / ignoring / 冲突日志发现。对 AOSP 纯净树来说这条路径通常为空操作;对集成了【厂商扩展】脚本的树来说,它反而比 lunch 本身更容易成为“环境不对”的根因。

第六,tapas/banchan 复用 TARGET_BUILD_APPS 与同一套环境落点。收益是少造一套导出机制;代价是产品列表在 apps 模式下被切换,阅读者若不分入口,会以为 list_products 与 tapas 看到的世界永远一致。源码在 AndroidProducts.mk 用 ifneq ($(TARGET_BUILD_APPS),) 切开两套 PRODUCT_MAKEFILES,已经把这种取舍写死在产品注册层。

版本演进与历史背景

其一,add_lunch_combo 仅保留报废提示,注册入口迁移到 COMMON_LUNCH_CHOICES。函数还故意打印调用点文件名与行号,说明维护者预期仍会有旧脚本触发它,需要把责任定位回调用方。其二,lunch 主路径不再调用交互菜单;print_lunch_menu 仍残留,但无参逻辑已改为报错。这种“函数残留、主路径切断”在 AOSP 里很常见,阅读时必须以调用关系为准。其三,常见组合从“产品-变体”扩展为“产品-release-变体”,平台 AndroidProducts.mk 中的四项 choice 已是三段;与此同时,product_config.mk 对 choice 的校验仍以 variant 后缀为合法性锚点,显示出演进过程中的兼容层痕迹。其四,默认 release 固定为 trunk_staging,并与 list 脚本的占位策略、release_configs/trunk_staging.textproto 并存;bp1a 一类配置通过 inherits 表达继承,说明 release 已从“几个枚举值”成长为可组合对象。其五,大量曾经塞在 envsetup 里的 helper 被 unset,改由 build/soong/bin 提供;lunch 继续留在 shell 函数,因为它必须改当前 shell。其六,spam_for_lunch 与 ANDROID_QUIET_BUILD 表明提示信息也被产品化地收敛,避免干净构建与日常切换使用同一套噪声策略。其七,补全同时存在 _lunch 与 _lunch_completion/lunch2,反映出命令行 UX 正从“单字符串组合”迁移到“多位置参数”,但又必须照顾旧习惯。

常见误区澄清

误区一:以为 lunch 不带参数会出菜单。反证是 $# -eq 0 分支直接报错并指向帮助。即便仓库里还能搜到 print_lunch_menu,也不能用“函数还在”代替“主路径仍调用”。

误区二:以为 aosp_arm64-eng 仍是合法两段式。反证是单参数含 - 时按三段拆分,两段会令某一段为空,从而在 shell 层就失败,未必能进入构建系统的产品查找。贯穿样例的合法旧格式是 aosp_arm64-trunk_staging-eng,不是 aosp_arm64-eng。

误区三:以为 lunch aosp_arm64 eng 表示 eng 变体。反证是新格式第二位置是 release,不是 variant。这个误区特别隐蔽,因为命令看起来“像旧习惯的合理简化”,但位置语义已经变了;在样例上会变成查找名为 eng 的 release。

误区四:以为产品列表写死在 lunch 函数里。反证是 all_named_products 来自各 AndroidProducts.mk 汇总;lunch 只负责解析字符串并触发校验。

误区五:以为 source 完 envsetup 就等于选好产品。反证是结尾只做 set_global_paths;set_lunch_paths 仅在 _lunch_meat 成功后发生。所以会出现“能 cgrep,不能依赖 $OUT”的中间态,这是设计如此,不是环境坏了。

误区六:以为 add_lunch_combo 还能注册菜单项。反证是函数只打印 obsolete,真正注册口是 COMMON_LUNCH_CHOICES。

误区七:以为 TARGET_BUILD_TYPE=release 表示选用了某个名为 release 的 release 配置。反证是源码注释写明它是字面量字符串,与 TARGET_RELEASE 不同。

误区八:以为 COMMON_LUNCH_CHOICES 校验还要求严格两段。反证是校验只要求后缀属于 -eng/-userdebug/-user,因此三段式可通过;真正决定 lunch 旧格式合法性的是 shell 三段拆分。两层校验服务的对象不同:一层服务“常见组合字符串是否写得像样”,一层服务“用户刚才到底输入了什么”。

误区九:以为 list_products 与 lunch 使用完全相同的环境前提。反证是 list_products 会强制 TARGET_RELEASE=trunk_staging 并清空 TARGET_BUILD_APPS,而 lunch 使用你指定的三角。它适合枚举产品名,不代表某一特殊 release 下所有标志都已按最终构建展开。

误区十:以为厂商 vendorsetup.sh 属于 AOSP 必经主路径。反证是纯净树可以没有任何此类文件;一旦出现,应按【厂商扩展】阅读,不能把它的行为写成平台原生语义。

误区十一:以为 lunch aosp_arm64 后产物一定在 out/target/product/aosp_arm64。反证是 PRODUCT_OUT := $(TARGET_PRODUCT_OUT_ROOT)/$(TARGET_DEVICE),而 aosp_arm64.mk 写明 PRODUCT_DEVICE := generic_arm64,故目录是 …/generic_arm64。

典型故障场景与调试手段

现象 A:lunch 失败,日志含 Cannot locate config makefile。对应 product_config.mk 中 current_product_makefile 为空。行为是 dumpvars 非 0,_lunch_meat 返回 1。排查先跑 list_products 过滤产品名,再核对所属 AndroidProducts.mk 是否进入模块扫描列表,最后确认拼写与大小写。

现象 B:No release config found 或 No release config set。对应 release_config.mk。排查用 list_releases <product>,并检查是否把 variant 写进了 release 槽位。

现象 C:source 之后没有 lunch 命令。对应 _gettop_once 失败。排查是否在树根执行,检查锚点文件 build/make/core/envsetup.mk 是否存在,以及 TOP 是否指到错误目录。

现象 D:lunch 显示成功,但 host 工具或 OUT 仍像旧产品。对应 set_lunch_paths 与缓存。排查重新 lunch,打印 echo $TARGET_PRODUCT $TARGET_RELEASE $TARGET_BUILD_VARIANT $OUT $ANDROID_HOST_OUT,确认没有手工 export 残留覆盖。

现象 E:厂商自定义环境没出现。对应 source_vendorsetup。查看 source 时的 including / ignoring / 多白名单冲突日志。涉及内容按【厂商扩展】处理。

现象 F:第一次 lunch 总刷一大段 user/userdebug/eng 说明。对应 spam_for_lunch 在缺少 build.${TARGET_PRODUCT}.ninja 时的行为。若需安静模式,设置 ANDROID_QUIET_BUILD。

推荐观测序列(与 §0 贯穿样例同一目标)。先 gettop 确认树根,再 list_products | grep aosp_arm64 与 list_releases aosp_arm64 看可选集合,然后执行 lunch aosp_arm64-trunk_staging-eng,最后核对接力棒:

echo "$TARGET_PRODUCT $TARGET_RELEASE $TARGET_BUILD_VARIANT"
# 期望:aosp_arm64 trunk_staging eng
get_build_var TARGET_DEVICE
# 期望:generic_arm64
echo "$OUT"
# 期望:…/out/target/product/generic_arm64

若要把排障过程做得更接近源码路径,再补三组对照。第一组对照新旧格式:lunch aosp_arm64-trunk_staging-eng 与 lunch aosp_arm64 trunk_staging eng 导出三角应一致。第二组对照位置误用:lunch aosp_arm64 eng 应死在 release 校验。第三组对照静默开关:ANDROID_QUIET_BUILD=true 后再 lunch,校验仍发生,只是 printconfig / spam_for_lunch 被抑制。

对输出目录类问题,不要只看 OUT,更不要假设目录名等于 TARGET_PRODUCT。样例上 PRODUCT_NAME=aosp_arm64 而 PRODUCT_OUT 使用 PRODUCT_DEVICE=generic_arm64。set_lunch_paths 同时导出 ANDROID_PRODUCT_OUT、ANDROID_HOST_OUT、ANDROID_SOONG_HOST_OUT 以及测试用例目录。对照 printconfig 与这组变量,通常比直接删 out/ 更便宜。

AOSP 原生 vs 厂商可定制点

类别内容
AOSP 原生envsetup.sh、shell_utils.sh、lunch/_lunch_meat、product_config.mk、release_config.mk、平台 AndroidProducts.mk、list_*、spam_for_lunch、tapas/banchan 框架
仍走原生机制的扩展点各设备树 AndroidProducts.mk、COMMON_LUNCH_CHOICES、build/release 下 release 配置、产品 makefile 的 inherit 链
【厂商扩展】vendorsetup.sh 正文、私有产品与闭源 makefile、公司内 lunch 别名或默认 release 改写、CHECK_MU_CONFIG 钩子实现、白名单策略的具体名单内容

划分标准可以更硬一点。只要机制由 AOSP 脚本定义、即使数据文件来自 device/ 树,仍算“走原生机制的扩展点”,例如某设备新增 AndroidProducts.mk 并声明自己的 COMMON_LUNCH_CHOICES。只要逻辑正文不在 AOSP 保证的行为合同内,例如 vendorsetup.sh 里重新定义同名函数、劫持 lunch、改写默认 release,就应按厂商扩展处理,并在对照实验时准备“去掉该脚本后是否恢复平台行为”的验证步骤。这样划分不是为了站队,而是为了避免把厂商补丁误写成 Android 平台通论。

小结

把本篇压缩成一条可记住的因果链,并用贯穿样例代入:先 source build/envsetup.sh 获得树根与全局工具面;再 lunch aosp_arm64-trunk_staging-eng 选定三角;dump 时 product_config.mk 命中 aosp_arm64.mk,PRODUCT_DEVICE=generic_arm64 决定 PRODUCT_OUT=out/target/product/generic_arm64,release_config.mk 放行 trunk_staging;最后 set_lunch_paths 把绝对路径写入 OUT 与 lunch 级 PATH。任何一步失败,都不宜继续假设“环境已经对齐”。

更细一点说,envsetup.sh 完成的是阅读与构建的共同前置:钉住树根、挂上全局工具 PATH、按策略加载厂商脚本、注册补全。它并不选择产品。lunch 才把产品、release、variant 解释成环境变量,并借 soong_ui dump 让产品注册表与 release 规则完成合法性校验。校验通过后,set_lunch_paths 把 OUT、JDK、host 工具链等一并切到当前产品视角;校验失败则任何 PATH 切换都不应被视为成功。当前树上的 UX 已经明确转向显式目标:无参不再出菜单,旧两段式不可再想当然,list_products / list_releases / list_variants 才是浏览入口。tapas 与 banchan 证明“选产品”不止 lunch 一条路,但只有分清 TARGET_BUILD_APPS 是否介入,才不会把三套入口看到的产品世界混成一套。

验收动作固定为样例目标:干净 shell → 树根 → source build/envsetup.sh → gettop → lunch aosp_arm64-trunk_staging-eng → 核对三角与 OUT 是否落在 …/generic_arm64 → 再故意执行 lunch aosp_arm64 eng 与 lunch aosp_arm64-eng,确认分别死在 release 层与 shell 拆段层。能独立解释这三条路径,才算读懂本篇,而不是只会背函数名。

到这里,模块 1 开篇要建立的判断标准已经齐了:看见构建问题时,先问“当前 shell 的产品三角是什么、PRODUCT_DEVICE 把 OUT 指到哪、校验有没有真的跑过”,再问“Soong 为什么这样生成规则”。顺序反了,就容易在错误配置上分析正确代码。

下一篇进入 Blueprint 语法与 Android.bp 模块模型:模块属性、类型与依赖声明。届时默认大家已经能够稳定 lunch 到 aosp_arm64-trunk_staging-eng(或等价合法组合),并且清楚这些变量是如何进入后续构建入口的。