Bazel 学习记录

2025-1-5|2025-1-5
FollyCoolly
FollyCoolly
type
status
date
slug
summary
tags
category
icon
password
 

概念

Repositories

Bazel 构建中使用的源文件组织在存储库(repositories)中(通常缩写为 repos)。一个 repo 是一个目录树,其根目录有一个边界标记文件。
这样的边界标记文件可以是 MODULE.bazel、REPO.bazel,或者在旧版中是 WORKSPACE 或 WORKSPACE.bazel。

Workspace

工作区(workspace)是所有从同一个主存储库运行的 Bazel 命令共享的环境。它包含主存储库和所有定义的外部存储库集合。
历史上“存储库”和“工作区”的概念经常被混淆;“工作区”一词通常用于指代主存储库,有时甚至用作“存储库”的同义词。

Packages

包(package)是存储库中代码组织的基本单位。包是相关文件的集合,并且规定了如何使用这些文件来生成输出工件(artifacts)。 包被定义为包含一个名为 BUILD 或 BUILD.bazel 的 BUILD 文件的目录。包包括其目录中的所有文件,以及其下的所有子目录,除了那些本身包含 BUILD 文件的子目录。根据这个定义,任何文件或目录都不能属于两个不同的包。

Targets

包中含有目标,这些目标在包的 BUILD 文件中定义。
大多数目标分为两种主要类型:文件和规则。
  • 文件(file)
    • 文件进一步分为两种类型。
    • 源文件(source files)通常由人们编写,并签入(checkin)存储库。
    • 生成的文件(generated files),有时称为派生文件或输出文件,不会签入,而是从源文件生成。
  • 规则(rule)
    • 每个规则实例指定一组输入文件和一组输出文件之间的关系。规则的输入可以是源文件,但也可以是其他规则的输出文件。规则的输入也可以包括其他规则。
    • 一个 C++ 库规则 A 可能将另一个 C++ 库规则 B 作为输入。这种依赖关系的效果是 B 的头文件在编译期间对 A 可用,B 的符号在链接期间对 A 可用,B 的运行时数据在执行期间对 A 可用。
    • 规则生成的文件始终属于与规则本身相同的包;不可能将文件生成到另一个包中。不过,规则的输入来自另一个包的情况并不少见。
    • 可以用包组(package groups)来限制某些规则的可访问性。

Label

标签(Label)是目标的标识符。 一个典型的标签在其完整的规范形式中看起来像这样:@@myrepo//my/app/main:app_binary
  • 标签的第一部分是存储库名称 @@myrepo。双 @ 语法表示这是一个规范的存储库名称,在工作区内是唯一的。在典型情况下,标签引用的是它所使用的同一个存储库,因此存储库名称部分可以省略。
  • 标签的第二部分是未限定的包名称 my/app/main,这是相对于存储库根目录的包路径。存储库名称和未限定的包名称一起构成了完全限定的包名称 @@myrepo//my/app/main
  • 标签中的冒号后面的部分 app_binary 是未限定的目标名称。当它与包路径的最后一个组件匹配时,它和冒号可以省略。由于这条规则,根据上下文不同,//my/app/lib可能指的是包(例如在 package_group 规范中),也可能指的是包中的一个同名目标(例如在 BUILD 文件中)。

Rules

规则指定了输入和输出之间的关系以及构建输出的步骤。规则可以是多种不同类型之一(有时称为规则类),这些规则生成编译的可执行文件和库、测试可执行文件以及其他支持的输出。
BUILD 文件通过调用规则来声明目标。 每个规则都有一组属性。
  • 每个规则调用都有一个 name 属性(必须是有效的目标名称),它在 BUILD 文件的包内声明一个目标。
  • 许多规则中存在的 srcs 属性的类型是“标签列表”;如果存在,其值是一个标签列表,每个标签都是该规则的输入目标的名称。

Extension

Bazel 扩展(Extension)是以 .bzl 结尾的文件。使用 load 语句从扩展中导入符号。
在 .bzl 文件中,以 _ 开头的符号不会导出,不能从另一个文件加载。
可以使用 load 可见性来限制谁可以加载 .bzl 文件。

Dependencies

actual dependencies

目标 X 实际上依赖于目标 Y,如果 Y 必须存在、构建并保持最新,以便 X 能够正确构建

declared dependencies

目标 X 对目标 Y 有声明的依赖关系,如果在 X 的包中存在从 X 到 Y 的依赖边。
为了正确构建,实际依赖图 A 必须是声明依赖图 D 的子图。
如果实际依赖图 A 不是声明依赖图 D 的子图,可能也能构建成功(可能声明了间接依赖,并在实际使用中直接依赖),但是会掩盖实际的依赖关系。

三种常见依赖关系

