Bazel Lockfile

7.3 · 7.2 · 7.1 · 7.0 · 6.5

Bazel 中的锁文件功能支持记录项目所需软件库或软件包的特定版本或依赖项。它通过存储模块解析和扩展程序评估的结果来实现这一点。锁文件有助于实现可重现的 build,确保开发环境保持一致。此外,它还允许 Bazel 跳过不受项目依赖项更改影响的解析流程部分,从而提高构建效率。此外,锁文件还可以防止外部库发生意外更新或破坏性更改,从而提高稳定性,并降低引入 bug 的风险。

锁文件生成

系统会在工作区根目录下生成名为 MODULE.bazel.lock 的锁定文件。它在构建流程中创建或更新,特别是在模块解析和扩展评估之后。重要的是,它仅包含 build 的当前调用中包含的依赖项。

当项目发生影响其依赖项的更改时,锁定文件会自动更新以反映新状态。这样可确保锁定文件始终专注于当前 build 所需的一组特定依赖项,从而准确表示项目的已解析依赖项。

锁文件用法

锁文件可以通过标志 --lockfile_mode 进行控制,以便在项目状态与锁文件不同时自定义 Bazel 的行为。可用的模式包括:

  • update(默认):使用锁定文件中的信息跳过已知注册表文件的下载,并避免重新评估结果仍处于最新状态的扩展程序。如果缺少信息,系统会将其添加到锁定文件中。在此模式下,Bazel 还会避免刷新未更改的依赖项的可变信息(例如已撤消的版本)。
  • refresh:与 update 类似,但在切换到此模式时,可变信息始终会刷新,并且在此模式下大约每小时刷新一次。
  • error:与 update 类似,但如果任何信息缺失或过时,Bazel 将会失败并报出错误。此模式在解析期间绝不会更改锁定文件或执行网络请求。自行标记为 reproducible 的模块扩展或许仍可执行网络请求,但应始终产生相同的结果。
  • off:系统未检查或更新锁定文件。

锁文件优势

锁文件具有多种优势,可通过多种方式使用:

  • 可重现的 build。通过捕获软件库的特定版本或依赖项,锁定文件可确保构建在不同环境中和随着时间的推移的可重现性。开发者在构建项目时可以获得一致且可预测的结果。

  • 快速增量解决方案。借助锁定文件,Bazel 可以避免下载之前 build 中已使用的注册表文件。这可以显著提高构建效率,尤其是在解决问题可能需要花费大量时间的情况下。

  • 稳定性和风险降低。锁文件有助于防止外部库发生意外更新或破坏性更改,从而保持稳定性。通过将依赖项锁定到特定版本,可以降低因不兼容或未经测试的更新而引入 bug 的风险。

锁文件内容

