Bazel 模块

报告问题 查看源代码

Bazel 模块是一种可以有多个版本的 Bazel 项目,每个版本都会发布有关其所依赖的其他模块的元数据。这类似于其他依赖项管理系统(例如 Maven 工件、npm 软件包、Go 模块或 Cargo crate)中熟悉的概念。

模块的代码库根目录中必须有一个 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 模块版本。此外,当且仅当相同的保全与 Bazel 模块版本进行比较时,SemVer 版本 ab 才会比较 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.xA 2.x 被视为不同的模块,并且可以共存于已解析的依赖关系图中。反过来,这又是通过对 Go 软件包路径中的主要版本进行编码来实现的,因此不存在任何编译时或链接时冲突。

但是,Bazel 无法提供此类保证,因此它需要“主要版本号”才能检测向后不兼容的版本。此数字称为“兼容性级别”,由每个模块版本在其 module() 指令中指定。有了这些信息,Bazel 检测到已解析的依赖关系图中存在具有不同兼容性级别的同一模块的版本时,可能会抛出错误。

覆盖对象

MODULE.bazel 文件中指定替换项,以更改 Bazel 模块解析的行为。只有根模块的替换项才会生效 - 将模块用作依赖项时,其替换项会被忽略。

每个替换项都针对特定模块名称指定,这会影响依赖关系图中该模块的所有版本。虽然只有根模块的替换项才会生效,但它们可以是根模块不直接依赖的传递依赖项。

单个版本替换

single_version_override 有多种用途:

  • 使用 version 属性,您可以将依赖项固定到特定版本,而不考虑在依赖项图中请求了哪个版本的依赖项。
  • 借助 registry 属性,您可以强制此依赖项来自特定的注册表,而不是遵循常规的注册表选择流程。
  • 借助 patch* 属性,您可以指定一组要应用于已下载模块的补丁。

这些属性都是可选的,并且可以相互混用和匹配。

多版本覆盖

可以指定 multiple_version_override,以允许同一模块的多个版本在已解析的依赖关系图中共存。

您可以为模块明确指定允许的版本列表,在解析之前,这些版本必须全部出现在依赖关系图中 - 必须存在某个传递依赖项,具体取决于每个允许使用的版本。问题解决后,只会保留该模块所允许的版本,而 Bazel 会将该模块的其他版本升级到相较于相同兼容性级别且最接近的更高版本。如果在同一兼容性级别不存在所允许的更高版本,Bazel 会抛出错误。

例如,如果依赖项图表中的版本 1.11.31.51.72.0 早于分辨率,且主要版本是兼容性级别:

  • 允许 1.31.72.0 的多版本替换会导致 1.1 升级到 1.31.5 升级到 1.7,而其他版本保持不变。
  • 允许 1.52.0 的多版本替换会导致错误,因为 1.7 没有可升级到相同兼容性级别的更高版本。
  • 允许 1.92.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.repo_nameLabelLabel("@bazel_skylib").repo_name。* 查找 runfile 时,使用 $(rlocationpath ...)@bazel_tools//tools/{bash,cpp,java}/runfiles 中的某个 runfiles 库;对于规则集 rules_foo,请使用 @rules_foo//foo/runfiles 中的一个。 * 通过 IDE 或语言服务器等外部工具与 Bazel 交互时,使用 bazel mod dump_repo_mapping 命令获取一组给定代码库的明显名称到规范名称的映射。

模块扩展还可以在模块的可见范围内引入其他代码库。