Bazel 代码库

报告问题 查看来源 每晚 · 7.2。 · 7.1敬上 · 7.0 · 6.5 · 6.4

本文档介绍了代码库以及 Bazel 的结构。它 适用于愿意为 Bazel 做贡献的人,而不是最终用户。

简介

Bazel 的代码库非常庞大(大约 350,000 份生产代码和约 260 KLOC 测试) 没有人熟悉整个环境:每个人都知道 但很少有人知道每个山谷都有些什么 。

为了让用户在旅程中途不会发现自己 森林的黑暗和迷失的直接道路, 简要介绍代码库,以便轻松上手 处理方法。

Bazel 源代码的公开版本位于 GitHub 上,网址为 github.com/bazelbuild/bazel。这不是 “可信来源”;它源自 Google 内部的源代码树, 包含对 Google 外部无用的其他功能。通过 长期目标是使 GitHub 成为可信来源。

通过常规 GitHub 拉取请求机制接受贡献内容, 然后由 Google 员工手动导入到内部源代码树 重新导出到 GitHub 中。

客户端/服务器架构

大部分 Bazel 位于一个服务器进程中,该进程在两次构建之间保留在 RAM 中。 这样,Bazel 可以在构建之间维持状态。

这就是 Bazel 命令行有两种选项的原因:启动和 命令。在类似如下的命令行中:

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

部分选项 (--host_jvm_args=) 位于要运行的命令的名称之前 还有一些在 (-c opt) 之后;前一种选项称为“启动选项”和 会影响整个服务器进程,而后一种进程是指“命令” 此选项只会影响一个命令。

每个服务器实例都有一个关联的源代码树(“工作区”),每个 工作区通常只有一个活跃的服务器实例。这可以规避 方法是指定自定义输出库(如需了解详情,请参阅“目录布局”部分) 信息)。

Bazel 作为单个 ELF 可执行文件进行分发,该可执行文件也是有效的 .zip 文件。 当您输入 bazel 时,上述 ELF 可执行文件会使用 C++( “client”)获得控制权。它使用 操作步骤:

  1. 检查是否已自行提取。否则,应用会这样做。这个 是实现服务器的源头。
  2. 检查是否有正常运行的活跃服务器实例:正在运行、 它具有正确的启动选项并使用正确的工作区目录。它 通过查看目录 $OUTPUT_BASE/server 找到正在运行的服务器 其中有一个锁文件,其中包含服务器正在监听的端口。
  3. 根据需要终止旧服务器进程
  4. 根据需要启动新的服务器进程

在合适的服务器进程准备就绪后,需要运行的命令就是 则 Bazel 的输出将通过 终端。一次只能运行一个命令。这是 使用复杂的锁定机制实现,其中部分采用 C++,部分采用 Java。有一些用于并行运行多个命令的基础架构, 因为无法与另一个命令并行运行 bazel version 有些尴尬主要障碍是 BlazeModule 的生命周期 以及 BlazeRuntime 中的某个状态。

命令结束时,Bazel 服务器将退出代码发送给客户端 应返回的值。bazel run 的实现有一个有趣的小差异: 此命令的作用是运行 Bazel 刚刚构建的内容,但无法做到 因为它没有终端相反,它告诉 客户端它应该 ujexec() 的二进制文件以及使用哪些参数。

按下 Ctrl-C 时,客户端会将其转换为 gRPC 上的“取消”调用 从而尝试尽快终止该命令。在 第三个 Ctrl-C,则客户端会改为向服务器发送 SIGKILL。

该客户端的源代码位于 src/main/cpp 下,并且用于 在 src/main/protobuf/command_server.proto 中与服务器通信。

服务器的主要入口点是 BlazeRuntime.main(),gRPC 调用 由 GrpcServerImpl.run() 处理。

目录布局

Bazel 会在构建期间创建一组有些复杂的目录。完整 description 在输出目录布局中提供。

“工作区”表示运行 Bazel 的源代码树。它通常对应于 从源代码控制工具中签出的内容

Bazel 将其所有数据放在“输出用户根目录”下。这通常是 $HOME/.cache/bazel/_bazel_${USER},但可使用 --output_user_root 启动选项。

“安装量”将 Bazel 解压到其中此操作由系统自动完成 每个 Bazel 版本都会根据其在 Bazel 目录下的校验和 安装量。默认位置为 $OUTPUT_USER_ROOT/install,您可更改 使用 --install_base 命令行选项。

“输出基准”是将 Bazel 实例挂接到 工作区写入内容。每个输出库至多有一个 Bazel 服务器实例 运行状态通常在$OUTPUT_USER_ROOT/<checksum of the path to the workspace>。您可以使用 --output_base 启动选项进行更改, 这样做的好处之一就是,您可以避免 在任何给定时间,一个 Bazel 实例可以在任何工作区中运行。

输出目录包含的内容包括但不限于:

  • 提取的位于 $OUTPUT_BASE/external 的外部代码库。
  • exec 根目录,一个目录,其中包含指向所有源代码的符号链接 当前 build 的代码。它位于 $OUTPUT_BASE/execroot。中 构建时,工作目录为 $EXECROOT/<name of main repository>。我们计划将其更改为$EXECROOT,尽管 长期计划,因为这项更改是不可行的。
  • 在构建期间构建的文件。

执行命令的过程

一旦 Bazel 服务器获得控制权并获知执行所需的命令, 会发生以下一系列事件:

  1. 通知 BlazeCommandDispatcher 有新请求。它会 是否需要在工作区中运行该命令(几乎所有命令 与源代码无关的查询,例如版本或 帮助)以及是否正在运行其他命令。

  2. 找到了正确的命令。每个命令都必须实现 BlazeCommand,并且必须具有 @Command 注解(这有点像 反模式,但如果命令需要的所有元数据都是 由 BlazeCommand 上的方法描述)

  3. 系统会解析命令行选项。每个命令都有不同的命令行 选项,详见 @Command 注解。

  4. 创建了一个事件总线。事件总线是已发生的事件的数据流 构建容器。其中一些会导出到 构建事件协议 。

  5. 由命令获得控制权。最有趣的命令是运行 build:build、test、run、Coverage 等:此功能 由 BuildTool 实现。

  6. 系统会解析命令行上的目标模式集, //pkg:all//pkg/... 已解决。这是在 AnalysisPhaseRunner.evaluateTargetPatterns(),并在 Skyframe 中以如下形式具体化为 TargetPatternPhaseValue

  7. 运行加载/分析阶段以生成动作图( 需要为构建执行的命令的无环图)。

  8. 运行执行阶段。这意味着,您需要执行 构建所请求的顶级目标。

命令行选项

有关 Bazel 调用的命令行选项,请参阅 OptionsParsingResult 对象,该对象反过来又包含“option”中的地图 类别”使用这些选项的值。“选项类”是 OptionsBase 会将与每个参数相关的命令行选项归为一组 其他。例如:

  1. 与编程语言相关的选项(CppOptionsJavaOptions)。 它们应该是 FragmentOptions 的子类,并最终封装 转换为 BuildOptions 对象。
  2. 与 Bazel 执行操作的方式相关的选项 (ExecutionOptions)

这些选项设计为在分析阶段使用,并且( 通过 Java 中的 RuleContext.getFragment() 或 Starlark 中的 ctx.fragments)。 其中部分命令(例如,C++ 是否包含扫描)会被读取 在执行阶段,但这始终需要显式管道,因为 那时无法使用BuildConfiguration。有关详情,请参阅 “配置”部分

警告:我们喜欢假设 OptionsBase 实例不可变,并且 以这种方式使用它们(例如 SkyKeys 的一部分)。事实并非如此, 修改它们是很好的破坏 Bazel 的微妙方式, 调试。遗憾的是,要使它们实际上不可变是一项艰巨的努力。 (在施工后立即修改FragmentOptions,必须早于其他任何人修改 有机会在 equals()hashCode() 之前保留对它的引用 也没关系)