锁文件包含确定项目状态是否已更改所需的所有信息。它还包含在当前状态下构建项目的结果。锁文件由两个主要部分组成:

  1. 作为模块解析输入的所有远程文件的哈希值。
  2. 对于每个模块扩展程序,锁定文件包含影响它的输入(由 bzlTransitiveDigestusagesDigest 和其他字段表示),以及运行该扩展程序的输出(称为 generatedRepoSpecs

以下示例展示了锁文件的结构,以及每个部分的说明:

{
  "lockFileVersion": 10,
  "registryFileHashes": {
    "https://bcr.bazel.build/bazel_registry.json": "8a28e4af...5d5b3497",
    "https://bcr.bazel.build/modules/foo/1.0/MODULE.bazel": "7cd0312e...5c96ace2",
    "https://bcr.bazel.build/modules/foo/2.0/MODULE.bazel": "70390338... 9fc57589",
    "https://bcr.bazel.build/modules/foo/2.0/source.json": "7e3a9adf...170d94ad",
    "https://registry.mycorp.com/modules/foo/1.0/MODULE.bazel": "not found",
    ...
  },
  "selectedYankedVersions": {
    "foo@2.0": "Yanked for demo purposes"
  },
  "moduleExtensions": {
    "//:extension.bzl%lockfile_ext": {
      "general": {
        "bzlTransitiveDigest": "oWDzxG/aLnyY6Ubrfy....+Jp6maQvEPxn0pBM=",
        "usagesDigest": "aLmqbvowmHkkBPve05yyDNGN7oh7QE9kBADr3QIZTZs=",
        ...,
        "generatedRepoSpecs": {
          "hello": {
            "bzlFile": "@@//:extension.bzl",
            ...
          }
        }
      }
    },
    "//:extension.bzl%lockfile_ext2": {
      "os:macos": {
        "bzlTransitiveDigest": "oWDzxG/aLnyY6Ubrfy....+Jp6maQvEPxn0pBM=",
        "usagesDigest": "aLmqbvowmHkkBPve05y....yDNGN7oh7r3QIZTZs=",
        ...,
        "generatedRepoSpecs": {
          "hello": {
            "bzlFile": "@@//:extension.bzl",
            ...
          }
        }
      },
      "os:linux": {
        "bzlTransitiveDigest": "eWDzxG/aLsyY3Ubrto....+Jp4maQvEPxn0pLK=",
        "usagesDigest": "aLmqbvowmHkkBPve05y....yDNGN7oh7r3QIZTZs=",
        ...,
        "generatedRepoSpecs": {
          "hello": {
            "bzlFile": "@@//:extension.bzl",
            ...
          }
        }
      }
    }
  }
}

注册表文件哈希值

registryFileHashes 部分包含模块解析期间访问的远程注册表中的所有文件的哈希值。由于在给定相同输入且所有远程输入都经过哈希处理的情况下,解析算法是完全确定性的,因此这可确保完全可重现的解析结果,同时避免锁文件中远程信息过度重复。请注意,当特定注册表不包含特定模块,但优先级较低的注册表包含特定模块时,这也需要记录(请参阅示例中的“未找到”条目)。这种固有可变的信息可以通过 bazel mod deps --lockfile_mode=refresh 进行更新。

Bazel 会使用锁文件中的哈希在代码库缓存中查找注册表文件,然后再下载这些文件,从而加快后续解析速度。

所选的已撤消版本

selectedYankedVersions 部分包含根据模块分辨率选择的模块的拉取版本。由于这通常会导致尝试构建时出错,因此仅当通过 --allow_yanked_versionsBZLMOD_ALLOW_YANKED_VERSIONS 明确允许被提取的版本时,此部分才是非空的。

由于与模块文件相比,已撤消的版本信息本身就是可变的,因此无法通过哈希引用。因此,此字段是必需的。您可以通过 bazel mod deps --lockfile_mode=refresh 更新此信息。

模块扩展

moduleExtensions 部分是一个映射,仅包含当前调用中或之前调用的扩展程序,但排除不再使用的扩展程序。换句话说,如果某个扩展程序在依赖项图中不再使用,则会从 moduleExtensions 映射中移除。

如果扩展程序与操作系统或架构类型无关,则此部分仅包含一个“常规”条目。否则,系统会包含多个条目,这些条目以操作系统和/或架构命名,每个条目都对应于针对这些具体信息评估扩展程序的结果。

扩展程序映射中的每个条目都对应于一个已使用的扩展程序,并通过其包含的文件和名称进行标识。每个条目的对应值包含与该扩展程序相关联的相关信息:

  1. bzlTransitiveDigest 是扩展程序实现及其以传递方式加载的 .bzl 文件的摘要。
  2. usagesDigest 是依赖项图中扩展程序用法(包括所有标记)的摘要。
  3. 用于跟踪扩展程序的其他输入(例如其读取的文件或目录的内容,或其使用的环境变量)的其他未指定字段。
  4. generatedRepoSpecs 会使用当前输入对扩展程序创建的代码库进行编码。
  5. 可选的 moduleExtensionMetadata 字段包含扩展程序提供的元数据,例如根模块是否应通过 use_repo 导入扩展程序创建的特定代码库。这些信息为 bazel mod tidy 命令提供支持。

使用 reproducible = True 设置返回元数据,模块扩展可以选择不包含在锁定文件中。这样一来,它们便保证在给定相同的输入时,始终会创建相同的代码库。

最佳做法

为了最大限度地发挥锁文件功能的好处,请考虑以下最佳实践:

  • 定期更新锁定文件,以反映项目依赖项或配置的更改。这样可确保后续 build 基于最新且准确的一组依赖项。如需一次锁定所有扩展程序,请运行 bazel mod deps --lockfile_mode=update

  • 将锁定文件添加到版本控制中,以便于协作并确保所有团队成员都可以访问同一锁定文件,从而促进整个项目中开发环境的一致性。

  • 使用 bazelisk 运行 Bazel,并在版本控制中添加 .bazelversion 文件,用于指定与锁定文件对应的 Bazel 版本。由于 Bazel 本身是构建的依赖项,因此锁定文件特定于 Bazel 版本,甚至在向后兼容的 Bazel 版本之间也会发生变化。使用 bazelisk 可确保所有开发者使用的 Bazel 版本与锁定文件相匹配。

遵循这些最佳实践,您可以有效利用 Bazel 中的锁文件功能,从而实现更高效、更可靠且更具协作性的软件开发工作流。

合并冲突

锁文件格式旨在尽可能减少合并冲突,但仍可能会发生冲突。

自动解决

Bazel 提供了自定义的 git 合并驱动程序,可帮助自动解决这些冲突。

将以下代码行添加到 Git 代码库根目录下的 .gitattributes 文件,以设置驱动程序:

# A custom merge driver for the Bazel lockfile.
# https://bazel.build/external/lockfile#automatic-resolution
MODULE.bazel.lock merge=bazel-lockfile-merge

然后,每个想要使用该驱动程序的开发者都必须按照以下步骤注册一次:

  1. 安装 jq(1.5 或更高版本)。
  2. 运行以下命令:
jq_script=$(curl https://raw.githubusercontent.com/bazelbuild/bazel/master/scripts/bazel-lockfile-merge.jq)
printf '%s\n' "${jq_script}" | less # to optionally inspect the jq script
git config --global merge.bazel-lockfile-merge.name   "Merge driver for the Bazel lockfile (MODULE.bazel.lock)"
git config --global merge.bazel-lockfile-merge.driver "jq -s '${jq_script}' -- %O %A %B > %A.jq_tmp && mv %A.jq_tmp %A"

手动解决

通过保留冲突双方的所有条目,可以安全地解决 registryFileHashesselectedYankedVersions 字段中的简单合并冲突。

不应手动解决其他类型的合并冲突。相反:

  1. 通过 git reset MODULE.bazel.lock && git checkout MODULE.bazel.lock 恢复锁文件的先前状态。
  2. 解决 MODULE.bazel 文件中的所有冲突。
  3. 运行 bazel mod deps 以更新锁定文件。