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 由两个主要部分组成:
- 作为模块解析输入的所有远程文件的哈希值。
- 对于每个模块扩展,lockfile 都包含影响它的输入,
由
bzlTransitiveDigest、usagesDigest和其他字段表示,以及 运行该扩展的输出(称为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_versions 或
BZLMOD_ALLOW_YANKED_VERSIONS 明确允许撤消版本时,此部分才不为空。
由于与模块文件相比,撤消版本信息
是固有可变的,因此无法通过哈希值引用,因此需要此字段。此信息
可以通过 bazel mod deps --lockfile_mode=refresh 进行更新。
模块扩展
moduleExtensions 部分是一个映射,其中仅包含当前调用或之前调用中使用的扩展
,而不包含任何不再使用的扩展
。换句话说,如果某个扩展在整个依赖项图中不再使用
,则会从 moduleExtensions
映射中移除。
如果某个扩展与操作系统或架构类型无关, 则此部分仅包含一个“general”条目。否则,系统会包含多个 条目,这些条目以操作系统、架构或两者命名,每个 条目都对应于在这些特定项上评估扩展的结果。
扩展映射中的每个条目都对应于一个使用的扩展,并由其包含文件和名称标识。每个 条目的相应值都包含与该扩展关联的相关信息:
bzlTransitiveDigest是扩展实现 及其以传递方式加载的 .bzl 文件的摘要。usagesDigest是依赖项图中扩展的 用法的摘要,其中包括所有标记。- 其他未指定的字段,用于跟踪扩展的其他输入, 例如它读取的文件或目录的内容,或它使用的环境变量。
generatedRepoSpecs使用当前输入对 扩展创建的代码库进行编码。- 可选的
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
然后,每个想要使用该驱动程序的开发者都必须按照以下步骤注册一次:
- 安装 jq(1.5 或更高版本)。
- 运行以下命令:
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"
手动解决
通过保留冲突双方的所有条目,可以安全地解决 registryFileHashes 和 selectedYankedVersions
字段中的简单合并冲突。
其他类型的合并冲突不应手动解决。相反:
- 通过
git reset MODULE.bazel.lock && git checkout MODULE.bazel.lock恢复 lockfile 的先前状态 。 - 解决
MODULE.bazel文件中的任何冲突。 - 运行
bazel mod deps以更新 lockfile。