从源码构建与运行 · Cargo、Bazel 与 npm 包装
Codex 同时维护两套构建:Cargo 是 crate 与依赖的事实来源,日常开发与正式发布都靠它;Bazel 从 Cargo.lock 导入依赖,负责封闭构建、跨平台产物与 CI,官方文档仍称其为实验性。发出去的只有一个 codex 二进制,arg0 按程序名和第一个参数把它分派成 Linux 沙箱助手、apply_patch 等多个入口;npm 包再用 optionalDependencies 别名只装上本平台的那一份。
从源码构建与运行 · Cargo、Bazel 与 npm 包装
上一篇说过,Codex 的本体是 codex-rs/ 下的 Rust workspace。这一篇回答三个工程问题:怎么把它编出来跑起来;同一份代码为什么同时有 Cargo 和 Bazel 两套构建;交到用户手里的那一个 codex 二进制,又是怎么被 npm 挑中、怎么一身兼几职的。
怎么用:从源码跑起来
装现成的发行版见手册安装、登录与升级。从源码构建,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 继承。
docs/install.md 的系统要求表仍写着 Windows 11 需经 WSL2 运行,但代码早已原生支持 Windows:README.md 给出了 PowerShell 安装脚本,npm 发布 win32-x64 与 win32-arm64 两个平台包,还有一整套 Windows 沙箱。以代码为准。
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带着一个sandboxfeature。所以代码里的开关几乎都是运行时的,集中在codex-featurescrate,通过配置里的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:
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 的清单:
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 都包在它里面,第一步先看两样东西:
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。
什么都没命中,才是正常启动。这时还有两件事必须赶在创建任何线程之前做完,因为它们要改进程的环境变量:
- 读
$CODEX_HOME/.env(默认~/.codex/.env),但名字以CODEX_开头的变量不论大小写一律跳过(codex-rs/arg0/src/lib.rs:302),防止一个.env文件改掉 Codex 自己的行为开关; - 在
$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 的元包。它怎么拿到对的原生二进制?答案在打包脚本里:
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 按同一张表反查:
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 优先是随包的那一份。
整条启动链画在一起:
cli_main 解析完子命令之后,选前端、连 app-server 的过程从一条消息的生命周期开始讲;测试与发布流水线见工程实践。
上一篇:Codex CLI 是什么 · 开源的是哪一部分 · 下一篇:workspace 全景 · 153 个成员怎么分层