如需查看 Build Event Protocol 的完整规范,请参阅其 Protocol Buffers 定义。不过,在查看规范之前,积累一些直觉可能会有所帮助。
请考虑一个简单的 Bazel 工作区,其中包含两个空 shell 脚本 foo.sh
和 foo_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 事件和大多数字段。
图 1. BEP 图。
最初,系统会发布 BuildStarted
事件。该事件会通知我们该 build 是通过 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
事件。
引用文件的 build 事件通常不会在事件中嵌入文件名和路径。而是包含 NamedSetOfFiles
事件的构建事件标识符,该标识符随后将包含实际文件名和路径。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 中的方面结果
普通 build 会评估与 (target, configuration)
对关联的操作。启用
尽管没有特定于方面事件类型,但 BEP 中提供了方面评估结果。对于具有适用方面(aspect)的每个 (target, configuration)
对,Bazel 都会发布一个额外的 TargetConfigured
和 TargetComplete
事件,其中包含将方面应用于目标的结果。例如,如果 //: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 可能包含数万个此类事件,在遍历时需要执行数亿次操作,而复杂度为二次方。
图 2. NamedSetOfFiles
BEP 图表。
NamedSetOfFiles
事件始终会在引用它的 TargetComplete
或 NamedSetOfFiles
事件之前出现在 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)