大多数构建规则有三个属性,用于指定不同类型的通用依赖关系:srcsdepsdata
  • secs:由规则直接消耗的文件或输出源文件的规则。
  • deps:指向提供头文件、符号、库、数据等的单独编译模块的规则。
  • data:构建目标可能需要一些数据文件才能正确运行。这些数据文件不是源代码:它们不会影响目标的构建方式。
    • 例如,单元测试可能会将函数的输出与文件的内容进行比较。当你构建单元测试时,你不需要该文件,但在运行测试时你需要它。同样的情况适用于在执行期间启动的工具。
    • 构建系统在一个隔离的目录中运行测试,其中只有列为数据的文件可用。因此,如果二进制文件/库/测试需要一些文件才能运行,请在 data 中指定它们。

Visibility

Bazel 有两种可见性系统:目标可见性和加载可见性。

Target visibility

目标可见性控制谁可以依赖你的目标——也就是说,谁可以在属性(如 deps)中使用你的目标标签。如果违反了其依赖项的可见性,目标将在分析阶段构建失败。

Load visibility

加载可见性控制是否可以从当前包外的其他 BUILD 或 .bzl 文件加载 .bzl 文件。
与目标可见性保护由目标封装的源代码一样,加载可见性保护由 .bzl 文件封装的构建逻辑。

Tips

在 package_group 规范或 .bzl 文件中使用 //my/app 来引用包是被鼓励的,因为它清楚地表明包名称是绝对的,并且根植于工作区的顶级目录。 相对标签不能用于引用其他包中的目标;在这种情况下,必须始终指定存储库标识符和包名称。 以 @@// 开头的标签是对主存储库的引用,即使在外部存储库中也能正常工作。因此,当从外部存储库引用时,@@//a/b/c//a/b/c 是不同的。前者指向主存储库,而后者在外部存储库中查找 //a/b/c。这在编写主存储库中引用主存储库目标的规则并将其用于外部存储库时尤其重要。 虽然 Bazel 支持工作区根包中的目标(例如,//:foo),但最好保持该包为空,以便所有有意义的包都有描述性的名称。 BUILD 文件可以命名为 BUILD 或 BUILD.bazel。如果两个文件都存在,BUILD.bazel 优先于 BUILD。 为了鼓励代码和数据之间的清晰分离,BUILD 文件不能包含函数定义、for 语句或 if 语句(但允许列表推导和 if 表达式)。函数可以在 .bzl 文件中声明。此外,BUILD 文件中不允许使用 *args **kwargs 参数;相反,显式列出所有参数。
 
冗余的声明依赖关系会使构建变慢并使二进制文件变大。BUILD 文件编写者必须明确声明每个规则的所有实际直接依赖关系,并且不多不少。
 
你不需要(也不应该)尝试列出所有间接导入的内容,即使它在执行时是 A 所需要的。
 
当你指定一个目录时,构建系统仅在目录本身发生变化(由于文件的添加或删除)时执行重建,但无法检测到对单个文件的编辑,因为这些更改不会影响包含目录。 与其将目录指定为构建系统的输入,不如枚举其中包含的文件集,可以显式地或使用 glob() 函数。(使用 ** 强制 glob() 递归。) 如果必须使用目录标签,请记住不能使用相对 ../ 路径引用父包;相反,使用绝对路径。 要使多个目标对同一组包可见,请使用 package_group,而不是在每个目标的 visibility 属性中重复列表。这增加了可读性,并防止列表不同步。 在授予另一个团队项目的可见性时,优先使用 __subpackages__ 而不是 __pkg__,以避免随着项目的发展和添加新子包而产生不必要的可见性变动。 避免将 default_visibility 设置为 public。虽然这在原型设计或小型代码库中可能很方便,但随着代码库的增长,无意中创建公共目标的风险也会增加。明确哪些目标是包的公共接口的一部分会更好
visibility() 的调用每个文件只能出现一次,在顶层(不在函数内),并且理想情况下紧跟在 load() 语句之后。
与目标可见性不同,默认的加载可见性始终是公共的。未调用 visibility() 的文件始终可以从工作区的任何位置加载。对于不专门用于包外使用的新 .bzl 文件,在顶部添加 visibility("private") 是个好主意。
 

封闭式构建(hermetic build)

构建的封闭性确保在给定相同的输入源代码和产品配置时,构建系统通过将构建与主机系统的更改隔离开来,总是返回相同的输出。这种隔离使构建对本地或远程主机上安装的库和其他软件不敏感,而是依赖于特定版本的构建工具和依赖项。

封闭构建的好处

  1. 速度:可以缓存操作的输出,除非输入发生变化,否则不需要再次运行操作。
  1. 并行执行:对于给定的输入和输出,构建系统可以构建所有操作的图,以计算高效的并行执行。
  1. 多次构建:可以在同一台机器上进行多次封闭构建,每次构建使用不同的工具和版本。
  1. 可重现性:封闭构建有助于故障排除,因为你知道生成构建的确切条件。
 
静态类型和动态类型、强类型和弱类型,还有Python 的泛型2025年的一些FLAG
Loading...