Bazel 通过以下方式了解选项类:

  1. 部分硬连接了 Bazel (CommonCommandOptions)
  2. 通过每个 Bazel 命令上的 @Command 注解
  3. ConfiguredRuleClassProvider 开始(这些是与 各个编程语言)
  4. Starlark 规则还可以定义自己的选项(请参阅 此处

每个选项(不包括 Starlark 定义的选项)都是 FragmentOptions 子类,具有 @Option 注解,该注解指定 命令行选项的名称和类型以及一些帮助文本。

命令行选项值的 Java 类型通常很简单 (字符串、整数、布尔值、标签等)。不过,我们也支持 较复杂类型的选项;在本示例中,从 数据类型的命令行字符串属于 com.google.devtools.common.options.Converter

源代码树,如 Bazel 所示

Bazel 的专注领域是构建软件 解读源代码运行 Bazel 的全部源代码 称为“工作区”它的结构包括代码库、软件包和 规则。

代码库

“仓库”是开发者用于开发的源代码树;通常 代表单个项目。运行在 monorepo 的 Bazel 的祖先实体 Blaze 也就是说,一个源代码树包含用于运行构建的所有源代码。 相比之下,Bazel 支持源代码跨越多个项目的项目, 代码库调用 Bazel 的“主代码库” 其他代码库称为“外部代码库”。

代码库由名为 WORKSPACE(或 WORKSPACE.bazel)的文件在 在其根目录下运行此文件包含“全局”整体 构建可用的外部代码库集它的工作原理类似于 常规 Starlark 文件,这意味着可以 load() 其他 Starlark 文件。 这通常用于拉取代码库所需的代码库 明确引用的模式(我们称之为“deps.bzl 模式”)

外部代码库的代码已在下方建立符号链接或下载至 $OUTPUT_BASE/external

运行 build 时,需要将整个源代码树拼在一起。这个 由 SymlinkForest 完成,通过符号链接主代码库中的每个软件包 导出到 $EXECROOT,并将每个外部代码库迁移到 $EXECROOT/external$EXECROOT/..(当然,前者导致 在主代码库中调用 external;因此我们决定弃用 )

软件包

每个仓库都由软件包、相关文件的集合以及 依赖项规范它们由名为 BUILDBUILD.bazel。如果两者都存在,Bazel 会优先选择 BUILD.bazel;原因 为什么仍然接受 BUILD 文件,因为 Bazel 的祖先实体 Blaze 文件名。不过,结果证明它是一条常用的路径段, (文件名不区分大小写)。

软件包相互独立:更改软件包的 BUILD 文件 不会导致其他软件包发生更改。添加或移除 BUILD 文件 _可以 _更改其他软件包,因为递归 glob 会在软件包边界处停止 因此,BUILD 文件的存在会停止递归。

BUILD 文件的评估称为“软件包加载”。实现 PackageFactory 类中的实现方式是调用 Starlark 解释器并 要求了解一组可用的规则类。软件包的结果 是一个 Package 对象。它主要是一个字符串( target) 映射到该目标本身。

软件包加载过程中的复杂性大大增加:Bazel 无法做到这一点 要求明确列出每个源文件,并且可以运行 glob (例如 glob(["**/*.java"]))。与 shell 不同,它支持递归 glob, 放入子目录(而不是子包)。这需要以下权限: 而且这个过程可能很慢,所以我们实施了各种技巧 尽可能地并行运行。

Globbing 在以下类中实现:

  • LegacyGlobber,一个对 Skyframe 无感知能力的快速且欢快的全球化应用
  • SkyframeHybridGlobber,此版本使用 Skyframe,还原为 旧版 globber,从而避免“Skyframe 重启”(如下所述)

Package 类本身包含一些专门用于 解析 WORKSPACE 文件,这对实际软件包没有意义。这是 因为描述常规软件包的对象不应包含 用于描述其他内容的字段。其中包括:

  • 代码库映射
  • 已注册的工具链
  • 已注册的执行平台

理想情况下,从命令行解析 WORKSPACE 文件, 解析常规软件包,以便 Package 无需满足需求 两者的结合。遗憾的是,这很难做到,因为这两者是交织在一起的。

标签、目标和规则

软件包由以下类型的目标组成:

  1. 文件:build 的输入或输出内容。在 我们称其为工件(在别处讨论)。并非所有 在构建期间创建的文件是目标文件;一种常见的做法是 不具有关联标签的 Bazel。
  2. 规则:用于描述从输入中提取输出的步骤。他们 通常与某种编程语言(例如 cc_libraryjava_librarypy_library),但有一些与语言无关的函数 (例如 genrulefilegroup
  3. 软件包组:请参阅可见性部分。

目标的名称称为标签。标签的语法为 @repo//pac/kage:name,其中 repo 是该标签所属的代码库的名称 pac/kage 是其 BUILD 文件所在的目录,name 是其 文件(如果标签引用源文件)的相对路径: 软件包。在命令行上引用目标时,标签的某些部分 可以省略:

  1. 如果缺少仓库,该标签将被视为位于主仓库 存储库
  2. 如果省略软件包部分(例如 name:name),则会采用该标签 位于当前工作目录(相对路径)的软件包中 不允许包含上级引用 (..)

一种规则(如“C++ 库”)称为“规则类”。规则类 可以在 Starlark(rule() 函数)中实现,也可以使用 Java(称为 “原生规则”, 类型 RuleClass)。从长远来看,每种语言 系统会在 Starlark 中实施规则,但有些旧版规则系列(例如 Java 或 C++)。

需要在 BUILD 文件的开头导入 Starlark 规则类 使用 load() 语句,而 Java 规则类“先天性地”已知 Bazel 注册,通过 ConfiguredRuleClassProvider.

规则类包含如下信息:

  1. 其属性(例如 srcsdeps):其类型、默认值 限制条件等
  2. 附加到每个属性的配置转换和切面(如果有)
  3. 规则的实施
  4. 规则“通常”的传递信息提供程序创建

术语说明:在代码库中,我们通常使用“Rule”是指 由规则类创建但在 Starlark 和面向用户的文档中 “规则”应仅用于指代规则类本身;目标 只是一个“目标”。另请注意,尽管 RuleClass 具有“class”在其 那么规则类和目标之间没有 Java 继承关系 该类型。

SkyFrame

Bazel 底层的评估框架称为 Skyframe。其模型是 我们会在构建过程中需要构建的所有内容 边从任何数据部分指向其依赖关系的无环图, 也就是构建数据所需的其他数据片段。

图中的节点称为 SkyValue,它们的名称称为 SkyKey。这两者都是绝对不可变的,只有不可变对象 访问量。这种不变性几乎始终成立,万一它并非如此 (例如对于各个选项类 BuildOptions,它是 BuildConfigurationValue 及其 SkyKey),我们会尽量不进行更改 或者以无法从外部观察到的方式对其进行更改。 由此,可了解在 Skyframe 内计算的所有内容(如 配置的目标)也必须不可变。

观察 Skyframe 图的最便捷方法是运行 bazel dump --skyframe=deps,它会转储图表,每行一个 SkyValue。最好 因为它可能会变得非常大

Skyframe 位于 com.google.devtools.build.skyframe 软件包中。通过 名称相似的软件包com.google.devtools.build.lib.skyframe中包含 在 Skyframe 上实施 Bazel。有关 Skyframe 的详细信息为 此处

为了将给定的 SkyKey 评估为 SkyValue,Skyframe 将调用 与密钥类型对应的 SkyFunction。在该函数的 评估时,它可能会通过调用 SkyFunction.Environment.getValue() 的各种过载。它包含 将这些依赖项注册到 Skyframe 的内部图中。 让 Skyframe 知道在该函数的任何依赖项发生时重新评估该函数 更改。换句话说,Skyframe 的缓存和增量计算 SkyFunctionSkyValue 的粒度。

每当 SkyFunction 请求不可用的依赖项时,getValue() 将返回 null。然后,该函数应该会通过以下方式将控制权交还给 Skyframe: 返回 null。Skyframe 会在稍后的某个时间 不可用的依赖项,则从头开始重启函数。 的时间,getValue() 调用会成功并返回非 null 结果。

这样做的结果是,在 SkyFunction 内执行的任何计算 必须重复执行相关操作但这不包括 评估已缓存的依赖项 SkyValues。因此,我们通常 围绕这个问题:

  1. 批量声明依赖项(通过使用 getValuesAndExceptions()) 限制重启次数。
  2. SkyValue 分解为由不同 SkyFunction,以便可以独立计算和缓存它们。这个 因为这可能会增加内存 。
  3. 在重启之间存储状态,可以使用 SkyFunction.Environment.getState(),或保留临时静态缓存 “behind the back of Skyframe”。

从根本上说,我们需要这些类型的解决方法,因为我们通常 数十万个运行中的 Skyframe 节点,而 Java 也不支持 轻量级线程

星鸟星

Starlark 是用户用于配置和扩展的域特定语言 Bazel。它被视为 Python 的受限子集,其类型要少得多, 对控制流的更多限制,最重要的是,它具有很强的不可变性, 保证支持并发读取这不是图灵完备, 阻止部分(但不是全部)用户尝试完成一般操作, 编程语言。

Starlark 是在 net.starlark.java 软件包中实现的。 它还具有独立的 Go 实现 此处。Java Bazel 中使用的实现目前是一种解释器。

Starlark 可用于多种环境,包括:

  1. BUILD 语言。这是定义新规则的地方。Starlark 代码 在此上下文中运行的应用只能访问 BUILD 文件的内容 以及由其加载的 .bzl 文件。
  2. 规则定义。就是这样,新规则(例如,对新规则的支持) 语言)。在此环境中运行的 Starlark 代码可以访问 其直接依赖项提供的配置和数据(有关 )。
  3. WORKSPACE 文件。这是外部代码库(即 )。
  4. 代码库规则定义。这是新的外部代码库类型 都已定义。在此上下文中运行的 Starlark 代码可以在 运行 Bazel 的机器,并转到工作区外部。

BUILD.bzl 文件的可用方言略有不同 因为它们表达不同的内容我们已提供差异列表 此处

有关于 Starlark 的更多信息 此处

加载/分析阶段

加载/分析阶段是 Bazel 确定需要执行的操作 创建一条特定规则它的基本单位是“配置的目标”,也就是 (目标、配置)对,非常合理。

这个过程称为“加载/分析阶段”因为它可以分成两个 不同部分,这些部分以前已序列化,但现在可以在时间上重叠:

  1. 加载软件包,即将 BUILD 文件转换为 Package 对象 代表它们
  2. 分析已配置的目标,即运行 用于生成操作图的规则

已配置目标的传递关闭关系中的每个配置目标 必须自下而上分析,即叶节点 再向上移动到命令行中的那些。要分析的 一个已配置的目标:

  1. 配置。(“如何”构建该规则;例如,目标 还可以选择命令行选项 传递给 C++ 编译器)
  2. 直接依赖项。可以使用其传递信息提供方 所分析的规则之所以这样命名,是因为它们 “总览”所配置的 目标文件,例如类路径上的所有 .jar 文件或 需要链接到 C++ 二进制文件)
  3. 目标本身。这是加载目标软件包的结果 。对于规则,这包括其属性 至关重要。
  4. 已配置目标的实现。对于规则,此项可以是 位于 Starlark 或 Java。已实施所有非规则配置的目标 。

