Bazel Lockfile

报告问题 查看源代码

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

生成锁文件

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

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

锁定文件使用情况

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

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

Lockfile 的优势

Lockfile 具有多种优势,可以通过多种方式加以利用:

  • 可重现的构建操作。通过捕获软件库的特定版本或依赖项,lockfile 可以确保构建在不同环境中以及随时间的推移能够重现。开发者在构建项目时可以依靠一致且可预测的结果。

  • 快速增量分辨率。利用 Lockfile,Bazel 可以避免下载已在先前 build 中使用的注册表文件。这可显著提高构建效率,尤其是在解析可能非常耗时的情况下。

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

锁定文件内容

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

  1. 作为模块分辨率输入的所有远程文件的哈希值。
  2. 对于每个模块扩展程序,lockfile 包含对其产生影响的输入(由 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 命令提供支持。

模块扩展可以选择不包含在 lockfile 中,方法是使用 reproducible = True 设置返回元数据。这样做可以保证,在给定相同的输入时,始终会创建相同的代码库。

最佳实践

为了充分利用锁定文件功能的优势,请考虑以下最佳实践:

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

  • 在版本控制中包含锁定文件,以促进协作,并确保所有团队成员都能访问相同的锁定文件,从而在整个项目中实现一致的开发环境。

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

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

合并冲突

锁定文件格式旨在最大限度地减少合并冲突,但也仍然会发生合并冲突。

自动分辨率

Bazel 提供了一个自定义 git merge 驱动程序,可帮助您自动解决这些冲突。

将下面这行代码添加到 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 以更新锁定文件。