上一篇建立了 Codex CLI 的整体架构:CLI 负责选择运行模式,TUI、Exec 和 SDK 是客户端,App Server 提供统一协议,Core 管理 Agent Session,模型与工具在一个 Turn 内循环执行。

真正开始阅读和修改源码前,还需要解决一个更实际的问题:如何稳定地构建、运行、测试和观察这个大型多语言仓库。

Codex 不是只有一个 cargo build 的普通 Rust 项目。仓库同时使用 Cargo、Just、Bazel、pnpm、Python 和 uv。它们并非重复建设,而是分别服务于本地开发、跨平台构建、发布打包、SDK 和仓库维护。

本篇将建立一套小步、可重复、可诊断的本地开发流程。

本篇目标

阅读完成后,你应该能够:

  1. 解释 Cargo、Just、Bazel、pnpm、Python 和 uv 的职责。
  2. 手动准备与仓库版本一致的开发工具。
  3. 从源码构建并运行 TUI、Exec 和 App Server。
  4. 只测试受影响的 crate,而不是每次运行完整测试矩阵。
  5. 区分格式检查、自动修复和测试命令。
  6. 打开 TUI、Exec 和 App Server 的诊断日志。
  7. 根据构建阶段和错误上下文快速定位环境问题。

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 的方式”。

工具主要职责
CargoRust 本地构建、运行、依赖解析和开发测试
Just统一常用命令、工作目录和跨平台参数转发
Bazel可复现构建、CI、远程执行、跨平台发布
pnpmnpm 启动器、Responses Proxy npm 包和 TypeScript SDK
Python仓库维护、打包、格式化调度和测试辅助
uvPython SDK 与仓库脚本的锁定依赖环境
DotSlash获取仓库固定版本的开发工具,例如 Buildifier
cargo-nextestRust 测试调度、重试、超时与并发控制

本地开发最常见的主路径是:

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-codex
  • bazel-lock-update
  • test-github-scripts
  • write-hooks-schema

这些配方会保留仓库根目录,因为其脚本或 Bazel Target 按根目录解析路径。

3. 系统与硬件前提

docs/install.md 给出的基础要求是:

项目要求
macOS12 或更高版本
LinuxUbuntu 20.04+ 或 Debian 10+
WindowsWindows 11,通过 WSL2
Git2.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 codexcargo run --bin codex否
just execcargo run --bin codex -- exec取决于 Agent 任务
just test通过 Nextest 运行测试否
just clippy检查 Rust Lint否
just fixClippy 自动修复是
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 更方便的方式是使用:

测试客户端可以启动 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_data
  • build_script_data
  • 测试 data

9. pnpm 工作区并不负责 Rust CLI

pnpm-workspace.yaml 只包含:

  • codex-cli
  • codex-rs/responses-api-proxy/npm
  • sdk/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 是仓库级格式化入口。它将格式器分成五组:

  1. Just。
  2. Rust。
  3. Bazel/Starlark。
  4. Python SDK。
  5. Python Scripts。

这些组使用 ThreadPoolExecutor 并行执行,互不依赖;每组内部的多个命令仍保持顺序。

这有两个直接影响。

10.1 just fmt-check 的依赖比 Cargo Build 多

格式检查需要:

  • just
  • cargo 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=8388608
  • NEXTEST_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-core
  • codex-protocol
  • codex-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。大型异步系统会产生大量日志,反而掩盖关键调用链。

更有效的方法是:

  1. 先用 info 或目标模块 debug。
  2. 根据 Thread ID、Turn ID 或 Request ID 过滤。
  3. 只对怀疑模块提升到 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:

  1. 第一次模型请求返回 Shell Tool Call。
  2. Codex 执行工具并发送第二次请求。
  3. 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 阶段失败

依次检查:

  1. 当前目录是否包含 .git。
  2. git ls-files 是否正常。
  3. dotslash 是否安装。
  4. 仓库 tools/buildifier 是否存在。
  5. 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 的开发环境可以分成三条主线:

  1. Cargo 与 Just 服务于日常 Rust 开发。
  2. Bazel 服务于可复现、远程和跨平台构建。
  3. pnpm、Python 与 uv 服务于 SDK、打包和仓库维护。

最重要的实践原则是:

  • 明确工作目录。
  • 使用仓库固定工具版本。
  • 从单个 Package 和最小命令开始。
  • 使用 just test 保留 Nextest 策略。
  • 区分只读检查与会修改文件的命令。
  • 先启用定向日志,再扩大日志级别。
  • 使用 Mock 和测试客户端隔离外部依赖。
  • 不在启动脚本中隐式安装依赖。
  • 遇到慢构建先观察状态,不要贸然结束进程。

至此,我们已经具备稳定阅读和验证源码的环境。下一篇将进入 codex-rs/cli/src/main.rs,分析一个 Rust 二进制如何通过 Clap、arg0 分发和子命令路由承载 TUI、Exec、App Server、MCP Server 等全部运行模式。

更多推荐