构建事件协议示例

报告问题 查看源代码

构建事件协议的完整规范可在其协议缓冲区定义中找到。不过,在查看规范之前,积累一些直觉可能会有所帮助。

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

别普图

图 1. BEP 图。

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

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

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

通过 PatternExpanded 构建事件,您可以深入了解将 ... 模式扩展为 //foo:foo_lib//foo:foo_test 的具体目标。为此,它会将两个 TargetConfigured 事件声明为子事件。请注意,即使 Configuration 是在 TargetConfigured 事件之前发布的,TargetConfigured 事件仍会将 Configuration 事件声明为子事件。

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

引用文件的构建事件通常不会在事件中嵌入文件名和路径。而是包含 NamedSetOfFiles 事件的构建事件标识符,该标识符随后将包含实际文件名和路径。NamedSetOfFiles 事件允许一组文件只报告一次并被多个目标引用。这种结构是必要的,因为在其他情况下,Build Event Protocol 输出大小会随着文件数量的平方增长。此外,NamedSetOfFiles 事件可能不会嵌入其所有文件,而是通过其构建事件标识符引用其他 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 中的切面结果

普通 build 会评估与 (target, configuration) 对关联的操作。在启用 aspects 的情况下进行构建时,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 可能包含数万个此类事件,因此需要以二次复杂度遍历数亿次操作。

namesetoffiles-bep-graph

图 2. NamedSetOfFiles BEP 图表。

NamedSetOfFiles 事件始终显示在 BEP 流中,在引用它的 TargetCompleteNamedSetOfFiles 事件之前。这与“父级-子级”事件关系相反,也就是说,除第一个事件外,所有其他事件都会在至少一个宣布事件后出现。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)