Bazel 코드베이스

문제 신고 소스 보기 나이틀리 · 8.3 · 8.2 · 8.1 · 8.0 · 7.6

이 문서는 코드베이스와 Bazel의 구조를 설명합니다. 최종 사용자가 아닌 Bazel에 기여하려는 사용자를 위한 것입니다.

소개

Bazel의 코드베이스는 크고 (프로덕션 코드 약 350,000줄, 테스트 코드 약 260,000줄) 전체 환경에 익숙한 사람은 없습니다. 모두가 특정 계곡을 잘 알고 있지만 모든 방향의 언덕 너머에 무엇이 있는지 아는 사람은 거의 없습니다.

여정 중간에 있는 사용자가 간단한 경로를 잃어버려 어두운 숲에 있지 않도록 이 문서에서는 코드베이스에 대한 개요를 제공하여 코드베이스 작업을 더 쉽게 시작할 수 있도록 합니다.

Bazel 소스 코드의 공개 버전은 GitHub의 github.com/bazelbuild/bazel에 있습니다. 이것은 '신뢰할 수 있는 소스'가 아닙니다. Google 외부에서는 유용하지 않은 추가 기능이 포함된 Google 내부 소스 트리에서 파생됩니다. 장기적인 목표는 GitHub를 정보 소스로 만드는 것입니다.

기여는 일반 GitHub 풀 요청 메커니즘을 통해 수락되며, Google 직원이 내부 소스 트리로 수동으로 가져온 후 GitHub로 다시 내보냅니다.

클라이언트/서버 아키텍처

Bazel의 대부분은 빌드 간에 RAM에 유지되는 서버 프로세스에 있습니다. 이를 통해 Bazel은 빌드 간에 상태를 유지할 수 있습니다.

이러한 이유로 Bazel 명령줄에는 시작 옵션과 명령 옵션의 두 가지 옵션이 있습니다. 다음과 같은 명령줄에서

    bazel --host_jvm_args=-Xmx8G build -c opt //foo:bar

일부 옵션 (--host_jvm_args=)은 실행할 명령어 이름 앞에 있고 일부는 뒤에 있습니다 (-c opt). 전자는 '시작 옵션'이라고 하며 전체 서버 프로세스에 영향을 미치고 후자는 '명령어 옵션'이라고 하며 단일 명령어에만 영향을 미칩니다.

각 서버 인스턴스에는 연결된 작업공간('저장소'라고 하는 소스 트리 모음)이 하나 있으며 각 작업공간에는 일반적으로 활성 서버 인스턴스가 하나 있습니다. 맞춤 출력 베이스를 지정하면 이 문제를 해결할 수 있습니다(자세한 내용은 '디렉터리 레이아웃' 섹션 참고).

Bazel은 유효한 .zip 파일이기도 한 단일 ELF 실행 파일로 배포됩니다. bazel를 입력하면 C++로 구현된 위의 ELF 실행 파일('클라이언트')이 제어권을 가져옵니다. 다음 단계를 사용하여 적절한 서버 프로세스를 설정합니다.

  1. 이미 추출되었는지 확인합니다. 그렇지 않으면 그렇게 합니다. 여기에서 서버 구현이 제공됩니다.
  2. 작동하는 활성 서버 인스턴스가 있는지 확인합니다. 실행 중이고, 올바른 시작 옵션이 있으며, 올바른 워크스페이스 디렉터리를 사용합니다. 서버가 수신 대기하는 포트가 있는 잠금 파일이 있는 디렉터리 $OUTPUT_BASE/server를 확인하여 실행 중인 서버를 찾습니다.
  3. 필요한 경우 이전 서버 프로세스를 종료합니다.
  4. 필요한 경우 새 서버 프로세스를 시작합니다.

적절한 서버 프로세스가 준비되면 실행해야 하는 명령어가 gRPC 인터페이스를 통해 전달되고 Bazel의 출력이 터미널로 다시 파이프됩니다. 한 번에 하나의 명령어만 실행할 수 있습니다. 이는 C++의 일부와 Java의 일부를 사용하여 정교한 잠금 메커니즘을 사용하여 구현됩니다. bazel version를 다른 명령어와 병렬로 실행할 수 없다는 것은 다소 당황스러운 일이므로 여러 명령어를 병렬로 실행하기 위한 인프라가 있습니다. 주요 차단 요소는 BlazeModule의 수명 주기와 BlazeRuntime의 일부 상태입니다.

명령어가 끝나면 Bazel 서버는 클라이언트가 반환해야 하는 종료 코드를 전송합니다. 흥미로운 점은 bazel run 구현입니다. 이 명령어의 작업은 Bazel이 빌드한 항목을 실행하는 것이지만 터미널이 없기 때문에 서버 프로세스에서 이를 실행할 수 없습니다. 따라서 클라이언트에게 어떤 바이너리를 어떤 인수로 exec()해야 하는지 알려줍니다.

사용자가 Ctrl-C를 누르면 클라이언트는 이를 gRPC 연결의 취소 호출로 변환하여 명령어를 최대한 빨리 종료하려고 합니다. 세 번째 Ctrl-C 후에는 클라이언트가 서버에 SIGKILL을 대신 보냅니다.

클라이언트의 소스 코드는 src/main/cpp에 있고 서버와 통신하는 데 사용되는 프로토콜은 src/main/protobuf/command_server.proto에 있습니다 .

서버의 기본 진입점은 BlazeRuntime.main()이고 클라이언트의 gRPC 호출은 GrpcServerImpl.run()에서 처리합니다.

디렉터리 레이아웃

Bazel은 빌드 중에 다소 복잡한 디렉터리 집합을 만듭니다. 전체 설명은 출력 디렉터리 레이아웃에서 확인할 수 있습니다.

'기본 저장소'는 Bazel이 실행되는 소스 트리입니다. 일반적으로 소스 관리에서 체크아웃한 항목에 해당합니다. 이 디렉터리의 루트를 '작업공간 루트'라고 합니다.

Bazel은 모든 데이터를 'output user root' 아래에 배치합니다. 일반적으로 $HOME/.cache/bazel/_bazel_${USER}이지만 --output_user_root 시작 옵션을 사용하여 재정의할 수 있습니다.

'설치 베이스'는 Bazel이 추출되는 위치입니다. 이 작업은 자동으로 실행되며 각 Bazel 버전에는 설치 베이스 아래에 체크섬을 기반으로 하는 하위 디렉터리가 할당됩니다. 기본적으로 $OUTPUT_USER_ROOT/install에 있으며 --install_base 명령줄 옵션을 사용하여 변경할 수 있습니다.

'출력 베이스'는 특정 작업공간에 연결된 Bazel 인스턴스가 쓰는 위치입니다. 각 출력 베이스에는 언제든지 실행되는 Bazel 서버 인스턴스가 최대 하나 있습니다. 일반적으로 $OUTPUT_USER_ROOT/<checksum of the path to the workspace>에 있습니다. 이 옵션은 --output_base 시작 옵션을 사용하여 변경할 수 있으며, 이는 특정 시점에 하나의 Bazel 인스턴스만 워크스페이스에서 실행될 수 있다는 제한을 해결하는 데 유용합니다.

출력 디렉터리에는 다음이 포함됩니다.

  • $OUTPUT_BASE/external에서 가져온 외부 저장소
  • 실행 루트입니다. 현재 빌드의 모든 소스 코드에 대한 심볼릭 링크가 포함된 디렉터리입니다. 이 파일은 $OUTPUT_BASE/execroot에 있습니다. 빌드 중에 작업 디렉터리는 $EXECROOT/<name of main repository>입니다. 매우 호환되지 않는 변경사항이므로 장기 계획이지만 $EXECROOT로 변경할 예정입니다.
  • 빌드 중에 빌드된 파일입니다.

명령어 실행 프로세스

Bazel 서버가 제어권을 획득하고 실행해야 하는 명령어를 알게 되면 다음 이벤트 시퀀스가 발생합니다.

  1. BlazeCommandDispatcher에 새 요청이 전달됩니다. 명령어를 실행하는 데 작업공간이 필요한지 (버전이나 도움말과 같이 소스 코드와 관련이 없는 명령어를 제외한 거의 모든 명령어)와 다른 명령어가 실행 중인지 여부를 결정합니다.

  2. 올바른 명령어가 발견됩니다. 각 명령어는 BlazeCommand 인터페이스를 구현해야 하며 @Command 주석이 있어야 합니다. 이는 약간의 안티패턴입니다. 명령어에 필요한 모든 메타데이터가 BlazeCommand의 메서드로 설명되면 좋을 것입니다.

  3. 명령줄 옵션이 파싱됩니다. 각 명령어에는 @Command 주석에 설명된 다양한 명령줄 옵션이 있습니다.

  4. 이벤트 버스가 생성됩니다. 이벤트 버스는 빌드 중에 발생하는 이벤트의 스트림입니다. 이러한 항목 중 일부는 빌드가 어떻게 진행되는지 알리기 위해 빌드 이벤트 프로토콜의 관리 하에 Bazel 외부로 내보내집니다.

  5. 명령어가 제어권을 가져옵니다. 가장 흥미로운 명령어는 빌드를 실행하는 명령어입니다(build, test, run, coverage 등). 이 기능은 BuildTool에 의해 구현됩니다.

  6. 명령줄의 타겟 패턴 집합이 파싱되고 //pkg:all//pkg/...와 같은 와일드 카드가 확인됩니다. 이는 AnalysisPhaseRunner.evaluateTargetPatterns()에서 구현되고 Skyframe에서 TargetPatternPhaseValue로 구체화됩니다.

  7. 로딩/분석 단계는 빌드를 위해 실행해야 하는 명령의 방향성 비순환 그래프인 작업 그래프를 생성하기 위해 실행됩니다.

  8. 실행 단계가 실행됩니다. 즉, 요청된 최상위 타겟을 빌드하는 데 필요한 모든 작업이 실행됩니다.

명령줄 옵션

Bazel 호출의 명령줄 옵션은 OptionsParsingResult 객체에 설명되어 있으며, 이 객체에는 '옵션 클래스'에서 옵션 값으로의 맵이 포함되어 있습니다. '옵션 클래스'는 OptionsBase의 하위 클래스이며 서로 관련된 명령줄 옵션을 함께 그룹화합니다. 예를 들면 다음과 같습니다.

  1. 프로그래밍 언어 (CppOptions 또는 JavaOptions)와 관련된 옵션입니다. FragmentOptions의 하위 클래스여야 하며 최종적으로 BuildOptions 객체로 래핑됩니다.
  2. Bazel이 작업을 실행하는 방식과 관련된 옵션 (ExecutionOptions)

이러한 옵션은 분석 단계에서 사용하도록 설계되었으며 (Java의 RuleContext.getFragment() 또는 Starlark의 ctx.fragments을 통해) 일부 (예: C++ 포함 스캔 실행 여부)는 실행 단계에서 읽지만 BuildConfiguration를 사용할 수 없으므로 항상 명시적 배관이 필요합니다. 자세한 내용은 '구성' 섹션을 참고하세요.

경고: OptionsBase 인스턴스가 불변이라고 가정하고 SkyKeys의 일부와 같은 방식으로 사용합니다. 하지만 이는 사실이 아니며 이를 수정하면 디버그하기 어려운 미묘한 방식으로 Bazel이 중단될 수 있습니다. 하지만 실제로 변경 불가능하게 만드는 것은 큰 노력입니다. (다른 사람이 참조를 유지할 기회를 얻기 전, 그리고 equals() 또는 hashCode()가 호출되기 전에 생성 직후 FragmentOptions를 수정하는 것은 괜찮습니다.)

Bazel은 다음과 같은 방법으로 옵션 클래스에 대해 알아봅니다.

  1. 일부는 Bazel에 하드와이어링되어 있습니다 (CommonCommandOptions).
  2. 각 Bazel 명령어의 @Command 주석
  3. ConfiguredRuleClassProvider (개별 프로그래밍 언어와 관련된 명령줄 옵션)
  4. Starlark 규칙은 자체 옵션을 정의할 수도 있습니다 (여기 참고).

각 옵션 (Starlark 정의 옵션 제외)은 @Option 주석이 있는 FragmentOptions 하위 클래스의 멤버 변수입니다. 이 주석은 명령줄 옵션의 이름과 유형을 도움말 텍스트와 함께 지정합니다.

