首页
/ codex-rs 的 Bazel 构建体系:codex_rust_crate 宏、Bzlmod 工具链与 BuildBuddy 远程执行实战

codex-rs 的 Bazel 构建体系:codex_rust_crate 宏、Bzlmod 工具链与 BuildBuddy 远程执行实战

2026-09-06 12:03:31作者:晏闻田Solitary

本篇基于仓库内的 Bazel 构建文档 展开,讲清楚 codex-rs Rust 工作区如何用 Bazel 实现 hermetic(可复现、自包含)构建:MODULE.bazel 如何声明工具链、rules_rs 如何从 Cargo.toml/Cargo.lock 导入第三方 crate、defs.bzl 中的 codex_rust_crate 宏如何把 Bazel 目标与 Cargo 约定对齐,以及如何通过 BuildBuddy 配置启用远程缓存与远程执行。读完后,你应能独立完成本地 Bazel 构建与测试、理解各 --config 的作用边界,并知道新增依赖、新增 crate 时的完整操作流程。

需要先说明适用前提:文档中注明,截至 2026-06-01,该 Bazel 体系仍处于实验阶段,正在持续稳定化。日常开发仍可以 Cargo 为主,Bazel 主要用于 CI、跨平台产物和可复现构建。

核心定位:Cargo 是事实来源,Bazel 是构建执行层

原文明确了这一分工原则:

This repository uses Bazel to build the Rust workspace under codex-rs. Cargo remains the source of truth for crates and features, while Bazel provides hermetic builds, toolchains, and cross-platform artifacts.

这意味着:

  • 依赖声明、feature、crate 划分全部以 codex-rs/Cargo.tomlcodex-rs/Cargo.lock 为准;
  • Bazel 负责的是构建可复现性:版本钉死的 Rust 工具链、自包含(hermetic)的 LLVM、macOS SDK 等,以及跨平台交叉编译产物(包括 Windows gnullvm 这种 Cargo 原生不直接支持的 ABI);
  • 因此你几乎不需要为 Bazel 维护一套并行的依赖清单——这正是后文 crate.from_cargo(...) 要解决的问题。

高层结构:四个关键构件

原文给出了四段式结构,下面逐一结合仓库中的实际文件展开。

1. MODULE.bazel:Bzlmod 依赖与工具链声明

根目录 MODULE.bazel 声明了整个仓库的 Bazel 模块(模块名为 codex),承担三类职责:

(1)外部模块依赖。 关键依赖包括 rules_rs 0.0.96(新一代 Rust 构建规则,见 MODULE.bazel#L93)、llvm 0.8.11(hermetic LLVM,见 MODULE.bazel#L5)、apple_supportaws-lc 等。

(2)上游模块打补丁。 由于 hermetic LLVM 需要一些尚未上游化的定制(例如 V8 所需的 custom libc++、Windows gnullvm 运行时),仓库通过 single_version_override + patchesllvmabseil-cpprules_ccbzip2 等模块打补丁,补丁文件集中在 patches/ 目录下(如 patches/llvm_rusty_v8_custom_libcxx.patch)。对 rules_rust 本身也打了三个补丁(build-script 工具 runfiles、Windows/MSVC 直接链接参数、Windows process wrapper),见 MODULE.bazel#L103-L117

(3)工具链注册。 包括:

  • macOS SDK 归档下载与 framework 白名单(@llvm//extensions:osx.bzl,见 MODULE.bazel#L31-L78);
  • 默认 Rust 工具链:edition = "2024"version = "1.95.0"MODULE.bazel#L190-L197);
  • 一个独立的 nightly 工具链(nightly/2025-09-18dev_components = True),专门服务于需要 rustc_private 的 argument-comment-lint(MODULE.bazel#L123-L131);
  • Windows 双 ABI 支持:MSVC 与 gnullvm 两套 repository_setMODULE.bazel#L135-L188)。

本地 Bazel 版本由 .bazelversion 钉在 9.0.0

2. rules_rscrate.from_cargo(...):从 Cargo 元数据导入第三方 crate

MODULE.bazel#L209-L228 展示了原文提到的核心机制:

crate = use_extension("@rules_rs//rs:extensions.bzl", "crate")
crate.from_cargo(
    cargo_lock = "//codex-rs:Cargo.lock",
    cargo_toml = "//codex-rs:Cargo.toml",
    platform_triples = [
        "aarch64-unknown-linux-gnu",
        "aarch64-unknown-linux-musl",
        "aarch64-apple-darwin",
        "aarch64-pc-windows-msvc",
        "aarch64-pc-windows-gnullvm",
        "x86_64-unknown-linux-gnu",
        "x86_64-unknown-linux-musl",
        "x86_64-apple-darwin",
        "x86_64-pc-windows-msvc",
        "x86_64-pc-windows-gnullvm",
    ],
)

crate.from_cargo 直接解析 Cargo 工作区的 Cargo.tomlCargo.lock,为每个第三方 crate 生成 Bazel 目标并统一暴露在 @crates 仓库下。platform_triples 列表同时列出了 9 种目标 triple(含 Linux musl、macOS、Windows MSVC 与 gnullvm),这正是"跨平台产物"能力的来源——同一次依赖解析覆盖了所有交叉编译目标。注释中还解释了为何同时保留 MSVC 与 gnullvm 两个 Windows triple:V8 实验仍在消费仅以 MSVC 命名发布的 release 资产。

对于需要"特殊照顾"的上游 crate,仓库用 crate.annotation 做定点修正,例如:

  • blake3:在 Windows gnullvm 上强制启用 pure feature,因为该工具链无法可靠地生成其 x86 原生汇编(MODULE.bazel#L249-L255);
  • zstd-sysring:打补丁修正 MSVC 头文件搜索路径(MODULE.bazel#L256-L270);
  • aws-lc-sys/aws-lc-rs:关闭 build script,改用预生成 bindgen 产物(MODULE.bazel#L271-L284)。

这就是原文"Evolving the setup"一节所说"上游 crate 可能需要 patch 或 crate.annotation 才能在 Bazel 沙箱中构建"的具体形态。

3. defs.bzlcodex_rust_crate:与 Cargo 约定对齐的宏

根目录 defs.bzl 提供 codex_rust_crate 宏(defs.bzl#L181),封装 rust_libraryrust_binaryrust_test,让 Bazel 目标与 Cargo 的目录约定一一对应。原文说它"提供了对大多数一方 crate 合理的默认值,但某些情况下需要微调"。

从宏的参数列表(defs.bazel#L181-L206)可以看到它的覆盖面:

参数 作用
crate_name / crate_features / crate_edition 对应 Cargo.toml 中的名称、feature、edition。宏文档特别提示 feature 会在整个工作区以单一配置编译(列表内全部启用),应慎用
build_script_data 暴露给 build.rs 运行时使用的数据文件
compile_data / lib_data_extra 库目标的编译期数据 / 运行期数据
rustc_flags_extra / rustc_env 追加 rustc 参数与环境变量(宏默认注入 BAZEL_PACKAGE=<包名>,见 defs.bazel#L282-L284
integration_test_args / unit_test_args / *_timeout 集成/单元测试参数与超时
test_shard_counts 按测试名映射的分片数,启用 Bazel 原生分片并标记 flaky(三次重试)
test_tags 传给单元 + 集成测试目标,典型用途是 no-sandbox
extra_binaries / extra_binaries_non_windows 把别的 crate 的二进制暴露为测试数据与 CARGO_BIN_EXE_* 环境变量
run_tests_with_wine_exec 为每个集成测试生成 Wine 执行变体(在 Linux 上跑交叉编译的 Windows exec-server)

宏内部的工作方式(均可在源码中验证):

  • 构建脚本:若存在 build.rs,生成 <name>-build-script 目标(cargo_build_script 规则,defs.bazel#L297-L305);
  • src/**/*.rs 非空时生成 rust_library(proc-macro crate 用 rust_proc_macro),并同步生成单元测试目标(defs.bazel#L309-L371);
  • 二进制:按 Cargo.toml 解析出的 binaries 表逐一生成 rust_binary,并导出 CARGO_BIN_EXE_<name> 环境变量供集成测试使用——完整复刻了 Cargo 的行为(defs.bazel#L376-L391);
  • 集成测试tests/*.rs 每个文件生成一个测试目标;源码注释(defs.bazel#L497-L513)明确列出四种生成形态:非分片原生测试、分片原生测试(拆成 manual 的 rust_test + 外层 workspace_root_test)、Windows 交叉测试、Wine 执行测试。

其中 workspace_root_testdefs.bazel#L142-L179)是一个自研规则,通过 workspace_root_test_launcher.sh.tpl / workspace_root_test_launcher.bat.tpl 生成跨平台启动器:它在运行时解析出真实的仓库根目录、cd 进去,并把 runfiles 路径改写为绝对路径。这是为了让 insta 快照测试看到 Cargo 风格的相对路径(配合 INSTA_WORKSPACE_ROOT/INSTA_SNAPSHOT_PATH 环境变量,defs.bazel#L260-L266),同时兼容仓库使用 --noenable_runfiles(manifest-only runfiles)的策略。

另一个细节:测试目标的 rustc_flags 中统一加入了 --remap-path-prefix=../codex-rs=--remap-path-prefix=codex-rs=defs.bazel#L526-L532),因为 Bazel 曾对 file!() 宏产生两种不同的路径前缀,剥掉后 insta 快照元数据才与 Cargo 构建一致。

4. 每个 crate 的 BUILD.bazel

各 crate 目录下的 BUILD.bazel 通常只是调用 codex_rust_crate 并做少量调整。最简单的形态见 codex-rs/code-mode-host/BUILD.bazel

load("//:defs.bzl", "codex_rust_crate")

codex_rust_crate(
    name = "code-mode-host",
    crate_name = "codex_code_mode_host",
)

当 crate 需要额外的编译期/运行期数据、特殊环境变量或测试定制时,再按宏的参数列表补充。

本地运行 Bazel:justfile 入口

仓库根目录 justfile 暴露了常用入口(该文件的工作目录为 codex-rs):

just bazel-test
just bazel-clippy

这两个 recipe 的实际展开(justfile#L158-L164):

# bazel-test
bazel test --test_tag_filters=-argument-comment-lint //... --keep_going

# bazel-clippy
bazel_targets="$(scripts/list-bazel-clippy-targets.sh)" && bazel build --config=clippy -- ${bazel_targets}

即:测试全仓库目标并排除 nightly-only 的 argument-comment-lint 标签;clippy 通过 --config=clippy 用 rules_rust 的 Clippy aspect 对目标做检查。此外还有几个直接可用的 recipe:

  • just bazel-codexbazel run //codex-rs/cli:codex,在 Bazel 构建产物上运行 CLI(justfile#L128-L135);
  • just bazel-lock-update / just bazel-lock-check:见下文"演进"一节。

一个重要的边界事实(原文也强调了):普通的本地 bazeljust 调用全部在本地执行;BuildBuddy 缓存、构建事件上报(BES)、远程下载与远程执行都是 opt-in 配置,不选 --config=buildbuddy-* 就不会接触任何远程服务。

BuildBuddy:远程缓存与远程执行

codex-rs 的 CI 与内部构建通过 BuildBuddy 做共享缓存和远程构建/测试。要提速,需要两件事:提供 API key,并选择一个配置。

API key 配置

按 BuildBuddy 的认证文档创建 key 后,加入 ~/.bazelrc

# Local machine only; this file contains a BuildBuddy credential.
common --remote_header=x-buildbuddy-api-key=<your-buildbuddy-api-key>

把凭据放在工作区外可以降低误提交的概率。如果不同项目需要不同的 key,放到 %workspace%/user.bazelrc——仓库的 .bazelrc 末尾以 try-import %workspace%/user.bazelrc 可选导入该文件(见 .bazelrc#L225),且 .gitignore 已将 user.bazelrc 排除在版本控制之外。切记不要提交或分享含凭据的文件。

选择远程构建配置

外部用户应使用 buildbuddy-generic-rbebuildbuddy-generic;OpenAI 内部用户默认 buildbuddy-openai-rbe。把配置写入 %workspace%/user.bazelrc

common --config=buildbuddy-openai-rbe

这组配置在 .bazelrc 中有明确定义(文件注释说明:这些配置"只有在用户显式选择时才会接触 BuildBuddy"):

  • buildbuddy-generic:cache/BES/下载均指向 remote.buildbuddy.io,无远程执行;
  • buildbuddy-generic-rbe:在上一项基础上叠加 --config=remote,使用 remote.buildbuddy.io 远程执行器;
  • buildbuddy-openai / buildbuddy-openai-rbe:同样的两级结构,但指向 openai.buildbuddy.io

--config=remote 本体设置 --strategy=remote--extra_execution_platforms=//:rbe 并把并发提到 --jobs=800。而 //:rbe 平台由 rbe.bzl 生成,其 container-image exec property 钉死了含 git/python3/dotslash 等测试依赖的 Ubuntu 镜像(按主机架构选 x86_64 或 aarch64 镜像及 sha256),保证远程执行环境一致。

各配置的完整对照表(原文表格)

调用/配置 需要 key Cache/BES 构建执行 测试执行
bazel ... 本地 本地
bazel ... --config=buildbuddy-generic remote.buildbuddy.io 本地 本地
bazel ... --config=buildbuddy-generic-rbe remote.buildbuddy.io 远程 远程
bazel ... --config=buildbuddy-openai openai.buildbuddy.io 本地 本地
bazel ... --config=buildbuddy-openai-rbe openai.buildbuddy.io 远程 远程

Cache/BES 主机同时用于远程下载(--experimental_remote_downloader)。

CI 侧:租户选择由统一 wrapper 完成

GitHub Actions 通过 .github/scripts/run_bazel_with_buildbuddy.py 路由所有 Bazel 构建与输出解析命令;更高层的辅助脚本(如 .github/scripts/run-bazel-ci.sh.github/scripts/rusty_v8_bazel.py)都把远程配置选择委托给它。wrapper 的设计要点(原文描述,代码可印证):

  • 它读取 GitHub Actions 的仓库与事件载荷来决定租户,而不是让每个 workflow 文件复制租户选择逻辑;
  • 它归一化 Bazel 启动选项,让同一 job 内的所有 Bazel 调用复用同一个 server 和内存中的分析缓存(见脚本中 startup_args 函数,.github/scripts/run_bazel_with_buildbuddy.py#L23-L33);
  • 加载阶段的 bazel query 目标发现命令在本地运行,因为它只枚举 label,不需要远程缓存或执行;
  • 没有 API key 时,wrapper 会剥离远程 CI 配置、退回本地运行;pull request 事件载荷缺失或畸形时"fail closed"到 generic 主机;
  • 只有在 GitHub Actions 中、且是受信运行(openai/codex 仓库内的 push/dispatch/同仓 PR)才选择 OpenAI 主机;fork 的 PR 一律本地运行。

CI 各配置与执行位置的对应关系(原文表格):

CI 配置 远程配置 构建执行 测试执行
ci-linux *-rbe 远程主机 远程主机
ci-v8 *-rbe 远程主机 远程主机
ci-macos *-rbe 远程主机 本地
ci-windows-cross *-rbe 远程主机 本地
ci-windows 非 RBE 本地 本地
无 key 的 CI 回退 本地 本地

这些 ci-* 配置同样定义在 .bazelrc 中,并各有明确注释:ci-linux 可全量远程构建/测试(覆盖 x86 与 arm runner);ci-macos 构建远程化、测试留在本地(--strategy=TestRunner=darwin-sandbox,local);ci-windows-cross 用 Linux 远程执行构建 Windows gnullvm 二进制、测试留在 Windows runner 以保留 Bazel 分片与 flaky 重试,并强制 V8 的 mksnapshot 在本地执行(因为 Windows 快照必须由 Windows 的 mksnapshot 二进制生成)。wrapper 源码中也维护了"哪些 CI 配置需要远程构建执行"的映射(ci-linuxci-macosci-v8ci-windows-cross,见 .github/scripts/run_bazel_with_buildbuddy.py#L13-L18)。

若想在本地验证 generic 远程配置:

BUILDBUDDY_API_KEY=... GITHUB_REPOSITORY=my-fork/codex \
  ./.github/scripts/run_bazel_with_buildbuddy.py \
  build --config=ci-linux //codex-rs/cli:codex

演进构建体系:改依赖、加 crate 的标准流程

更新依赖后刷新 Bzlmod 锁文件

改动 Cargo.toml/Cargo.lock 后,在仓库根目录运行:

just bazel-lock-update

它执行 bazel mod deps --lockfile_mode=updatejustfile#L146-L147),按需更新 MODULE.bazel.lock要把锁文件变更和 Cargo 锁文件一起提交。

本地验证锁文件对齐(与 CI 相同检查):

just bazel-lock-check

该 recipe 指向 scripts/check-module-bazel-lock.sh,脚本内部调用同一个 BuildBuddy wrapper 执行 bazel mod deps --lockfile_mode=error,失败时会明确提示运行 just bazel-lock-update 并提交锁文件。

如果某个上游 crate 无法在 Bazel 沙箱中构建、或需要做交叉编译适配,正确姿势是给它打 patch 或在 MODULE.bazel 中加 crate.annotation(前文 blake3/zstd-sys/ring 即是现成范例),而不是改 crate 源码。

新增 crate / binary 的三步流程

  1. 像往常一样把 crate 加入 Cargo 工作区;
  2. 创建 BUILD.bazel 并调用 codex_rust_crate(参考相邻 crate,如 codex-rs/code-mode-host/BUILD.bazel);
  3. 如果依赖需要特殊处理(编译期/运行期数据、集成测试附加二进制、环境变量等),调整 codex_rust_crate 参数。

原文特别提到一个常见定制:把 test_tags = ["no-sandbox"] 加给测试目标,让测试在无沙箱模式下运行。文档建议尽量避免,因为它绕过了沙箱隔离;典型必要场景是测试本身使用 Seatbelt——Bazel 沙箱在 macOS 上也是 Seatbelt 实现,而 Seatbelt 不能嵌套。为进一步限制影响面,可以把这类测试隔离到独立 crate。

顺带一提:Bazel 侧的 lint 与测试基础设施

  • just bazel-clippy 使用的 --config=clippy 会启用 rust_clippy_aspect,且因为 rules_rust 不读取 Cargo 的 lint 级别,.bazelrc 里手工维护了一份与 codex-rs/Cargo.toml [workspace.lints.clippy] 对齐的 deny 列表;
  • 测试环境默认 RUST_MIN_STACK=8388608(8 MiB),因为 Rust libtest 在 Windows 上以 std-spawned 线程跑测试体,默认 2 MiB 栈对大型 async 测试 future 不够;
  • 仓库采用 --noenable_runfiles(manifest-only runfiles)策略,这也是 workspace_root_test 启动器存在的直接原因之一。

参考与延伸阅读

原文给出的外部参考为 Bazel 官方文档(Bazel 概览与 Bzlmod 模块系统)、rules_rustrules_rs 两个规则项目的仓库,此处不再重复外链,可分别在对应官方渠道检索。仓库内的深入阅读路径:

小结:codex-rs 的 Bazel 体系本质上是"Cargo 管声明、Bazel 管执行"的双轨制——crate.from_cargo 消除依赖清单的双写,codex_rust_crate 消除构建目标的重复描述,MODULE.bazel + patches/ 保证工具链与上游 crate 的可复现性,BuildBuddy 各 --config 则以 opt-in 方式提供从纯缓存到全远程执行的分级加速。本地开发用 just bazel-test/just bazel-clippy 即可起步,新增依赖后记住 just bazel-lock-update 并提交锁文件,就能跟上这套仍在演进中的构建体系。

登录后查看全文
热门项目推荐
相关项目推荐