날짜 비워 두기: BazelCon 2023이 10월 24~25일에 Google 뮌헨에서 열립니다. 자세히 알아보기

빌드 이벤트 프로토콜 예

문제 신고 소스 보기

빌드 이벤트 프로토콜의 전체 사양은 프로토콜 버퍼 정의에서 확인할 수 있습니다. 그러나 사양을 살펴보기 전에 직관을 쌓는 것이 도움이 될 수 있습니다.

두 개의 빈 셸 스크립트 foo.shfoo_test.sh와 다음 BUILD 파일로 구성된 간단한 Bazel 작업공간을 고려합니다.

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

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

이 프로젝트에서 bazel test ...를 실행하면 생성된 빌드 이벤트의 빌드 그래프는 아래 그래프와 유사합니다. 화살표는 앞서 언급한 상위 및 하위 관계를 나타냅니다. 간결성을 위해 일부 빌드 이벤트와 대부분의 필드를 생략했습니다.

bep-graph

그림 1. BEP 그래프

처음에는 BuildStarted 이벤트가 게시됩니다. 이 이벤트는 빌드가 bazel test 명령어를 통해 호출되었음을 알리고 하위 이벤트를 알려줍니다.

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

처음 3개의 이벤트는 Bazel이 호출된 방식에 대한 정보를 제공합니다.

PatternExpanded 빌드 이벤트는 ... 패턴(//foo:foo_lib//foo:foo_test으로 확장됨)을 타겟팅하는 구체적인 정보를 제공합니다. 그렇게 하려면 TargetConfigured 이벤트 두 개를 하위 요소로 선언합니다. Configuration 이벤트는 TargetConfigured 이벤트 전에 게시되었더라도 TargetConfigured 이벤트는 Configuration 이벤트를 하위 이벤트로 선언합니다.

상위 및 하위 관계 외에도 이벤트는 빌드 이벤트 식별자를 사용하여 서로를 참조할 수 있습니다. 예를 들어 위 그래프에서 TargetComplete 이벤트는 fileSets 필드의 NamedSetOfFiles 이벤트를 참조합니다.

파일을 참조하는 빌드 이벤트는 일반적으로 이벤트에 파일 이름과 경로를 삽입하지 않습니다. 대신, NamedSetOfFiles 이벤트의 빌드 이벤트 식별자가 포함됩니다. 여기에는 실제 파일 이름과 경로가 포함됩니다. NamedSetOfFiles 이벤트를 사용하면 파일 집합을 한 번 보고하고 여러 타겟에서 참조할 수 있습니다. 이 구조가 필요한 이유는 경우에 따라 빌드 이벤트 프로토콜 출력 크기가 파일 수와 함께 2차적으로 증가하기 때문입니다. 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의 가로세로 결과

일반 빌드는 (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 사용

지정된 대상 (또는 측면)에서 생성된 아티팩트를 결정하는 것은 일반적인 준비 작업이며 일부 준비로 효율적으로 수행할 수 있습니다. 이 섹션에서는 NamedSetOfFiles 이벤트에서 제공하는 재귀적이고 공유된 구조를 설명합니다. 이는 Starlark Depset의 구조와 일치합니다.

소비자는 NamedSetOfFiles 이벤트를 처리할 때 2차 알고리즘을 피해야 합니다. 대규모 빌드에는 이러한 이벤트가 수만 개 포함될 수 있으며 순회에서 2차 복잡성을 갖는 수억 개의 연산이 필요하기 때문입니다.

이름이 지정된 파일-bep-그래프

그림 2. NamedSetOfFiles BEP 그래프

NamedSetOfFiles 이벤트는 항상 이를 참조하는 TargetComplete 또는 NamedSetOfFiles 이벤트 전에 BEP 스트림에 나타납니다. 이는 '상위-하위' 이벤트 관계의 역으로, 첫 번째 이벤트를 제외한 모든 이벤트가 이를 알립니다. NamedSetOfFiles 이벤트는 시맨틱스가 없는 Progress 이벤트에서 발표합니다.

이러한 정렬 및 공유 제약 조건을 고려할 때 일반적인 소비자는 BEP 스트림이 소진될 때까지 모든 NamedSetOfFiles 이벤트를 버퍼링해야 합니다. 다음 JSON 이벤트 스트림과 Python 코드는 '기본' 출력 그룹에서 대상/aspect의 빌드된 아티팩트로 지도를 채우는 방법과 빌드된 대상/aspects의 하위 집합을 위해 출력을 처리하는 방법을 보여줍니다.

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)