명령줄 옵션 값의 Java 유형은 일반적으로 간단한 유형(문자열, 정수, 불리언, 라벨 등)입니다. 하지만 더 복잡한 유형의 옵션도 지원합니다. 이 경우 명령줄 문자열에서 데이터 유형으로 변환하는 작업은 com.google.devtools.common.options.Converter 구현에 속합니다.

Bazel에서 보는 소스 트리

Bazel은 소스 코드를 읽고 해석하여 소프트웨어를 빌드하는 데 사용됩니다. Bazel이 작동하는 소스 코드의 전체를 '작업공간'이라고 하며 저장소, 패키지, 규칙으로 구성됩니다.

저장소

'저장소'는 개발자가 작업하는 소스 트리로, 일반적으로 단일 프로젝트를 나타냅니다. Bazel의 조상인 Blaze는 빌드를 실행하는 데 사용되는 모든 소스 코드가 포함된 단일 소스 트리인 모노레포에서 작동했습니다. 반면 Bazel은 소스 코드가 여러 저장소에 걸쳐 있는 프로젝트를 지원합니다. Bazel이 호출되는 저장소를 '기본 저장소'라고 하고 나머지는 '외부 저장소'라고 합니다.

저장소는 루트 디렉터리에 있는 저장소 경계 파일 (MODULE.bazel, REPO.bazel 또는 기존 컨텍스트에서는 WORKSPACE 또는 WORKSPACE.bazel)로 표시됩니다. 기본 저장소는 Bazel을 호출하는 소스 트리입니다. 외부 저장소는 다양한 방식으로 정의됩니다. 자세한 내용은 외부 종속 항목 개요를 참고하세요.

외부 저장소의 코드는 $OUTPUT_BASE/external 아래에 심볼릭 링크되거나 다운로드됩니다.

빌드를 실행할 때는 전체 소스 트리를 함께 연결해야 합니다. 이는 SymlinkForest에 의해 실행되며, SymlinkForest는 기본 저장소의 모든 패키지를 $EXECROOT에 심볼릭 링크하고 모든 외부 저장소를 $EXECROOT/external 또는 $EXECROOT/..에 심볼릭 링크합니다.

패키지

모든 저장소는 패키지, 관련 파일 모음, 종속 항목 사양으로 구성됩니다. 이러한 항목은 BUILD 또는 BUILD.bazel이라는 파일로 지정됩니다. 두 파일이 모두 있는 경우 Bazel은 BUILD.bazel를 선호합니다. BUILD 파일이 여전히 허용되는 이유는 Bazel의 상위 버전인 Blaze가 이 파일 이름을 사용했기 때문입니다. 하지만 파일 이름이 대소문자를 구분하지 않는 Windows에서는 이 경로 세그먼트가 일반적으로 사용되는 것으로 밝혀졌습니다.

패키지는 서로 독립적입니다. 패키지의 BUILD 파일 변경으로 인해 다른 패키지가 변경될 수는 없습니다. BUILD 파일의 추가 또는 삭제는 패키지 경계에서 재귀 glob이 중지되므로 다른 패키지를 변경할 수 있습니다. 따라서 BUILD 파일이 있으면 재귀가 중지됩니다.

BUILD 파일의 평가는 '패키지 로드'라고 합니다. PackageFactory 클래스에 구현되어 있고 Starlark 인터프리터를 호출하여 작동하며 사용 가능한 규칙 클래스 집합에 관한 지식이 필요합니다. 패키지 로드의 결과는 Package 객체입니다. 주로 문자열 (타겟 이름)에서 타겟 자체로 이어지는 맵입니다.

패키지 로드 중에 발생하는 복잡성의 상당 부분은 globbing입니다. Bazel에서는 모든 소스 파일을 명시적으로 나열할 필요가 없으며 대신 glob(["**/*.java"])와 같은 glob을 실행할 수 있습니다. 셸과 달리 하위 디렉터리(하위 패키지 아님)로 내려가는 재귀적 glob을 지원합니다. 이렇게 하려면 파일 시스템에 액세스해야 하며, 파일 시스템 액세스는 느릴 수 있으므로 병렬로 최대한 효율적으로 실행되도록 온갖 트릭을 구현합니다.

글로빙은 다음 클래스에서 구현됩니다.

  • LegacyGlobber: 빠르고 Skyframe을 인식하지 않는 globber
  • SkyframeHybridGlobber: Skyframe을 사용하고 'Skyframe 다시 시작' (아래 설명)을 방지하기 위해 기존 globber로 되돌아가는 버전

Package 클래스 자체에는 '외부' 패키지 (외부 종속 항목과 관련됨)를 파싱하는 데만 사용되고 실제 패키지에는 적합하지 않은 일부 멤버가 포함되어 있습니다. 일반 패키지를 설명하는 객체에는 다른 것을 설명하는 필드가 포함되어서는 안 되므로 이는 설계 결함입니다. 예를 들면 다음과 같습니다.

  • 저장소 매핑
  • 등록된 도구 모음
  • 등록된 실행 플랫폼

Package가 두 패키지의 요구사항을 모두 충족하지 않아도 되도록 '외부' 패키지 파싱과 일반 패키지 파싱 간에 더 많은 분리가 있으면 이상적입니다. 두 가지가 매우 깊이 얽혀 있어 이렇게 하기가 어렵습니다.

라벨, 타겟, 규칙

패키지는 다음 유형의 타겟으로 구성됩니다.

  1. 파일: 빌드의 입력 또는 출력인 항목입니다. Bazel 용어로는 이를 아티팩트라고 합니다 (다른 곳에서 설명). 빌드 중에 생성된 파일이 모두 타겟은 아닙니다. Bazel의 출력에 연결된 라벨이 없는 것이 일반적입니다.
  2. 규칙: 입력에서 출력을 도출하는 단계를 설명합니다. 일반적으로 프로그래밍 언어 (예: cc_library, java_library, py_library)와 관련이 있지만 언어에 구애받지 않는 것도 있습니다(예: genrule, filegroup).
  3. 패키지 그룹: 공개 설정 섹션에서 설명합니다.

타겟의 이름을 라벨이라고 합니다. 라벨의 구문은 @repo//pac/kage:name입니다. 여기서 repo은 라벨이 있는 저장소의 이름이고, pac/kageBUILD 파일이 있는 디렉터리이며, name은 패키지 디렉터리를 기준으로 하는 파일의 경로입니다 (라벨이 소스 파일을 참조하는 경우). 명령줄에서 타겟을 참조할 때 라벨의 일부를 생략할 수 있습니다.

  1. 저장소가 생략되면 라벨이 기본 저장소에 있는 것으로 간주됩니다.
  2. 패키지 부분이 생략되면 (예: name 또는 :name) 라벨이 현재 작업 디렉터리의 패키지에 있는 것으로 간주됩니다 (상위 수준 참조 (..)가 포함된 상대 경로는 허용되지 않음).

규칙의 종류('C++ 라이브러리' 등)를 '규칙 클래스'라고 합니다. 규칙 클래스는 Starlark (rule() 함수) 또는 Java('네이티브 규칙', RuleClass 유형)로 구현할 수 있습니다. 장기적으로 모든 언어별 규칙은 Starlark로 구현되지만 일부 기존 규칙 패밀리 (예: Java 또는 C++)는 당분간 Java로 유지됩니다.

Starlark 규칙 클래스는 load() 문을 사용하여 BUILD 파일 시작 부분에서 가져와야 하는 반면 Java 규칙 클래스는 ConfiguredRuleClassProvider에 등록되어 있으므로 Bazel에서 '본질적으로' 알고 있습니다.

규칙 클래스에는 다음과 같은 정보가 포함됩니다.

  1. 속성 (예: srcs, deps): 유형, 기본값, 제약 조건 등
  2. 각 속성에 연결된 구성 전환 및 측면(있는 경우)
  3. 규칙의 구현
  4. 전이 정보 제공자는 규칙이 '일반적으로' 생성합니다.

용어 참고: 코드베이스에서는 규칙 클래스에 의해 생성된 타겟을 의미하는 '규칙'을 자주 사용합니다. 하지만 Starlark와 사용자 대상 문서에서는 '규칙'을 규칙 클래스 자체를 지칭하는 데만 사용해야 합니다. 타겟은 그냥 '타겟'입니다. 또한 RuleClass의 이름에 'class'가 있지만 규칙 클래스와 해당 유형의 타겟 간에는 Java 상속 관계가 없습니다.

Skyframe

Bazel의 기본 평가 프레임워크를 Skyframe이라고 합니다. 이 모델은 빌드 중에 빌드해야 하는 모든 항목이 데이터 조각에서 종속 항목(즉, 빌드하는 데 필요한 다른 데이터 조각)으로 향하는 가장자리가 있는 방향성 비순환 그래프로 구성된다는 것입니다.

그래프의 노드를 SkyValue라고 하며, 노드의 이름을 SkyKey라고 합니다. 둘 다 변경할 수 없으며 변경할 수 없는 객체만 도달할 수 있어야 합니다. 이 불변식은 거의 항상 유지되며, 유지되지 않는 경우(예: BuildConfigurationValueSkyKey의 멤버인 개별 옵션 클래스 BuildOptions의 경우) 이를 변경하지 않거나 외부에서 관찰할 수 없는 방식으로만 변경하려고 노력합니다. 따라서 Skyframe 내에서 계산되는 모든 것 (예: 구성된 타겟)도 변경할 수 없어야 합니다.

Skyframe 그래프를 관찰하는 가장 편리한 방법은 그래프를 덤프하는 bazel dump --skyframe=deps를 실행하는 것입니다. 각 줄에 SkyValue 하나가 있습니다. 이 방법은 매우 큰 빌드가 될 수 있으므로 작은 빌드에 사용하는 것이 좋습니다.

Skyframe은 com.google.devtools.build.skyframe 패키지에 있습니다. 이와 비슷한 이름의 패키지 com.google.devtools.build.lib.skyframe에는 Skyframe 기반 Bazel 구현이 포함되어 있습니다. Skyframe에 대한 자세한 내용은 여기에서 확인하세요.

지정된 SkyKeySkyValue로 평가하기 위해 Skyframe은 키 유형에 해당하는 SkyFunction를 호출합니다. 함수 평가 중에 SkyFunction.Environment.getValue()의 다양한 오버로드를 호출하여 Skyframe에서 다른 종속 항목을 요청할 수 있습니다. 이렇게 하면 이러한 종속 항목이 Skyframe의 내부 그래프에 등록되어 종속 항목이 변경될 때 Skyframe에서 함수를 다시 평가할 수 있습니다. 즉, Skyframe의 캐싱 및 증분 계산은 SkyFunctionSkyValue의 세부사항에서 작동합니다.

SkyFunction가 사용할 수 없는 종속 항목을 요청할 때마다 getValue()은 null을 반환합니다. 그런 다음 함수는 null을 반환하여 제어를 Skyframe에 다시 양보해야 합니다. 나중에 Skyframe은 사용할 수 없는 종속 항목을 평가한 다음 처음부터 함수를 다시 시작합니다. 이번에는 getValue() 호출이 null이 아닌 결과로 성공합니다.

이로 인해 다시 시작하기 전에 SkyFunction 내부에서 실행된 모든 계산을 반복해야 합니다. 하지만 캐시된 종속 항목 SkyValues를 평가하기 위해 수행된 작업은 포함되지 않습니다. 따라서 일반적으로 다음 방법으로 이 문제를 해결합니다.

  1. getValuesAndExceptions()를 사용하여 일괄적으로 종속 항목을 선언하여 다시 시작 횟수를 제한합니다.
  2. SkyValue을 서로 다른 SkyFunction에서 계산한 별도의 부분으로 나누어 독립적으로 계산하고 캐시할 수 있습니다. 메모리 사용량을 늘릴 수 있으므로 전략적으로 실행해야 합니다.
  3. SkyFunction.Environment.getState()를 사용하거나 'Skyframe 뒤'에 임시 정적 캐시를 유지하여 다시 시작 간에 상태를 저장합니다. 복잡한 SkyFunctions를 사용하면 다시 시작 간의 상태 관리가 까다로워질 수 있으므로 SkyFunction 내에서 계층적 계산을 일시중지하고 재개하는 후크를 비롯하여 논리적 동시성에 대한 구조화된 접근 방식을 위해 StateMachines가 도입되었습니다. 예: DependencyResolver#computeDependenciesgetState()가 있는 StateMachine를 사용하여 구성된 타겟의 잠재적으로 큰 직접 종속 항목 집합을 계산합니다. 그렇지 않으면 비용이 많이 드는 다시 시작이 발생할 수 있습니다.