分析已配置的目标的输出如下:

  1. 所配置目标的传递信息提供方依赖于该传递信息提供方 访问权限
  2. 它可以创建的工件以及生成这些工件的操作。

为 Java 规则提供的 API 是 RuleContext,它相当于 Starlark 规则的 ctx 参数。它的 API 功能更强大,但 可以更轻松地进行 Bad ThingsTM,例如编写代码,编写代码, 空间复杂度是二次的(甚至更差),这导致 Bazel 服务器崩溃, Java 异常或违反不变量(例如,无意中修改了 Options 实例)或将配置的目标设为可变)

用于确定已配置目标的直接依赖关系的算法 居住在DependencyResolver.dependentNodeMap()

配置

配置就是目标的确定:针对什么平台、针对什么 命令行选项等

您可以为同一 build 中的多个配置构建同一目标。这个 会非常有用 而对于目标代码,我们会进行交叉编译 构建胖 Android 应用(包含多个 CPU 的原生代码) 架构)

从概念上讲,该配置是一个 BuildOptions 实例。不过,在 练习时,BuildOptionsBuildConfiguration 封装,它提供 添加一些额外的功能它从 放在底部。如果更改,则版本需要 进行重新分析。

这会导致异常情况,例如在以下情况下,必须重新分析整个 build: 例如,请求的测试运行数量会发生变化,尽管只有 影响测试目标(我们计划“削减”配置, 情况并非如此,但目前尚未就绪)。

当规则实现需要配置的一部分时,需要声明 使用 RuleClass.Builder.requiresConfigurationFragments() 在其定义中添加 ,了解所有最新动态。这样做既是为了避免错误(例如使用 Java fragment 的 Python 规则), 以便于配置修剪,以便在 Python 选项发生变化时,借助 C++ 目标也就无需重新分析

