构建事件协议示例

如需查看构建事件协议的完整规范,请参阅其协议缓冲区定义。不过,在查看规范之前建立一些直觉可能很有帮助。

假设有一个简单的 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 图将如下图所示。箭头表示上述父级和子级关系。请注意,为简洁起见,我们省略了某些构建事件和大多数字段。

比曲线图

图 1. BEP 图表。

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

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

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

PatternExpanded 构建事件可让您深入了解 ... 模式扩展到哪些特定目标://foo:foo_lib//foo:foo_test。为此,它会将两个 TargetConfigured 事件声明为子项。请注意,TargetConfigured 事件将 Configuration 事件声明为子事件,即使 Configuration 是在 TargetConfigured 事件之前发布的。

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

引用文件的构建事件通常不会在事件中嵌入文件名和路径。而是包含 NamedSetOfFiles 事件的构建事件标识符,此标识符随后将包含实际的文件名和路径。NamedSetOfFiles 事件允许一组文件被报告一次,并被多个目标引用。此结构是必需的,因为在某些情况下,Build Event Protocol 的输出大小会随着文件数量的增加而成正比增加。NamedSetOfFiles 事件也可能不嵌入其所有文件,而是通过其 build 事件标识符引用其他 NamedSetOfFiles 事件。

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

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

BEP 中的切面结果

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

尽管没有针对切面的事件类型,但 BEP 中会提供切面的评估结果。对于具有适用切面的每个 (target, configuration) 对,Bazel 会额外发布一个 TargetConfiguredTargetComplete 事件,用于传送将切面应用于目标时的结果。例如,如果使用 --aspects=aspects/myaspect.bzl%custom_aspect 构建 //:foo_lib,此事件也会出现在 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 可能包含数以万计的此类事件,在二次复杂性的遍历中需要执行数亿次操作。

namesetoffiles-bep-graph

图 2. NamedSetOfFiles BEP 图表。

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)