기본적으로 Bazel은 이러한 유형의 해결 방법이 필요합니다. 진행 중인 Skyframe 노드가 수십만 개에 달하는 경우가 많고 2023년 현재 Java의 경량 스레드 지원이 StateMachine 구현보다 성능이 뛰어나지 않기 때문입니다.

Starlark

Starlark는 사용자가 Bazel을 구성하고 확장하는 데 사용하는 도메인별 언어입니다. 이는 유형이 훨씬 적고, 제어 흐름에 대한 제한이 더 많으며, 가장 중요한 점은 동시 읽기를 지원하는 강력한 불변성 보장이 있는 제한된 Python 하위 집합으로 설계되었습니다. 튜링 완전이 아니므로 일부 (전부는 아님) 사용자가 언어 내에서 일반 프로그래밍 작업을 수행하려고 하지 않습니다.

Starlark는 net.starlark.java 패키지에서 구현됩니다. 또한 여기에 독립적인 Go 구현이 있습니다. Bazel에서 사용되는 Java 구현은 현재 인터프리터입니다.

Starlark는 다음을 비롯한 여러 컨텍스트에서 사용됩니다.

  1. BUILD 파일 새 빌드 타겟이 정의되는 곳입니다. 이 컨텍스트에서 실행되는 Starlark 코드는 BUILD 파일 자체의 콘텐츠와 이 파일에서 로드한 .bzl 파일에만 액세스할 수 있습니다.
  2. MODULE.bazel 파일 여기에 외부 종속 항목이 정의됩니다. 이 컨텍스트에서 실행되는 Starlark 코드는 미리 정의된 몇 가지 지시문에만 매우 제한적으로 액세스할 수 있습니다.
  3. .bzl 파일 여기에서 새 빌드 규칙, 저장소 규칙, 모듈 확장 프로그램이 정의됩니다. 여기서 Starlark 코드는 새 함수를 정의하고 다른 .bzl 파일에서 로드할 수 있습니다.

BUILD.bzl 파일에 사용할 수 있는 언어는 서로 다른 것을 표현하므로 약간 다릅니다. 차이점 목록은 여기에서 확인할 수 있습니다.

Starlark에 관한 자세한 내용은 여기에서 확인하세요.

로드/분석 단계

로딩/분석 단계는 Bazel이 특정 규칙을 빌드하는 데 필요한 작업을 결정하는 단계입니다. 기본 단위는 '구성된 타겟'이며, 이는 (타겟, 구성) 쌍입니다.

이 단계는 두 개의 서로 다른 부분으로 나눌 수 있기 때문에 '로드/분석 단계'라고 합니다. 이전에는 직렬화되었지만 이제는 시간상 겹칠 수 있습니다.

  1. 패키지 로드, 즉 BUILD 파일을 이를 나타내는 Package 객체로 변환
  2. 구성된 타겟 분석(즉, 규칙 구현을 실행하여 작업 그래프 생성)

명령줄에서 요청된 구성된 타겟의 전이적 폐쇄에 있는 각 구성된 타겟은 하향식으로 분석해야 합니다. 즉, 리프 노드를 먼저 분석한 다음 명령줄에 있는 노드까지 분석합니다. 구성된 단일 타겟 분석의 입력은 다음과 같습니다.

  1. 구성 ('how' to build that rule; 예를 들어 타겟 플랫폼뿐만 아니라 사용자가 C++ 컴파일러에 전달하기를 원하는 명령줄 옵션과 같은 항목)
  2. 직접 종속 항목 전이 정보 제공자는 분석 중인 규칙에 사용할 수 있습니다. 이러한 규칙은 구성된 타겟의 전이적 폐쇄에 있는 정보(예: 클래스 경로에 있는 모든 .jar 파일 또는 C++ 바이너리에 연결해야 하는 모든 .o 파일)의 '롤업'을 제공하므로 이러한 이름이 붙었습니다.
  3. 타겟 자체 이는 타겟이 있는 패키지를 로드한 결과입니다. 규칙의 경우 일반적으로 중요한 속성이 포함됩니다.
  4. 구성된 타겟의 구현입니다. 규칙의 경우 Starlark 또는 Java에 있을 수 있습니다. 규칙으로 구성되지 않은 모든 타겟은 Java로 구현됩니다.

구성된 타겟 분석의 출력은 다음과 같습니다.

  1. 이에 종속된 타겟을 구성한 트랜지티브 정보 제공자는
  2. 생성할 수 있는 아티팩트와 이를 생성하는 작업

Java 규칙에 제공되는 API는 RuleContext이며 이는 Starlark 규칙의 ctx 인수와 동일합니다. API가 더 강력하지만 동시에 나쁜 일™을 하기가 더 쉽습니다. 예를 들어 시간 또는 공간 복잡도가 2차인 코드를 작성하거나, Java 예외로 Bazel 서버를 비정상 종료하거나, 불변량을 위반(예: Options 인스턴스를 실수로 수정하거나 구성된 타겟을 변경 가능하게 만듦)할 수 있습니다.

구성된 타겟의 직접 종속 항목을 결정하는 알고리즘은 DependencyResolver.dependentNodeMap()에 있습니다.

구성

구성에서는 대상 빌드의 '방법'을 나타냅니다. 어떤 플랫폼에서 어떤 명령줄 옵션을 사용하는지 등을 나타냅니다.

동일한 타겟을 동일한 빌드에서 여러 구성으로 빌드할 수 있습니다. 이는 빌드 중에 실행되는 도구와 타겟 코드에 동일한 코드가 사용되고 교차 컴파일하는 경우나 팻 Android 앱 (여러 CPU 아키텍처용 네이티브 코드가 포함된 앱)을 빌드하는 경우에 유용합니다.

개념적으로 구성은 BuildOptions 인스턴스입니다. 하지만 실제로는 BuildOptions가 추가적인 다양한 기능을 제공하는 BuildConfiguration로 래핑됩니다. 종속 항목 그래프의 상단에서 하단으로 전파됩니다. 변경되면 빌드를 다시 분석해야 합니다.

이로 인해 요청된 테스트 실행 수가 변경되는 경우 테스트 타겟에만 영향을 미치더라도 전체 빌드를 다시 분석해야 하는 등의 비정상적인 상황이 발생합니다('트리밍' 구성을 통해 이러한 상황이 발생하지 않도록 할 계획이지만 아직 준비되지 않았습니다).

규칙 구현에 구성의 일부가 필요한 경우 RuleClass.Builder.requiresConfigurationFragments()를 사용하여 정의에 선언해야 합니다. 이는 실수 (예: Java 프래그먼트를 사용하는 Python 규칙)를 방지하고 Python 옵션이 변경되는 경우 C++ 타겟을 다시 분석할 필요가 없도록 구성 트리밍을 용이하게 하기 위한 것입니다.

규칙의 구성이 '상위' 규칙의 구성과 동일하지 않을 수도 있습니다. 종속 항목 가장자리에서 구성을 변경하는 프로세스를 '구성 전환'이라고 합니다. 이 문제는 다음 두 위치에서 발생할 수 있습니다.

  1. 종속 항목 가장자리에 있습니다. 이러한 전환은 Attribute.Builder.cfg()에 지정되며 Rule (전환이 발생하는 위치) 및 BuildOptions (원래 구성)에서 하나 이상의 BuildOptions (출력 구성)로의 함수입니다.
  2. 구성된 타겟으로 들어오는 모든 에지 이러한 테스트는 RuleClass.Builder.cfg()에 지정되어 있습니다.

관련 클래스는 TransitionFactoryConfigurationTransition입니다.

구성 전환은 다음과 같은 경우에 사용됩니다.

  1. 특정 종속 항목이 빌드 중에 사용되므로 실행 아키텍처에서 빌드되어야 한다고 선언하려면
  2. 특정 종속 항목을 여러 아키텍처 (예: 팻 Android APK의 네이티브 코드)용으로 빌드해야 한다고 선언하는 경우

구성 전환으로 인해 여러 구성이 발생하는 경우 이를 분할 전환이라고 합니다.

구성 전환은 Starlark에서도 구현할 수 있습니다 (문서 여기).

전이적 정보 제공자

전이 정보 제공자는 구성된 타겟이 종속된 다른 구성된 타겟에 관해 학습하는 방법 (이자 _유일한_ 방법)이며, 자신에 관한 사항을 자신에게 종속된 다른 구성된 타겟에 알리는 유일한 방법입니다. 이름에 '전이적'이 포함된 이유는 일반적으로 구성된 타겟의 전이적 폐쇄의 일종의 롤업이기 때문입니다.

일반적으로 Java 전이 정보 제공자와 Starlark 정보 제공자 간에는 1:1 대응 관계가 있습니다. 예외는 DefaultInfo입니다. 이 API는 Java API의 직접적인 트랜스리터레이션보다 Starlark에 더 적합하다고 간주되었기 때문에 FileProvider, FilesToRunProvider, RunfilesProvider의 통합입니다. 키는 다음 중 하나입니다.

  1. Java 클래스 객체입니다. 이는 Starlark에서 액세스할 수 없는 제공자에만 사용할 수 있습니다. 이러한 제공자는 TransitiveInfoProvider의 서브클래스입니다.
  2. 문자열. 이 방법은 이름 충돌이 발생하기 쉬우므로 사용이 권장되지 않습니다. 이러한 전이 정보 제공자는 build.lib.packages.Info의 직접 하위 클래스입니다 .
  3. 제공업체 기호입니다. provider() 함수를 사용하여 Starlark에서 만들 수 있으며 새 제공자를 만드는 데 권장되는 방법입니다. 기호는 Java에서 Provider.Key 인스턴스로 표시됩니다.

Java로 구현된 새 제공자는 BuiltinProvider를 사용하여 구현해야 합니다. NativeProvider는 지원 중단되었으며 (아직 삭제할 시간이 없음) TransitiveInfoProvider 하위 클래스는 Starlark에서 액세스할 수 없습니다.

구성된 타겟

구성된 타겟은 RuleConfiguredTargetFactory로 구현됩니다. Java로 구현된 각 규칙 클래스에는 하위 클래스가 있습니다. Starlark 구성 타겟은 StarlarkRuleConfiguredTargetUtil.buildRule()을 통해 생성됩니다 .

구성된 타겟 팩토리는 RuleConfiguredTargetBuilder를 사용하여 반환 값을 구성해야 합니다. 다음과 같이 구성됩니다.

  1. filesToBuild: '이 규칙이 나타내는 파일 집합'이라는 모호한 개념입니다. 이러한 파일은 구성된 타겟이 명령줄에 있거나 genrule의 srcs에 있는 경우 빌드됩니다.
  2. 실행 파일(일반 및 데이터)
  3. 출력 그룹입니다. 규칙이 빌드할 수 있는 다양한 '기타 파일 세트'입니다. BUILD의 filegroup 규칙의 output_group 속성을 사용하여 액세스할 수 있으며 Java에서는 OutputGroupInfo 제공자를 사용하여 액세스할 수 있습니다.

Runfiles

일부 바이너리는 실행하려면 데이터 파일이 필요합니다. 대표적인 예는 입력 파일이 필요한 테스트입니다. 이는 Bazel에서 'runfiles' 개념으로 표현됩니다. 'runfiles tree'는 특정 바이너리의 데이터 파일 디렉터리 트리입니다. 소스 또는 출력 트리의 파일을 가리키는 개별 심볼릭 링크가 있는 심볼릭 링크 트리로 파일 시스템에 생성됩니다.

실행 파일 집합은 Runfiles 인스턴스로 표현됩니다. 개념적으로는 실행 파일 트리의 파일 경로를 나타내는 Artifact 인스턴스로 매핑됩니다. 다음 두 가지 이유로 단일 Map보다 약간 더 복잡합니다.

  • 대부분의 경우 파일의 실행 파일 경로는 실행 경로와 동일합니다. 이를 통해 RAM을 절약합니다.
  • 실행 파일 트리에는 다양한 기존 종류의 항목이 있으며, 이러한 항목도 표현해야 합니다.

