构建事件协议示例

报告问题 查看来源 Nightly · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

如需查看 Build Event Protocol 的完整规范,请参阅其协议缓冲区定义。不过,在查看规范之前,先培养一些直觉可能会有所帮助。

假设有一个简单的 Bazel 工作区,其中包含两个空的 shell 脚本 foo.shfoo_test.sh,以及以下 BUILD 文件:

sh_library(
    name = "foo_lib",
    srcs = ["foo.sh"],
)

sh_test(
    name = "foo_test",
    srcs = ["foo_test.sh"],
    deps = [":foo_lib"],
)

在此项目上运行 bazel test ... 时,生成的 build 事件的 build 图将类似于下图。箭头表示上述父级和子级关系。请注意,为简便起见,我们省略了一些 build 事件和大多数字段。

bep-graph

图 1. BEP 图。

最初,系统会发布 BuildStarted 事件。该事件会告知我们 build 是通过 bazel test 命令调用的,并会公布子事件:

  • OptionsParsed
  • WorkspaceStatus
  • CommandLine
  • UnstructuredCommandLine
  • BuildMetadata
  • BuildFinished
  • PatternExpanded
  • Progress

前三个事件提供有关如何调用 Bazel 的信息。

PatternExpanded build 事件可提供有关 ... 模式扩展到的具体目标的深入分析://foo:foo_lib//foo:foo_test。为此,它声明了两个 TargetConfigured 事件作为子事件。请注意,即使 Configuration 事件是在 TargetConfigured 事件之前发布的,TargetConfigured 事件也会将 Configuration 事件声明为子事件。

除了父事件和子事件关系之外,事件还可以使用其 build 事件标识符相互引用。例如,在上图中,TargetComplete 事件在其 fileSets 字段中引用了 NamedSetOfFiles 事件。

引用文件的 build 事件通常不会在事件中嵌入文件名和路径。相反,它们包含 NamedSetOfFiles 事件的 build 事件标识符,该标识符随后将包含实际的文件名和路径。NamedSetOfFiles 事件允许一次报告一组文件,并供多个目标引用。这种结构是必需的,因为否则在某些情况下,Build Event Protocol 输出大小会随着文件数量的增加而呈二次方增长。NamedSetOfFiles 事件也可能不会嵌入其所有文件,而是通过其 build 事件标识符引用其他 NamedSetOfFiles 事件。

以下是上述图表中 //foo:foo_lib 目标的 TargetComplete 事件的实例,以协议缓冲区的 JSON 表示形式打印。 构建事件标识符包含目标(以不透明字符串的形式),并通过其构建事件标识符引用 Configuration 事件。该事件不会公布任何子事件。载荷包含有关目标是否成功构建、输出文件集以及构建的目标类型的信息。

{
  "id": {
    "targetCompleted": {
      "label": "//foo:foo_lib",
      "configuration": {
        "id": "544e39a7f0abdb3efdd29d675a48bc6a"
      }
    }
  },
  "completed": {
    "success": true,
    "outputGroup": [{
      "name": "default",
      "fileSets": [{
        "id": "0"
      }]
    }],
    "targetKind": "sh_library rule"
  }
}

BEP 中的 Aspect 结果

普通 build 会评估与 (target, configuration) 对相关联的操作。在启用方面的情况下进行构建时,对于受给定已启用方面影响的每个目标,Bazel 还会评估与 (target, configuration, aspect) 三元组关联的目标。

即使没有特定于方面的事件类型,BEP 中也提供了方面的评估结果。对于每个具有适用方面的 (target, configuration) 对,Bazel 都会发布额外的 TargetConfiguredTargetComplete 事件,其中包含将方面应用于目标的结果。例如,如果 //:foo_lib 是使用 --aspects=aspects/myaspect.bzl%custom_aspect 构建的,则此事件也会显示在 BEP 中:

{
  "id": {
    "targetCompleted": {
      "label": "//foo:foo_lib",
      "configuration": {
        "id": "544e39a7f0abdb3efdd29d675a48bc6a"
      },
      "aspect": "aspects/myaspect.bzl%custom_aspect"
    }
  },
  "completed": {
    "success": true,
    "outputGroup": [{
      "name": "default",
      "fileSets": [{
        "id": "1"
      }]
    }]
  }
}

正在消耗 NamedSetOfFiles

确定给定目标(或方面)生成的制品是常见的 BEP 使用情形,只需稍作准备即可高效完成。本部分讨论了 NamedSetOfFiles 事件提供的递归共享结构,该结构与 Starlark Depset 的结构相匹配。

使用方在处理 NamedSetOfFiles 事件时必须注意避免使用二次方算法,因为大型 build 可能包含数万个此类事件,在具有二次方复杂度的遍历中需要数亿次操作。

namedsetoffiles-bep-graph

图 2. NamedSetOfFiles 盈亏平衡点图表。

NamedSetOfFiles 事件始终在引用它的 TargetCompleteNamedSetOfFiles 事件之前出现在 BEP 流中。这是“父-子”事件关系的逆关系,其中除第一个事件之外的所有事件都出现在至少一个声明它的事件之后。NamedSetOfFiles 事件由没有语义的 Progress 事件宣布。

鉴于这些排序和共享限制,典型的消费者必须缓冲所有 NamedSetOfFiles 事件,直到 BEP 流耗尽。以下 JSON 事件流和 Python 代码演示了如何填充从目标/方面到“默认”输出组中已构建制品的映射,以及如何处理部分已构建目标/方面的输出:

named_sets = {}  # type: dict[str, NamedSetOfFiles]
outputs = {}     # type: dict[str, dict[str, set[str]]]

for event in stream:
  kind = event.id.WhichOneof("id")
  if kind == "named_set":
    named_sets[event.id.named_set.id] = event.named_set_of_files
  elif kind == "target_completed":
    tc = event.id.target_completed
    target_id = (tc.label, tc.configuration.id, tc.aspect)
    outputs[target_id] = {}
    for group in event.completed.output_group:
      outputs[target_id][group.name] = {fs.id for fs in group.file_sets}

for result_id in relevant_subset(outputs.keys()):
  visit = outputs[result_id].get("default", [])
  seen_sets = set(visit)
  while visit:
    set_name = visit.pop()
    s = named_sets[set_name]
    for f in s.files:
      process_file(result_id, f)
    for fs in s.file_sets:
      if fs.id not in seen_sets:
        visit.add(fs.id)
        seen_sets.add(fs.id)