Bazel Lockfile

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

lockfile 生成

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

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

lockfile 用法

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

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

lockfile 的优势

lockfile 具有多项优势,并且可以多种方式使用:

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

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

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

lockfile 内容

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

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

以下示例展示了 lockfile 的结构,并对每个部分进行了说明:

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

Bazel 在下载注册表文件之前,会使用 lockfile 中的哈希值在 代码库缓存中查找注册表文件,这有助于加快后续 解析速度。

选定的撤消版本

selectedYankedVersions 部分包含模块解析选择的模块的撤消版本 。由于这通常会在尝试构建时导致错误 ,因此仅当通过 --allow_yanked_versionsBZLMOD_ALLOW_YANKED_VERSIONS 明确允许撤消版本时,此部分才不为空。

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

模块扩展

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

如果某个扩展与操作系统或架构类型无关, 则此部分仅包含一个“general”条目。否则,系统会包含多个 条目,这些条目以操作系统、架构或两者命名,每个 条目都对应于在这些特定项上评估扩展的结果。

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

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

模块扩展可以通过将 返回的元数据设置为 reproducible = True,选择不包含在 lockfile 中。这样做时,它们会承诺 在给定相同输入的情况下始终创建相同的代码库。

最佳实践

如需最大限度地发挥 lockfile 功能的优势,请考虑以下最佳 实践:

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

  • 将 lockfile 纳入版本控制,以方便协作并 确保所有团队成员都可以访问相同的 lockfile,从而在整个项目中实现一致的开发环境。

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

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

合并冲突

lockfile 格式旨在最大限度地减少合并冲突,但冲突仍有可能 发生。

自动解决

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 恢复 lockfile 的先前状态 。
  2. 解决 MODULE.bazel 文件中的任何冲突。
  3. 运行 bazel mod deps 以更新 lockfile。