실행 파일은 RunfilesProvider를 사용하여 수집됩니다. 이 클래스의 인스턴스는 구성된 타겟 (예: 라이브러리)과 그 전이적 클로저에 필요한 실행 파일을 나타내며 중첩된 집합처럼 수집됩니다 (실제로 중첩된 집합을 사용하여 구현됨). 각 타겟은 종속 항목의 실행 파일을 결합하고 자체 실행 파일을 추가한 후 종속 항목 그래프에서 결과 집합을 위로 보냅니다. RunfilesProvider 인스턴스에는 두 개의 Runfiles 인스턴스가 포함됩니다. 하나는 'data' 속성을 통해 규칙이 종속되는 경우에 사용되고 다른 하나는 다른 모든 종류의 수신 종속 항목에 사용됩니다. 이는 타겟이 데이터 속성을 통해 종속될 때와 그렇지 않을 때 서로 다른 실행 파일을 제공하는 경우가 있기 때문입니다. 이는 아직 삭제되지 않은 원치 않는 기존 동작입니다.

바이너리의 실행 파일은 RunfilesSupport의 인스턴스로 표현됩니다. 이는 Runfiles와 다릅니다. RunfilesSupport은 실제로 빌드될 수 있기 때문입니다 (매핑일 뿐인 Runfiles와 달리). 이를 위해서는 다음 추가 구성요소가 필요합니다.

  • 입력 실행 파일 매니페스트입니다. 실행 파일 트리의 직렬화된 설명입니다. 실행 파일 트리 콘텐츠의 프록시로 사용되며, 매니페스트의 콘텐츠가 변경되는 경우에만 실행 파일 트리가 변경된다고 Bazel이 가정합니다.
  • 출력 실행 파일 매니페스트입니다. 이는 런파일 트리를 처리하는 런타임 라이브러리에서 사용됩니다. 특히 심볼릭 링크를 지원하지 않는 경우가 있는 Windows에서 사용됩니다.
  • RunfilesSupport 객체가 나타내는 바이너리를 실행하기 위한 명령줄 인수입니다.

관점

측면은 '종속 항목 그래프를 따라 계산을 전파'하는 방법입니다. Bazel 사용자를 위해 여기에 설명되어 있습니다. 좋은 동기 부여 예는 프로토콜 버퍼입니다. proto_library 규칙은 특정 언어에 관해 알아서는 안 되지만 프로토콜 버퍼 메시지('프로토콜 버퍼의 기본 단위')의 구현을 프로그래밍 언어로 빌드하는 것은 동일한 언어의 두 타겟이 동일한 프로토콜 버퍼에 종속되는 경우 한 번만 빌드되도록 proto_library 규칙에 결합되어야 합니다.

구성된 타겟과 마찬가지로 Skyframe에 SkyValue로 표시되며 구성된 타겟이 빌드되는 방식과 매우 유사하게 구성됩니다. RuleContext에 액세스할 수 있는 ConfiguredAspectFactory라는 팩토리 클래스가 있지만 구성된 타겟 팩토리와 달리 연결된 구성된 타겟과 그 제공자에 대해서도 알고 있습니다.

종속 항목 그래프 아래로 전파되는 측면 집합은 Attribute.Builder.aspects() 함수를 사용하여 각 속성에 지정됩니다. 이 프로세스에 참여하는 혼동스러운 이름의 클래스가 몇 개 있습니다.

  1. AspectClass은 측면의 구현입니다. Java(이 경우 서브클래스) 또는 Starlark (이 경우 StarlarkAspectClass 인스턴스)에 있을 수 있습니다. RuleConfiguredTargetFactory와 유사합니다.
  2. AspectDefinition는 측면의 정의입니다. 필요한 제공자, 제공하는 제공자를 포함하며 적절한 AspectClass 인스턴스와 같은 구현에 대한 참조를 포함합니다. RuleClass와 유사합니다.
  3. AspectParameters는 종속 항목 그래프 아래로 전파되는 측면을 매개변수화하는 방법입니다. 현재는 문자열 대 문자열 지도입니다. 유용한 이유의 좋은 예는 프로토콜 버퍼입니다. 언어에 API가 여러 개 있는 경우 프로토콜 버퍼를 빌드해야 하는 API에 관한 정보가 종속 항목 그래프 아래로 전파되어야 합니다.
  4. Aspect는 종속성 그래프 아래로 전파되는 측면을 계산하는 데 필요한 모든 데이터를 나타냅니다. 측면 클래스, 정의, 매개변수로 구성됩니다.
  5. RuleAspect은 특정 규칙이 전파해야 하는 측면을 결정하는 함수입니다. Rule -> Aspect 함수입니다.

약간 예상치 못한 복잡한 점은 측면이 다른 측면에 연결될 수 있다는 것입니다. 예를 들어 Java IDE의 클래스 경로를 수집하는 측면은 클래스 경로에 있는 모든 .jar 파일에 관해 알고 싶어할 수 있지만 그중 일부는 프로토콜 버퍼입니다. 이 경우 IDE 측면은 (proto_library 규칙 + Java proto 측면) 쌍에 연결하려고 합니다.

측면의 복잡성은 AspectCollection 클래스에 포착됩니다.

플랫폼 및 도구 모음

Bazel은 멀티 플랫폼 빌드를 지원합니다. 즉, 빌드 작업이 실행되는 아키텍처가 여러 개이고 코드가 빌드되는 아키텍처가 여러 개일 수 있는 빌드를 지원합니다. 이러한 아키텍처는 Bazel 용어로 플랫폼이라고 합니다 (전체 문서는 여기 참고).

플랫폼은 제약 조건 설정('CPU 아키텍처' 개념 등)에서 제약 조건 값 (x86_64와 같은 특정 CPU)으로의 키-값 매핑으로 설명됩니다. @platforms 저장소에는 가장 일반적으로 사용되는 제약 조건 설정과 값이 포함된 '사전'이 있습니다.

도구 모음이라는 개념은 빌드가 실행되는 플랫폼과 타겟팅되는 플랫폼에 따라 다른 컴파일러를 사용해야 할 수 있다는 사실에서 비롯됩니다. 예를 들어 특정 C++ 도구 모음은 특정 OS에서 실행되고 다른 OS를 타겟팅할 수 있습니다. Bazel은 설정된 실행 및 타겟 플랫폼을 기반으로 사용되는 C++ 컴파일러를 결정해야 합니다(도구 모음 관련 문서는 여기 참고).

이를 위해 툴체인에는 지원하는 실행 및 타겟 플랫폼 제약 조건 집합이 주석으로 추가됩니다. 이를 위해 도구 모음의 정의는 다음 두 부분으로 나뉩니다.

  1. 도구 모음이 지원하는 실행 및 타겟 제약 조건을 설명하고 도구 모음의 종류 (예: C++ 또는 Java)를 알려주는 toolchain() 규칙 (후자는 toolchain_type() 규칙으로 표시됨)
  2. 실제 도구 모음 (예: cc_toolchain())을 설명하는 언어별 규칙

툴체인 해결을 위해 모든 툴체인의 제약 조건을 알아야 하기 때문에 이렇게 합니다. 언어별 *_toolchain() 규칙에는 이보다 훨씬 많은 정보가 포함되어 있으므로 로드하는 데 시간이 더 오래 걸립니다.

실행 플랫폼은 다음 방법 중 하나로 지정됩니다.

  1. register_execution_platforms() 함수를 사용하는 MODULE.bazel 파일
  2. --extra_execution_platforms 명령줄 옵션을 사용하여 명령줄에서

사용 가능한 실행 플랫폼 집합은 RegisteredExecutionPlatformsFunction에서 계산됩니다 .

구성된 타겟의 타겟 플랫폼은 PlatformOptions.computeTargetPlatform()에 의해 결정됩니다 . 결국 여러 타겟 플랫폼을 지원하고 싶기 때문에 플랫폼 목록이지만 아직 구현되지는 않았습니다.

구성된 타겟에 사용할 도구 모음은 ToolchainResolutionFunction에 의해 결정됩니다. 다음과 같은 요소에 따라 달라집니다.

  • 등록된 도구 모음 집합 (MODULE.bazel 파일 및 구성에 있음)
  • 원하는 실행 및 타겟 플랫폼 (구성)
  • 구성된 타겟에 필요한 툴체인 유형의 집합입니다 (UnloadedToolchainContextKey)
  • 구성된 타겟(exec_compatible_with 속성) 및 구성(--experimental_add_exec_constraints_to_targets)의 실행 플랫폼 제약 조건 집합(UnloadedToolchainContextKey)

결과는 UnloadedToolchainContext입니다. 이는 기본적으로 툴체인 유형 (ToolchainTypeInfo 인스턴스로 표시됨)에서 선택한 툴체인의 라벨로의 맵입니다. 도구 체인 자체는 포함하지 않고 라벨만 포함하므로 '언로드됨'이라고 합니다.

그런 다음 툴체인이 실제로 ResolvedToolchainContext.load()를 사용하여 로드되고 이를 요청한 구성된 타겟의 구현에서 사용됩니다.

또한 단일 '호스트' 구성과 --cpu와 같은 다양한 구성 플래그로 표현되는 타겟 구성을 사용하는 기존 시스템도 있습니다 . 위 시스템으로 점진적으로 전환하고 있습니다. 사용자가 기존 구성 값을 사용하는 사례를 처리하기 위해 기존 플래그와 새로운 스타일 플랫폼 제약 조건 간에 변환하는 플랫폼 매핑을 구현했습니다. 코드가 PlatformMappingFunction에 있고 Starlark가 아닌 '작은 언어'를 사용합니다.

제약조건

타겟이 몇몇 플랫폼과만 호환되도록 지정해야 하는 경우가 있습니다. Bazel에는 이 목표를 달성하기 위한 여러 메커니즘이 있습니다.

  • 규칙별 제약 조건
  • environment_group()/environment()
  • 플랫폼 제약

규칙별 제약 조건은 주로 Google 내에서 Java 규칙에 사용됩니다. 이러한 제약 조건은 더 이상 사용되지 않으며 Bazel에서는 사용할 수 없지만 소스 코드에 참조가 포함될 수 있습니다. 이를 관리하는 속성을 constraints=이라고 합니다 .

environment_group() 및 environment()

이러한 규칙은 기존 메커니즘이며 널리 사용되지 않습니다.

모든 빌드 규칙은 빌드할 수 있는 '환경'을 선언할 수 있으며, 여기서 '환경'은 environment() 규칙의 인스턴스입니다.

규칙에 지원되는 환경을 지정하는 방법은 다양합니다.

  1. restricted_to= 속성을 통해 이는 가장 직접적인 사양 형식으로, 규칙에서 지원하는 정확한 환경 세트를 선언합니다.
  2. compatible_with= 속성을 통해 이는 기본적으로 지원되는 '표준' 환경 외에 규칙이 지원하는 환경을 선언합니다.
  3. 패키지 수준 속성 default_restricted_to=default_compatible_with=을 통해
  4. environment_group() 규칙의 기본 사양을 통해 모든 환경은 테마별로 관련된 동종 업계 (예: 'CPU 아키텍처', 'JDK 버전', '모바일 운영체제') 그룹에 속합니다. 환경 그룹의 정의에는 restricted_to= / environment() 속성으로 달리 지정되지 않은 경우 '기본값'으로 지원되어야 하는 환경이 포함됩니다. 이러한 속성이 없는 규칙은 모든 기본값을 상속합니다.
  5. 규칙 클래스 기본값을 통해 이렇게 하면 지정된 규칙 클래스의 모든 인스턴스에 대한 전역 기본값이 재정의됩니다. 예를 들어 이 기능을 사용하면 각 인스턴스가 이 기능을 명시적으로 선언하지 않아도 모든 *_test 규칙을 테스트할 수 있습니다.

environment()는 일반 규칙으로 구현되는 반면 environment_group()Target의 하위 클래스이면서 Rule (EnvironmentGroup)는 아니고 Starlark(StarlarkLibrary.environmentGroup())에서 기본적으로 사용할 수 있는 함수로, 결국 동명의 타겟을 생성합니다. 이는 각 환경이 속한 환경 그룹을 선언해야 하고 각 환경 그룹이 기본 환경을 선언해야 하므로 발생하는 순환 종속성을 방지하기 위한 것입니다.

--target_environment 명령줄 옵션을 사용하여 특정 환경으로 빌드를 제한할 수 있습니다.

제약 조건 확인 구현은 RuleContextConstraintSemanticsTopLevelConstraintSemantics에 있습니다.

플랫폼 제약

타겟이 호환되는 플랫폼을 설명하는 현재 '공식' 방법은 도구 모음과 플랫폼을 설명하는 데 사용되는 동일한 제약 조건을 사용하는 것입니다. 이 기능은 pull 요청 #10945에서 구현되었습니다.

