Bazel 模块是指可以有多个版本的 Bazel 项目,每个版本都会发布有关其所依赖的其他模块的元数据。这类似于其他依赖项管理系统(如 Maven 工件、npm 软件包、Go 模块或 Cargo crate)中的熟悉概念。
模块的 Repo 根目录(WORKSPACE
文件旁边)必须有一个 MODULE.bazel
文件。此文件是模块的清单,声明了其名称、版本、直接依赖项列表和其他信息。下面是一个基本示例:
module(name = "my-module", version = "1.0")
bazel_dep(name = "rules_cc", version = "0.0.1")
bazel_dep(name = "protobuf", version = "3.19.0")
请参阅 MODULE.bazel
文件中可用指令的完整列表。
为了执行模块解析,Bazel 会先读取根模块的 MODULE.bazel
文件,然后反复从 Bazel 注册表中请求任何依赖项的 MODULE.bazel
文件,直到找到整个依赖关系图。
默认情况下,Bazel 随后会选择要使用的每个模块的一个版本。Bazel 表示具有代码库的每个模块,并再次查询注册表以了解如何定义每个代码库。
版本格式
Bazel 拥有多元化的生态系统,项目使用各种版本控制方案。迄今为止最受欢迎的是 SemVer,但也有一些采用不同架构的知名项目,例如 Abseil,其版本基于日期,例如 20210324.2
。
因此,Bzlmod 采用了更为宽松的 SemVer 规范版本。区别包括:
- SemVer 规定,版本的“发布”部分必须包含以下 3 个细分:
MAJOR.MINOR.PATCH
。在 Bazel 中,此要求放宽了 允许任意数量的细分 - 在 SemVer 中,“发布”部分中的每个片段都只能是数字。 在 Bazel 中,这已放宽,也允许使用字母,并且比较语义与“预发布”部分中的“标识符”相匹配。
- 此外,不会强制执行主要、次要和补丁版本递增的语义。不过,请参阅兼容性级别,详细了解我们如何表示向后兼容性。
任何有效的 SemVer 版本都是有效的 Bazel 模块版本。此外,当且仅当相同的 SemVer 版本 a
和 b
与 Bazel 模块版本进行比较时,这两个 SemVer 版本才会比较 a < b
。
版本选择
考虑菱形依赖项问题,这是版本化依赖项管理空间中的一个重要问题。假设您有以下依赖关系图:
A 1.0
/ \
B 1.0 C 1.1
| |
D 1.0 D 1.1
应使用哪个版本的 D
?为解决此问题,Bzlmod 使用 Go 模块系统中引入的最低版本选择 (MVS) 算法。MVS 会假定模块的所有新版本均向后兼容,因此会选择任何依赖项(在我们的示例中为 D 1.1
)指定的最高版本。之所以称之为“最小”,是因为 D 1.1
是能够满足我们要求的最低版本 - 即使存在 D 1.2
或更高版本,我们也不会选择它们。使用 MVS 可以创建一个高保真且可重现的版本选择过程。
被取消的版本
如果应避免某些版本(例如针对安全漏洞),则注册表可以将其声明为“被抢劫”。在选择被提取的模块版本时,Bazel 会抛出错误。如需修复此错误,请升级到未拉取的较新版本,或使用 --allow_yanked_versions
标志明确允许被拉取的版本。
兼容性级别
在 Go 中,MVS 关于向后兼容性的假设是可行的,因为它将向后不兼容的模块版本视为单独的模块。就 SemVer 而言,这意味着 A 1.x
和 A 2.x
被视为不同的模块,并且可以在已解析的依赖关系图中共存。这反过来又可以通过在 Go 中对软件包路径中的主要版本进行编码来实现,从而避免出现任何编译时或链接时冲突。
但是,Bazel 无法提供此类保证,因此它需要“主要版本号”来检测向后不兼容的版本。此数字称为兼容性级别,由每个模块版本在其 module()
指令中指定。有了这些信息,如果 Bazel 检测到已解析的依赖项图中存在具有不同兼容性级别的同一模块的版本,则可能会抛出错误。
覆盖对象
在 MODULE.bazel
文件中指定替换项,以更改 Bazel 模块解析的行为。只有根模块的替换项才会生效 - 如果某个模块用作依赖项,其替换项会被忽略。
每个替换项都是为特定模块名称指定的,这会影响依赖项图中的所有版本。虽然只有根模块的替换项才会生效,但它们适用于根模块不直接依赖的传递依赖项。
单个版本替换
single_version_override
有多种用途:
- 使用
version
属性,您可以将依赖项固定到特定版本,无论依赖关系图中请求的是哪个版本的依赖项。 - 借助
registry
属性,您可以强制此依赖项来自特定注册表,而不是遵循常规的注册表选择流程。 - 借助
patch*
属性,您可以指定一组要应用于所下载模块的补丁。
这些属性均为可选属性,可以相互混合和匹配。
多版本替换
可以指定 multiple_version_override
,以允许同一模块的多个版本在已解析的依赖关系图中共存。
您可以为模块指定明确的允许版本列表,该列表必须全部存在于依赖关系图中,才能解析;必须存在一些传递依赖项,具体取决于每个允许的版本。解决后,只有允许的模块版本会保留下来,而 Bazel 的其他版本会将该模块升级到在同一兼容性级别且允许使用的更接近的版本。如果同一兼容性级别没有更高但允许的版本,则 Bazel 会抛出错误。
例如,如果依赖项图在解析之前存在版本 1.1
、1.3
、1.5
、1.7
和 2.0
,并且主要版本是兼容性级别:
- 允许
1.3
、1.7
和2.0
的多版本替换会导致1.1
升级到1.3
,1.5
升级到1.7
,而其他版本保持不变。 - 允许
1.5
和2.0
的多版本替换会导致错误,因为1.7
没有相同兼容性级别的更高版本可供升级。 - 允许
1.9
和2.0
的多版本替换会导致错误,因为在解析之前,依赖关系图中不存在1.9
。
此外,与单版本替换类似,用户还可以使用 registry
属性替换注册表。
非注册表覆盖
非注册表替换会从版本解析中完全移除模块。Bazel 不是从注册表请求这些 MODULE.bazel
文件,而是从代码库本身请求。
Bazel 支持以下非注册表替换项:
定义不代表 Bazel 模块的代码库
借助 bazel_dep
,您可以定义代表其他 Bazel 模块的代码库。有时,需要定义的代码库不代表 Bazel 模块;例如,包含要作为数据读取的普通 JSON 文件的代码库。
在这种情况下,您可以使用 use_repo_rule
指令,通过调用 Repo 规则直接定义代码库。此代码库仅对定义它的模块可见。
从本质上讲,这是使用与模块扩展相同的机制实现的,可让您更灵活地定义代码库。
代码库名称和严格依赖项
将模块支持到其直接依赖项的代码库的表观名称默认为其模块名称,除非 bazel_dep
指令的 repo_name
属性另有说明。请注意,这意味着模块只能找到其直接依赖项。这有助于防止因传递依赖项发生变化而导致的意外中断。
支持某个模块的代码库的规范名称为 module_name~version
(例如 bazel_skylib~1.0.3
)或 module_name~
(例如 bazel_features~
),具体取决于整个依赖关系图中是否存在该模块的多个版本(请参阅 multiple_version_override
)。请注意,规范名称格式不是您应该依赖的 API,并且可能会随时更改。您无需对规范名称进行硬编码,而是使用受支持的方式直接从 Bazel 获取规范名称:
* 在 BUILD 和 .bzl
文件中,对通过代码库名称提供的标签字符串构造的 Label
实例使用 Label.repo_name
,例如Label("@bazel_skylib").repo_name
。* 在查找 runfile 时,在 @bazel_tools//tools/{bash,cpp,java}/runfiles
中使用 $(rlocationpath ...)
或其中一个 runfile 库;对于规则集 rules_foo
,可以在 @rules_foo//foo/runfiles
中使用其中一个 runfile 库。
* 从 IDE 或语言服务器等外部工具与 Bazel 进行交互时,请使用 bazel mod dump_repo_mapping
命令获取给定代码库集从显而易见名称到规范名称的映射。
模块扩展还可以在模块的可见范围内引入其他代码库。