Bzlmod 很容易被低估,因为最先映入眼帘的变化很小:一个 MODULE.bazel 文件,替掉了旧 WORKSPACE 里大量依赖设置。陷阱就在这里。这场迁移远超整洁的语法替换。它改变了一个 Bazel 仓库声明直接依赖的方式,改变了传递模块版本的选择方式,改变了生成仓库如何对外可见,改变了 registries 如何参与信任,也改变了一个 build 还能依赖多少偶然积累下来的全局状态。
时间点让这个问题具有现实压力。Bazel 当前迁移指南把 Bzlmod 定位为离开 legacy WORKSPACE 系统的方向,说明 WORKSPACE 已在 Bazel 8 中默认关闭,并把迁移描述为 Bazel 9 时代 build 的必要工作。[1] 由此很容易产生一种错误采用计划:打开 flag,跑一组宽目标,把 CI 报出的错误逐个补上,然后宣布完成。更可靠的做法,是把 MODULE.bazel 当作一份新的 dependency graph 契约。
题图来自 2024 年 BazelCon,地点是 Mountain View 的 Computer History Museum。Google 把这次活动描述为第一届并非完全由 Google 组织的 BazelCon,Linux Foundation 与多家 build-tooling 赞助方也参与其中;Bazel blog 的回顾文章则记录了 332 人到场。[8][9] 这层社区语境很重要。Bzlmod 的成功不能落在某一个仓库的本地清理上,核心 rulesets、Bazel Central Registry、语言包集成与企业 build 负责人都需要在同一条依赖模型边界上汇合。
先审计 WORKSPACE 藏住了什么
迁移第一步应先做盘点,编辑放在后面。一个成熟的 WORKSPACE 文件,常常包含直接第三方 archives、自定义 repository rules、特定语言的 *_deps() macros、生成出来的 toolchain repositories、本地仓库快捷方式、patch files、mirrors,以及保存组织记忆的注释。在旧模型下,这些语句还经常制造出宽泛的全局 namespace:某个依赖之所以可见,只是因为某个 macro 刚好足够早地运行过。
Bazel 迁移指南建议先使用 Bzlmod migration tool,再手动处理剩余错误。[1] 这个工具自己的文档说明了它能提供的帮助:分析 WORKSPACE,使用已解析的依赖信息,识别直接依赖,把它们引入 MODULE.bazel,随后在 Bzlmod 下反复 build targets,暴露仍然缺失的部分。[2] 这很有用,但它还不足以充当完整迁移策略。它可以提出第一版 graph,却不能替团队判断哪些旧快捷方式值得留下。
面对严肃仓库,动根目录之前先选一组有代表性的 targets。纳入生产 binaries、仅测试依赖、code generators、toolchains、generated clients、特定平台 targets,以及历史上经常破坏 clean builds 的部分。接着写下这些 targets 用到的 external repositories,区分哪些是直接产品选择,哪些是传递性规则实现细节,哪些只是 legacy macro 顺手带来的便利。Bzlmod 会回报这种区分。
优先使用 module declarations,少保留私有 fetch scripts
最清爽的 Bzlmod 形态,是用 bazel_dep() 声明直接 module dependency。EngFlow 的迁移系列抓住了这层实际收益:当依赖已经存在于 Bazel Central Registry 中,很多 http_archive() 声明都可以缩成短得多的 bazel_dep() 行;项目仍要阅读该 module 自己的文档,补上额外配置。[6] 这是早期就值得拿下的简单收益。
registry 模型带来的收益更重要。Bazel 的 registry 文档把 Bazel Central Registry 描述为一个由 bazelbuild/bazel-central-registry GitHub 仓库支撑的 index registry,并带有可浏览前端与贡献流程。它还说明了可重复使用的 --registry flag、registry precedence,以及当自定义 registry 列表替换默认列表时,需要显式把 central registry 加回去。[4] 放到企业环境中,Bzlmod 迁移也会进入采购决策层:直接信任 public BCR,做 mirror,做 fork,还是把内部 modules 放进 internal index。
采用规则应当保守。存在真实 module 且 BCR metadata 足够好时,使用 bazel_dep()。网络控制、可用性、review policy 或 archive provenance 提出要求时,使用自定义或 mirrored registry。只有在仍找不到更合适 module path 的情况下,才用 use_repo_rule() 处理残留 repository rules。[6] 如果每个依赖都作为私有 fetch script 原样带到新体系里,仓库仍能在技术上跑在 Bzlmod 下,却保留旧有运转形状。
这种区分会影响后续维护。module declaration 可以参与版本选择和 graph inspection。私有 repository rule 也能工作,只是更接近本地例外。例外有时确有必要。它们需要被命名、被 review,并在合适时机退出,不能藏在一次迁移提交里。
把 module extensions 当成 policy code
module extensions 是许多真实迁移变得棘手也变得有意思的地方。官方 extension 文档展示了基本模式:对承载 extension 的 module 添加 bazel_dep(),调用 use_extension() 把 extension 引入当前作用域,通过类似 tag 的点号语法完成配置,再调用 use_repo() 让生成出来的 repositories 对当前 module 可见。[3] 同一页还说明,extensions 采用 lazy evaluation,而 bazel mod deps 可以在测试时强制触发 evaluation。[3]
这层 lazy behavior 不是旁枝细节。它会让迁移在某个 target 真正引用 generated repository 之前显得风平浪静。仓库负责人需要测试 production 与 CI 实际使用的 extension paths,而不仅仅看 root graph。build 负责人也要知道哪些 repositories 属于 extension 的 public API。若某个 extension 承诺生成名为 maven 的 repo,use_repo(maven, "maven") 就是一条依赖边界,不能只当成一行 setup code。[3]
EngFlow 关于 module extensions 的文章有价值,因为它点明了相对 WORKSPACE 的行为变化:MODULE.bazel 不允许普通 load() statements,module extensions 可以携带 tag schemas,extensions 内部生成的 repos 位于 namespaces 中,调用方必须显式让它们可见。[7] 这会清掉一类全局状态惊喜,同时也迫使团队重写旧 macros;那些 macros 过去假定自己可以在一段松散脚本里 load constants、调用 repository rules,再注册可见 repos。
迁移异味,是用一个巨大的 extension file 以不同拼写重建旧 WORKSPACE。面对 platform archives、语言包 lockfiles 或 toolchain repositories,其中一部分负担确实会留下。即便如此,extension 仍要有清楚的 API:用户设置哪些 tags,调用者可以 use_repo 哪些 generated repos,哪些底层 repository names 属于内部实现,以及 root module 何时可以用 override_repo() 或 inject_repo() 处理 vendor 或 patch lane。[3]
把 lockfile 当成运维边界
MODULE.bazel.lock 不是装饰。Bazel lockfile 文档把它描述为 module resolution 与 extension evaluation 之后在 workspace root 下生成的文件,用来保存 resolution result,让 builds 具备更强 reproducibility,也让 Bazel 可以跳过未变化的 resolution work。[5] 同一份文档还说明了 --lockfile_mode,覆盖 update behavior、refresh behavior、面向 CI 类 enforcement 的 error behavior,以及关闭 lockfile handling 的方式。[5]
这给了团队一个具体 rollout 选择。迁移早期,lockfile 会很吵,因为大量依赖与 extension results 同时变化。EngFlow 明确提醒,迁移期间 lockfile diffs 会很大,尤其当不同开发者使用不同 Bazel 或 tooling versions 时;团队在理解取舍后,可以暂时推迟提交 lockfile。[6] 终点仍要清楚:生产 build 需要 lockfile policy,不能在每次 dependency metadata 变化时重新争论。
一条务实规则是,迁移期间允许开发者在本地 update,待 module graph 稳定后,再把 CI 推向更严格的行为。到那时,MODULE.bazel、MODULE.bazel.lock、registry configuration 与 pinned Bazel version 应该一起进入 review。dependency updates 变成正常可审查的变更,而不是 build cache miss 之后才发现的副作用。
一条不靠运气的迁移路径
更可靠的采用计划有五道关口。第一,固定 CI 与本地开发使用的 Bazel version,然后针对一组覆盖仓库难点的 targets 运行 migration tool,而不是只跑 happy path。[2] 第二,把直接且有 registry 支持的依赖替换成 bazel_dep(),并记录哪些旧 http_archive() 或 repository-rule declarations 因为还没有 module path 而继续保留。[4][6]
第三,按职责隔离 module extensions。语言包 resolution、toolchain setup、vendored platform archives 与 generated repositories,不该因为 port 期间方便,就全塞进同一个匿名 extension。extension identity 会成为 public API 的一部分,日后移动它会破坏 users。[3][7] 第四,决定 registries 如何选择。需要 internal mirror 的团队,应把 --registry 写进 .bazelrc,记录 precedence,并把 BCR-addback behavior 写清楚。[4]
第五,把 legacy WORKSPACE path 当作有退出日期的临时 compatibility lane。有些项目自身也是依赖项,迁移期间会需要为非 Bzlmod consumers 保留 dual support。[6] 没有这类义务的 application repos 应避免无限期运行两套 dependency systems。双路径会把 review 工作翻倍,也会让哪张 graph 才是 source of truth 变得含混。
主要 failure modes 都能提前预见。自定义 macros 会依赖如今应放进 extension file 的 load() behavior。toolchain registration 会假定 repositories 全局可见。仅测试依赖只有在构建狭窄 target 时才冒出来。本地 repository overrides 会把用户特定 paths 泄进 shared state。lockfile churn 会把真实 dependency changes 淹没在噪声里。registry decisions 会一直拖到第一次 network 或 provenance incident 才暴露。这些都不是回避 Bzlmod 的理由,它们指向同一件事:这场迁移应按 infrastructure work 来跑。
最直接的读法是:Bzlmod 让 Bazel dependency management 更显式,而显式系统对经验传闻的容忍度更低。小仓库通常能很快 lift and shift。大型 monorepos 与 rule authors 需要一份迁移计划,写清 module declarations、extension APIs、registry trust、lockfile policy,以及 WORKSPACE 保持权威的最后一天。收益不在于 MODULE.bazel 更短,而在于 dependency graph 变得足够可检查,进而足以被运维。
来源
- Bazel 文档,《Bzlmod Migration Guide》—— WORKSPACE 退场语境、迁移理由,以及 WORKSPACE 与 Bzlmod 的对照说明。
- Bazel 文档,《Bzlmod Migration Tool》—— helper tool workflow,涵盖分析 WORKSPACE、识别直接依赖、转换到 MODULE.bazel,以及围绕 build errors 迭代。
- Bazel 文档,《Module extensions》——
use_extension、use_repo、lazy evaluation、repository visibility、extension identity,以及 override / injection behavior。 - Bazel 文档,《Bazel registries》—— Bazel Central Registry 结构、
--registryselection、registry precedence,以及 BCR contribution / CI expectations。 - Bazel 文档,《Bazel Lockfile》——
MODULE.bazel.lockgeneration、lockfile modes、reproducibility,以及跳过 resolution 的行为。 - Mike Bland,《Migrating to Bazel Modules (a.k.a. Bzlmod) - The Easy Parts.》EngFlow Blog,2024-06-27——关于
bazel_dep()、BCR 使用、use_repo_rule()、dual support 与 lockfile noise 的独立迁移笔记。 - Mike Bland,《Migrating to Bazel Modules (a.k.a. Bzlmod) - Module Extensions.》EngFlow Blog,2025-01-16——关于 extension constraints、
load()differences、namespaces 与 modularizing extension code 的独立解释。 - Google Open Source Blog,《BazelCon 2024: A celebration of community and the launch of Bazel 8》,2024 年 12 月——会议语境,以及本文真实 BazelCon 观众照片的来源页。
- Bazel Blog,《BazelCon 2024 Recap: Recordings and Birds of a Feather Session Notes》,2024-11-19——BazelCon 2024 的到场人数与 session recording 语境。