공개 상태

Google과 같이 많은 개발자가 있는 대규모 코드베이스에서 작업하는 경우 다른 모든 사람이 내 코드를 임의로 사용하지 않도록 주의해야 합니다. 그렇지 않으면 Hyrum의 법칙에 따라 사용자가 구현 세부정보로 간주되는 동작에 의존하게 됩니다.

Bazel은 공개 상태라는 메커니즘을 통해 이를 지원합니다. 공개 상태 속성을 사용하여 특정 타겟에 종속될 수 있는 타겟을 제한할 수 있습니다. 이 속성은 라벨 목록을 보유하지만 이러한 라벨이 특정 타겟에 대한 포인터가 아닌 패키지 이름에 대한 패턴을 인코딩할 수 있으므로 약간 특별합니다. (예, 이는 설계 결함입니다.)

이는 다음 위치에서 구현됩니다.

  • RuleVisibility 인터페이스는 공개 상태 선언을 나타냅니다. 상수 (완전 공개 또는 완전 비공개) 또는 라벨 목록일 수 있습니다.
  • 라벨은 패키지 그룹 (사전 정의된 패키지 목록) 또는 패키지 (//pkg:__pkg__)나 패키지의 하위 트리(//pkg:__subpackages__)를 직접 참조할 수 있습니다. 이는 //pkg:* 또는 //pkg/...를 사용하는 명령줄 문법과는 다릅니다.
  • 패키지 그룹은 자체 타겟 (PackageGroup) 및 구성된 타겟 (PackageGroupConfiguredTarget)으로 구현됩니다. 원하는 경우 간단한 규칙으로 대체할 수 있습니다. 이러한 로직은 //pkg/...과 같은 단일 패턴에 해당하는 PackageSpecification, 단일 package_grouppackages 속성에 해당하는 PackageGroupContents, package_group 및 전이적 includes에 집계되는 PackageSpecificationProvider를 사용하여 구현됩니다.
  • 공개 상태 라벨 목록에서 종속 항목으로의 변환은 DependencyResolver.visitTargetVisibility 및 기타 몇몇 위치에서 실행됩니다.
  • 실제 확인은 CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()에서 실행됩니다.

중첩된 집합

구성된 타겟은 종속 항목에서 파일 집합을 집계하고 자체 파일을 추가하며 집계된 집합을 전이적 정보 제공자로 래핑하여 이에 종속된 구성된 타겟이 동일한 작업을 할 수 있도록 합니다. 예:

  • 빌드에 사용되는 C++ 헤더 파일
  • cc_library의 전이적 폐쇄를 나타내는 객체 파일
  • Java 규칙이 컴파일되거나 실행되려면 클래스 경로에 있어야 하는 .jar 파일 집합
  • Python 규칙의 전이적 폐쇄에 있는 Python 파일 집합

예를 들어 List 또는 Set를 사용하여 단순한 방식으로 이를 실행하면 2차 메모리 사용량이 발생합니다. N개의 규칙 체인이 있고 각 규칙이 파일을 추가하는 경우 1+2+...+N 컬렉션 구성원이 있습니다.

이 문제를 해결하기 위해 Google에서는 NestedSet라는 개념을 도입했습니다. 다른 NestedSet 인스턴스와 자체 멤버로 구성된 데이터 구조로, 집합의 비순환 유향 그래프를 형성합니다. 변경할 수 없으며 멤버를 반복할 수 있습니다. 여러 반복 순서 (NestedSet.Order)를 정의합니다. 선위 순회, 후위 순회, 위상(노드는 항상 상위 항목 뒤에 옴), '상관없지만 매번 동일해야 함'

동일한 데이터 구조를 Starlark에서는 depset이라고 합니다.

아티팩트 및 작업

실제 빌드는 사용자가 원하는 출력을 생성하기 위해 실행해야 하는 명령어 집합으로 구성됩니다. 명령어는 Action 클래스의 인스턴스로 표시되고 파일은 Artifact 클래스의 인스턴스로 표시됩니다. 이러한 작업은 '작업 그래프'라고 하는 이분 방향성 비순환 그래프로 정렬됩니다.

아티팩트에는 두 가지 종류가 있습니다. 소스 아티팩트 (Bazel이 실행되기 전에 사용할 수 있는 아티팩트)와 파생 아티팩트 (빌드해야 하는 아티팩트)입니다. 파생 아티팩트는 여러 종류일 수 있습니다.

  1. 일반 아티팩트 이러한 파일은 mtime을 바로가기로 사용하여 체크섬을 계산하여 최신 상태인지 확인합니다. ctime이 변경되지 않은 경우 파일의 체크섬을 계산하지 않습니다.
  2. 해결되지 않은 심볼릭 링크 아티팩트 이러한 항목은 readlink()를 호출하여 최신 상태인지 확인합니다. 일반 아티팩트와 달리 이러한 항목은 끊어진 심볼릭 링크일 수 있습니다. 일반적으로 일부 파일을 특정 보관 파일로 압축하는 경우에 사용됩니다.
  3. 트리 아티팩트 단일 파일이 아니라 디렉터리 트리입니다. 이러한 파일은 파일 집합과 콘텐츠를 확인하여 최신 상태인지 확인합니다. TreeArtifact로 표시됩니다.
  4. 상수 메타데이터 아티팩트 이러한 아티팩트를 변경해도 리빌드가 트리거되지 않습니다. 이는 빌드 스탬프 정보에만 사용됩니다. 현재 시간이 변경되었다고 해서 다시 빌드하고 싶지는 않습니다.

소스 아티팩트가 트리 아티팩트 또는 해결되지 않은 심볼릭 링크 아티팩트가 될 수 없는 근본적인 이유는 없습니다. 아직 구현하지 않았을 뿐입니다 (하지만 구현해야 함. BUILD 파일에서 소스 디렉터리를 참조하는 것은 Bazel의 몇 안 되는 알려진 장기적인 부정확성 문제 중 하나임. BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM 속성으로 사용 설정되는 일종의 작동하는 구현이 있음).

작업은 실행해야 하는 명령어, 필요한 환경, 생성되는 출력 집합으로 이해하는 것이 가장 좋습니다. 다음은 작업 설명의 주요 구성요소입니다.

  • 실행해야 하는 명령줄
  • 필요한 입력 아티팩트
  • 설정해야 하는 환경 변수
  • 실행해야 하는 환경 (예: 플랫폼)을 설명하는 주석\

콘텐츠가 Bazel에 알려진 파일을 쓰는 것과 같은 몇 가지 다른 특수한 경우도 있습니다. AbstractAction의 서브클래스입니다. 대부분의 작업은 SpawnAction 또는 StarlarkAction입니다 (동일하며 별도의 클래스가 아니어야 함). 하지만 Java와 C++에는 자체 작업 유형(JavaCompileAction, CppCompileAction, CppLinkAction)이 있습니다.

결국 모든 것을 SpawnAction로 이동하려고 합니다. JavaCompileAction는 거의 비슷하지만 C++은 .d 파일 파싱과 include 스캔으로 인해 약간 특별한 사례입니다.

작업 그래프는 대부분 Skyframe 그래프에 '삽입'됩니다. 개념적으로 작업 실행은 ActionExecutionFunction 호출로 표시됩니다. 작업 그래프 종속 항목 가장자리에서 Skyframe 종속 항목 가장자리로의 매핑은 ActionExecutionFunction.getInputDeps()Artifact.key()에 설명되어 있으며 Skyframe 가장자리 수를 낮게 유지하기 위해 몇 가지 최적화가 있습니다.

  • 파생된 아티팩트에는 자체 SkyValue가 없습니다. 대신 Artifact.getGeneratingActionKey()를 사용하여 이를 생성하는 작업의 키를 알아냅니다.
  • 중첩된 집합에는 자체 Skyframe 키가 있습니다.

공유 작업

일부 작업은 구성된 여러 타겟에 의해 생성됩니다. Starlark 규칙은 파생된 작업을 구성 및 패키지로 결정된 디렉터리에만 넣을 수 있으므로 더 제한적입니다 (하지만 동일한 패키지의 규칙도 충돌할 수 있음). 반면 Java로 구현된 규칙은 파생된 아티팩트를 어디에나 넣을 수 있습니다.

이는 잘못된 기능으로 간주되지만, 예를 들어 소스 파일을 어떤 식으로든 처리해야 하고 해당 파일이 여러 규칙에서 참조되는 경우 실행 시간을 크게 절약하므로 이를 없애는 것은 매우 어렵습니다. 이 경우 RAM이 일부 소모됩니다. 공유 작업의 각 인스턴스를 메모리에 별도로 저장해야 하기 때문입니다.

두 작업이 동일한 출력 파일을 생성하는 경우 입력과 출력, 실행되는 명령줄이 동일해야 합니다. 이 동등성 관계는 Actions.canBeShared()에 구현되며 모든 작업을 살펴봄으로써 분석 단계와 실행 단계 간에 확인됩니다. 이는 SkyframeActionExecutor.findAndStoreArtifactConflicts()에 구현되어 있으며 빌드의 '전역' 뷰가 필요한 Bazel의 몇 안 되는 위치 중 하나입니다.

실행 단계

이때 Bazel은 출력을 생성하는 명령어와 같은 빌드 작업을 실제로 실행하기 시작합니다.

분석 단계가 끝나면 Bazel은 먼저 빌드해야 하는 아티팩트를 결정합니다. 이 로직은 TopLevelArtifactHelper에 인코딩되어 있습니다. 대략적으로 말하면 명령줄에 구성된 타겟의 filesToBuild와 '이 타겟이 명령줄에 있으면 이러한 아티팩트를 빌드'라는 명시적 목적을 위한 특수 출력 그룹의 콘텐츠입니다.

다음 단계는 실행 루트를 만드는 것입니다. Bazel은 파일 시스템의 여러 위치에서 소스 패키지를 읽을 수 있으므로 (--package_path) 로컬에서 실행되는 작업에 전체 소스 트리를 제공해야 합니다. 이는 SymlinkForest 클래스에 의해 처리되며 분석 단계에서 사용된 모든 타겟을 기록하고 사용된 타겟이 있는 모든 패키지를 실제 위치에서 심볼릭 링크하는 단일 디렉터리 트리를 빌드하여 작동합니다. 또는 --package_path를 고려하여 올바른 경로를 명령어에 전달할 수 있습니다. 이는 다음과 같은 이유로 바람직하지 않습니다.

  • 패키지가 패키지 경로 항목에서 다른 항목으로 이동할 때 작업 명령줄을 변경합니다 (이전에는 흔한 일이었음).
  • 작업을 원격으로 실행하는 경우와 로컬로 실행하는 경우 명령줄이 다릅니다.
  • 사용 중인 도구에 특정한 명령줄 변환이 필요합니다(예: Java 클래스 경로와 C++ 포함 경로의 차이 고려).
  • 작업의 명령줄을 변경하면 작업 캐시 항목이 무효화됩니다.
  • --package_path는 점진적으로 지원 중단되고 있습니다.

그러면 Bazel이 작업 그래프 (작업과 입력 및 출력 아티팩트로 구성된 이분 그래프)를 순회하고 작업을 실행합니다. 각 작업의 실행은 SkyValue 클래스 ActionExecutionValue의 인스턴스로 표시됩니다.

작업 실행은 비용이 많이 들기 때문에 Skyframe 뒤에 적중할 수 있는 몇 가지 캐싱 레이어가 있습니다.

  • ActionExecutionFunction.stateMap에는 ActionExecutionFunction의 Skyframe 재시작을 저렴하게 만드는 데이터가 포함되어 있습니다.
  • 로컬 작업 캐시에는 파일 시스템 상태에 관한 데이터가 포함되어 있습니다.
  • 원격 실행 시스템에는 자체 캐시도 포함되는 경우가 많습니다.

오프라인 액션 캐시

이 캐시는 Skyframe 뒤에 있는 또 다른 레이어입니다. Skyframe에서 작업이 다시 실행되더라도 로컬 작업 캐시에서 적중할 수 있습니다. 로컬 파일 시스템의 상태를 나타내며 디스크에 직렬화되므로 새 Bazel 서버를 시작할 때 Skyframe 그래프가 비어 있어도 로컬 작업 캐시 적중을 얻을 수 있습니다.

이 캐시는 ActionCacheChecker.getTokenIfNeedToExecute() 메서드를 사용하여 적중 여부를 확인합니다 .

이름과 달리 파생된 아티팩트의 경로에서 이를 내보낸 작업으로 이어지는 맵입니다. 작업은 다음과 같이 설명됩니다.

  1. 입력 및 출력 파일과 그 체크섬의 집합
  2. 일반적으로 실행된 명령줄인 '작업 키'는 입력 파일의 체크섬으로 캡처되지 않는 모든 것을 나타냅니다 (예: FileWriteAction의 경우 작성된 데이터의 체크섬).

아직 개발 중인 매우 실험적인 '하향식 작업 캐시'도 있습니다. 이 캐시는 전이 해시를 사용하여 캐시로 여러 번 이동하지 않습니다.

입력 검색 및 입력 가지치기

일부 작업은 입력 세트만 있는 것보다 더 복잡합니다. 작업의 입력 집합에 대한 변경사항은 두 가지 형태로 제공됩니다.

  • 작업은 실행 전에 새로운 입력을 발견하거나 일부 입력이 실제로 필요하지 않다고 결정할 수 있습니다. 표준 예는 C++입니다. 여기서 모든 파일을 원격 실행기에 전송하지 않도록 C++ 파일이 전이적 클로저에서 사용하는 헤더 파일을 추측하는 것이 좋습니다. 따라서 모든 헤더 파일을 '입력'으로 등록하지 않고 전이적으로 포함된 헤더에 대해 소스 파일을 스캔하고 #include 문에 언급된 헤더 파일만 입력으로 표시하는 옵션이 있습니다 (전체 C 전처리기를 구현하지 않도록 과대평가함). 이 옵션은 현재 Bazel에서 'false'로 하드와이어링되어 있으며 Google에서만 사용됩니다.
  • 작업이 실행되는 동안 일부 파일이 사용되지 않았음을 알 수 있습니다. C++에서는 이를 '.d 파일'이라고 합니다. 컴파일러는 사후에 사용된 헤더 파일을 알려주며 Make보다 증분성이 떨어지는 당황스러운 상황을 방지하기 위해 Bazel은 이 사실을 활용합니다. 컴파일러를 사용하므로 include 스캐너보다 더 나은 추정치를 제공합니다.

이는 작업의 메서드를 사용하여 구현됩니다.

  1. Action.discoverInputs()가 호출됩니다. 필요한 것으로 확인된 아티팩트의 중첩된 집합을 반환해야 합니다. 구성된 타겟 그래프에 상응하는 항목이 없는 작업 그래프의 종속 항목 가장자리가 없도록 소스 아티팩트여야 합니다.
  2. Action.execute()를 호출하여 작업을 실행합니다.
  3. Action.execute()가 끝나면 작업에서 Action.updateInputs()를 호출하여 Bazel에 모든 입력이 필요하지 않다고 알릴 수 있습니다. 사용된 입력이 사용되지 않은 것으로 보고되면 증분 빌드가 잘못될 수 있습니다.

작업 캐시가 새 작업 인스턴스 (예: 서버 재시작 후 생성됨)에서 적중을 반환하면 Bazel은 입력 집합이 이전에 실행된 입력 검색 및 가지치기의 결과를 반영하도록 updateInputs()를 직접 호출합니다.

Starlark 작업은 ctx.actions.run()unused_inputs_list= 인수를 사용하여 일부 입력을 사용되지 않는 것으로 선언하는 기능을 사용할 수 있습니다.

다양한 방법으로 작업 실행: 전략/ActionContexts

일부 작업은 여러 가지 방법으로 실행할 수 있습니다. 예를 들어 명령줄은 로컬로, 로컬이지만 다양한 종류의 샌드박스에서 또는 원격으로 실행할 수 있습니다. 이를 구현하는 개념을 ActionContext (또는 이름 바꾸기를 절반만 성공했으므로 Strategy)라고 합니다.

작업 컨텍스트의 수명 주기는 다음과 같습니다.

  1. 실행 단계가 시작되면 BlazeModule 인스턴스에 어떤 작업 컨텍스트가 있는지 묻습니다. 이는 ExecutionTool 생성자에서 발생합니다. 작업 컨텍스트 유형은 ActionContext의 하위 인터페이스를 참조하고 작업 컨텍스트가 구현해야 하는 인터페이스인 Java Class 인스턴스로 식별됩니다.
  2. 사용 가능한 작업 컨텍스트 중에서 적절한 컨텍스트가 선택되고 ActionExecutionContextBlazeExecutor에 전달됩니다 .
  3. 작업은 ActionExecutionContext.getContext()BlazeExecutor.getStrategy()을 사용하여 컨텍스트를 요청합니다 (실제로 한 가지 방법만 있어야 함).

전략은 다른 전략을 호출하여 작업을 실행할 수 있습니다. 이는 예를 들어 로컬과 원격으로 모두 작업을 시작한 다음 먼저 완료되는 작업을 사용하는 동적 전략에서 사용됩니다.

주목할 만한 전략 중 하나는 영구 작업자 프로세스(WorkerSpawnStrategy)를 구현하는 것입니다. 일부 도구의 시작 시간이 길기 때문에 각 작업에 대해 새로 시작하는 대신 작업 간에 재사용해야 한다는 아이디어입니다. Bazel은 개별 요청 간에 관찰 가능한 상태를 전달하지 않는 작업자 프로세스의 약속에 의존하므로 이는 잠재적인 정확성 문제를 나타냅니다.

도구가 변경되면 작업자 프로세스를 다시 시작해야 합니다. 작업자를 재사용할 수 있는지 여부는 WorkerFilesHash를 사용하여 사용된 도구의 체크섬을 계산하여 결정됩니다. 이는 동작의 어떤 입력이 도구의 일부를 나타내고 어떤 입력이 입력을 나타내는지 아는 데 의존합니다. 이는 동작의 생성자가 결정합니다. Spawn.getToolFiles()Spawn의 runfile은 도구의 일부로 간주됩니다.

전략 (또는 작업 컨텍스트)에 대한 자세한 내용은 다음을 참고하세요.

  • 작업 실행을 위한 다양한 전략에 관한 정보는 여기에서 확인할 수 있습니다.
  • 로컬과 원격으로 작업을 실행하여 먼저 완료되는 작업을 확인하는 동적 전략에 관한 정보는 여기에서 확인할 수 있습니다.
  • 로컬에서 작업을 실행하는 방법에 관한 자세한 내용은 여기를 참고하세요.

로컬 리소스 관리자

Bazel은 여러 작업을 병렬로 실행할 수 있습니다. 동시에 실행해야 하는 로컬 작업의 수는 작업마다 다릅니다. 작업에 필요한 리소스가 많을수록 로컬 머신에 과부하가 걸리지 않도록 동시에 실행되는 인스턴스 수가 적어야 합니다.

이는 ResourceManager 클래스에서 구현됩니다. 각 작업에는 ResourceSet 인스턴스 (CPU 및 RAM) 형식으로 필요한 로컬 리소스의 추정치를 주석으로 추가해야 합니다. 그런 다음 작업 컨텍스트가 로컬 리소스가 필요한 작업을 실행하면 ResourceManager.acquireResources()를 호출하고 필요한 리소스를 사용할 수 있을 때까지 차단됩니다.

로컬 리소스 관리에 대한 자세한 설명은 여기에서 확인할 수 있습니다.

출력 디렉터리의 구조

각 작업에는 출력을 배치하는 출력 디렉터리의 별도 위치가 필요합니다. 파생된 아티팩트의 위치는 일반적으로 다음과 같습니다.

$EXECROOT/bazel-out/<configuration>/bin/<package>/<artifact name>

특정 구성과 연결된 디렉터리의 이름은 어떻게 결정되나요? 충돌하는 두 가지 바람직한 속성이 있습니다.

  1. 두 구성이 동일한 빌드에서 발생할 수 있는 경우 두 구성 모두 동일한 작업의 자체 버전을 가질 수 있도록 디렉터리가 달라야 합니다. 그렇지 않으면 두 구성이 동일한 출력 파일을 생성하는 작업의 명령줄과 같은 항목에 동의하지 않는 경우 Bazel은 선택할 작업을 알 수 없습니다('작업 충돌').
  2. 두 구성이 '대략' 동일한 것을 나타내는 경우 명령줄이 일치하면 한 구성에서 실행된 작업을 다른 구성에서 재사용할 수 있도록 이름이 동일해야 합니다. 예를 들어 Java 컴파일러의 명령줄 옵션 변경으로 인해 C++ 컴파일 작업이 다시 실행되어서는 안 됩니다.

지금까지는 구성 트리밍 문제와 유사한 이 문제를 원칙에 따라 해결할 방법을 찾지 못했습니다. 옵션에 관한 자세한 내용은 여기에서 확인하세요. 주요 문제 영역은 Starlark 규칙 (작성자가 일반적으로 Bazel에 익숙하지 않음)과 측면입니다. 측면은 '동일한' 출력 파일을 생성할 수 있는 항목의 공간에 또 다른 차원을 추가합니다.

현재 접근 방식은 Java로 구현된 구성 전환이 작업 충돌을 일으키지 않도록 다양한 접미사가 추가된 <CPU>-<compilation mode>가 구성의 경로 세그먼트라는 것입니다. 또한 사용자가 작업 충돌을 일으킬 수 없도록 Starlark 구성 전환 집합의 체크섬이 추가됩니다. 완벽하지는 않습니다. 이는 OutputDirectories.buildMnemonic()에서 구현되며 각 구성 프래그먼트가 출력 디렉터리의 이름에 자체 부분을 추가하는 데 의존합니다.

테스트

Bazel은 테스트 실행을 위한 다양한 기능을 지원합니다. 지원되는 옵션은 다음과 같습니다.

  • 원격으로 테스트 실행 (원격 실행 백엔드가 있는 경우)
  • 병렬로 여러 번 테스트 실행 (테스트 안정화 또는 타이밍 데이터 수집)
  • 테스트 샤딩 (속도를 위해 동일한 테스트의 테스트 사례를 여러 프로세스에 걸쳐 분할)
  • 불안정한 테스트 재실행
  • 테스트를 테스트 모음으로 그룹화

테스트는 테스트 실행 방법을 설명하는 TestProvider가 있는 정규 구성 타겟입니다.

  • 빌드 결과로 테스트가 실행되는 아티팩트입니다. 직렬화된 TestResultData 메시지가 포함된 '캐시 상태' 파일입니다.
  • 테스트를 실행해야 하는 횟수
  • 테스트를 분할해야 하는 샤드 수입니다.
  • 테스트 실행 방법에 관한 일부 매개변수 (예: 테스트 시간 제한)

실행할 테스트 결정

실행되는 테스트를 결정하는 것은 복잡한 프로세스입니다.

먼저 타겟 패턴 파싱 중에 테스트 도구 모음이 재귀적으로 확장됩니다. 확장은 TestsForTargetPatternFunction에서 구현됩니다. 약간 놀라운 점은 테스트 모음이 테스트를 선언하지 않으면 패키지의 모든 테스트를 참조한다는 것입니다. 이는 테스트 모음 규칙에 $implicit_tests이라는 암시적 속성을 추가하여 Package.beforeBuild()에서 구현됩니다.

그런 다음 명령줄 옵션에 따라 크기, 태그, 제한 시간, 언어별로 테스트가 필터링됩니다. 이는 TestFilter에서 구현되며 타겟 파싱 중에 TargetPatternPhaseFunction.determineTests()에서 호출되고 결과는 TargetPatternPhaseValue.getTestsToRunLabels()에 배치됩니다. 필터링할 수 있는 규칙 속성을 구성할 수 없는 이유는 분석 단계 전에 발생하므로 구성을 사용할 수 없기 때문입니다.

그런 다음 BuildView.createResult()에서 추가로 처리됩니다. 분석이 실패한 타겟은 필터링되고 테스트는 독점 테스트와 비독점 테스트로 분할됩니다. 그런 다음 AnalysisResult에 배치되며, ExecutionTool가 실행할 테스트를 아는 방식입니다.

이 복잡한 프로세스를 좀 더 투명하게 만들기 위해 tests() 쿼리 연산자 (TestsFunction에 구현됨)를 사용하여 명령줄에 특정 타겟이 지정될 때 실행되는 테스트를 알 수 있습니다. 불행히도 재구현이므로 위의 내용과 여러 미묘한 방식으로 다를 수 있습니다.

테스트 실행

테스트는 캐시 상태 아티팩트를 요청하여 실행됩니다. 그러면 TestRunnerAction이 실행되어 결국 요청된 방식으로 테스트를 실행하는 --test_strategy 명령줄 옵션에 의해 선택된 TestActionContext이 호출됩니다.

테스트는 환경 변수를 사용하여 테스트에 예상되는 사항을 알려주는 정교한 프로토콜에 따라 실행됩니다. 테스트에서 Bazel에 기대하는 것과 Bazel에서 테스트에 기대하는 것에 관한 자세한 설명은 여기에서 확인할 수 있습니다. 가장 간단한 경우 종료 코드 0은 성공을 의미하고 그 외의 값은 실패를 의미합니다.

캐시 상태 파일 외에도 각 테스트 프로세스는 여러 다른 파일을 내보냅니다. 이러한 파일은 타겟 구성의 출력 디렉터리인 testlogs 하위 디렉터리인 '테스트 로그 디렉터리'에 배치됩니다.

  • test.xml: 테스트 샤드의 개별 테스트 사례를 자세히 설명하는 JUnit 스타일 XML 파일
  • test.log: 테스트의 콘솔 출력입니다. stdout과 stderr이 분리되지 않습니다.
  • test.outputs: '선언되지 않은 출력 디렉터리'입니다. 터미널에 출력하는 것 외에 파일을 출력하려는 테스트에서 사용됩니다.

테스트 실행 중에는 일반 타겟 빌드 중에는 발생하지 않는 두 가지 일이 발생할 수 있습니다. 독점 테스트 실행과 출력 스트리밍입니다.

일부 테스트는 독점 모드로 실행해야 합니다(예: 다른 테스트와 병렬로 실행하지 않음). 이는 테스트 규칙에 tags=["exclusive"]를 추가하거나 --test_strategy=exclusive로 테스트를 실행하여 유도할 수 있습니다 . 각 독점 테스트는 'main' 빌드 후 테스트 실행을 요청하는 별도의 Skyframe 호출에 의해 실행됩니다. 이는 SkyframeExecutor.runExclusiveTest()에서 구현됩니다.

작업이 완료되면 터미널 출력이 덤프되는 일반 작업과 달리 사용자는 테스트 출력이 스트리밍되도록 요청하여 장기 실행 테스트의 진행 상황을 알 수 있습니다. 이는 --test_output=streamed 명령줄 옵션으로 지정되며 서로 다른 테스트의 출력이 섞이지 않도록 독점 테스트 실행을 의미합니다.

이 기능은 적절한 이름의 StreamedTestOutput 클래스에서 구현되며, 문제의 테스트 test.log 파일의 변경사항을 폴링하고 Bazel 규칙이 있는 터미널에 새 바이트를 덤프하는 방식으로 작동합니다.

실행된 테스트의 결과는 다양한 이벤트 (예: TestAttempt, TestResult, TestingCompleteEvent)를 관찰하여 이벤트 버스에서 확인할 수 있습니다. 빌드 이벤트 프로토콜에 덤프되고 AggregatingTestListener에 의해 콘솔에 출력됩니다.

커버리지 수집

커버리지는 bazel-testlogs/$PACKAGE/$TARGET/coverage.dat 파일에서 LCOV 형식으로 테스트에 의해 보고됩니다 .

범위를 수집하기 위해 각 테스트 실행은 collect_coverage.sh이라는 스크립트로 래핑됩니다 .

이 스크립트는 테스트 환경을 설정하여 커버리지 수집을 사용 설정하고 커버리지 런타임이 커버리지 파일을 쓰는 위치를 결정합니다. 그런 다음 테스트를 실행합니다. 테스트 자체에서 여러 하위 프로세스를 실행하고 여러 프로그래밍 언어로 작성된 부분으로 구성될 수 있습니다 (별도의 커버리지 수집 런타임 사용). 래퍼 스크립트는 필요한 경우 결과 파일을 LCOV 형식으로 변환하고 단일 파일로 병합합니다.

collect_coverage.sh의 인터포지션은 테스트 전략에 의해 실행되며 테스트 입력에 collect_coverage.sh가 있어야 합니다. 이는 구성 플래그 --coverage_support의 값으로 확인되는 암시적 속성 :coverage_support에 의해 달성됩니다 (TestConfiguration.TestOptions.coverageSupport 참고).

일부 언어는 오프라인 계측을 실행합니다. 즉, 컴파일 시간에 커버리지 계측이 추가됩니다 (예: C++). 다른 언어는 온라인 계측을 실행합니다. 즉, 실행 시간에 커버리지 계측이 추가됩니다.

또 다른 핵심 개념은 기준 보장 범위입니다. 라이브러리, 바이너리 또는 테스트의 커버리지입니다(내부에 실행된 코드가 없는 경우). 이 문제가 해결하는 것은 바이너리의 테스트 범위를 계산하려는 경우 모든 테스트의 범위를 병합하는 것만으로는 충분하지 않다는 것입니다. 바이너리에 어떤 테스트에도 연결되지 않은 코드가 있을 수 있기 때문입니다. 따라서 커버리지를 수집하는 파일만 포함하고 커버된 행이 없는 바이너리마다 커버리지 파일을 내보냅니다. 타겟의 기본 기준선 커버리지 파일은 bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat에 있지만 규칙은 소스 파일 이름뿐만 아니라 더 의미 있는 콘텐츠로 자체 기준선 커버리지 파일을 생성하는 것이 좋습니다.

각 규칙의 커버리지 수집을 위해 계측된 파일 집합과 계측 메타데이터 파일 집합이라는 두 그룹의 파일을 추적합니다.

계측된 파일 집합은 계측할 파일 집합일 뿐입니다. 온라인 커버리지 런타임의 경우 런타임에 이를 사용하여 계측할 파일을 결정할 수 있습니다. 기준 보장 범위를 구현하는 데도 사용됩니다.

계측 메타데이터 파일 집합은 테스트에서 Bazel에 필요한 LCOV 파일을 생성하는 데 필요한 추가 파일 집합입니다. 실제로 이는 런타임별 파일로 구성됩니다. 예를 들어 gcc는 컴파일 중에 .gcno 파일을 내보냅니다. 이러한 항목은 커버리지 모드가 사용 설정된 경우 테스트 작업의 입력 세트에 추가됩니다.

커버리지 수집 여부는 BuildConfiguration에 저장됩니다. 이 방법은 이 비트에 따라 테스트 작업과 작업 그래프를 쉽게 변경할 수 있어 유용하지만, 이 비트가 전환되면 모든 타겟을 다시 분석해야 합니다 (C++와 같은 일부 언어에서는 커버리지를 수집할 수 있는 코드를 내보내려면 다른 컴파일러 옵션이 필요하므로 이 문제가 다소 완화됩니다. 이 경우 어쨌든 다시 분석해야 하기 때문입니다).

커버리지 지원 파일은 암시적 종속성의 라벨을 통해 종속되므로 호출 정책으로 재정의할 수 있으며, 이를 통해 Bazel의 여러 버전 간에 다를 수 있습니다. 이상적으로는 이러한 차이점을 삭제하고 둘 중 하나로 표준화해야 합니다.

또한 Bazel 호출에서 각 테스트에 대해 수집된 범위를 병합하는 '범위 보고서'도 생성합니다. 이는 CoverageReportActionFactory에 의해 처리되며 BuildView.createResult()에서 호출됩니다 . 실행되는 첫 번째 테스트의 :coverage_report_generator 속성을 확인하여 필요한 도구에 액세스합니다.

쿼리 엔진

Bazel에는 다양한 그래프에 관해 다양한 사항을 요청하는 데 사용되는 작은 언어가 있습니다. 다음과 같은 쿼리 종류가 제공됩니다.

  • bazel query는 타겟 그래프를 조사하는 데 사용됩니다.
  • bazel cquery는 구성된 타겟 그래프를 조사하는 데 사용됩니다.
  • bazel aquery는 작업 그래프를 조사하는 데 사용됩니다.

각각은 AbstractBlazeQueryEnvironment를 서브클래싱하여 구현됩니다. QueryFunction을 서브클래싱하여 추가 추가 쿼리 함수를 실행할 수 있습니다. 스트리밍 쿼리 결과를 허용하기 위해 일부 데이터 구조에 수집하는 대신 query2.engine.CallbackQueryFunction에 전달되며, QueryFunction는 반환할 결과를 위해 이를 호출합니다.

쿼리 결과는 라벨, 라벨 및 규칙 클래스, XML, protobuf 등 다양한 방식으로 내보낼 수 있습니다. 이러한 클래스는 OutputFormatter의 서브클래스로 구현됩니다.

일부 쿼리 출력 형식 (proto)의 미묘한 요구사항은 Bazel이 패키지 로딩에서 제공하는 모든 정보를 내보내야 한다는 것입니다. 그래야 출력을 비교하고 특정 타겟이 변경되었는지 확인할 수 있습니다. 따라서 속성 값은 직렬화 가능해야 합니다. 복잡한 Starlark 값이 있는 속성이 없는 속성 유형이 몇 개에 불과한 이유가 바로 이 때문입니다. 일반적인 해결 방법은 라벨을 사용하고 복잡한 정보를 해당 라벨이 있는 규칙에 연결하는 것입니다. 만족스러운 해결 방법은 아니며 이 요구사항을 해제하면 매우 좋을 것입니다.

모듈 시스템

Bazel은 모듈을 추가하여 확장할 수 있습니다. 각 모듈은 BlazeModule (이름은 Blaze라고 불리던 Bazel의 역사의 유물임)를 서브클래싱해야 하며 명령어를 실행하는 동안 다양한 이벤트에 관한 정보를 가져옵니다.

이러한 기능은 주로 Google에서 사용하는 버전과 같은 일부 Bazel 버전에서만 필요한 다양한 '비핵심' 기능을 구현하는 데 사용됩니다.

  • 원격 실행 시스템 인터페이스
  • 새 명령어

BlazeModule에서 제공하는 확장 프로그램 포인트 집합은 다소 무작위입니다. 좋은 설계 원칙의 예로 사용하지 마세요.

이벤트 버스

BlazeModule이 Bazel의 나머지 부분과 통신하는 주요 방법은 이벤트 버스(EventBus)를 사용하는 것입니다. 모든 빌드에 대해 새 인스턴스가 생성되고 Bazel의 다양한 부분에서 이 인스턴스에 이벤트를 게시할 수 있으며 모듈은 관심 있는 이벤트의 리스너를 등록할 수 있습니다. 예를 들어 다음 항목은 이벤트로 표현됩니다.

  • 빌드할 빌드 타겟 목록이 결정되었습니다(TargetParsingCompleteEvent).
  • 최상위 구성이 결정되었습니다(BuildConfigurationEvent).
  • 타겟이 빌드되었습니다(성공 여부와 관계없음)(TargetCompleteEvent)
  • 테스트가 실행됨 (TestAttempt, TestSummary)

이러한 이벤트 중 일부는 빌드 이벤트 프로토콜에서 Bazel 외부로 표현됩니다(BuildEvent임). 이를 통해 BlazeModule뿐만 아니라 Bazel 프로세스 외부의 항목도 빌드를 관찰할 수 있습니다. 프로토콜 메시지가 포함된 파일로 액세스할 수 있으며, Bazel이 빌드 이벤트 서비스라는 서버에 연결하여 이벤트를 스트리밍할 수도 있습니다.

이는 build.lib.buildeventservicebuild.lib.buildeventstream Java 패키지에서 구현됩니다.

외부 저장소

Bazel은 원래 모노레포 (빌드에 필요한 모든 항목이 포함된 단일 소스 트리)에서 사용하도록 설계되었지만, Bazel은 반드시 그렇지는 않은 환경에서 사용됩니다. '외부 저장소'는 이러한 두 세계를 연결하는 데 사용되는 추상화입니다. 빌드에 필요하지만 기본 소스 트리에 없는 코드를 나타냅니다.

WORKSPACE 파일

외부 저장소 집합은 WORKSPACE 파일을 파싱하여 결정됩니다. 예를 들어 다음과 같은 선언이 있습니다.

    local_repository(name="foo", path="/foo/bar")

@foo이라는 저장소에서 결과를 사용할 수 있습니다. 여기서 복잡해지는 점은 Starlark 파일에서 새 저장소 규칙을 정의할 수 있다는 것입니다. 그러면 새 Starlark 코드를 로드하는 데 사용할 수 있고, 새 저장소 규칙을 정의하는 데 사용할 수 있습니다.

이 경우를 처리하기 위해 WorkspaceFileFunction의 WORKSPACE 파일 파싱이 load() 문으로 구분된 청크로 분할됩니다. 청크 색인은 WorkspaceFileKey.getIndex()로 표시되며 색인 X까지 WorkspaceFileFunction를 계산하는 것은 X번째 load() 문장까지 평가하는 것을 의미합니다.

저장소 가져오기

저장소의 코드를 Bazel에서 사용하려면 먼저 가져와야 합니다. 이렇게 하면 Bazel이 $OUTPUT_BASE/external/<repository name> 아래에 디렉터리를 만듭니다.

저장소 가져오기는 다음 단계로 진행됩니다.

  1. PackageLookupFunction는 저장소가 필요하다는 것을 인식하고 SkyKeyRepositoryName를 만들어 RepositoryLoaderFunction를 호출합니다.
  2. RepositoryLoaderFunction가 불분명한 이유로 요청을 RepositoryDelegatorFunction에 전달합니다 (코드에는 Skyframe이 다시 시작될 경우 항목을 다시 다운로드하지 않기 위해서라고 나와 있지만 매우 확실한 이유는 아님).
  3. RepositoryDelegatorFunction는 요청된 저장소가 발견될 때까지 WORKSPACE 파일의 청크를 반복하여 가져오도록 요청된 저장소 규칙을 알아냅니다.
  4. 저장소 가져오기를 구현하는 적절한 RepositoryFunction이 발견됩니다. 이는 저장소의 Starlark 구현이거나 Java로 구현된 저장소의 하드 코딩된 맵입니다.

저장소를 가져오는 데 비용이 많이 들 수 있으므로 다양한 캐싱 레이어가 있습니다.

  1. 체크섬(RepositoryCache)으로 키가 지정된 다운로드된 파일의 캐시가 있습니다. 이렇게 하려면 체크섬이 WORKSPACE 파일에 있어야 하지만 이는 hermeticity에 좋습니다. 이는 워크스페이스나 출력 베이스에 관계없이 동일한 워크스테이션의 모든 Bazel 서버 인스턴스에서 공유됩니다.
  2. '마커 파일'은 $OUTPUT_BASE/external 아래의 각 저장소에 대해 작성되며, 여기에는 가져오는 데 사용된 규칙의 체크섬이 포함됩니다. Bazel 서버가 다시 시작되지만 체크섬이 변경되지 않으면 다시 가져오지 않습니다. 이는 RepositoryDelegatorFunction.DigestWriter에서 구현됩니다 .
  3. --distdir 명령줄 옵션은 다운로드할 아티팩트를 조회하는 데 사용되는 다른 캐시를 지정합니다. 이는 Bazel이 인터넷에서 임의의 항목을 가져오면 안 되는 엔터프라이즈 설정에서 유용합니다. 이는 DownloadManager에 의해 구현됩니다 .

저장소를 다운로드하면 저장소의 아티팩트가 소스 아티팩트로 취급됩니다. Bazel은 일반적으로 소스 아티팩트에서 stat()을 호출하여 최신 상태인지 확인하고 이러한 아티팩트는 포함된 저장소의 정의가 변경될 때도 무효화되므로 문제가 됩니다. 따라서 외부 저장소의 아티팩트 FileStateValue는 해당 외부 저장소에 종속되어야 합니다. 이 작업은 ExternalFilesHelper에서 처리됩니다.

저장소 매핑

여러 저장소가 동일한 저장소에 종속되기를 원하지만 버전이 다른 경우가 있을 수 있습니다('다이아몬드 종속성 문제'의 인스턴스). 예를 들어 빌드의 별도 저장소에 있는 두 바이너리가 Guava에 종속되려고 하면 둘 다 @guava//로 시작하는 라벨로 Guava를 참조하고 서로 다른 버전을 의미한다고 예상할 것입니다.

따라서 Bazel을 사용하면 문자열 @guava//가 한 바이너리의 저장소에서는 한 Guava 저장소 (예: @guava1//)를 참조하고 다른 바이너리의 저장소에서는 다른 Guava 저장소 (예: @guava2//)를 참조하도록 외부 저장소 라벨을 다시 매핑할 수 있습니다.

또는 다이아몬드를 연결하는 데 사용할 수도 있습니다. 저장소가 @guava1//에 종속되고 다른 저장소가 @guava2//에 종속되는 경우 저장소 매핑을 사용하면 표준 @guava// 저장소를 사용하도록 두 저장소를 모두 다시 매핑할 수 있습니다.

매핑은 개별 저장소 정의의 repo_mapping 속성으로 WORKSPACE 파일에 지정됩니다. 그런 다음 Skyframe에 WorkspaceFileValue의 멤버로 표시되며, 여기에서 다음으로 연결됩니다.

  • Package.Builder.repositoryMapping 패키지의 라벨 값 속성을 RuleClass.populateRuleAttributeValues()로 변환하는 데 사용됩니다.
  • 분석 단계에서 사용되는 Package.repositoryMapping (로딩 단계에서 파싱되지 않는 $(location)과 같은 항목을 해결하는 데 사용됨)
  • load() 문에서 라벨을 해결하기 위한 BzlLoadFunction

JNI 비트

Bazel 서버는 대부분 Java로 작성됩니다. 예외는 Java가 자체적으로 할 수 없거나 Google에서 구현할 때 자체적으로 할 수 없었던 부분입니다. 이는 주로 파일 시스템, 프로세스 제어, 기타 다양한 하위 수준 항목과의 상호작용으로 제한됩니다.

C++ 코드는 src/main/native에 있고 네이티브 메서드가 있는 Java 클래스는 다음과 같습니다.

  • NativePosixFilesNativePosixFileSystem
  • ProcessUtils
  • WindowsFileOperationsWindowsFileProcesses
  • com.google.devtools.build.lib.platform

콘솔 출력

콘솔 출력을 내보내는 것은 간단한 일처럼 보이지만 여러 프로세스 (때로는 원격으로)를 실행하고, 세부적인 캐싱을 하고, 멋지고 다채로운 터미널 출력을 원하고, 장기 실행 서버를 사용하면 간단하지 않습니다.

클라이언트에서 RPC 호출이 들어온 직후 stdout 및 stderr용 RpcOutputStream 인스턴스 두 개가 생성되어 인쇄된 데이터를 클라이언트로 전달합니다. 그런 다음 OutErr(stdout, stderr 쌍)로 래핑됩니다. 콘솔에 출력해야 하는 모든 항목은 이러한 스트림을 거칩니다. 그런 다음 이러한 스트림이 BlazeCommandDispatcher.execExclusively()에 전달됩니다.

출력은 기본적으로 ANSI 이스케이프 시퀀스로 인쇄됩니다. 이러한 값이 필요하지 않은 경우 (--color=no) AnsiStrippingOutputStream에 의해 삭제됩니다. 또한 System.outSystem.err은 이러한 출력 스트림으로 리디렉션됩니다. 이렇게 하면 System.err.println()를 사용하여 디버깅 정보를 출력할 수 있으며 서버의 터미널 출력과 다른 클라이언트의 터미널 출력에 표시됩니다. 프로세스에서 바이너리 출력 (예: bazel query --output=proto)을 생성하는 경우 stdout이 멍글링되지 않도록 주의합니다.

짧은 메시지 (오류, 경고 등)는 EventHandler 인터페이스를 통해 표현됩니다. 특히 이러한 값은 EventBus에 게시하는 내용과 다릅니다 (혼동스러움). 각 Event에는 EventKind (오류, 경고, 정보 등)가 있으며 Location (이벤트가 발생한 소스 코드의 위치)가 있을 수도 있습니다.

일부 EventHandler 구현은 수신한 이벤트를 저장합니다. 이는 캐시된 구성된 타겟에서 내보낸 경고와 같이 다양한 종류의 캐시된 처리로 인해 UI에 재생되는 정보를 위해 사용됩니다.

일부 EventHandler는 결국 이벤트 버스로 이동하는 이벤트를 게시할 수도 있습니다 (일반 Event는 여기에 표시되지 _않음_). 이는 ExtendedEventHandler 구현이며 기본 용도는 캐시된 EventBus 이벤트를 재생하는 것입니다. 이러한 EventBus 이벤트는 모두 Postable를 구현하지만 EventBus에 게시된 모든 항목이 이 인터페이스를 구현하는 것은 아닙니다. ExtendedEventHandler에 의해 캐시된 항목만 구현합니다 (대부분의 항목이 구현되므로 좋지만 강제되지는 않음).

터미널 출력은 Bazel이 실행하는 모든 멋진 출력 형식 지정과 진행 상황 보고를 담당하는 UiEventHandler를 통해 대부분 내보내집니다. 입력은 두 개입니다.

  • 이벤트 버스
  • 리포터를 통해 파이프된 이벤트 스트림

명령어 실행 메커니즘 (예: Bazel의 나머지 부분)이 클라이언트에 대한 RPC 스트림에 직접 연결되는 유일한 방법은 이러한 스트림에 직접 액세스할 수 있는 Reporter.getOutErr()을 통해서입니다. 명령어가 가능한 바이너리 데이터를 대량으로 덤프해야 하는 경우에만 사용됩니다 (예: bazel query).

Bazel 프로파일링

Bazel은 빠릅니다. 빌드가 견딜 수 있는 한계까지 커지는 경향이 있어 Bazel도 느립니다. 이러한 이유로 Bazel에는 빌드와 Bazel 자체를 프로파일링하는 데 사용할 수 있는 프로파일러가 포함되어 있습니다. Profiler라는 적절한 이름의 클래스에서 구현됩니다. 기본적으로 사용 설정되어 있지만 오버헤드가 허용되도록 축약된 데이터만 기록합니다. --record_full_profiler_data 명령줄을 사용하면 가능한 모든 항목이 기록됩니다.

Chrome 프로파일러 형식으로 프로필을 내보냅니다. Chrome에서 보는 것이 가장 좋습니다. 데이터 모델은 작업 스택입니다. 작업을 시작하고 종료할 수 있으며 서로 깔끔하게 중첩되어야 합니다. 각 Java 스레드는 자체 작업 스택을 가져옵니다. 할 일: 작업 및 연속 전달 스타일과 어떻게 작동하나요?

프로파일러는 각각 BlazeRuntime.initProfiler()BlazeRuntime.afterCommand()에서 시작 및 중지되며 모든 것을 프로파일링할 수 있도록 최대한 오랫동안 라이브 상태를 유지하려고 시도합니다. 프로필에 항목을 추가하려면 Profiler.instance().profile()를 호출합니다. 작업의 종료를 나타내는 클로저가 있는 Closeable를 반환합니다. try-with-resources 문과 함께 사용하는 것이 가장 좋습니다.

MemoryProfiler에서는 기본적인 메모리 프로파일링도 실행합니다. 또한 항상 사용 설정되어 있으며 주로 최대 힙 크기와 GC 동작을 기록합니다.

Bazel 테스트

Bazel에는 두 가지 주요 테스트가 있습니다. Bazel을 '블랙박스'로 관찰하는 테스트와 분석 단계만 실행하는 테스트입니다. 전자를 '통합 테스트'라고 하고 후자를 '단위 테스트'라고 하지만, 후자는 통합 테스트에 더 가깝습니다. 필요한 경우 실제 단위 테스트도 있습니다.

통합 테스트에는 두 가지 종류가 있습니다.

  1. src/test/shell 아래에 매우 정교한 bash 테스트 프레임워크를 사용하여 구현된 항목
  2. Java로 구현된 항목 이러한 클래스는 BuildIntegrationTestCase의 서브클래스로 구현됩니다.

BuildIntegrationTestCase는 대부분의 테스트 시나리오에 적합하므로 선호되는 통합 테스트 프레임워크입니다. Java 프레임워크이므로 디버깅 가능성과 여러 일반적인 개발 도구와의 원활한 통합을 제공합니다. Bazel 저장소에는 BuildIntegrationTestCase 클래스의 예가 많이 있습니다.

분석 테스트는 BuildViewTestCase의 서브클래스로 구현됩니다. BUILD 파일을 쓰는 데 사용할 수 있는 스크래치 파일 시스템이 있으며, 다양한 도우미 메서드는 구성된 타겟을 요청하고, 구성을 변경하고, 분석 결과에 관한 다양한 사항을 어설션할 수 있습니다.