Bazel 外部依赖系统完全指南:模块、仓库、抓取与 WORKSPACE 迁移
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
Bazel 的外部依赖系统用于引入构建中需要、但又不属于当前工作区的源码(文本与二进制皆可),例如 GitHub 上的规则集、Maven 制品或本机其他目录。本文以 Bazel 文档《External dependencies overview》为骨架,完整讲解模块(module)与仓库(repository)两个基本概念、从MODULE.bazel到注册表解析再到仓库抓取的完整链路、规范名/表面名等关键概念,并结合本仓库(Bazel 自身的构建工程)的真实MODULE.bazel与源码实现进行印证。读完本文,你将能读懂并维护任意 Bazel 项目的MODULE.bazel文件,理解bazel mod、bazel fetch等命令的用途,并掌握从遗留WORKSPACE体系迁移到 Bzlmod 的背景与路径。
系统总览:模块、仓库与工作区
Bazel 的外部依赖系统建立在两个基础概念之上:
- Bazel 模块(module):一个带版本号的 Bazel 项目,一个模块可以有多个版本,每个版本都声明了它对其他模块的依赖。
- 仓库(repository,简称 repo):一棵包含源文件的目录树,其根部有边界标记文件。
Bazel 从**根模块(root module)**出发——也就是你当前正在开发的项目。与所有模块一样,根模块的目录根部必须有一个MODULE.bazel文件,声明其基本元数据与直接依赖。下面是一个最基本的例子:
module(name = "my-module", version = "1.0") bazel_dep(name = "rules_cc", version = "0.1.1") bazel_dep(name = "platforms", version = "0.0.11")从这个文件开始,Bazel 会在Bazel 注册表(registry)中查找所有传递依赖模块——默认是 Bazel Central Registry(BCR)。注册表提供各依赖的MODULE.bazel文件,使 Bazel 能在执行版本解析之前先发现完整的传递依赖图。
完成版本解析(为每个模块选出一个版本)之后,Bazel 再次咨询注册表,学习如何为每个模块定义一个仓库——即每个依赖模块的源码应该如何被获取。大多数情况下,就是"从互联网下载压缩包并解压"。
模块还可以声明自定义的数据片段,称为标签(tags),它们会在模块解析完成后被模块扩展(module extensions)消费,用来定义额外的仓库。模块扩展可以执行文件 I/O、发起网络请求等操作,这使得 Bazel 能在尊重 Bazel 模块依赖图的前提下,与其他包管理系统(如 Maven、PyPI、Go Modules、Cargo)交互。
最终,三类仓库共同组成了工作区(workspace):
- 主仓库:你正在其中工作的源码树;
- 代表各传递依赖模块的仓库;
- 由模块扩展创建的仓库。
外部仓库(非主仓库)是按需抓取的,例如当 BUILD 文件中的标签(如@repo//pkg:target)引用到它们时才会被抓取。
最小示例的完整解析链路
把上面的例子展开,Bazel 实际经历的过程是:
- 读取根模块的
MODULE.bazel,得知直接依赖rules_cc@0.1.1与platforms@0.0.11; - 向注册表请求这两个模块的
MODULE.bazel,发现它们各自的依赖,重复此过程直到整个传递依赖图浮现; - 对每个模块执行版本解析,选出一个版本(详见下文 MVS);
- 再次访问注册表,获取每个选中版本的
source.json等信息,确定用何种仓库规则(通常是http_archive)来定义对应仓库; - 当构建真正引用到某个仓库中的目标时,按需抓取该仓库。
这一条链路正是 Bazel 外部依赖系统"声明式、可复现"的核心:你只需声明直接依赖,其余全部交给系统。
外部依赖系统的收益
自动依赖解析
- 确定性版本解析:Bazel 采用确定性的 MVS(Minimal Version Selection,最小版本选择)算法,该算法源于 Go module 系统,假设所有新版本向后兼容,因此在菱形依赖中总是挑选被依赖方指定的最高版本;例如
A → B → D 1.0与A → C → D 1.1并存时选择D 1.1。它之所以叫"最小",是因为D 1.1是能满足需求的最早版本——即使存在D 1.2或更新版本也不会被选中。MVS 最小化版本冲突、解决菱形依赖问题,并保证解析过程的高保真与可复现。 - 简化依赖管理:
MODULE.bazel只声明直接依赖,传递依赖自动解析,让项目依赖全景更加清晰。 - 严格依赖可见性:只有直接依赖可见,防止传递依赖的意外变化破坏构建,保证正确性与可预测性。详见 模块与仓库名。
生态集成
- Bazel Central Registry(BCR):发现和管理常见依赖的集中式注册表,默认即从它拉取模块。
- 非 Bazel 项目的引入:当一个非 Bazel 项目(通常是 C++ 库)被适配为 Bazel 并发布到 BCR 后,整个社区都能直接以模块方式使用它,消除了各自维护自定义 BUILD 文件的重复劳动与冲突。
- 与各语言包管理器的统一集成:规则集把非 Bazel 依赖与外部包管理器对接,例如:
rules_jvm_external—— Maven;rules_python—— PyPI;bazel-gazelle(配合 rules_go)—— Go Modules;rules_rust—— Cargo。
高级特性
- 模块扩展:
use_repo_rule指令与模块扩展特性,允许灵活地使用自定义仓库规则与解析逻辑,引入任意非 Bazel 依赖。 bazel mod命令:提供强大的外部依赖检视能力,让你确切知道某个外部依赖如何定义、来自哪里。详见 mod-command 文档。- Vendor 模式:预取所需的确切外部依赖,方便离线构建。详见 vendor 文档。
- Lockfile:
MODULE.bazel.lock记录模块解析与扩展求值的结果,提升构建可复现性、加速依赖解析。详见 lockfile 文档。 - (即将推出)BCR Provenance Attestations:通过验证依赖的可信来源增强供应链安全。
核心概念详解
模块(Module)
模块是可以有多个版本的 Bazel 项目,每个版本都声明对其他模块的依赖——类似于 Maven 的artifact、npm 的package、Go 的module或 Cargo 的crate。在本地 Bazel 工作区中,一个模块由一个仓库表示。
关于模块的版本格式、版本选择(MVS)、yanked 版本、各类 override(single_version_override、multiple_version_override、archive_override/git_override/local_path_override)以及use_repo_rule的详细规则,请参阅 模块文档。
仓库(Repository)
仓库是一棵含源文件的目录树,根部有边界标记文件,常简称为 repo。边界标记文件可以是:
MODULE.bazel:表示该仓库代表一个 Bazel 模块;REPO.bazel:见下文专节;- 遗留语境下的
WORKSPACE或WORKSPACE.bazel。
任意边界标记文件都标识着仓库的边界;同一目录下可以共存多个这类文件。
主仓库与工作区
主仓库是当前 Bazel 命令运行所在的仓库,其根目录即工作区根(workspace root)。工作区是在同一个主仓库中运行的所有 Bazel 命令共享的环境,涵盖主仓库与全部已定义的外部仓库。历史上"repository"与"workspace"两个概念经常被混用——"workspace"曾被用来指代主仓库,甚至被当作"repository"的同义词。
规范仓库名(Canonical repository name)
每个仓库在工作区内都有一个始终可寻址的规范名。规范名为canonical_name的仓库中的目标,可以用双@标签@@canonical_name//package:target寻址。主仓库的规范名恒为空字符串。
一个模块对应的仓库的规范名形如<module_name>+<version>(例如bazel_skylib+1.0.3)或<module_name>+(例如bazel_features+),具体取决于依赖图中该模块是否存在多个版本。注意:规范名格式不是你可以依赖的 API,随时可能变化,不应在代码中硬编码。官方推荐的方式包括:
- 在 BUILD 与
.bzl文件中,对以表面名构造的Label使用Label.repo_name(如Label("@bazel_skylib").repo_name); - 查找 runfiles 时使用
$(rlocationpath ...)或@bazel_tools//tools/{bash,cpp,java}/runfiles中的 runfiles 库; - 与 IDE 或语言服务器等外部工具交互时,使用
bazel mod dump_repo_mapping获取给定仓库集合从表面名到规范名的映射。
表面仓库名(Apparent repository name)与仓库映射
表面名是仓库在某个特定其他仓库的语境下可寻址的名字,可理解为该仓库的"昵称":规范名为michael的仓库,在仓库alice的语境下表面名可能是mike,在仓库bob的语境下则可能是mickey。此时,alice中可以用单@标签@mike//package:target寻址michael中的目标。
反过来理解,这正是一张仓库映射(repository mapping):每个仓库维护一张从"表面仓库名"到"规范仓库名"的映射表。模块扩展也能向模块的可见作用域引入额外仓库。
仓库规则(Repository rule)
仓库规则是仓库定义的"模式",告诉 Bazel 如何物化一个仓库,例如"从某 URL 下载 zip 并解压"、"抓取某个 Maven 制品并暴露为java_import目标"或"直接符号链接本地目录"。每个仓库都是通过以适当参数调用一个仓库规则来定义的。最常用的两个仓库规则是:
http_archive:从 URL 下载压缩包并解压;local_repository:符号链接一个已经是 Bazel 仓库的本地目录。
如何编写自定义仓库规则,参见 Repository rules 文档。
抓取仓库(Fetch a repository)
抓取是指运行仓库关联的仓库规则、把仓库物化到本地磁盘的动作。工作区中定义的仓库在抓取前并不存在于本地磁盘。正常情况下,Bazel只在需要仓库中的内容且仓库尚未被抓取时才抓取;若之前已抓取过,则只有当其定义发生变化时才重新抓取。
bazel fetch命令可以预抓取某个仓库、某个目标或执行任意构建所需的全部仓库。这一能力配合--nofetch选项可实现离线构建:
--fetch选项用于管理网络访问,默认值为true;- 当设为
false(即--nofetch)时,命令会使用依赖的缓存版本;若缓存不存在,命令直接失败。
在 FetchCommand.java 中可以看到该命令的实现:它注册了FetchOptions.class选项类,并在--nofetch与fetch同时出现时直接报错("You cannot run fetch with --nofetch",见第 152 行附近),印证了"fetch 命令本身就要求网络访问"这一约束。
抓取后的目录布局
仓库被抓取后,会出现在输出基目录(output base) 下的external子目录中,以规范名命名。可以用以下命令查看规范名为canonical_name的仓库内容:
ls $(bazel info output_base)/external/<canonical_name>REPO.bazel 文件
REPO.bazel文件用来标记构成一个仓库的目录树的最顶层边界。它不需要包含任何内容即可充当边界文件;但它也可以用来为仓库内所有构建目标指定公共属性。
REPO.bazel的语法与 BUILD 文件类似,区别在于不支持load语句。文件中的repo()函数与 BUILD 文件中的package()函数接受相同参数:package()为包内所有构建目标指定公共属性,repo()则相应地为其所在仓库内所有构建目标指定公共属性。例如,为仓库内所有目标指定公共许可证:
repo( default_package_metadata = ["//:my_license"], )在源码层面,REPO.bazel的语法由 RepoFileGlobals.java 与 DotBazelFileSyntaxChecker.java 等实现,而LabelConstants.java等路径常量中定义了REPO.bazel作为边界标记文件的相关逻辑。
实战印证:Bazel 自身的 MODULE.bazel
本仓库根目录的 MODULE.bazel 就是外部依赖系统的真实大样本,几乎所有指令都有出现:
module( name = "bazel", version = "10.0.0-prerelease", repo_name = "io_bazel", )bazel_dep声明直接依赖:例如bazel_dep(name = "rules_cc", version = "0.2.19")、bazel_dep(name = "rules_python", version = "1.7.0"),并可用repo_name参数定制表面名,如bazel_dep(name = "googletest", version = "1.17.0.bcr.2", repo_name = "com_google_googletest")。single_version_override打补丁或固定版本:例如对rules_jvm_external同时指定version = "6.8"与两个 patch 文件(对应 third_party 下的 patch);对grpc-java指定 4 个 patch;对c-ares固定版本。local_path_override指向本地路径:例如local_path_override(module_name = "remoteapis", path = "./third_party/remoteapis"),对应仓库中的 third_party/remoteapis 目录。use_extension使用模块扩展:例如引入rules_jvm_external的maven扩展并调用maven.install(...)声明数百个 Maven 制品、maven.override(...)与maven.artifact(...);引入rules_python的python与pip扩展(pip.parse(hub_name = "bazel_pip_dev_deps", requirements_lock = "//:requirements.txt"));随后用use_repo(maven, "maven")把生成的仓库引入当前模块作用域。use_repo_rule直接调用仓库规则:例如用use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_file")定义jq_linux_amd64等二进制制品(带integrity校验),以及local_repository(name = "kythe_release", path = "/usr/local/kythe")。register_toolchains/register_execution_platforms:注册工具链与执行平台,同样是模块系统的一部分。
文件头部还有一条关键注释:"When editing this file, also update the lockfile.bazel mod deps --lockfile_mode=update"——对应仓库根目录确实存在 MODULE.bazel.lock,这正是 lockfile 机制在真实项目中的落地。
此外,extensions.bzl 是本仓库自定义模块扩展的实现示例:bazel_build_deps = module_extension(implementation = _bazel_build_deps),其中_bazel_build_deps通过ctx.path(Label("//:MODULE.bazel"))确保 MODULE.bazel 变化时相关仓库缓存更新,调用repo_cache_tar、graalvm_repository等仓库规则生成仓库,最后return ctx.extension_metadata(reproducible = True)——这正是 extension 文档 中"指定可复现性"最佳实践的实例。
底层实现速览:bazel mod与抓取机制
外部依赖体系的 CLI 支撑同样有源码可查:
- ModCommand.java 实现了
bazel mod命令族,其执行逻辑位于 ModExecutor.java。后者实现了graph(从根模块展开完整依赖图)、deps、all_paths(展示从--from模块到目标模块的全部依赖路径)、path、explain(展示指定模块在依赖图中出现的所有位置及其直接依赖者)、show_repo、show_extension等子命令(见ModExecutor.java中graph、all_paths等方法的实现,含 BFS 遍历与ResultGraphPruner剪枝逻辑)。 - 模块扩展与锁文件相关的实现集中在 bzlmod 包下,例如 ModuleFileGlobals.java 定义了
MODULE.bazel文件中的可用指令(module、bazel_dep、use_extension、use_repo、single_version_override等),RepositoryFetchFunction.java 负责仓库抓取。
这些源码印证了文档所述的行为:模块解析先行、扩展求值随后、仓库按需抓取。
遗留 WORKSPACE 系统及其局限
在更早的 Bazel 版本(9.0 之前)中,外部依赖通过在WORKSPACE(或WORKSPACE.bazel)文件中定义仓库来引入。该文件语法与 BUILD 文件类似,只不过用的是仓库规则而非构建规则。下面是在WORKSPACE中使用http_archive的示例:
load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") http_archive( name = "foo", urls = ["https://example.com/foo.zip"], sha256 = "c9526390a7cd420fdcec2988b4f3626fe9c5b51e2959f685e8f4d170d1a9bd96", )这段代码定义了一个规范名为foo的仓库。在 WORKSPACE 体系下,仓库的规范名默认同时也是它对所有其他仓库的表面名。
WORKSPACE 体系的缺陷
WORKSPACE 体系推出后的几年里,用户反馈了大量痛点:
- Bazel 不会求值任何依赖的
WORKSPACE文件,因此除直接依赖外,所有传递依赖都必须定义在主仓库的WORKSPACE文件中。 - 为绕开这一点,社区演化出"deps.bzl" 模式:项目定义一个宏,宏内部定义多个仓库,并要求用户在各自的
WORKSPACE中调用该宏。这又带来新问题:- 宏无法
load其他.bzl文件,于是项目要么把传递依赖全部写在这个 "deps" 宏里,要么要求用户层层调用多个 "deps" 宏; - Bazel 顺序求值
WORKSPACE文件,且依赖以不带版本信息的 URL(http_archive)形式指定,因此在菱形依赖(A依赖B与C,而B、C各自依赖不同版本的D)场景下没有可靠的方式进行版本解析。
- 宏无法
正是由于这些缺陷,新的基于模块的体系(代号Bzlmod)在 Bazel 6 到 9 之间逐步取代了遗留 WORKSPACE 体系。迁移指南见 Bzlmod migration 文档,常见问题见 external FAQ。
延伸阅读
- Bazel 模块详解(版本格式、MVS、overrides、仓库名与严格依赖)
- Bazel 注册表(index registry 格式、BCR、--registry 选择)
- 模块扩展(use_extension、tag_class、可见性与最佳实践)
- 仓库规则编写指南
bazel mod命令参考(graph、deps、all_paths、explain 等子命令)- Lockfile 与 --lockfile_mode
- Vendor 模式与离线构建
- 从 WORKSPACE 迁移到 Bzlmod
- 输出目录与 output base
【免费下载链接】bazela fast, scalable, multi-language and extensible build system项目地址: https://gitcode.com/GitHub_Trending/ba/bazel
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考