Codex 中 zsh 环境差异相关的终端截图

20260327 · 2342 字

在 Codex 里重新理解 zsh

同一软件, 一条命令, 两种结果. 让我开始区分 zsh 的不同启动方式.

为什么我会重新理解 zsh

最近在 Codex 里排一个 Android native 构建问题.

现象有点别扭: 同一台机器, 同一份仓库, 同一条 ./gradlew clean :app:assembleDebug, 在 Codex 桌面里的交互终端手动执行能成功, assistant 通过工具去执行却稳定失败.

第一眼看上去像是普通的构建失败. 怀疑 Gradle, 怀疑 NDK, 怀疑 JDK, 怀疑缓存.

但查看 daemon 进程的运行参数发现都不是.

命令没变, 执行入口变, 结果变.

这件事让我想起曾经一笔带过的环境配置问题: zsh -czsh -lczsh -ic 这些启动方式的差异是什么?

以前对这些参数的理解比较松散, 觉得无非就是 shell 的几种启动形式.

但这次排查到后面发现, 差异不是“语法细节”, 而是会直接决定你拿到的是不是同一套环境.

一开始把它当成构建问题

这次失败最终落在 native 构建链路里, 报错也很像工具链坏了:

  • fatal error: 'stdio.h' file not found
  • fatal error: 'stdlib.h' file not found

这类错误很容易让人想歪.

因为它出现在构建阶段, 自然把注意力放在:

  1. Gradle 有没有问题.
  2. NDK 版本是不是不对.
  3. JDK 是不是和项目不兼容.
  4. CMake/Ninja 缓存是不是脏了.

与此同时发现一个更值得警觉的信号:

用户手动终端能成功, 工具子进程却失败

那优先级其实应该先切换一下.

这时候比 "项目是不是坏了" 更值得问的是:

这两个入口, 真的是同一种运行环境吗?

这一步很关键.

很多时候我们会下意识觉得, 都在同一个 App 里执行命令, 差别不该太大.

但这次让我真正建立起来的直觉是:

同一个产品里的不同执行入口, 完全可能对应不同的 shell 启动环境.

zsh -czsh -lczsh -ic 差的不是语法, 是环境

这次我没有一上来继续包在 Gradle 外面猜, 而是先拿 shell 启动方式做切片.

结果非常直接:

  • zsh -c 下构建失败.
  • zsh -lc 下仍然失败.
  • zsh -ic 下构建成功.

这三个结果一出来, 问题其实已经被压缩得很小了.

它至少说明了三件事:

  1. 问题不在命令文本本身.
  2. 问题不在仓库本身.
  3. 问题在 shell 初始化链条.

如果只从排障角度理解, 这几个参数可以先粗暴地记成:

  • zsh -c: -c 是 command, 表示执行后面给的一段命令后退出. 这通常是非交互、非 login 的子进程执行方式.
  • zsh -lc: -l 是 login, -c 是 command. 也就是“以 login shell 的方式启动, 但只执行一段命令”.
  • zsh -ic: -i 是 interactive, -c 是 command. 也就是“强制走交互 shell 语义, 但只执行一段命令”.

这三个参数真正有价值的地方, 是它们背后的启动链路不同.

截图

翻了 zsh 手册, 启动顺序可以先记成:

  • 所有 zsh 实例都会先读 /etc/zshenv~/.zshenv
  • 如果是 login shell, 会继续读 /etc/zprofile~/.zprofile
  • 如果是 interactive shell, 会继续读 /etc/zshrc~/.zshrc
  • 如果是 login shell, 最后还会继续读 /etc/zlogin~/.zlogin

所以把它映射回这次遇到的三种启动方式, 大概就是:

  • zsh -c
    • 全称可以理解为: execute command
    • 常见链路: /etc/zshenv -> ~/.zshenv -> 执行命令 -> 退出
  • zsh -lc
    • 全称可以理解为: login shell + execute command
    • 常见链路: /etc/zshenv -> ~/.zshenv -> /etc/zprofile -> ~/.zprofile -> /etc/zlogin -> ~/.zlogin -> 执行命令 -> 退出
  • zsh -ic
    • 全称可以理解为: interactive shell + execute command
    • 常见链路: /etc/zshenv -> ~/.zshenv -> /etc/zshrc -> ~/.zshrc -> 执行命令 -> 退出

这也解释了为什么 zsh -lczsh -ic 虽然都不像纯粹的 zsh -c, 但它们拿到的环境仍然可能不同:

  • 如果关键变量放在 ~/.zprofile, -lc 可能拿得到, -ic 不一定.
  • 如果关键变量放在 ~/.zshrc, -ic 可能拿得到, -lc 不一定.
  • 如果关键变量下沉到了 ~/.zshenv, 那三者理论上都能拿到.

