codex cli 源码教程 | 第二篇:从源码构建并建立调试环境
上一篇建立了 Codex CLI 的整体架构:CLI 负责选择运行模式,TUI、Exec 和 SDK 是客户端,App Server 提供统一协议,Core 管理 Agent Session,模型与工具在一个 Turn 内循环执行。
真正开始阅读和修改源码前,还需要解决一个更实际的问题:如何稳定地构建、运行、测试和观察这个大型多语言仓库。
Codex 不是只有一个 cargo build 的普通 Rust 项目。仓库同时使用 Cargo、Just、Bazel、pnpm、Python 和 uv。它们并非重复建设,而是分别服务于本地开发、跨平台构建、发布打包、SDK 和仓库维护。
本篇将建立一套小步、可重复、可诊断的本地开发流程。
本篇目标
阅读完成后,你应该能够:
- 解释 Cargo、Just、Bazel、pnpm、Python 和 uv 的职责。
- 手动准备与仓库版本一致的开发工具。
- 从源码构建并运行 TUI、Exec 和 App Server。
- 只测试受影响的 crate,而不是每次运行完整测试矩阵。
- 区分格式检查、自动修复和测试命令。
- 打开 TUI、Exec 和 App Server 的诊断日志。
- 根据构建阶段和错误上下文快速定位环境问题。
1. 为什么这个仓库需要多套工具
Codex 的主要业务代码使用 Rust,但完整产品包含多种交付形态:
- macOS、Linux 和 Windows 原生二进制。
- npm 平台包和 JavaScript 启动器。
- TypeScript SDK。
- Python SDK 和内置原生二进制包。
- Bazel 远程构建与跨平台 CI。
- 配置 Schema、App Server Schema 和 Hooks Schema。
- Rust、Python、Bazel、Just 等多种文件格式。
因此,不能简单地把所有工具理解为“另一种构建 Codex 的方式”。
| 工具 | 主要职责 |
|---|---|
| Cargo | Rust 本地构建、运行、依赖解析和开发测试 |
| Just | 统一常用命令、工作目录和跨平台参数转发 |
| Bazel | 可复现构建、CI、远程执行、跨平台发布 |
| pnpm | npm 启动器、Responses Proxy npm 包和 TypeScript SDK |
| Python | 仓库维护、打包、格式化调度和测试辅助 |
| uv | Python SDK 与仓库脚本的锁定依赖环境 |
| DotSlash | 获取仓库固定版本的开发工具,例如 Buildifier |
| cargo-nextest | Rust 测试调度、重试、超时与并发控制 |
本地开发最常见的主路径是:
Just -> Cargo -> Rust Crate
完整格式化路径则更接近:
Just -> Python format.py
|-> rustfmt
|-> Just formatter
|-> Buildifier
|-> Ruff for Python SDK
`-> Ruff for scripts
发布和 CI 还会进一步进入 Bazel 与平台打包脚本。
2. 先统一两个工作目录
很多“命令找不到文件”并不是依赖问题,而是工作目录错误。
本系列统一使用以下约定:
REPO_ROOT = 仓库根目录
CARGO_WORKSPACE = REPO_ROOT/codex-rs
不同命令的推荐执行位置如下:
| 命令类型 | 推荐目录 |
|---|---|
just ... | REPO_ROOT |
bazel ... | REPO_ROOT |
pnpm ... | REPO_ROOT |
cargo ... | CARGO_WORKSPACE |
uv --project sdk/python ... | REPO_ROOT |
python scripts/... | REPO_ROOT |
根目录 justfile 声明了:
set working-directory := "codex-rs"
因此,即使从仓库根目录运行 just codex,配方内部的 Cargo 命令也会自动在 codex-rs 下执行。
少数配方带有 [no-cd],例如:
bazel-codexbazel-lock-updatetest-github-scriptswrite-hooks-schema
这些配方会保留仓库根目录,因为其脚本或 Bazel Target 按根目录解析路径。
3. 系统与硬件前提
docs/install.md 给出的基础要求是:
| 项目 | 要求 |
|---|---|
| macOS | 12 或更高版本 |
| Linux | Ubuntu 20.04+ 或 Debian 10+ |
| Windows | Windows 11,通过 WSL2 |
| Git | 2.23+,可选但推荐 |
| 内存 | 最低 4 GB,推荐 8 GB |
最低要求只代表能够运行。对于完整工作区编译,更实际的建议是:
- 预留足够磁盘空间给
codex-rs/target和 Bazel Cache。 - 不要在日常开发中使用
--all-features。 - 优先编译和测试单个 crate。
- 第一次 Cargo 或 Bazel 构建明显较慢属于正常现象。
- Rust 构建持有全局锁时,不要因为短时间无输出就强制结束进程。
4. 仓库固定的工具版本
4.1 Rust 1.95.0
codex-rs/rust-toolchain.toml 固定了:
[toolchain]
channel = "1.95.0"
components = ["clippy", "rustfmt", "rust-src"]
进入 codex-rs 后,rustup 会根据这个文件选择工具链。为了避免首次构建时隐式下载,建议先手动安装和检查:
rustup toolchain install 1.95.0 \
--component clippy \
--component rustfmt \
--component rust-src
rustup show active-toolchain
rustc --version
cargo --version
预期 rustc 主版本为 1.95.0。
4.2 Bazel 9.0.0
根目录 .bazelversion 固定 Bazel:
9.0.0
如果使用 Bazelisk,它会读取该文件选择对应版本。无论采用 Bazelisk 还是手动安装 Bazel,都应先确认:
bazel --version
版本漂移可能导致 Module Lock、Rules Rust 或平台配置行为不同。
4.3 Node.js 与 pnpm
根目录 package.json 要求:
- Node.js
>=22 - pnpm
>=10.33.0 - 仓库固定 package manager 为 pnpm
10.33.0
检查命令:
node --version
pnpm --version
不要因为 codex-cli/package.json 的最低 Node.js 版本更低,就降低整个 Monorepo 的开发版本。发布后的 npm 启动器与仓库开发环境是两个不同约束。
4.4 Python 与 uv
仓库内部脚本和 Python SDK 都要求 Python >=3.10:
检查命令:
python3 --version
uv --version
这里不建议让启动脚本自动创建虚拟环境或安装依赖。Python 环境由开发者手动准备;需要执行 SDK 或格式化任务时,再显式运行对应的 uv sync 或 uv run。
4.5 Just、DotSlash 与 cargo-nextest
仓库常用的 Cargo 工具包括:
cargo install --locked just
cargo install --locked dotslash
cargo install --locked cargo-nextest
安装后检查:
just --version
dotslash --version
cargo nextest --version
它们分别用于:
just:执行仓库配方。dotslash:运行固定版本的 Buildifier 等工具。cargo-nextest:执行 Rust 测试。
5. 最小环境检查
不要一开始就运行完整构建。先完成一组低成本检查:
rustc --version
cargo --version
just --version
git --version
python3 --version
如果当前任务涉及对应子系统,再增加:
bazel --version
node --version
pnpm --version
uv --version
cargo nextest --version
环境检查的原则是按任务加载能力:
- 只阅读和修改 Rust Core,不一定需要 pnpm。
- 只运行 TypeScript SDK,不一定需要 Bazel。
- 执行
just fmt时,需要 Rust、Git、DotSlash、uv 和 Python。 - 执行
just test时,需要 cargo-nextest。 - 执行 Bazel Release Build 时,才需要完整 Bazel 工具链和平台依赖。
6. 第一次构建:优先使用 Cargo
第一次从源码验证 Codex,建议进入 Cargo Workspace:
cd codex-rs
cargo build -p codex-cli --bin codex
这里显式指定:
- Package:
codex-cli - Binary:
codex
codex-rs/cli/Cargo.toml 中的定义是:
[[bin]]
name = "codex"
path = "src/main.rs"
构建完成后,二进制位于:
codex-rs/target/debug/codex
先做不访问模型的验证:
./target/debug/codex --version
./target/debug/codex --help
./target/debug/codex features list
这三条命令可以验证:
- Rust 二进制成功链接。
- Clap 命令树可用。
- 基础运行时初始化没有立即失败。
为什么不先使用 cargo build --workspace
codex-rs 包含大量 crate。工作区全量构建会:
- 编译当前任务不需要的目标。
- 增加首次反馈时间。
- 占用更多磁盘。
- 让环境问题更难定位到具体 crate。
对于入口验证,codex-cli 已经会拉起其依赖链,没有必要主动构建所有独立工具和测试目标。
7. 使用 Just 简化日常命令
从仓库根目录执行:
just help
可以查看所有配方。最常用配方如下:
| 配方 | 实际作用 | 是否修改源码 |
|---|---|---|
just codex | cargo run --bin codex | 否 |
just exec | cargo run --bin codex -- exec | 取决于 Agent 任务 |
just test | 通过 Nextest 运行测试 | 否 |
just clippy | 检查 Rust Lint | 否 |
just fix | Clippy 自动修复 | 是 |
just fmt-check | 检查多语言格式 | 否 |
just fmt | 格式化多语言文件 | 是 |
just bazel-codex | 使用 Bazel 构建并运行 Codex | 否 |
just app-server-test-client | 构建 CLI 并运行测试客户端 | 否 |
just log | 查询本地 SQLite 日志 | 否 |
7.1 运行 TUI
just codex
传入初始 Prompt:
just codex "解释当前项目的目录结构"
如果 Just 参数解析与 Shell 引号产生歧义,可以退回最直接的 Cargo 命令:
cd codex-rs
cargo run --bin codex -- "解释当前项目的目录结构"
7.2 运行 Exec
just exec "总结 README.md"
等价的 Cargo 形式:
cd codex-rs
cargo run --bin codex -- exec "总结 README.md"
机器消费事件时使用 JSON 输出:
cargo run --bin codex -- exec --json "总结 README.md"
注意:Exec 任务可能修改工作区。运行前应明确 Sandbox、Approval 和 Prompt 的行为边界。
7.3 启动 App Server
cd codex-rs
cargo run --bin codex -- app-server --listen stdio://
stdio 模式下,标准输出承载协议消息,不要向标准输出添加调试文本。日志应写入标准错误。
调试 App Server 更方便的方式是使用:
- codex-rs/app-server-test-client/README.md
just app-server-test-client
测试客户端可以启动 WebSocket App Server、发送请求并观察原始事件。
8. Cargo 与 Bazel 应该怎么选
8.1 Cargo 适合本地快速开发
Cargo 的优势是:
- 与 Rust IDE 和 rust-analyzer 集成自然。
- 单 crate 构建和测试反馈更快。
- 错误信息直接。
- 适合频繁小步修改。
常用命令:
cargo check -p codex-core
cargo build -p codex-cli --bin codex
cargo clippy -p codex-core --tests
8.2 Bazel 适合可复现和跨平台构建
Bazel 配置覆盖:
- Linux、macOS 和 Windows 平台。
- BuildBuddy Cache 与远程执行。
- CI 元数据。
- Bazel Clippy。
- Release Binaries。
- Windows Cross Compile。
从仓库根目录运行 Codex:
just bazel-codex -- --help
构建发布目标:
just build-for-release
该配方最终执行:
bazel build //codex-rs/cli:release_binaries
8.3 双构建体系带来的约束
一项 Rust 修改通过 Cargo,不代表一定通过 Bazel。
典型差异包括:
- 新增依赖后
MODULE.bazel.lock尚未更新。 include_str!或include_bytes!读取的文件未声明为 Bazel Data。- Cargo Feature 与 Bazel Target 配置不同。
- 平台条件编译只在部分系统暴露问题。
修改 Rust 依赖后需要:
just bazel-lock-update
提交时应同时包含更新后的 MODULE.bazel.lock。
新增编译期文件读取时,还要更新对应 crate 的 BUILD.bazel:
compile_databuild_script_data- 测试
data
9. pnpm 工作区并不负责 Rust CLI
pnpm-workspace.yaml 只包含:
codex-clicodex-rs/responses-api-proxy/npmsdk/typescript
它负责 npm 启动器、Responses API Proxy 包和 TypeScript SDK,不负责构建 Rust Workspace。
准备 Node.js 依赖时,从根目录显式执行:
pnpm install --frozen-lockfile
仓库的 pnpm 配置还启用了较严格的供应链策略:
- 依赖最短发布时间。
- 阻止 Exotic Subdependency。
- 严格控制依赖 Build Script。
- Trust Policy 防止降级。
因此,依赖安装失败不一定是网络问题,也可能是某个包不满足仓库信任策略。
10. Python 与格式化调度
scripts/format.py 是仓库级格式化入口。它将格式器分成五组:
- Just。
- Rust。
- Bazel/Starlark。
- Python SDK。
- Python Scripts。
这些组使用 ThreadPoolExecutor 并行执行,互不依赖;每组内部的多个命令仍保持顺序。
这有两个直接影响。
10.1 just fmt-check 的依赖比 Cargo Build 多
格式检查需要:
justcargo fmt- Git
dotslash- Buildifier
- Python 3.10+
uv- Ruff 锁定依赖
如果只安装了 Rust,cargo build 可能成功,但 just fmt-check 仍会失败。
10.2 源码归档包可能缺少 Git 元数据
Buildifier 文件列表通过以下命令获取:
git ls-files --cached --others --exclude-standard
如果工作目录只是没有 .git 的源码快照,仓库级 just fmt 和 just fmt-check 可能在 Bazel/Starlark 格式组失败。
此时应区分:
- 代码格式确实错误。
- 格式化脚本缺少 Git 元数据。
不要把环境错误误判为源码错误。
11. 测试:先缩小到受影响 crate
根目录 AGENTS.md 明确要求不要直接使用 cargo test,而应通过:
just test
just test 实际运行:
cargo nextest run --no-fail-fast
并额外设置:
RUST_MIN_STACK=8388608NEXTEST_PROFILE=local
11.1 运行单个 crate
修改 TUI:
just test -p codex-tui
修改 App Server Protocol:
just test -p codex-app-server-protocol
修改 Core:
just test -p codex-core
11.2 运行单个测试
Nextest 的过滤参数会继续传递:
just test -p codex-core test_name_fragment
先运行最接近变更的测试,可以更快区分:
- 编译错误。
- 单元行为错误。
- 跨模块协议错误。
- 全局并发或环境问题。
11.3 什么时候运行完整测试
以下修改通常具有较大影响范围:
codex-corecodex-protocolcodex-common或共享工具层- App Server 公共协议
- 配置加载和会话恢复
目标 crate 测试通过后,再评估:
just test
完整测试不应成为每个小改动的第一步。
11.4 为什么使用 Nextest
codex-rs/.config/nextest.toml 配置了:
- 默认失败重试一次。
- 慢测试超时策略。
- App Server 协议生成测试串行执行。
- 本地 App Server 集成测试最多并发四个进程。
- Windows 重进程测试限制并发。
这些策略减少测试争抢和偶发失败。直接使用 cargo test 会绕过这套仓库约定。
11.5 不要默认使用 --all-features
日常测试无需 --all-features。它会:
- 扩大编译矩阵。
- 增加构建时间。
- 明显增加
target占用。 - 启用当前修改无关的实验能力。
只有明确验证 Feature 组合时才应使用。
12. 格式、Lint 与自动修复
这三类命令的行为不同。
12.1 只读格式检查
just fmt-check
它检查格式,不修改文件,适合修改前确认基线或提交前验证。
12.2 Clippy 检查
just clippy -p codex-core
该命令执行 cargo clippy --tests,不会自动修改源码。
12.3 Clippy 自动修复
just fix -p codex-core
该命令带有:
cargo clippy --fix --tests --allow-dirty
它会修改文件。执行前必须确认工作区中的已有修改,执行后必须审查 Diff,避免把无关机械变化混入当前任务。
12.4 仓库级格式化
just fmt
它同样会修改文件,并且影响范围可能覆盖:
- Rust。
- Justfile。
- Bazel/Starlark。
- Python SDK。
- Python Scripts。
项目要求 Rust 代码改动完成后执行 just fmt。如果只希望检查,使用 just fmt-check。
13. 推荐的小步开发流程
一个窄范围 Rust 修改可以按以下顺序执行。
第一步:识别所属 crate
从最近的 Cargo.toml 确认 Package Name:
rg -n '^name = ' codex-rs/<crate>/Cargo.toml
目录名和 Package Name 不一定完全相同。例如:
- 目录:
codex-rs/core - Package:
codex-core
第二步:建立测试基线
just test -p <package>
如果测试很慢,先定位最相关的单个测试。
第三步:只修改一个行为
保持改动集中,避免同时重构无关模块。新增功能优先遵循已有模块边界,不要默认加入 codex-core。
第四步:再次运行目标测试
just test -p <package>
第五步:运行 Lint 修复和格式化
just fix -p <package>
just fmt
第六步:审查变更范围
重点检查:
- Clippy 是否改写了非目标逻辑。
- 格式化是否触碰无关文件。
- Schema 或 Bazel Lock 是否需要同步。
- 用户可见输出是否需要更新 Snapshot。
这种顺序的目标不是让命令更多,而是让每次失败都能定位到一个较小阶段。
14. 打开可观测性
构建成功只说明代码可以运行。调试 Agent 还需要观察事件、状态和错误。
14.1 TUI 日志
TUI 默认将诊断信息写入有界本地存储。需要明文日志时,显式配置:
codex -c log_dir=./.codex-log
tail -F ./.codex-log/codex-tui.log
从源码运行:
cd codex-rs
cargo run --bin codex -- -c log_dir=./.codex-log
日志位于当前工作目录下的 .codex-log/codex-tui.log。
14.2 使用 RUST_LOG
Rust Tracing 支持按 Target 设置级别:
RUST_LOG=codex_core=debug,codex_app_server=debug \
cargo run --bin codex
不建议一开始设置全局 trace。大型异步系统会产生大量日志,反而掩盖关键调用链。
更有效的方法是:
- 先用
info或目标模块debug。 - 根据 Thread ID、Turn ID 或 Request ID 过滤。
- 只对怀疑模块提升到
trace。
14.3 Exec 日志
Exec 默认日志过滤级别为 error,普通结果写入标准输出,诊断写入标准错误。
JSON 模式下尤其需要保持:
stdout = JSONL 协议事件
stderr = 日志与诊断
不要通过临时 println! 调试库代码。Core 和 TUI Library 已通过 Clippy 配置禁止意外写入 stdout/stderr。
14.4 App Server JSON 日志
App Server 支持:
LOG_FORMAT=json RUST_LOG=codex_app_server=debug \
cargo run --bin codex -- app-server --listen stdio://
JSON 日志写入 stderr,stdio JSON-RPC 写入 stdout。这样日志采集器与协议客户端可以分别处理两条流。
14.5 查询 SQLite 日志
根目录提供:
just log
它启动 codex-state 的 logs_client,用于查看本地状态数据库中的结构化日志。
与分散搜索进程输出相比,SQLite 日志更适合按时间、Thread 和级别集中排查。
15. 使用 Mock 与测试客户端隔离问题
真实调试会同时依赖:
- Codex 本地代码。
- 登录状态。
- 模型服务。
- 网络。
- MCP 或其他扩展。
如果所有依赖同时存在,很难判断错误属于哪一层。
15.1 App Server Test Client
codex-rs/app-server-test-client/README.md 提供了调试客户端。
典型流程:
cd codex-rs
cargo build -p codex-cli --bin codex
cargo run -p codex-app-server-test-client -- \
--codex-bin ./target/debug/codex \
serve --listen ws://127.0.0.1:4222 --kill
另一个终端发送请求:
cargo run -p codex-app-server-test-client -- model-list
这可以独立验证:
- App Server 能否启动。
- WebSocket 连接是否正常。
- 初始化握手是否成功。
- 请求与通知能否往返。
15.2 Mock Responses WebSocket Server
scripts/mock_responses_websocket_server.py 实现了一个最小 Responses WebSocket Server:
- 第一次模型请求返回 Shell Tool Call。
- Codex 执行工具并发送第二次请求。
- Mock Server 返回最终 Agent Message。
它适合观察“模型、工具、模型”的完整循环,而不依赖真实模型响应。
该脚本依赖 websockets,应在已准备好的 Python 环境中显式安装或通过受控项目依赖运行,不应让 Codex 启动脚本自动安装。
16. 常见问题排查
16.1 cargo 找不到 Workspace Package
现象
package ID specification ... did not match any packages
检查
- 当前目录是否为
codex-rs。 - 使用的是目录名还是 Cargo Package Name。
- Package 是否真的在 Workspace Members 中。
16.2 Rust 版本或语言特性错误
现象
- 未知 Edition。
- 标准库 API 不存在。
- Rustfmt 输出与 CI 不一致。
检查
rustup show active-toolchain
rustc --version
确认当前目录触发了 rust-toolchain.toml,版本为 1.95.0。
16.3 just test 找不到 Nextest
现象
no such command: nextest
处理
手动安装:
cargo install --locked cargo-nextest
不要临时改写 Justfile 回退到 cargo test,否则会绕过超时、并发和重试策略。
16.4 just fmt-check 在 Buildifier 阶段失败
依次检查:
- 当前目录是否包含
.git。 git ls-files是否正常。dotslash是否安装。- 仓库
tools/buildifier是否存在。 - Bazel 文件是否真的存在格式差异。
16.5 Python 格式组失败
依次检查:
- Python 是否至少为 3.10。
uv是否可用。- Lockfile 是否与项目一致。
- 命令是否从仓库根目录运行。
- 网络或镜像是否阻止锁定依赖。
16.6 构建长时间无输出
Rust 首次构建、大型依赖和构建锁都可能导致等待。
建议先确认:
- 是否有另一个 Cargo 进程持有锁。
- CPU、内存和磁盘是否仍有活动。
- 是否正在编译 V8、TLS 或平台依赖。
- 是否错误启用了完整 Feature Matrix。
不要直接通过 PID 强制结束 Rust 构建。先等待或确认持锁进程的真实状态。
16.7 App Server 客户端解析失败
stdio 模式最常见的问题是 stdout 被日志污染。
检查:
- 协议消息是否只写 stdout。
RUST_LOG输出是否写 stderr。- 临时调试代码是否使用了
println!。 - 客户端是否完成
initialize与initialized。
16.8 TUI 无法启动
检查:
- stdin 和 stderr 是否为 TTY。
TERM是否被设置为dumb。- 终端是否支持 Codex 所需能力。
- 是否应改用
codex exec。
17. 动手练习
练习一:生成环境报告
记录以下版本:
rustc --version
cargo --version
just --version
cargo nextest --version
bazel --version
node --version
pnpm --version
python3 --version
uv --version
将缺失工具按当前任务分为“必须安装”和“暂时不需要”,不要无差别安装全部工具。
练习二:完成最小 CLI 构建
执行:
cd codex-rs
cargo build -p codex-cli --bin codex
./target/debug/codex --version
./target/debug/codex --help
记录:
- 首次构建时间。
target增量。- 最慢的依赖阶段。
- 二次构建时间。
练习三:对比 TUI 与 Exec
分别运行:
just codex "只回复 READY"
just exec "只回复 READY"
观察:
- 标准输出差异。
- 交互流程差异。
- 日志位置差异。
- 两者是否都启动进程内 App Server。
练习四:运行最小测试集
选择一个 crate,例如:
just test -p codex-terminal-detection
再选择一个大型 crate:
just test -p codex-app-server-protocol
比较编译、测试数量、并发和缓存效果。
练习五:建立日志排查链路
运行 TUI 明文日志:
cd codex-rs
cargo run --bin codex -- -c log_dir=./.codex-log
另一个终端观察:
tail -F codex-rs/.codex-log/codex-tui.log
完成一次简单 Turn,定位:
- Thread 创建日志。
- Turn 开始和结束。
- 模型请求。
- 工具调用。
- 错误或警告。
18. 本篇小结
Codex 的开发环境可以分成三条主线:
- Cargo 与 Just 服务于日常 Rust 开发。
- Bazel 服务于可复现、远程和跨平台构建。
- pnpm、Python 与 uv 服务于 SDK、打包和仓库维护。
最重要的实践原则是:
- 明确工作目录。
- 使用仓库固定工具版本。
- 从单个 Package 和最小命令开始。
- 使用
just test保留 Nextest 策略。 - 区分只读检查与会修改文件的命令。
- 先启用定向日志,再扩大日志级别。
- 使用 Mock 和测试客户端隔离外部依赖。
- 不在启动脚本中隐式安装依赖。
- 遇到慢构建先观察状态,不要贸然结束进程。
至此,我们已经具备稳定阅读和验证源码的环境。下一篇将进入 codex-rs/cli/src/main.rs,分析一个 Rust 二进制如何通过 Clap、arg0 分发和子命令路由承载 TUI、Exec、App Server、MCP Server 等全部运行模式。
更多推荐


所有评论(0)