Bazel Lockfile

报告问题 查看源代码 每夜版 · 8.4 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

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

锁定文件生成

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

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

锁定文件使用情况

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

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

锁定文件的优势

锁定文件具有多项优势,可用于多种用途:

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

  • 快速增量分辨率。锁定文件可让 Bazel 避免下载之前 build 中已使用的注册表文件。这可显著提高构建效率,尤其是在解析可能非常耗时的场景中。

  • 稳定性和风险降低。锁定文件有助于防止外部库中出现意外更新或重大更改,从而保持稳定性。通过将依赖项锁定到特定版本,可以降低引入 bug 的风险

    因不兼容或未经测试的更新而导致的问题。

隐藏的锁定文件

Bazel 还在 "$(bazel info output_base)"/MODULE.bazel.lock 中维护另一个锁定文件。此锁定文件的格式和内容未明确指定。它仅用于优化性能。虽然可以通过 bazel clean --expunge 将其与输出库一起删除,但任何需要这样做的需求都是 Bazel 本身或模块扩展中的 bug。

lockfile 内容

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

  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 本身是 build 的依赖项,因此锁定文件特定于 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 以更新锁定文件。