Bazel Lockfile

报告问题 查看源代码 每夜 build · 7.4 . 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。通过捕获软件库的特定版本或依赖项,锁定文件可确保 build 在不同环境中和随着时间的推移可重现。开发者在构建项目时可以获得一致且可预测的结果。

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

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

锁文件内容

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

  1. 作为模块解析输入的所有远程文件的哈希值。
  2. 对于每个模块扩展程序,锁定文件包含影响它的输入(由 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 部分包含模块解析期间访问的远程注册表中的所有文件的哈希值。由于在给定相同输入且所有远程输入都经过哈希处理的情况下,解析算法是完全确定性的,因此这可确保完全可重现的解析结果,同时避免锁文件中远程信息过度重复。请注意,当特定注册表不包含某个模块,但优先级较低的注册表包含该模块时,也需要进行记录(请参阅示例中的“未找到”条目)。这种固有可变的信息可以通过 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 设置返回元数据,模块扩展可以选择不包含在锁定文件中。这样一来,它们便保证在给定相同的输入时,始终会创建相同的代码库。

最佳做法

为了最大限度地发挥 Lockfile 功能的优势,请考虑以下最佳做法:

  • 定期更新锁定文件,以反映项目依赖项或配置的更改。这可确保后续构建基于最新、最准确的一组依赖项。如需一次锁定所有扩展程序,请运行 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 以更新锁定文件。