做安卓系统方案的人几乎每天都要和lunch打交道。这个命令是整套源码编译流程里的第一个关键节点运行无参数的lunch之后终端会打印一份构建目标列表我们从中选择“产品-编译类型”的组合后续的make才知道该编出什么样的镜像。我接手过不少编译环境问题排在最前面的、几乎每个团队都会遇到的一种是“lunch 不显示构建列表了”。具体表现就是执行source build/envsetup.sh之后运行lunch终端要么直接回到$提示符要么只出现一句 Youre building on Linux然后没有任何菜单。新人碰到这种情况很容易怀疑代码同步有问题、服务器有问题甚至怀疑自己这个终端是不是不配做系统开发。其实九成以上是环境注册或产品配置路径的问题。这篇文章就从原理到排查再到修复把我在实际工作中踩过的坑一次性讲清楚希望能帮你少走弯路。1. lunch 的构建列表究竟是怎么生成的在动手排查之前必须先理解机制。我第一次遇到“lunch 不显示构建列表”时也是慌的后来发现很多坑其实都源于对这套壳层脚本的工作原理不够了解。下面先把机制拆开讲明白后面所有步骤才有依据。1.1 它不是一个命令而是一个 shell 函数先说一个反直觉的事实lunch并不是一个可以被which找到的可执行程序它是build/envsetup.sh脚本注册到当前 shell 会话里的一批函数之一。既然是函数就意味着它只在“当前终端窗口”以及“当前 shell 进程”中有效。新打开一个终端直接输入lunch大概率得到的是command not found。所以每次新开终端都要执行source build/envsetup.sh。这一点很多熟练工也会栽跟头把source写成了bash build/envsetup.sh脚本虽然在子进程里跑了一遍但函数注册到了子进程的 shell回到当前窗口后没有任何变化看起来就“什么都没发生”。lunch这个函数本身做的事情可以粗略分为两段一是打印菜单二是根据用户选择或传入的参数去设置一堆环境变量比如TARGET_PRODUCT、TARGET_BUILD_VARIANT、TARGET_DEVICE等等。我们要排查的“不显示构建列表”问题基本都出在第一段也就是菜单打印这一步。搞清楚这个边界非常关键因为你后面会看到有时候产品定义其实是好的只是菜单函数本身提前退出了。1.2 列表的数据来源两层 mk 文件lunch要展示的产品不是凭空列出来的。它依赖源码树里两类文件的组合device/厂商/设备型号/AndroidProducts.mkvendor/厂商/设备型号/AndroidProducts.mkprint_lunch_menu这段函数会扫描这两个目录层级把每一个AndroidProducts.mk当作一个产品的聚合入口。这个文件内部通过PRODUCT_MAKEFILES变量指向真正描述这个产品的.mk文件。举个例子一个入口文件device/mycompany/mydevice/AndroidProducts.mk里会写着PRODUCT_MAKEFILES : \ $(LOCAL_DIR)/mydevice.mk而mydevice.mk里会有一行类似PRODUCT_NAME : mydevice的定义。整个链路是扫描入口文件 → 解析它引用的产品 mk 列表 → 逐个读取里面的PRODUCT_NAME→ 拼上编译类型 → 生成菜单的每一行。如果中间任何一环断了要么某个产品不出现要么整个列表直接消失。这里还要注意一个方向性问题AndroidProducts.mk是可以指向多个产品 mk 的。也就是说一个设备目录下可以同时维护多个产品变体比如平板版和车载版。正因为每个产品最终由PRODUCT_NAME决定在菜单里长什么样所以你在排查时一定要先分清“目录里有文件”和“文件被正确解析成了 PRODUCT_NAME”是两件事后者才是决定菜单显示的关键。1.3 显示出来的组合名是怎么拼出来的每个菜单项默认格式是产品名-编译类型编译类型有三种eng面向工程师开发调试编译时关掉不少优化包含大量调试工具适合日常开发。user接近正式发布形态权限限制严格一般用于出厂和验收测试。userdebug在user基础上保留 root 和调试能力是系统集成阶段最常用的组合。所以你会看到aosp_arm-eng、aosp_blueline-userdebug这样的条目。菜单会针对每个产品把三种类型都生成一遍除非产品文件里明确限制了变体。了解这个生成规则有助于你判断如果一家厂商的产品只允许user和userdebug两个组合那很可能是产品 mk 里做了条件限制而不是扫描出了问题。另外历史遗留的full-eng这种名字经常会让人困惑。full其实是一个真实的产品名源自早期参考实现build/target/product/full.mk它代表一个“功能完整的参考设备”和后加的aosp_*系列产品不是一回事。如果菜单里只剩full-eng通常说明真正的设备产品树没有被扫到这是我们后面要重点排查的方向。2. 为什么构建列表会消失常见诱因机制了解之后再来归纳原因。我在不同团队、不同源码版本里遇到的“lunch 不显示构建列表”真正原因翻来覆去就那么几类这里按出现频率从高到低排一遍。2.1 最常见envsetup.sh 根本没生效这个原因占了至少一半。最典型的就是没有source或者是用sh build/envsetup.sh、bash build/envsetup.sh这样去“执行”脚本。记住source是在当前 shell 进程里读取脚本内容并逐行执行而bash xxx.sh是起了一个新的子进程脚本里注册的函数只存在于那个子进程里等子进程退出一切归零。另一个常见场景是脚本里有一段早期返回逻辑。某些源码版本里envsetup.sh开头会做环境检查和依赖判断如果返回了非零值后面的函数注册可能没执行完。我自己就撞过一次某台机器上把 JDK 版本切到了不兼容的版本envsetup.sh在中间某一段直接报错退出我还在奇怪为什么lunch后面杳无音信。还有一个干扰因素有些公司会在/etc/profile.d或用户的.bashrc里预置一堆环境脚本其中某个脚本恰好定义了一个叫lunch的函数或别名。你输入lunch时系统找了半天执行的可能根本就不是 AOSP 那个函数自然也就不会有构建列表。这类“同名覆盖”问题靠直觉很难发现必须用type lunch去验证这个我们放到排查流程里细说。2.2 产品或设备目录根本没有被扫到print_lunch_menu的扫描范围是有讲究的。标准实现里默认扫描的路径是device/*/*/AndroidProducts.mk和vendor/*/*/AndroidProducts.mk。如果你的产品对应设备树不在这个标准位置或者目录结构深度不对它就不会出现在列表里。我遇到过这么几种具体情形第一repo sync的时候清单文件里根本没有包含 device/vendor 相关的仓库这种情况多发生在“只想编一个轻量模拟器镜像于是只拉了 build 和 framework 代码”的场景第二代码仓库是拉下来了但目录名不符合device/厂商/设备这种两层结构比如多了一层device/厂商/产品线/设备第三局域网文件权限问题导致当前用户对某个目录没有读权限find能看到目录但脚本里的 glob 展开后因为权限不足被过滤掉。这些场景表面现象都一样菜单空空如也。2.3 mk 文件有问题或被 add_lunch_combo 卡住如果产品文件存在于标准位置但里面的内容有问题列表同样出不来。常见错误包括AndroidProducts.mk里写错了路径引用的产品 mk 文件实际不存在产品 mk 文件里PRODUCT_NAME没有定义文件编码或者行尾符异常比如在 Windows 上编辑过带了一堆回车符make 系统直接解析失败。这种情况下lunch通常不会给你一个像样的错误信息可能只是静默跳过或者在屏幕某个角落滚过一行不明不白的 warning然后回到提示符。还有一条容易忽略的路径是add_lunch_combo。这个函数是给手动添加组合用的它和目录扫描是两条线。不少厂商会在build/envsetup.sh或者自己的device脚本里调用add_lunch_combo xxx-userdebug以便让某个产品不受目录扫描规则限制。如果你的构建列表里出现了“看似不来自任何 AndroidProducts.mk”的组合那就是被add_lunch_combo加进来的。如果全表只剩这类手工添加项反过来说明目录扫描这条线全军覆没。排查时要学会区分这两个来源否则会一直纠结“为什么我的文件没生效”。2.4 终端环境污染与函数覆盖终端环境这个因素常常被当成玄学其实背后是有规律的。最典型的是在tmux或screen会话里恢复了之前的窗口但 shell 环境变量已经过期ANDROID_BUILD_TOP还指向另一个源码目录。这种情况下lunch函数虽然存在但它扫描的目录却是旧的甚至envsetup.sh里依赖的路径全部失效。另一个隐蔽问题是 shell 兼容性。有些发行版默认把/bin/sh指向dash而envsetup.sh里有大量 bash 特有的语法。如果你在#!/bin/sh的脚本里间接执行了source或者用非交互的sh -c方式加载环境脚本大概率会在某个不起眼的语法点直接失败。你可能会想“我在自己终端里明明可以啊为什么换到自动化脚本里就不行”这往往就是 shell 身份变了的缘故。处理方式也很简单统一用bash -c或bash -l来跑或者保证整个链路都从 bash 出发。3. 从现象到根因一套可以直接照做的排查流程前面说的原因比较多遇上实际问题时不要猜按下面这个流程一步步走通常能在十分钟内定位到根因。我把每一步的验证命令和判断标准都写出来直接照做就行。3.1 第一步验证 lunch 的真实身份先确认当前会话里到底有没有一个正确的lunch函数以及它是不是 AOSP 的那个。在终端里执行type lunch如果输出是lunch 是一个函数那说明envsetup.sh至少在某个版本里成功注册了。如果输出是lunch is /usr/bin/lunch之类的路径说明这是个可执行文件可能不是你期望的那个。再执行command -v lunch这个命令会返回函数的名称或者可执行文件路径。只看到函数名的输出才是正常状态。这里特别提醒不要用which lunch因为它对 shell 函数经常没有输出容易误导你得出“lunch 不存在”的错误结论。如果第一步就不对回头重新执行source build/envsetup.sh echo $?echo $?输出 0 才说明脚本完整执行。如果非零去看脚本输出的最后几行错误信息问题多半出在环境依赖上。3.2 第二步检查目录扫描范围确认lunch函数没问题之后手动扫描一次目录看看能匹配到多少入口文件find device vendor -maxdepth 3 -name AndroidProducts.mk 2/dev/null正常情况会列出若干AndroidProducts.mk。如果一条都没有说明设备树没有被同步进来或者放在了非标准位置。如果只有零星几个再对比一下你当前要编的产品所属目录到底在不在结果里。这里一个很容易犯的错是把vendor目录当成“所有非 AOSP 代码的集合”但标准扫描只会看vendor/厂商/设备/AndroidProducts.mk这种二层结构千万别把产品目录放在vendor/厂商/中间层/设备里还指望它能被扫到。3.3 第三步确认 AndroidProducts.mk 与产品定义找到一个待检查的AndroidProducts.mk查看内容cat device/mycompany/mydevice/AndroidProducts.mk重点看PRODUCT_MAKEFILES指向的文件是否存在以及最终那个产品 mk 里有没有PRODUCT_NAMEls -l $(dirname device/mycompany/mydevice/AndroidProducts.mk)/mydevice.mk grep -H PRODUCT_NAME device/mycompany/mydevice/mydevice.mk如果文件存在但PRODUCT_NAME注释掉或者拼写错误菜单里当然不会出现。这里还会遇到一个比较烦人的情况产品 mk 内容里带有了include或inherit-product调用这些被引入的基础文件本身有问题也会导致整个解析中断。如果目录和文件都在但菜单仍然没有建议用下面一个更直接的验证方式。3.4 第四步绕过菜单直接指定目标菜单展示的环节虽然断了但直接给lunch传参数往往还能把产品环境拉起来。这一步的目的就是把问题范围缩小到“只有菜单打印坏了”还是“整个产品解析链路都坏了”。source build/envsetup.sh lunch mydevice-userdebug如果能成功执行说明产品和 make 系统本身没问题。继续验证get_build_var TARGET_PRODUCT正常输出就是你指定的产品名。这一步成功的话问题就被锁定在print_lunch_menu函数及其调用的枚举逻辑上这一步失败则说明产品 mk 的解析链路本身有硬伤要回头修正文件内容和路径。3.5 第五步打开调用链路的调试输出如果前面都正常但菜单还是不出现那就要在脚本内部找原因了。最简单的办法是临时编辑build/envsetup.sh里与菜单打印相关的函数加上调试输出。不同源码版本里函数名可能有差异常见的是print_lunch_menu。在遍历目录的循环里加一行echo scan: $f 12然后重新source build/envsetup.sh看每次运行lunch时实际扫描了哪些文件。这样可以立刻分辨出是“压根没扫描到文件”还是“扫描到了但解析失败”。我一般不建议直接整个脚本做set -x因为输出会爆炸几万行的滚动日志反而让人抓不住重点不如定点加两行 echo 来得直接。调试完记得把改动还原毕竟这种脚本平时没人会去动留着调试痕迹容易坑到后面的人。4. 一个完整的实操案例让新增产品出现在列表里理论讲了那么多下面用一个模拟项目 X 的场景做完整演示。假设我从零给某个设备添加一个新的产品定义目标就是让它在lunch菜单里出现。如果你跟着做一遍会对这个流程产生非常直观的体感。4.1 目录结构与两个关键文件首先建立设备树目录结构产品代号我起名为 “demo”厂商目录叫 “mycompany”device/mycompany/demo/ ├── AndroidProducts.mk ├── BoardConfig.mk └── demo.mk这里AndroidProducts.mk是菜单扫描的入口demo.mk是产品定义BoardConfig.mk是板级配置。后两个文件在lunch阶段不一定立刻用到但目录里保留它们是良好习惯。AndroidProducts.mk的内容最简单就一行引用PRODUCT_MAKEFILES : \ $(LOCAL_DIR)/demo.mk注意变量名必须是PRODUCT_MAKEFILES这个拼写连字符和下划线都不能错。$(LOCAL_DIR)是 make 系统提供的变量它指向当前文件所在目录这样写的好处是目录结构即使移动引用关系也不会断。4.2 从零写一份能被 lunch 识别的产品定义接下来是demo.mk里面至少要有一个稳定的PRODUCT_NAME这才是菜单里显示的名字$(call inherit-product, $(SRC_TARGET_DIR)/product/full.mk) PRODUCT_NAME : demo PRODUCT_DEVICE : demo PRODUCT_BRAND : MyCompany PRODUCT_MANUFACTURER : MyCompany第一行inherit-product表示继承系统提供的full.mk也就是我们前面说的“完整功能参考设备”它会让这个产品具备基础的软件包集合。如果替换成aosp_base.mk或其他基础配置也会影响产物内容但在lunch菜单阶段不是决定性因素。真正决定菜单显示的只有PRODUCT_NAME。写完这两个文件在终端里再次 source 一次环境然后执行source build/envsetup.sh lunch这次菜单里就会出现1. demo-eng、2. demo-userdebug、3. demo-user这样的条目和aosp_*系列排在一起。所谓“新增一个产品”核心技术动作其实就是这两步放好入口文件和写好产品名。4.3 为什么有时候添加完还是看不到我见过不少同事按这个步骤操作后菜单里依然没有新条目。复盘下来原因主要有三个。第一修改文件之后没有重新source。这里要解释一下lunch函数本身在每一次调用时会重新扫描目录所以严格来说文件新增后不需要重新sourceenvsetup.sh也能看到。但如果你改的是产品 mk 里的变量而某个开关函数提前把它缓存过那就有必要重新source一次让所有函数和环境变量回到干净状态。第二目录结构深度不对。比如把AndroidProducts.mk放到了device/mycompany/demo/xxx/下面或者多了一层产品线目录扫描 glob 就匹配不上了。有时候find能找到文件但脚本里写死的 glob 就是扫不到这种情况非常容易迷惑人。第三PRODUCT_NAME和文件名不一致。菜单展示用的是PRODUCT_NAME不要求它和产品文件名一致但人脑里默认它们是同一回事。比如文件名是demo.mk里面却写了PRODUCT_NAME : demoboard那菜单里出现的就是demoboard-userdebug。如果你一直在找 “demo”自然觉得没显示。排查时优先用 grep 确认 PRODUCT_NAME 的真实值再对照菜单搜索。4.4 外部设备树和 vendor 目录的特殊处理有些产品代码不在device/下而在vendor/mycompany/demo/。这样的目录同样会被print_lunch_menu扫描到前提是深度正确。但有一个常见差异vendor目录往往承担着闭源二进制、预编译库的存放职责很多人习惯把产品配置也塞在里面却忽略了目录层级。建议是产品 mk 文件尽量统一放在device/下vendor/只放编译期要打进去的二进制和库文件。这样不仅lunch扫描稳定后续做代码评审和配置管理也清晰得多。另外如果你是自定义了一个完全独立的产品仓库而它不在device和vendor这两个顶层目录里就需要确认源码版本是否支持扩展扫描路径否则永远别指望它自动出现在菜单中。5. 常见问题速查表与避坑经验最后把高频问题整理成一张速查表方便你在现场快速定位。这些条目都是我实际遇到过的真实场景不是从文档里抄来的针对性非常强。5.1 问题速查表现象可能原因快速验证解决措施lunch报 command not found没有执行source build/envsetup.shtype lunch重新 source 环境脚本which lunch有路径但行为异常同名可执行文件覆盖了内部函数command -v lunch检查 PATH或用函数方式调用菜单只有full-eng一条设备树没同步或扫描不到find device vendor -maxdepth 3 -name AndroidProducts.mk补齐设备仓库检查目录深度菜单有内容但缺某个特定产品该产品的 mk 路径错误或 PRODUCT_NAME 拼写错grep PRODUCT_NAME 产品mk修正路径和名字lunch菜单不出现但执行lunch demo-userdebug成功菜单枚举函数出问题但产品解析正常直接带参调用get_build_var TARGET_PRODUCT加调试输出定位枚举函数在 CI 或脚本里运行没有任何输出非交互 shell 或 shell 身份不对手动bash -c source ... lunch ...统一走 bash避免 sh 直接加载lunch输出一堆 error 然后退出环境依赖被破坏比如 JDK 版本不对echo $?看返回值观察脚本尾部输出修复依赖后重新 source5.2 几个容易踩的坑提前说第一个坑不要在 CI 脚本里跑无参数的lunch。无参数lunch本质是交互式菜单它会等待用户输入编号。在无人值守环境里它要么静默跳过要么直接报错看起来就像“构建列表消失了”。CI 里永远要写成lunch 明确的产品-变体不要试图让它自己弹出菜单。第二个坑envsetup.sh必须在源码树根目录下执行。它的内部实现大量依赖相对路径和PWD推断。如果你在build/子目录或者某个子模块目录里执行source build/envsetup.sh很多路径变量会算错导致后面lunch扫描目录全部落空。我见过有人写了半天脚本最后发现是“在子目录里启动了一个新终端然后直接 source 了相对路径”这个低级错误浪费了一下午。第三个坑代码同步不完整时菜单也可能静默为空。有些项目的产品仓库依赖多个仓库拼装比如设备树的AndroidProducts.mk引用了vendor/mycompany/common/common.mk但 common 仓库没拉下来解析时就会失败。这种情况下lunch不会给出清晰报错而是像什么都没发生过一样。遇到这种情况优先检查repo manifest里设备相关仓库是否齐全。第四个坑环境变量污染。如果你在.bashrc里设置了ANDROID_PRODUCT_OUT、ANDROID_BUILD_TOP之类的固定值并且指向旧代码目录那lunch的行为会变得非常奇怪。你看着当前目录是新的这套代码实际扫描的却是旧目录。建议不要在.bashrc里硬编码这些变量统一通过envsetup.sh动态设置。第五个坑不同安卓大版本之间envsetup.sh的细节实现差异很大。旧版本里菜单展示可能依赖一些已经被废弃的 make 函数新版本里则改成了更简单的文件枚举。所以网上搜到的排查思路可以参考但不要直接照搬一段老命令到新版源码上。我在不同版本上就踩过同一套命令不同结果的坑现在养成的习惯是先看当前源码里print_lunch_menu的实际实现再决定怎么改。回到最初那个“lunch 不显示构建列表”的问题你仔细想想绝大多数时候不是一个大工程而是一个很小的链路断点。可能是 shell 身份不对可能是产品树没被扫描到也可能是某个 mk 文件里少了一行变量定义。排查的关键是永远不要靠眼睛去“猜”而是用type、find、grep一步步把链路验证出来。我个人在实际操作中还有一个习惯每次都把envsetup.sh的加载封装成一个固定别名比如在自己终端里定义一个alias envssource build/envsetup.sh这样至少能保证每次加载都是同一套命令减少手动输入带来的低级失误。另外如果团队里有多个成员都碰到过同类问题我强烈建议把这些排查命令整理成一段脚本放在仓库的tools/目录下下次谁再遇到跑一次脚本就知道卡在哪一环比自己从头推一遍要快得多。
阅读完成 · 觉得有帮助?