# 从源码构建与运行 · Cargo、Bazel 与 npm 包装

> Codex 同时维护两套构建：Cargo 是 crate 与依赖的事实来源，日常开发与正式发布都靠它；Bazel 从 Cargo.lock 导入依赖，负责封闭构建、跨平台产物与 CI，官方文档仍称其为实验性。发出去的只有一个 codex 二进制，arg0 按程序名和第一个参数把它分派成 Linux 沙箱助手、apply_patch 等多个入口；npm 包再用 optionalDependencies 别名只装上本平台的那一份。

- 作者：David（道雾轩）
- 专栏：Codex 源码解读（https://daiw.net/manual/codex-source.md）
- 最后更新：2026-09-29
- 原文：https://daiw.net/manual/codex-source/build-and-run
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

# 从源码构建与运行 · Cargo、Bazel 与 npm 包装

[上一篇](https://daiw.net/manual/codex-source/what-is-codex)说过，Codex 的本体是 `codex-rs/` 下的 Rust workspace。这一篇回答三个工程问题：怎么把它编出来跑起来；同一份代码为什么同时有 Cargo 和 Bazel 两套构建；交到用户手里的那一个 `codex` 二进制，又是怎么被 npm 挑中、怎么一身兼几职的。

## 怎么用：从源码跑起来

装现成的发行版见手册[安装、登录与升级](https://daiw.net/manual/codex/installation)。从源码构建，`docs/install.md` 给的步骤就几条：`git clone` 之后进入 `codex/codex-rs`，装好 Rust 工具链，再装三个辅助工具 `just`、`dotslash`、`cargo-nextest`，然后 `cargo build`，最后 `cargo run --bin codex -- "explain this codebase to me"` 带着一句提示词进入 TUI。

工具链不用自己挑：`codex-rs/rust-toolchain.toml` 钉死了 `channel = "1.95.0"`，并要求 `clippy`、`rustfmt`、`rust-src` 三个组件。workspace 统一用 Rust 2024 edition，版本号集中写在 `codex-rs/Cargo.toml` 的 `[workspace.package]` 里（`version = "0.158.0"`），各 crate 用 `version.workspace = true` 继承。

<Callout type="warn">
  `docs/install.md` 的系统要求表仍写着 Windows 11 需经 WSL2 运行，但代码早已原生支持 Windows：`README.md` 给出了 PowerShell 安装脚本，npm 发布 `win32-x64` 与 `win32-arm64` 两个平台包，还有一整套 Windows 沙箱。以代码为准。
</Callout>

## Cargo：事实来源

`codex-rs/Cargo.toml` 里的 profile 各有分工：`dev` 只保留行号级的调试信息（`debug = "line-tables-only"`），注释说是为了普通构建也能留住源码位置，需要看局部变量时再用环境变量临时调成 `full`；`release` 开 thin LTO，`codegen-units = 4` 在并行编译与体积之间取折中，符号先保留、打包归档后再剥离；另有本地打包默认用的 `dev-small`，以及 `profiling`、`ci-test`。正式发布走的也是 Cargo：`.github/workflows/rust-release.yml` 在推送 `rust-v*.*.*` 形式的 tag 时触发，逐个目标平台执行 `cargo build --release`。

两条 workspace 级别的纪律会影响你读代码：

- **不准定义 crate feature。** CI 脚本 `.github/scripts/verify_cargo_workspace_manifests.py` 会拒绝带 `[features]` 的成员，理由是 Bazel 构建不认 feature，藏在 feature 后面的问题会被漏掉，还会多出构建组合；目前只有登记过例外的 `v8-poc` 带着一个 `sandbox` feature。所以代码里的开关几乎都是运行时的，集中在 `codex-features` crate，通过配置里的 `features.*` 打开。
- **每个成员都要 `[lints] workspace = true`。** `[workspace.lints.clippy]` 把 36 条 lint 设成 `deny`，包括 `unwrap_used`、`expect_used`、`uninlined_format_args`，不声明继承就会漏检。

平台相关的小设置也在这一层：Linux musl 目标上 `codex` 用 jemalloc 做全局分配器（`codex-rs/cli/src/main.rs:45`）；`codex-rs/.cargo/config.toml` 给 Windows 目标设了 8 MiB 主线程栈，MSVC 目标还静态链接 C 运行时。

## justfile：日常命令入口

日常操作都包在仓库根的 `justfile` 里，第一行就把工作目录定到 `codex-rs`：

```just
set working-directory := "codex-rs"
# ...
alias c := codex
codex *args:
    cargo run --bin codex -- {args}
# ...
test *args:
    RUST_MIN_STACK={{ rust_min_stack }} NEXTEST_PROFILE=local cargo nextest run --no-fail-fast "$@"
```

（`justfile:1`）

`{args}` 不是 just 的语法，而是 `scripts/just-shell.py` 约定的占位符：justfile 让 Python 执行这个脚本充当 shell，它在 Unix 上把占位符换成 `"$@"`，在 Windows 上换成 PowerShell 的等价写法，一份 recipe 两个平台都能用。上面的 `test` 是 Unix 版，用 nextest 跑测试并把 `RUST_MIN_STACK` 设为 8 MiB。其余常用的还有：`just fmt` 与 `just fix -p <crate>`（格式化与 clippy 自动修复）；`just write-config-schema`、`just write-app-server-schema`（改了配置类型或 app-server 协议后重新生成 schema，后者连带 Python SDK 的类型）；`just bazel-codex`、`just bazel-test`；`just log`（从 state SQLite 数据库里跟踪日志）。

## Bazel：封闭构建与跨平台产物

`codex-rs/docs/bazel.md` 开头交代了两套构建的关系：Cargo 仍是 crate 与 feature 的事实来源，Bazel 提供封闭构建、工具链与跨平台产物，并注明截至 2026-06-01 仍是实验性的。落到配置上，Bazel 模块直接读 Cargo 的清单：

```python
toolchains = use_extension("@rules_rs//rs/toolchains:module_extension.bzl", "toolchains")
toolchains.toolchain(
    edition = "2024",
    version = "1.95.0",
)
# ...
crate = use_extension("@rules_rs//rs:extensions.bzl", "crate")
crate.from_cargo(
    cargo_lock = "//codex-rs:Cargo.lock",
    cargo_toml = "//codex-rs:Cargo.toml",
```

（`MODULE.bazel:290`）

第三方依赖由 `rules_rs` 的 `crate.from_cargo` 从 `Cargo.lock` 导入，工具链版本与 `rust-toolchain.toml` 一致，`.bazelversion` 要求 Bazel 9.0.0；另注册的一套 `nightly/2025-09-18` 工具链，注释说明是给需要 `rustc_private` 的自研 lint 工具 `argument-comment-lint` 准备的。153 个成员的目录下都有一个 `BUILD.bazel`，都调用根目录 `defs.bzl` 里的宏 `codex_rust_crate`，按 Cargo 的约定生成库、二进制与测试目标。`just build-for-release` 构建的 `//codex-rs/cli:release_binaries`，平台清单与 npm 的六个平台一一对应。

两套构建并存的代价由开发者承担，`AGENTS.md` 为此立了两条规矩：改了 Rust 依赖必须顺手跑 `just bazel-lock-update` 并提交 `MODULE.bazel.lock`，CI 会检查漂移；用 `include_str!`、`sqlx::migrate!` 之类在编译期读文件时，要在该 crate 的 `BUILD.bazel` 里登记 `compile_data`，否则 Cargo 能过、Bazel 会挂。远程缓存与远程执行（BuildBuddy）要 API key，默认不启用。

## 一个二进制，多个入口：arg0

发布包里的主程序只有一个 `codex`，却要扮演好几个角色：Linux 上以沙箱助手的身份重新执行自己，给模型提供一个叫 `apply_patch` 的命令，为 exec-server 当文件系统助手……`codex-arg0` crate 里 `arg0_dispatch_or_else` 的文档注释把这叫作“arg0 trick”：为了部署简单只发一个可执行文件，又想把部分功能暴露成独立的命令行程序，于是看程序是以什么名字被调用的。`codex`、`codex-tui`、`codex-exec`、`codex-app-server` 这些二进制的 `main` 都包在它里面，第一步先看两样东西：

```rust
    if exe_name == CODEX_LINUX_SANDBOX_ARG0 {
        // Safety: [`run_main`] never returns.
        codex_linux_sandbox::run_main();
    } else if exe_name == APPLY_PATCH_ARG0 || exe_name == MISSPELLED_APPLY_PATCH_ARG0 {
        codex_apply_patch::main();
    }

    let argv1 = args.next().unwrap_or_default();
    if argv1 == codex_sandboxing::CODEX_WINDOWS_MXC_ARG1 {
        codex_sandboxing::run_windows_mxc_main();
    }
    #[cfg(unix)]
    if argv1 == CODEX_ARG0_EXEC_HELPER_ARG1 {
        codex_exec_server::run_arg0_exec_helper_main();
    }
    if argv1 == CODEX_FS_HELPER_ARG1 {
        codex_exec_server::run_fs_helper_main();
    }
```

（`codex-rs/arg0/src/lib.rs:97`）

程序名 `argv[0]` 是 `codex-linux-sandbox`、`apply_patch`（连拼错的 `applypatch` 也认），或 Unix 上的 `codex-execve-wrapper`，就直接进入对应的入口。另一类入口看第一个参数：`--codex-run-as-apply-patch`、`--codex-run-as-fs-helper`、仅 Windows 的 `--run-as-windows-sandbox` 等隐藏参数同样会截走控制流。文档注释坦言 arg0 这招只在 macOS 和 Linux 上管用，所以 Windows 上的 `apply_patch` 别名是一个 `.bat` 脚本，靠的正是 `--codex-run-as-apply-patch`。

什么都没命中，才是正常启动。这时还有两件事必须赶在创建任何线程之前做完，因为它们要改进程的环境变量：

1. 读 `$CODEX_HOME/.env`（默认 `~/.codex/.env`），但名字以 `CODEX_` 开头的变量不论大小写一律跳过（`codex-rs/arg0/src/lib.rs:302`），防止一个 `.env` 文件改掉 Codex 自己的行为开关；
2. 在 `$CODEX_HOME/tmp/arg0/` 下建一个 `codex-arg0` 前缀的临时目录，放进指向当前可执行文件的符号链接 `apply_patch`、`applypatch`、Linux 上的 `codex-linux-sandbox`、Unix 上的 `codex-execve-wrapper`（Windows 上是 `.bat`），再把这个目录插到 `PATH` 最前面。模型在 shell 里敲 `apply_patch`，找到的就是 Codex 自己。目录里有一个进程存活期间一直加锁的 `.lock` 文件，下次启动时，锁已释放的旧目录会被清理掉。

最后，`arg0_dispatch_or_else` 起一个名为 `codex-main`、栈大小 16 MiB 的线程，在上面建 tokio 多线程运行时，把当前可执行文件、沙箱助手别名等路径打包成 `Arg0DispatchPaths` 交给真正的入口函数。连测试都遵守这套约定：`codex-rs/core/tests/suite/mod.rs` 用 `#[ctor]` 让集成测试的二进制在启动时同样按 arg0 分派。

## npm 分发：怎么只装上本平台的二进制

`npm install -g @openai/codex` 装下来的是一个只有 `bin/codex.js` 的元包。它怎么拿到对的原生二进制？答案在打包脚本里：

```python
    if package == "codex":
        package_json["files"] = ["bin/codex.js"]
        package_json["optionalDependencies"] = {
            CODEX_PLATFORM_PACKAGES[platform_package]["npm_name"]: (
                f"npm:{CODEX_NPM_NAME}@"
                f"{compute_platform_package_version(version, CODEX_PLATFORM_PACKAGES[platform_package]['npm_tag'])}"
            )
            for platform_package in PACKAGE_EXPANSIONS["codex"]
            if platform_package != "codex"
        }
# ...
def compute_platform_package_version(version: str, platform_tag: str) -> str:
    # npm forbids republishing the same package name/version, so each
    # platform-specific tarball needs a unique version string.
    return f"{version}-{platform_tag}"
```

（`codex-cli/scripts/build_npm_package.py:299`）

六个平台包发布时用的都是同一个包名 `@openai/codex`，只是版本号带后缀，例如 `0.158.0-linux-x64`。元包的 `optionalDependencies` 用 npm 的别名语法把它们挂成 `@openai/codex-linux-x64` 这样的名字；每个平台包的 `package.json` 又声明了 `os` 与 `cpu` 字段，npm 只会装上与当前系统匹配的那一个。运行时，`codex.js` 按同一张表反查：

```js
const PLATFORM_PACKAGE_BY_TARGET = {
  "x86_64-unknown-linux-musl": "@openai/codex-linux-x64",
  "aarch64-unknown-linux-musl": "@openai/codex-linux-arm64",
  "x86_64-apple-darwin": "@openai/codex-darwin-x64",
  "aarch64-apple-darwin": "@openai/codex-darwin-arm64",
  "x86_64-pc-windows-msvc": "@openai/codex-win32-x64",
  "aarch64-pc-windows-msvc": "@openai/codex-win32-arm64",
};
```

（`codex-cli/bin/codex.js:16`）

平台包的 `vendor/<三元组>/` 就是 `scripts/codex_package/README.md` 描述的标准包布局：`bin/` 下是入口 `codex` 与 Code mode 用的 `codex-code-mode-host`；`codex-resources/` 放随包的辅助程序，Linux 上是从 `codex-rs/vendor/bubblewrap` 源码编出的 `bwrap`，Windows 上是沙箱用的 `codex-command-runner.exe` 与 `codex-windows-sandbox-setup.exe`，受支持的 Unix 平台上还有一份打过补丁的 `zsh`；`codex-path/` 里是 `rg`；根上的 `codex-package.json` 记录版本与布局。Rust 侧的 `codex-install-context` 读这份布局，arg0 设置别名目录之前会先把 `codex-path` 插进 `PATH`，所以 Codex 用的 `rg` 优先是随包的那一份。

整条启动链画在一起：

```mermaid
flowchart TB
  U[敲下 codex] --> Q{经 npm 安装？}
  Q -->|是| JS[node 执行 codex.js<br/>按平台与架构找 vendor 下的二进制]
  Q -->|否| BIN
  JS -->|spawn 并转发信号| BIN[原生二进制 codex]
  BIN --> A0{argv0 是特殊名字？}
  A0 -->|codex-linux-sandbox| LS[Linux 沙箱助手]
  A0 -->|apply_patch| AP[补丁工具]
  A0 -->|否| A1{argv1 是隐藏参数？}
  A1 -->|是| HP[fs helper、Windows 沙箱等入口]
  A1 -->|否| ENV[读 .env，过滤 CODEX_ 前缀<br/>建别名目录并改 PATH]
  ENV --> MAIN[codex-main 线程<br/>tokio 运行时<br/>cli_main 解析子命令]
```

`cli_main` 解析完子命令之后，选前端、连 app-server 的过程从[一条消息的生命周期](https://daiw.net/manual/codex-source/message-lifecycle)开始讲；测试与发布流水线见[工程实践](https://daiw.net/manual/codex-source/engineering)。

---

上一篇：[Codex CLI 是什么 · 开源的是哪一部分](https://daiw.net/manual/codex-source/what-is-codex) · 下一篇：[workspace 全景 · 153 个成员怎么分层](https://daiw.net/manual/codex-source/workspace-map)