这里最容易犯的错是:

把 "也是 zsh" 误解成 "环境应该差不多" .

其实不是.

同样叫 zsh, 只要启动参数不同, 加载的初始化文件链就可能不同, 最后拿到的环境变量也可能不同.

我是怎么把问题收敛到 shell 环境上的

排障不是 "经验丰富地乱猜", 而是遇到难题先知道自己不能掌控全局, 先退一步, 控制变量, 二分法排查.

1. 承认这不是一条命令的问题, 而是两个入口的问题

如果同一条命令在两个入口结果相反, 那最应该先做的是: 冻结命令文本, 去比较入口差异.

做实验时先控制变量.

不能一边改项目, 一边猜缓存, 一边再换命令, 最后把自己绕晕.

2. 判断是不是仓库代码本身的问题

如果用户在同一台机器、同一份仓库里手动执行可以成功, 那仓库代码本身大概率不是主因.

这里不是说代码一定没问题, 而是说:

它不值得成为第一怀疑对象.

3. 用 zsh -czsh -lczsh -ic 切环境

前段时间听到别人说, 问题一旦具体起来, 就不再可怕, 相当于 boss 亮血条了.

Codex 里的工具执行和手动终端不一样 需要切换成更加具体的语言: 差异在 shell 是否走到了交互初始化链条上

  • -c 不行
  • -lc 也不行
  • -ic 可以

所以, 问题不是 "有没有 zsh", 而是 "zsh 是按什么语义启动的".

4. 验证最小失败点

环境问题不能一直包在大系统外面猜.

Gradle 套 CMake, CMake 套 Ninja, Ninja 再去调编译器, 中间层太多了, 就会把真正的信号埋掉.

不仅浪费时间, 还浪费我的 token.

继续往下追后, 看到失败链路里实际调用的是 NDK 的 host clang, 然后再去直接看它的头文件搜索路径, 问题就更清楚了.

失败环境里看不到 Darwin SDK 的系统头搜索路径.

问 AI, 说是补上 SDKROOT 试试.

"构建系统可能有很多复杂原因" 的问题收敛为 "当前 shell 没把关键环境带进来"

问题本质

回头看, 这次最值得记住的东西并不是某个 Android native 项目该怎么修, 而是:

在 Codex 里, 交互终端和工具子进程不是同一种 shell 环境.

只要这个前提成立, 后面很多现象就都不奇怪了:

  • 同样都在 Codex 里执行, 结果却像两台机器.
  • 同样写的是 zsh, 加载到的变量却不一样.
  • 手工执行能成功, 自动化执行却失败.

说到底, 命令从来都不是脱离上下文运行的.

一条命令真正的执行条件至少包括:

  • 当前目录
  • 当前进程环境变量
  • shell 的启动语义
  • 实际解析到的工具链路径

只盯着命令文本, 很容易卡住: "我运行的明明是同一条命令."

但从系统视角看, 如果这几个前提不同, 那它根本就不是同一次执行.

以后怎么排这类环境问题

1. 看到 "手工成功, 自动化失败", 不急改代码

先怀疑进程环境, 不是仓库内容.

尤其是在 IDE、Agent、终端工具、CI 这些多入口系统里, 默认不要假设环境一致.

2. 先比较 shell 语义, 不只比较命令文本

zsh -czsh -lczsh -ic 的差异, 本质不是参数背诵题, 而是启动链路差异.

一旦你知道:

  • -c 更接近“跑完即走”的 command mode
  • -lc 是 login shell 语义
  • -ic 是 interactive shell 语义

很多“同命令不同结果”的问题就会突然具体起来.

3. 验证最小失败点, 不要一直包在大系统外面猜

如果外面套着 Gradle、脚本、任务编排器, 就尽量往里打.

打到真正失败的那一层工具, 看它到底缺什么.

很多环境问题一旦能落到最小命令, 判断速度会快很多.

4. 看到标准头文件缺失, 优先怀疑 sysroot/SDK

stdio.hstdlib.h 这种系统标准头文件找不到时, 第一反应通常不该是 "业务代码写错了"

更应该先看:

  • 当前编译器的搜索路径
  • 当前进程有没有正确的 SDK 线索
  • 当前 shell 初始化链有没有把关键变量带进来

最后

以前我会把 zsh -czsh -lczsh -ic 这种区别当成 shell 使用细节.

但这次在 Codex 里排完问题后, 我对它们的理解变了.

它们不只是 "几种启动参数" , 而是几种不同的环境入口.

而很多构建问题、自动化问题、Agent 跑命令的问题, 本质上都不是命令写错了, 而是你没有先分清:

你到底是在同一个环境里执行, 还是只是在看起来相似的两个入口里执行.

评论