Bazel Lockfile

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

生成 lockfile

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

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

使用 lockfile

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

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

lockfile 的优势

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

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

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

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

隐藏的 lockfile

Bazel 还在 "$(bazel info output_base)"/MODULE.bazel.lock 处维护另一个 lockfile。此 lockfile 的格式和内容未明确指定。它仅用作性能 优化。虽然可以通过 bazel clean --expunge 将其与输出库一起删除,但任何需要执行此操作的情况都是 Bazel 本身或 模块扩展中的 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,以反映项目依赖项或 配置的更改。这可确保后续 build 基于最新且准确的 依赖项集。如需一次锁定所有扩展 ,请运行 bazel mod deps --lockfile_mode=update

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

  • 使用 bazelisk 运行 Bazel,并在版本控制中添加一个 .bazelversion 文件,该文件指定与 lockfile 对应的 Bazel 版本。由于 Bazel 本身是 build 的依赖项,因此 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。