Build Event Protocol 的完整规范位于其协议缓冲区定义中。不过,在查看该规范之前,建立一些直观的机制可能会有所帮助。
设想一个简单的 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 ...
时,生成的构建事件的构建图将如下图所示。箭头指示上述父级和子级关系。请注意,为简单起见,省略了一些构建事件和大多数字段。
图 1. BEP 图表。
最初,BuildStarted
事件已发布。该事件通知我们通过 bazel test
命令调用了 build,并公告了子事件:
OptionsParsed
WorkspaceStatus
CommandLine
UnstructuredCommandLine
BuildMetadata
BuildFinished
PatternExpanded
Progress
前三个事件提供了有关如何调用 Bazel 的信息。
借助 PatternExpanded
构建事件,您可以深入了解 ...
格式扩展为 //foo:foo_lib
和 //foo:foo_test
的具体目标。为此,它会将两个 TargetConfigured
事件声明为子项。请注意,即使 Configuration
是在 TargetConfigured
事件之前发布的,TargetConfigured
事件也会将 Configuration
事件声明为子事件。
除了父关系和子关系之外,事件还可以使用其构建事件标识符来相互引用。例如,在上图中,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)
对关联的操作。在启用方面的情况下,对于受指定启用方面影响的每个目标,Bazel 还会评估与 (target, configuration,
aspect)
三元组关联的目标。
尽管没有特定方面的事件类型,但 BEP 中可以提供各方面的评估结果。对于具有适用宽高比的每个 (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
事件时必须注意避免二次算法,因为大型构建可能包含数万个此类事件,需要通过具有二次复杂性的遍历执行数亿次操作。
图 2. NamedSetOfFiles
BEP 图表。
NamedSetOfFiles
事件始终出现在引用 BEP 流的TargetComplete
或 NamedSetOfFiles
事件之前。这与“父级-子级”事件关系相反,即,除了第一个事件之外,除了第一个事件之外,所有其他事件都显示。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)