规则的配置不一定与其“父级”的配置相同 规则。在依赖项边缘更改配置的过程称为 “配置转换”。可能发生在两个地方:

  1. 依赖性边缘。这些过渡在 Attribute.Builder.cfg(),它们来自 Rule(其中 并将 BuildOptions(原始配置)转换为 或更多 BuildOptions(输出配置)。
  2. 在连接到配置目标的任何传入边缘上。它们是在 RuleClass.Builder.cfg()

相关类为 TransitionFactoryConfigurationTransition

使用配置转换,例如:

  1. 要声明在构建过程中使用了特定的依赖项, 因此应该在执行架构中
  2. 如需声明必须针对多个 架构(例如针对胖 Android APK 中的原生代码)

如果一次配置转换导致出现多个配置,则称为 分屏过渡

还可以在 Starlark 中实现配置转换(文档 此处

传递信息提供方

传递信息提供程序是配置目标的一种方式(也是唯一方式) 以告知依赖于它的其他已配置目标的信息。原因 “及物”这通常是某种汇总方式 已配置目标的传递关闭。

Java 传递信息提供程序之间通常存在 1 对 1 的对应关系 和 Starlark 星系(但 DefaultInfo 例外,它是 FileProviderFilesToRunProviderRunfilesProvider,因为该 API 被认为比 Java 应用的直接音译更像 Starlark)。 其密钥是以下其中一项:

  1. 一个 Java 类对象。这仅适用于 可直接从 Starlark 访问。这些提供程序是 TransitiveInfoProvider
  2. 字符串。这是传统方法,我们强烈建议不要这样做,因为 名称冲突。此类传递信息提供程序是 build.lib.packages.Info
  3. 提供商符号。您可以使用 provider() 从 Starlark 创建该实例。 函数,因此建议通过这种方法创建新的提供程序。该符号为 由 Java 中的 Provider.Key 实例表示。

在 Java 中实现的新提供程序应使用 BuiltinProvider 来实现。 NativeProvider 已弃用(我们还没时间将其移除)并且 无法从 Starlark 访问 TransitiveInfoProvider 子类。

配置的目标

配置的目标以 RuleConfiguredTargetFactory 的形式实现。这里有 子类。Starlark 配置的目标 通过 StarlarkRuleConfiguredTargetUtil.buildRule() 创建。

已配置的目标工厂应使用 RuleConfiguredTargetBuilder 来 构造其返回值。它包含以下内容:

  1. 他们的 filesToBuild,这是一个模糊的概念,表示“此规则的一组文件” 表示。”这些是在配置目标时构建的文件 在命令行或 Genrule 的 src 中。
  2. 它们的 Runfile、常规文件和数据。
  3. 输出组。这些是各种“其他文件组”该规则可以 build。可以使用 filegroup 规则(在 build 中使用),并在 Java 中使用 OutputGroupInfo 提供程序。

Runfile

某些二进制文件需要数据文件才能运行。一个典型的例子就是需要 输入文件。在 Bazel 中,这用“runfiles”的概念来表示。答 "runfiles 树"是特定二进制文件的数据文件的目录树。 在文件系统中,它是以包含单个符号链接的符号链接树的形式创建的。 指向输出树源中的文件。

一组 Runfile 表示为一个 Runfiles 实例。从概念上讲, 从 runfiles 树中某个文件的路径映射到 Artifact 实例, 代表它。这比为两个容器Map 原因:

  • 大多数情况下,文件的 runfiles 路径与其 execpath 相同。 我们用它来节省一些内存。
  • Runfile 树中有各种旧版条目,这些条目也需要 要表达的意思。

Runfile 是使用 RunfilesProvider 收集的:此类的实例 表示已配置目标(如库)的 runfile 及其传递 闭包需求,并且它们像嵌套集一样收集(实际上,它们是 使用封面下的嵌套集实现):每个目标都会将 runfile 并集 添加其自身的一些依赖项,然后将生成的设置 在依赖关系图中。一个 RunfilesProvider 实例包含两个 Runfiles 实例,一个实例表示通过“数据”属性和 为每种其他传入的依赖项分别创建一个。这是因为 当通过数据属性依赖时,有时会提供不同的 runfile 这是我们未曾发现的不良旧版行为 。

二进制文件的 Runfile 表示为 RunfilesSupport 的一个实例。这个 与 Runfiles 不同,因为 RunfilesSupport 具有 (与 Runfiles 不同,后者只是一个映射)。这个 需要使用以下附加组件:

  • 输入 runfiles 清单。这是 Runfiles 树。它用作 runfiles 树内容的代理 当且仅当运行文件树的内容 更改清单
  • 输出 runfiles 清单。供运行时库使用 处理 runfile 树,特别是在 Windows 上,这种树有时不支持 符号链接。
  • Runfile 中间人。为了让 Runfiles 树存在, 来构建符号链接树以及符号链接指向的工件。订单 为减少依赖项边缘的数量,可以 用于表示所有这些条件
  • 命令行参数:用于运行相应二进制文件(其 Runfile 文件会 RunfilesSupport 对象的列表。

切面

切面是一种“将计算沿依赖关系图向下传播”的方式。它们分别是 面向 Bazel 用户的说明 此处。较好 激励因素示例是协议缓冲区:proto_library 规则不应知道 任何特定语言的概念, 缓冲区消息(协议缓冲区的“基本单位”) 应与 proto_library 规则组合起来,这样一来,如果两个目标语言分别 相同的语言依赖于相同的协议缓冲区,因此只会构建一次。

与配置的目标一样,它们在 Skyframe 中以 SkyValue 的形式表示。 它们的构建方式与配置目标的方式非常相似 它们有一个名为 ConfiguredAspectFactory 的工厂类, RuleContext,但与配置的目标工厂不同,它也知道 连接到的已配置目标及其提供商的相关信息。

系统会为每个项指定沿依赖关系图向下传播的一组切面, 使用 Attribute.Builder.aspects() 函数指定属性。有几个 参与该过程且名称令人困惑的类:

  1. AspectClass 是切面的实现。可以是 Java 中 (在本示例中是一个子类)或者 Starlark 中(在此情况下,它是 StarlarkAspectClass 的实例)。类似于 RuleConfiguredTargetFactory
  2. AspectDefinition 是切面的定义;其中包含 所需的提供程序、它提供的提供程序,并且包含对 例如相应的 AspectClass 实例。时间是 类似于 RuleClass
  3. AspectParameters 是一种对向下传播的切面进行参数化的方法 依赖关系图它目前是字符串到字符串的映射。正面示例 协议缓冲区为何有用:如果一种语言有多个 API, 关于协议缓冲区应该为哪个 API 构建的信息 沿依赖关系图向下传播。
  4. Aspect 表示计算切面所需的所有数据, 沿依赖关系图向下传播。它包含切面类、 定义及其参数。
  5. RuleAspect 是用于确定特定规则的哪些方面的函数 。是Rule ->Aspect 函数。

有点出乎意料的复杂情况是各个方面可能附加到其他方面; 例如,收集 Java IDE 的类路径的一个方面很可能会 您可能想了解类路径上的所有 .jar 文件, Protocol Buffers 中的数据。在这种情况下,IDE 方面需要附加到 (proto_library 规则 + Java proto 切面)对。

类中捕获切面方面的复杂性 AspectCollection

平台和工具链

Bazel 支持多平台构建 运行构建操作的多个架构以及 具体构建的是哪个代码这些架构在 Bazel 中称为“平台” parlance(完整文档) 此处

平台由限制条件设置(例如 “CPU 架构”的概念)限制值(例如某个特定的 CPU) 例如 x86_64)。我们有“字典”最常用的限制条件 @platforms 代码库中的设置和值。

工具链的概念源自以下事实: 以及所针对的目标平台 不同编译器;例如,特定的 C++ 工具链可以在 并且能够定位一些其他操作系统Bazel 必须决定 C++ 基于设置执行和目标平台使用的编译器 (工具链的 此处)。

为此,工具链会使用执行集和 它们支持的目标平台限制条件为此, 工具链拆分为两部分:

  1. 描述一组执行和目标的 toolchain() 规则 工具链支持的约束条件,并告诉 工具链(后者由 toolchain_type() 规则表示)
  2. 特定于语言的规则,用于描述实际工具链(例如 cc_toolchain()

之所以这样说,是因为我们需要知道每个 工具链,以便执行工具链解析和特定于语言的 *_toolchain() 规则包含的信息远多于此,因此它们需要的 加载时间。

可通过以下方式之一指定执行平台:

  1. 在 WORKSPACE 文件中,使用 register_execution_platforms() 函数
  2. 在命令行上使用 --extra_execution_platforms 命令行 选项

可用的执行平台集以 RegisteredExecutionPlatformsFunction

已配置目标的目标平台由 PlatformOptions.computeTargetPlatform()这是一个平台列表 它最终希望支持多个目标平台,但这并未实现 。

用于配置目标的工具链集由 ToolchainResolutionFunction。它是以下各项的函数:

  • 一组已注册的工具链(在 WORKSPACE 文件中, 配置)
  • 所需的执行平台和目标平台(在配置中)
  • 已配置目标所需的一组工具链类型(在 UnloadedToolchainContextKey)
  • 已配置目标( exec_compatible_with 属性)和配置 (--experimental_add_exec_constraints_to_targets)、在 UnloadedToolchainContextKey

其结果是一个 UnloadedToolchainContext,它本质上是来自 工具链类型(表示为 ToolchainTypeInfo 实例)映射到 选定的工具链。称为“已卸载”因为它不包含 仅限其标签

然后,使用 ResolvedToolchainContext.load() 实际加载工具链 并用于请求它们的已配置目标的实现。

我们还有一个旧系统,它依赖于单一的“主机” 这些配置和目标配置 配置标志,例如 --cpu。我们正在逐步过渡到 系统。处理用户依赖于旧配置的情况 我们已经实现 平台映射 在旧版标志与新式平台限制之间转换。 其代码位于 PlatformMappingFunction 中,并使用非 Starlark“小 语言”。

限制条件

有时,广告客户希望将某个目标指定为与少数几个广告兼容 平台。但遗憾的是,Bazel 有多种机制来实现这一目的:

  • 特定于规则的限制条件
  • environment_group()/environment()
  • 平台限制

特定于规则的约束条件主要在 Google for Java 规则中使用;它们是 而且它们在 Bazel 中不可用, 包含对它的引用。控制此属性的属性称为 constraints=

environment_group()和 environment()

这些规则是一种旧机制,并未得到广泛使用。

所有构建规则都可以声明适合的平台 "环境"是 environment() 规则的一个实例。

您可以通过多种方式为规则指定受支持的环境:

  1. 通过 restricted_to= 属性。这是最直接的形式 规范;用于声明规则支持的确切环境 。
  2. 通过 compatible_with= 属性。这会声明环境 不仅支持“standard”支持的环境 默认值。
  3. 通过软件包级属性 default_restricted_to=default_compatible_with=
  4. 通过 environment_group() 规则中的默认规范。每个 环境属于一组主题相关的对等设备(例如,“CPU” 架构", "JDK 版本"或“移动操作系统”)。通过 环境组的定义包括这些环境 “default”应支持(由 restricted_to= / environment() 属性。不含此类实体的规则 属性继承所有默认值。
  5. 通过规则类默认设置。这会覆盖所有 多个实例。这有多种用途,例如 所有 *_test 规则均可测试,而无需每个实例明确 声明此功能。

environment() 作为常规规则实现,而 environment_group() 既是 Target 的子类,但不是 Rule (EnvironmentGroup),并且 函数,该函数默认可从 Starlark 获得 (StarlarkLibrary.environmentGroup()),该函数最终会创建一个 目标。这是为了避免循环依赖,因为每个 需要声明其所属的环境组, 环境组需要声明其默认环境。

您可以使用 --target_environment 命令行选项。

约束检查的实现位于 RuleContextConstraintSemanticsTopLevelConstraintSemantics

平台限制

现行的“官方”说明目标平台与哪些平台兼容 与描述工具链和平台相同的约束条件。 它正在拉取请求中接受审核 #10945

公开范围

如果您与许多开发者共同处理大型代码库(例如在 Google 工作), 您需要小心谨慎,防止其他人随意根据您的 代码。否则,根据 Hyrum 定律, 用户依赖你认为已实现的行为 。

Bazel 通过名为 visibility 的机制来支持此功能:您可以声明 都只能依赖于 visibility 属性。这个 属性有点特殊,因为虽然它包含一系列标签, 标签可能会通过软件包名称对模式进行编码,而不是使用指向 特定目标。(没错,存在设计缺陷。)

这是在以下位置实现的:

  • RuleVisibility 接口表示可见性声明。它可以 是常量(完全公开或完全私有)或标签列表。
  • 标签可以引用任一软件包组(预定义的软件包列表), 直接软件包 (//pkg:__pkg__) 或软件包的子树 (//pkg:__subpackages__).这与命令行语法不同 (使用 //pkg:*//pkg/...)。
  • 软件包组作为自己的目标 (PackageGroup) 实现,并且 配置的目标 (PackageGroupConfiguredTarget)。我们或许可以 如果需要的话,请用简单的规则替换这些规则。其逻辑已实现 PackageSpecification,它对应于 一种模式,如 //pkg/...PackageGroupContents,分别对应 单个 package_grouppackages 属性;和 PackageSpecificationProvider,通过 package_group 和 其传递性 includes
  • 从可见性标签列表到依赖项的转换在以下代码中完成: DependencyResolver.visitTargetVisibility 和另外几项 。
  • 实际检查会在 CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()

嵌套集

通常,配置的目标会聚合其依赖项中的一组文件, 添加自己的,并将聚合集封装到传递信息提供程序中, 依赖于它的已配置目标也可以实现同样的效果。示例:

  • 用于 build 的 C++ 头文件
  • 表示 cc_library 的传递闭包的对象文件
  • 一组 .jar 文件,它们需要位于类路径中,以便 Java 规则 编译或运行
  • Python 规则传递闭包中的一组 Python 文件

如果我们通过使用 ListSet 等简单的方式来实现这一点,最终会得到 二次内存用量:如果有 N 条规则链,并且每条规则都添加一个 这样我们就有 1+2+...+N 个集合成员。

为了解决这个问题,我们提出了一种 NestedSet。它是由其他 NestedSet 组成的数据结构, 从而形成一个有向无环图 集合。它们是不可变的,它们的成员可以迭代。我们定义了 多次迭代顺序 (NestedSet.Order):preorder、postorder、topological (节点始终在其祖先实体之后)和“不在乎,但它应该是 每次都相同”。

相同的数据结构在 Starlark 中称为 depset

制品和操作

实际 build 包含一组命令,需要运行这些命令才能生成 用户想要的输出这些命令表示为 类 Action,并且文件表示为该类的实例 Artifact。它们排列在一个称为 “操作图”。

工件有两种:源工件(可用的工件) 和派生工件(需要 构建而成)。派生的工件本身可以有多种类型:

  1. **常规工件。**这些信息通过计算 其校验和,使用 mtime 作为快捷方式;如果文件未经过校验和 但 ctime 没有变化
  2. 未解析的符号链接工件。这些报告由 调用 readlink()。与常规工件不同,这类工件可能会悬挂 符号链接。通常用于以下情形:将一些文件打包到 归档某种内容
  3. 树工件。它们不是单个文件,而是目录树。他们 并检查文件中的文件集及其 内容。它们以 TreeArtifact 表示。
  4. 常量元数据工件。更改这些工件不会触发 重建。此属性仅用于 build 戳记信息:我们不希望 只因为当前时间发生了变化就要进行重建。

没有根本原因会导致源制品不能是树制品或 符号链接工件,只是我们还没有实现它( 在 BUILD 文件中引用源目录是以下做法之一: 一些已知的长期存在的 Bazel 错误问题;我们有 这种工作方式是由 BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM 属性)

值得注意的一种 Artifact 是中间人。以 Artifact 表示。 这些实例是 MiddlemanAction 的输出。它们用于 一些特殊情况:

  • 聚合中间层用于将工件组合在一起。就是这样 如果很多操作都使用相同的大型输入集,则我们没有 N*M 依存边缘,仅 N+M(将替换为嵌套集)
  • 调度依赖项中间层可确保操作先于另一操作运行。 它们主要用于执行 lint 请求,但也用于 C++ 编译(请参阅 CcCompilationContext.createMiddleman() 查看说明)
  • Runfile 中间人用于确保存在 Runfile 树, 因此无需单独依赖于输出清单, Runfiles 树引用的单个工件。

最好将操作理解为需要运行的命令, 以及它生成的一组输出。完成这些任务 各个部分:

  • 需要运行的命令行
  • 所需的输入制品
  • 需要设置的环境变量
  • 描述应用运行所需的环境(如平台)的注释 \

还有一些其他特殊情况,例如编写一个文件,其内容 Vertex AI SDK。它们是 AbstractAction 的子类。大多数操作都是 SpawnActionStarlarkAction(同样,它们应该不是 单独的类),尽管 Java 和 C++ 有各自的操作类型 (JavaCompileActionCppCompileActionCppLinkAction)。

我们最终希望将所有内容移至 SpawnAction;“JavaCompileAction”现为 非常接近,但由于 .d 文件解析和 包括扫描。

操作图大多是“嵌入式”的添加到 Skyframe 图中 操作的执行表示为对 ActionExecutionFunction。从操作图依赖关系边到 有关 SkyFrame 依赖项边缘的说明,请参阅 ActionExecutionFunction.getInputDeps()Artifact.key()和一些 进行优化,以减少 Skyframe 边缘数量:

  • 派生的工件没有自己的 SkyValue。相反, Artifact.getGeneratingActionKey() 用于找出 可生成
  • 嵌套集拥有自己的 Skyframe 密钥。

共享操作

有些操作是由多个已配置的目标生成的;Starlark 规则 因为他们只能将其派生操作放入 根据其配置和软件包确定的目录(但即使如此, 同一软件包中的规则可能会发生冲突,但在 Java 中实施的规则可能会将 生成的工件

这被视为功能错误,但要将其删除却并非易事 因为它可以显著节省执行时间,例如, 需要以某种方式处理源文件,而该文件 多条规则(手挥手)。这需要占用一些 RAM 空间: 共享操作的实例需要单独存储在内存中。

如果两项操作生成了相同的输出文件,则它们必须完全相同: 具有相同的输入和输出并运行相同的命令行。这个 在 Actions.canBeShared() 中实现等价关系, 通过查看每个 Action 在分析和执行阶段之间进行验证。 这是在 SkyframeActionExecutor.findAndStoreArtifactConflicts() 中实现的。 它是 Bazel 中少数需要“全局”变量的位置之一视图的 build。

执行阶段

这是 Bazel 实际开始运行构建操作(例如 生成输出。

分析阶段过后,Bazel 首先要确定 需要构建工件。其逻辑编码为 TopLevelArtifactHelper;大致上是filesToBuild 以及特殊输出内容的内容 以明确表示“如果这个目标在 构建这些工件”。

下一步是创建执行根。由于 Bazel 可以选择 从文件系统中的不同位置获取软件包 (--package_path), 它需要提供本地执行的操作以及完整的源代码树。这是 由 SymlinkForest 类处理,其工作原理是记录每个目标, 和构建单个目录树,将符号链接 每个包含从实际位置使用目标的软件包。另一种做法是 将正确的路径传递给命令(将 --package_path 考虑在内)。 这种做法不可取,原因如下:

  • 从软件包路径中移出软件包时,它会更改操作命令行 另一个条目(之前很常见)
  • 与远程运行操作不同 在本地运行
  • 它需要特定于所用工具的命令行转换 (请考虑 Java 类路径和 C++ include 路径之间的区别)
  • 更改操作的命令行会使其操作缓存条目失效
  • --package_path 正在逐步弃用

然后,Bazel 开始遍历操作图(两部分的有向图) 由操作及其输入和输出制品组成)和正在运行的操作。 每个操作的执行均由 SkyValue 的实例表示。 ActionExecutionValue 类。

由于运行操作的成本高昂,因此我们有几层缓存, 在 Skyframe 之后被命中:

  • ActionExecutionFunction.stateMap 包含用于重启 Skyframe 的数据 共 ActionExecutionFunction 件便宜
  • 本地操作缓存包含有关文件系统状态的数据
  • 远程执行系统通常还包含自己的缓存

本地操作缓存

此缓存是位于 Skyframe 后面的另一个层;即使某个操作是 在 Skyframe 中重新执行时,它在本地操作缓存中仍然是成功的。它 表示本地文件系统的状态, 也就是说,当启动新的 Bazel 服务器时 即使 Skyframe 图表为空,也仍然会获得点击次数。

使用 方法检查此缓存是否存在命中 ActionCacheChecker.getTokenIfNeedToExecute()

与其名称相反,它是从派生工件的路径到 触发它的操作。该操作的说明如下:

  1. 它的输入和输出文件集及其校验和
  2. 它的“操作键”(通常是已执行的命令行) 通常表示 输入文件(例如,对于 FileWriteAction,它是数据的校验和) 就是这样)

还有一个高度实验性的“自上而下的操作缓存”仍然低于 开发方式,使用传递哈希避免过多的缓存 次。

输入发现和输入删减

有些操作比只有一组输入要复杂一些。更改为 操作的输入集采用两种形式:

  • 操作可能会在执行之前发现新的输入,或者决定某些 其输入实际上并不是必需的。典型示例是 C++, 最好对 C++ 组件的头文件进行有根据的推测 使用经过传递的闭包,这样我们就不必将每个 写入远程执行器;因此,可以选择不注册 头文件作为“输入”,但以传递方式扫描源文件 并且仅将这些头文件标记为 #include 语句中提到过(我们高估了,这样就不需要 实现完整的 C 预处理器)此选项目前通过硬连接 “false”且只能在 Google 使用。
  • 操作可能会意识到执行期间某些文件没有被使用。在 C++,则称为“.d files”:编译器会告知 这样为了避免在事后使用更糟糕的 而 Bazel 充分利用了这一点。这能提供更好的 而不是包含扫描器,因为它依赖于编译器。

这些操作是使用 Action 上的方法实现的:

  1. 调用 Action.discoverInputs()。它应该返回一组 确定为必需的软件制品。这些必须是源制品 以确保操作图中没有缺少 等效于所配置的目标图。
  2. 通过调用 Action.execute() 来执行该操作。
  3. Action.execute() 结束时,操作可以调用 Action.updateInputs(),告知 Bazel,并非所有输入 所需的资源。如果所使用的输入源是 报告为未使用。

当操作缓存针对新的操作实例(例如已创建 服务器重启后),Bazel 会自行调用 updateInputs() 输入反映了之前执行的输入发现和剪枝的结果。

Starlark 操作可以利用该设施将某些输入声明为未使用 使用 unused_inputs_list= 参数, ctx.actions.run()

运行操作的各种方式:策略/ActionContext

某些操作可以通过不同的方式执行。例如,命令行可以是 或在本地执行,也可以在各种沙盒中或远程执行。通过 体现这一点的概念称为 ActionContext(或 Strategy,因为我们 重命名后就成功了一半...)

操作上下文的生命周期如下:

  1. 执行阶段开始时,系统会询问 BlazeModule 实例 操作上下文。这是在 ExecutionTool。操作上下文类型由 Java Class 标识 一个引用了 ActionContext 的子接口的实例, 操作上下文必须实现的接口。
  2. 从可用操作上下文中选择适当的操作上下文, 转发给 ActionExecutionContextBlazeExecutor
  3. 操作使用 ActionExecutionContext.getContext()BlazeExecutor.getStrategy()(实际上应该只能通过一种方式 它...)

策略可以随意调用其他策略来发挥作用;用于 例如,在在本地和远程启动操作的动态策略中, 则使用最先完成的变量。

一项值得注意的策略是实现永久性工作器进程 (WorkerSpawnStrategy).其中的想法是,有些工具的启动时间很长 因此您应该在各项操作之间重复使用 (这确实存在潜在的正确性问题, 依赖于工作器进程的承诺,即它不会携带可观察对象 状态)

如果工具发生变化,则需要重启工作器进程。无论是工作器 使用 WorkerFilesHash。它依赖于了解操作的哪些输入代表 是工具的一部分,代表输入;由创作者决定 Action 的 Spawn.getToolFiles() 的 runfile 和 Spawn 的 runfile 都属于该工具的一部分

详细了解策略(或操作情境!):

  • 提供有关执行操作的各种策略的信息 此处
  • 有关动态策略(我们在其中运行操作)的信息 在本地或远程访问,以查看哪个先完成 此处
  • 提供有关在本地执行操作的复杂性的信息 此处

本地资源管理器

Bazel 可以并行运行多项操作。本地操作的数量 应该根据不同的操作并行运行: 则同时运行的实例应该越少, 使本地机器过载

这在 ResourceManager 类中实现:每个操作都必须 标注了需要的当地资源的估算值,格式为 ResourceSet 实例(CPU 和 RAM)。然后,当操作上下文执行某些操作时, 需要本地资源,它们会调用 ResourceManager.acquireResources() 在所需资源可用之前处于屏蔽状态。

提供了有关本地资源管理的更详细的说明 此处

输出目录的结构

每项操作都需要在输出目录中有一个单独的位置 输出。派生工件的位置通常如下所示:

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

与特定代码关联的目录的名称 配置决定?理想属性有两个冲突:

  1. 如果同一个 build 中可以出现两项配置,它们应该 这样两个文件都可以拥有自己的同一内容 操作;否则,如果两个配置不一致,例如命令 操作行,而 Bazel 不知道哪个操作 要选择的操作(“操作冲突”)
  2. 如果两个配置代表“大致”两者是一样的 因此,如果存在以下情况,可以将一个文件中执行的操作重复用于另一个对象: 命令行均匹配:例如,将命令行选项更改为 Java 编译器不应导致 C++ 编译操作重新运行。

到目前为止,我们还没有找到解决这个问题的原则性方法, 与配置缩减问题有相似之处。详细讨论 共有 个选项 此处。 主要有问题的方面是 Starlark 规则(其作者通常不是 非常熟悉 Bazel)和切面,而这又添加了一个维度 可以产生“相同”的输出文件。

目前的方法是,配置的路径段是 添加了各种后缀的 <CPU>-<compilation mode>,以便配置 在 Java 中实现的过渡不会导致操作冲突。此外, 添加一组 Starlark 配置转换的校验和 不会导致操作冲突。这远远称不上完美。这是在 OutputDirectories.buildMnemonic(),并且依赖于每个配置 fragment 自己的部分添加到输出目录的名称中。

测试

Bazel 为运行测试提供了丰富的支持。它支持:

  • 远程运行测试(如果远程执行后端可用)
  • 并行运行测试多次(用于消除不稳定或收集时间信息) 数据)
  • 分片测试(将同一测试中的测试用例拆分到多个进程中) 速度)
  • 重新运行不稳定的测试
  • 将测试分组到测试套件中

测试是常规的已配置目标,具有 TestProvider, 应如何运行测试:

  • 其构建导致运行测试的工件。这是一个“缓存” 状态"包含序列化 TestResultData 消息的文件
  • 应运行测试的次数
  • 应将测试拆分成的分片数
  • 关于应如何运行测试的一些参数(例如测试超时)

确定要运行哪些测试

确定要运行哪些测试是一个复杂的过程。

首先,在目标模式解析期间,以递归方式扩展测试套件。通过 扩展是在 TestsForTargetPatternFunction 中实现的。有点符合 令人惊讶的一点是,如果测试套件声明没有测试, 每个测试。这在 Package.beforeBuild() 中通过以下方法实现: 向测试套件规则添加了一个名为 $implicit_tests 的隐式属性。

然后根据 命令行选项。这在 TestFilter 中实现,并从 在目标解析期间为 TargetPatternPhaseFunction.determineTests(), 结果会放入 TargetPatternPhaseValue.getTestsToRunLabels()。原因 为什么可过滤的规则属性不可配置? 发生在分析阶段之前,因此, 可用。

然后,系统会在 BuildView.createResult() 中进一步处理: 会过滤掉失败的分析,并将测试分为独占模式和 非专有测试。然后将其放入 AnalysisResult 中, ExecutionTool 知道要运行哪些测试。

为了更清楚地说明这一复杂的过程,tests() 查询运算符(在 TestsFunction 中实现)可用于判断哪些测试 在命令行中指定了特定目标时运行。时间是 因为这是一次重新实现,所以在 多种微妙的方式。

运行测试

运行测试的方式是请求缓存状态工件。然后 会导致执行 TestRunnerAction,而这最终会调用 由 --test_strategy 命令行选项选择的 TestActionContext 按照请求的方式运行测试。

根据使用环境变量的详尽协议运行测试 来告诉测试对它们的预期Bazel 的 对测试的期望以及 Bazel 可以预期的测试 此处。在 最简单的,退出代码为 0 表示成功,任何其他值表示失败。

除了缓存状态文件之外,每个测试过程还会发出一些其他的 文件。它们会放在“测试日志目录”中也就是名为 testlogs

  • test.xml,一个 JUnit 样式的 XML 文件,详细说明了 测试分片
  • test.log 是测试的控制台输出。stdout 和 stderr 不是 。
  • test.outputs,“未声明的输出目录”;供测试使用 除了输出到终端的内容外,还想输出文件。

测试执行期间可能会出现两种情况,而 构建常规目标:专属测试执行和输出流式传输。

有些测试需要在独占模式下执行,例如,并非与 其他测试可通过将 tags=["exclusive"] 添加到 测试规则,或使用 --test_strategy=exclusive 运行测试。每个 由单独的 Skyframe 调用运行,请求执行 在“main”命令之后build。这是在 SkyframeExecutor.runExclusiveTest()

与常规操作不同,在常规操作中,系统会在用户执行特定操作时 完成时,用户可以请求流式传输测试的输出, 获取有关长时间运行的测试的进度的通知。这是由 --test_output=streamed 命令行选项,意味着排他测试 以便不同测试的输出不会分散开来。

这在适当命名的 StreamedTestOutput 类中实现,并由 轮询对相关测试的 test.log 文件的更改,并转储新的 将字节字节传输到 Bazel 规则的终端。

通过观察 各种事件(例如 TestAttemptTestResultTestingCompleteEvent)。 转储到构建事件协议,并发送到控制台 上传者:AggregatingTestListener

覆盖范围收集

覆盖率由测试在文件中以 LCOV 格式报告 bazel-testlogs/$PACKAGE/$TARGET/coverage.dat

为了收集覆盖率,每个测试作业都封装在名为 collect_coverage.sh

此脚本会设置测试环境,以启用覆盖率收集功能 并确定覆盖率运行时写入覆盖率文件的位置。 然后运行测试。一项测试本身可能会运行多个子进程,包括 使用多种不同的编程语言编写的部分(每个部分都有单独的 覆盖率收集运行时)。封装容器脚本负责将 如有必要,可将生成的文件转换为 LCOV 格式, 文件。

collect_coverage.sh 的插入由测试策略完成, 要求 collect_coverage.sh 位于测试的输入上。这是 通过隐式属性 :coverage_support 完成,此属性将解析为 配置标志 --coverage_support 的值(请参阅 TestConfiguration.TestOptions.coverageSupport)

有些语言支持离线插桩,这意味着覆盖率 插桩在编译时添加(例如 C++),其他插桩则在线添加 插桩,即在执行时添加覆盖率插桩 。

另一个核心概念是基准覆盖率。这是库的覆盖范围 或者测试该文件是否未运行任何代码。这种方法解决的问题是 计算一个二进制文件的测试覆盖率,但是将 因为二进制文件中可能存在 任何测试的链接因此,我们要针对每一天 二进制文件,其中仅包含我们收集的相应文件,而不包含 代码。目标的基准覆盖率文件位于 bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat它也是 针对二进制文件和库进行检查 --nobuild_tests_only 标志传递给 Bazel。

基准覆盖范围目前已损坏。

我们会跟踪两组文件,以便收集每条规则的覆盖范围: 插桩文件和一组插桩元数据文件。

一组插桩文件是指一组要插桩的文件。对于 可在运行时决定要将哪些文件 instrument。它还可用于实现基准覆盖率。

插桩元数据文件集是测试所需的一组额外文件 生成 Bazel 所需的 LCOV 文件实际上,这包括 运行时专用文件;例如,gcc 在编译期间会发出 .gcno 文件。 如果覆盖率模式处于 。

是否在收集覆盖率信息存储在 BuildConfiguration。这样做非常方便 操作和操作图依赖于此位,但这也意味着, 该位会被翻转,所有目标都需要重新分析(有些语言, C++ 需要不同的编译器选项来发出可以收集覆盖率的代码, 这在一定程度上缓解了这一问题,因为无论如何,都需要重新分析)。

覆盖率支持文件通过隐式 以便被调用政策覆盖,从而允许 它们在 Bazel 的不同版本之间有所不同。理想情况下,这些 差异将会消除,并且我们对其中一项指标进行了标准化。

我们还生成了“覆盖率报告”它合并了所收集的针对 在 Bazel 调用中进行的每个测试这由 CoverageReportActionFactory,从 BuildView.createResult() 调用。它 通过查看:coverage_report_generator 属性。

查询引擎

Bazel 的 小语言 向它询问各种图表的各种问题。以下查询种类 提供:

  • bazel query 用于研究目标图
  • bazel cquery 用于调查已配置的目标图表
  • bazel aquery 用于调查操作图

其中每个都是通过创建 AbstractBlazeQueryEnvironment 的子类实现的。 可以通过创建 QueryFunction 的子类来实现其他查询函数 ,了解所有最新动态。允许流式查询结果,而不是将其收集到 数据结构,query2.engine.Callback 会传递给 QueryFunction,后者 调用该函数以获取想要返回的结果。

可通过多种方式发出查询结果:标签、标签和规则 类、XML、protobuf 等。这些是作为 OutputFormatter

某些查询输出格式(当然是 proto)的微妙要求是: Bazel 需要发出软件包加载提供的信息 可以对比输出,并确定特定目标是否已更改。 因此,属性值需要可序列化 只有极少数的属性类型没有任何具有复杂 Starlark 的属性 值。通常的解决方法是使用标签,并将复杂的 向带有该标签的规则添加额外的信息这并不是一个非常令人满意的解决方法 最好取消这项要求

模块系统

可以通过向 Bazel 添加模块来扩展 Bazel。每个模块都必须子类化 BlazeModule(这个名称沿用了 Bazel 的历史 名为 Blaze),并在代码执行期间获取各种事件的相关信息。 一个命令。

它们主要用于实现各种“非核心”组件,功能 确保只有部分 Bazel 版本(比如我们在 Google 使用的版本)需要:

  • 远程执行系统的接口
  • 新指令

BlazeModule 提供的这组扩展积分有点杂乱。错误做法 作为良好设计原则的范例。

事件总线

BlazeModules 与 Bazel 的其余部分进行通信的主要方式是通过事件总线 (EventBus):为每个构建、Bazel 的各个部分创建一个新实例 模块可以向其发布事件,而模块可以为其中的事件注册监听器 。例如,以下内容表示为事件:

  • 已确定要构建的构建目标列表 (TargetParsingCompleteEvent)
  • 顶级配置已确定 (BuildConfigurationEvent)
  • 目标构建成功(无论是否成功)(TargetCompleteEvent)
  • 运行测试(TestAttemptTestSummary

其中一些事件在 构建事件协议 (为 BuildEvent)。这不仅允许 BlazeModule,还允许 在 Bazel 流程之外观察构建它们可以作为 包含协议消息或 Bazel 的文件,可以连接到 Build Event Service)流式传输事件。

这在 build.lib.buildeventservice 中实现,并且 build.lib.buildeventstream Java 软件包。

外部代码库

而 Bazel 最初设计为在 monorepo(单一源 包含构建所需的全部内容),而 Bazel 存在于 不一定正确。"外部代码库"是一种抽象概念,用于 衔接了这两个概念:它们代表的是构建所需的代码, 不在主源代码树中。

WORKSPACE 文件

外部代码库集通过解析 WORKSPACE 文件来确定。 例如,如下所示的声明:

    local_repository(name="foo", path="/foo/bar")

名为 @foo 的代码库中的结果可用。位置 复杂之处在于,用户可以在 Starlark 文件中定义新的代码库规则, 之后可以用来加载新的 Starlark 代码,这些代码可用于定义新的 代码库规则等等...

为处理这种情况,解析 WORKSPACE 文件(在 WorkspaceFileFunction)拆分为由 load() 分隔的区块 语句。分块索引由 WorkspaceFileKey.getIndex() 表示, 计算 WorkspaceFileFunction 直到索引 X 意味着计算该索引,直至 第 X 个 load() 语句。

提取代码库

代码库的代码必须满足以下条件,才能供 Bazel 使用 已提取。这会让 Bazel 在 $OUTPUT_BASE/external/<repository name>

提取代码库的步骤如下:

  1. PackageLookupFunction 意识到它需要一个代码库,并创建了一个 将 RepositoryName 作为 SkyKey,它会调用 RepositoryLoaderFunction
  2. RepositoryLoaderFunction 将请求转发给 RepositoryDelegatorFunction,原因不明(代码表明这是 避免在 Skyframe 重启时重新下载内容,但这并不是 有非常可靠的推理)
  3. RepositoryDelegatorFunction 会查明要求其执行的代码库规则 通过迭代 WORKSPACE 文件块,直到请求 代码库
  4. 找到了实现仓库的相应 RepositoryFunction fetching;可以是代码库的 Starlark 实现,也可以是 在 Java 中实现的代码库的硬编码映射。

缓存存在多个层,因为提取存储库 较贵:

  1. 缓存了已下载文件(由校验和进行键控) (RepositoryCache).这要求校验和在 WORKSPACE 文件,但这对于封闭性也很有用。此信息由以下用户共享: 同一个工作站上的每个 Bazel 服务器实例, 工作区或输出库。
  2. “标记文件”是针对 $OUTPUT_BASE/external 下的每个代码库编写的, 包含用于提取它的规则的校验和。如果 Bazel 服务器重新启动,但校验和没有改变,也没有重新获取校验和。这个 是在 RepositoryDelegatorFunction.DigestWriter 中实现的。
  3. --distdir 命令行选项指定了另一个缓存,用于 查找要下载的制品。这在企业设置中非常有用 其中,Bazel 不应从互联网上随机提取内容这是 由 DownloadManager 实现。

代码库下载完毕后,其中的工件会被视为源代码 工件这会带来问题,因为 Bazel 通常会检查最新版本 对源工件调用 stat() 来对其进行分析,这些工件也是 会在其所在代码库的定义发生更改时失效。因此, 外部代码库中工件的 FileStateValue 需要依赖于 外部代码库此操作由 ExternalFilesHelper 处理。

托管目录

有时,外部代码库需要修改工作区根目录下的文件 (例如,在文件包管理器的 源代码树)。这与 Bazel 让该来源相悖的假设 文件只能由用户修改,而不能由其自身修改,这允许软件包 引用工作区根目录下的每个目录。为了制作这种 运行外部代码库时,Bazel 会执行以下两项操作:

  1. 允许用户指定工作区的子目录 Bazel 不是 访问的 IP 地址。它们列在名为 .bazelignore 的文件中, 该功能在 BlacklistedPackagePrefixesFunction 中实现。
  2. 我们对从工作区子目录到外部 将其处理的存储库放入 ManagedDirectoriesKnowledge 中, FileStateValue 引用它们的方式与常规 外部代码库

代码库映射

有时,多个代码库都希望依赖于同一个代码库, 但版本不同(这是“diamond 依赖项 问题”)。例如,如果在 build 中位于不同仓库中的两个二进制文件 想要依靠 Guava,系统应该都会提及 Guava 从 @guava// 开始,并期望这意味着它的不同版本。

因此,Bazel 允许用户重新映射外部代码库标签, 字符串 @guava// 可以引用@guava1// 一个二进制文件的代码库,以及另一个 Guava 代码库(例如 @guava2//)的代码库, 另一个 Pod 的代码库

或者,这也可以用于联接菱形。如果代码库 另一个依赖于 @guava2//,代码库映射@guava1// 允许重新映射两个代码库,以使用规范的 @guava// 代码库。

映射在 WORKSPACE 文件中指定为 repo_mapping 属性 各个代码库定义然后,它会以 WorkspaceFileValue 中,它会被连接至:

  • Package.Builder.repositoryMapping,用于转换标签值 所有规则的属性 RuleClass.populateRuleAttributeValues()
  • Package.repositoryMapping,用于分析阶段( 解析在加载过程中未解析的 $(location) 等问题 阶段)
  • BzlLoadFunction,用于解析 load() 语句中的标签

JNI 位

Bazel 的服务器主要使用 Java 编写。例外情况是 当我们实现 Java 时,Java 无法自行执行,也无法自行执行。这个 主要限于与文件系统、进程控制和 其他各种低层内容

C++ 代码位于 src/main/native 和具有原生代码的 Java 类下 方法包括:

  • NativePosixFilesNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsWindowsFileProcesses
  • com.google.devtools.build.lib.platform

控制台输出

发出控制台输出看似很简单, 支持多个进程(有时远程)、精细缓存、 拥有漂亮多彩的终端输出 而一个长期运行的服务器 非常重要。

从客户端传入 RPC 调用后,立即有两个 RpcOutputStream (针对 stdout 和 stderr)创建实例,这些实例将输出到 发送给客户端然后将它们封装在 OutErr (stdout, stderr) 中 对)。需要在控制台上输出的所有内容都会经过这些 。然后,系统会将这些视频流 BlazeCommandDispatcher.execExclusively()

默认情况下,输出使用 ANSI 转义序列输出。如果它们 需要 (--color=no),它们会被 AnsiStrippingOutputStream 删除。在 此外,System.outSystem.err 会重定向到这些输出流。 这样就可以使用以下命令输出调试信息: System.err.println(),但仍在客户端的终端输出中 (与服务器不同)。请注意,如果某个流程 生成二进制输出(例如 bazel query --output=proto),不删除 stdout 事件。

简短消息(错误、警告等)通过 EventHandler 接口。值得注意的是,这些信息不同于 EventBus(这会造成混淆)。每个 Event 都有一个 EventKind(错误、 警告、信息及其他一些信息),并且这些结果中可能含有Location( 导致事件发生的源代码)。

某些 EventHandler 实现会存储它们收到的事件。这是 将由各种缓存处理导致的信息重放至界面, 例如,由缓存的已配置目标发出的警告。

有些 EventHandler 还允许发布活动,这些活动最终会找到 事件总线(常规 Event 不会出现在该总线中)。这些是 ExtendedEventHandler 的实现,其主要用途是重放缓存内容 EventBus 个事件。这些 EventBus 事件都实现了 Postable,但不 发布到 EventBus 的所有内容都必须实现此接口; 只有由 ExtendedEventHandler 缓存的内容(这样就最好 大部分功能则它不会强制执行)

终端输出主要通过 UiEventHandler 发出,该 负责所有花哨的输出格式和进度报告 Bazel 用途。它有两个输入:

  • 事件总线
  • 事件流通过报告程序导入

命令执行机器(例如其他机器) Bazel)连接到客户端的 RPC 流必须通过 Reporter.getOutErr(), 以便直接访问此类视频流它仅在命令需要 用于转储大量可能的二进制数据(例如 bazel query)。

剖析 Bazel 的性能

Bazel 速度很快。Bazel 的运行速度也很慢 因此,Bazel 包括一个性能分析器, 来分析构建和 Bazel 本身它是在具有以下特征的类中实现的: 恰如其分地命名为 Profiler。此功能默认处于开启状态,不过它只能录制视频 删节数据,以使其开销可承受;命令行 --record_full_profiler_data 可以录制一切。

以 Chrome 性能分析器格式发出配置文件;最好在 Chrome 中查看。 它的数据模型就是任务堆栈的数据模型:一个可以启动任务、结束任务, 它们应该相互整齐地嵌套在一起。每个 Java 线程都会获得 自己的任务堆栈。TODO:它如何与操作一起使用 继承传递样式?

性能分析器在 BlazeRuntime.initProfiler() 中启动和停止, BlazeRuntime.afterCommand(),并尝试长期存在 以便我们能够分析所有内容。要向个人资料添加内容 调用 Profiler.instance().profile()。它会返回一个 Closeable,其闭包 表示任务的结束。最适合与 try-with-resources 搭配使用 语句。

我们还在 MemoryProfiler 中进行了基本的内存性能分析。此功能也一直处于开启状态 它主要记录最大堆大小和 GC 行为。

测试 Bazel

Bazel 主要有两种测试:将 Bazel 视为“黑盒子”和 只运行分析阶段的应用我们将之前的“集成测试” 和后一种“单元测试”,尽管它们更像是集成测试, 集成程度较低还有一些实际的单元测试 。

集成测试分为两种:

  1. 它使用非常复杂的 Bash 测试框架 src/test/shell
  2. 使用 Java 实现的 API。这些是作为 BuildIntegrationTestCase

BuildIntegrationTestCase 是首选的集成测试框架,因为它 已经可以为大多数测试场景做好充分准备。由于它是一个 Java 框架 提供可调试性以及与许多常见开发项目的无缝集成 工具。以下页面中有很多 BuildIntegrationTestCase 类的示例 Bazel 代码库。

分析测试以 BuildViewTestCase 的子类的形式实现。这里有 暂存文件系统,可用于写入 BUILD 文件,以及各种辅助工具 方法可以请求配置的目标、更改配置并断言 有关分析结果的各种信息。