BazelCon 2022는 11월 16~17일에 뉴욕과 온라인에서 개최됩니다.
지금 등록하기

Bazel 코드베이스

컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요.

이 문서는 코드베이스 및 Bazel의 구조화 방식에 대한 설명입니다. 최종 사용자가 아닌 Bazel에 기여하고자 하는 사용자를 대상으로 합니다.

소개

Bazel의 코드베이스는 규모가 크고 (~350KLOC 프로덕션 코드 및 ~260 KLOC 테스트 코드) 누구도 전체 환경에 익숙하지 않습니다. 모두가 자신의 골짜기를 매우 잘 알고 있지만 모든 방향에서 언덕 위에 있는 무언가를 아는 사람은 거의 없습니다.

이 경로에서는 숲이 어두워져서 길을 잃지 않는 사람들을 쉽게 찾을 수 있도록 이 코드베이스를 더 쉽게 시작할 수 있도록 코드베이스의 개요를 제공합니다.

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

참여는 일반 GitHub pull 요청 메커니즘을 통해 수락되고, Google 직원이 내부 소스 트리로 직접 가져온 다음 GitHub로 다시 내보냅니다.

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

Bazel의 대량은 서버 프로세스 내에 위치하며 빌드 사이에는 RAM이 유지됩니다. 따라서 Bazel은 빌드 간에 상태를 유지할 수 있습니다.

Bazel 명령줄에는 시작 및 명령어라는 두 가지 옵션이 있습니다. 명령줄은 다음과 같습니다.

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

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

각 서버 인스턴스에는 연결된 소스 트리 ("workspace&quot) 한 개가 있으며 각 작업공간에는 일반적으로 단일 활성 서버 인스턴스가 있습니다. 맞춤 출력 기반을 지정하면 이를 회피할 수 있습니다 (자세한 내용은 '디렉터리 레이아웃' 섹션 참고).

Bazel은 유효한 .zip 파일인 단일 ELF 실행 파일로 배포됩니다. bazel을 입력하면 C++로 구현된 위의 ELF 실행 파일("client")이 제어됩니다. 다음 단계에 따라 적절한 서버 프로세스를 설정합니다.

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

적합한 서버 프로세스가 준비되면 실행해야 하는 명령어가 gRPC 인터페이스를 통해 통신됩니다. 그러면 Bazel의 출력이 터미널로 다시 전달됩니다. 한 번에 하나의 명령어만 실행할 수 있습니다. 이는 C++의 구성요소와 자바의 부분을 사용하는 정교한 잠금 메커니즘을 사용하여 구현됩니다. 여러 명령어를 동시에 실행할 수 있는 인프라가 있습니다. bazel version를 다른 명령어와 동시에 실행할 수 없기 때문에 다소 창피한 상황입니다. 기본 차단 기능은 BlazeModule의 수명 주기와 BlazeRuntime의 일부 상태입니다.

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

Ctrl-C를 누르면 클라이언트가 gRPC 연결의 Cancel 호출로 변환되어 최대한 빨리 명령어를 종료하려고 시도합니다. 세 번째 Ctrl-C 이후에는 클라이언트가 대신 서버로 SIGKILL을 전송합니다.

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

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

디렉터리 레이아웃

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

'workspace'는 Bazel이 실행되는 소스 트리입니다. 일반적으로 소스 컨트롤에서 체크아웃한 항목에 해당합니다.

Bazel은 모든 데이터를 '출력 사용자 루트' 아래에 배치합니다. 일반적으로 $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에서 가져온 외부 저장소
  • exec 루트, 현재 빌드의 모든 소스 코드에 대한 심볼릭 링크가 포함되어 있습니다. $OUTPUT_BASE/execroot에 있습니다. 빌드 중에 작업 디렉터리는 $EXECROOT/<name of main repository>입니다. $EXECROOT로 변경할 계획이지만 호환되지 않는 변경사항이기 때문에 장기적인 요금제입니다.
  • 빌드 중 빌드된 파일.

명령어를 실행하는 프로세스

Bazel 서버가 제어권을 갖고 실행해야 하는 명령어에 대해 알림을 받으면 다음 이벤트 시퀀스가 발생합니다.

  1. BlazeCommandDispatcher에서 새 요청에 관해 알립니다. 명령어를 실행할 작업공간이 필요한지 (버전이나 도움말 등 소스 코드와 관련이 없는 명령어를 제외한 거의 모든 명령) 그리고 다른 명령어가 실행 중인지 여부를 결정합니다.

  2. 올바른 명령어를 찾았습니다. 각 명령어는 BlazeCommand 인터페이스를 구현하고 @Command 주석을 보유해야 합니다. 이는 일종의 안티패턴이며, 명령어에 필요한 모든 메타데이터가 BlazeCommand의 메서드에 의해 설명되면 유용합니다.

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

  4. 이벤트 버스가 생성되었습니다. 이벤트 버스는 빌드 중에 발생하는 이벤트의 스트림입니다. 그중 일부는 빌드 진행 방법을 알리기 위해 빌드 이벤트 프로토콜에 따라 Bazel 외부로 내보내집니다.

  5. 명령어가 제어됩니다. 가장 흥미로운 명령어는 빌드 실행, 테스트, 실행, 적용 등입니다. 이 기능은 BuildTool에 의해 구현됩니다.

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

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

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

명령줄 옵션

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

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

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

경고: Google은 OptionsBase 인스턴스가 불변인 것으로 간주하고 인스턴스를 그러한 방식으로 사용합니다 (예: SkyKeys의 경우). 따라서 인스턴스를 수정해서 Bazel이 까다로운 방법으로 디버그할 수 있는 아주 좋은 방법입니다. 안타깝게도 변경사항을 변경할 수 없도록 만드는 작업은 매우 번거롭습니다. 생성 후 즉시 FragmentOptions를 수정하면 다른 사용자가 이 참조를 유지할 수 있게 되며 equals() 또는 hashCode()가 호출되기 전에는 괜찮습니다.

Bazel은 다음과 같은 방식으로 옵션 클래스에 대해 학습합니다.

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

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

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

Bazel에서 본 소스 트리

Bazel은 소스 코드를 읽고 해석하여 소프트웨어를 빌드하는 업무를 담당합니다. Bazel이 운영하는 소스 코드의 총합을 '작업공간'이라고 하며, 저장소, 패키지 및 규칙으로 구조화됩니다.

저장소

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

저장소는 루트 디렉터리에 WORKSPACE (또는 WORKSPACE.bazel)라는 파일로 표시됩니다. 이 파일에는 전체 빌드에 해당하는 'global' 정보가 포함됩니다(예: 사용 가능한 외부 저장소 집합). 일반 Starlark 파일처럼 작동합니다. 즉, 다른 Starlark 파일을 load()할 수 있습니다. 이는 명시적으로 참조되는 저장소에서 필요한 저장소를 가져오는 데 사용됩니다 (여기서 'deps.bzl 패턴'이라고 함).

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

빌드를 실행할 때 전체 소스 트리를 서로 연결해야 합니다. 이 작업은 SymlinkForest에 의해 실행됩니다. SymlinkForest는 기본 저장소의 모든 패키지를 $EXECROOT에 심볼릭 링크를 만들고 외부 저장소는 $EXECROOT/external 또는 $EXECROOT/..로 연결합니다 (물론 전자는 기본 저장소에 external라는 패키지를 가질 수 없습니다. 이 때문에 패키지에서 마이그레이션하는 것입니다).

패키지

모든 저장소는 패키지, 관련 파일 모음, 종속 항목 사양으로 구성됩니다. 이는 BUILD 또는 BUILD.bazel라는 파일로 지정됩니다. 둘 다 있는 경우 Bazel이 BUILD.bazel을 선호합니다. BUILD 파일이 계속 승인되는 이유는 Bazel의 상위 요소인 Blaze가 이 파일 이름을 사용하기 때문입니다. 그러나 파일 이름에서 대소문자를 구분하지 않는, 특히 Windows에서는 흔히 사용되는 경로 세그먼트로 확인되었습니다.

패키지는 서로 독립적입니다. 패키지의 BUILD 파일을 변경하면 다른 패키지가 변경될 수 없습니다. 재귀 glob이 패키지 경계에서 중지되므로 BUILD 파일이 있으면 반복이 중지되므로 BUILD 파일을 추가하거나 삭제하면 다른 패키지가 변경될 수 있습니다.

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

패키지 로드 중 복잡성이 상당히 많아지고 있습니다. Bazel은 모든 소스 파일을 명시적으로 나열할 필요 없이 대신 glob(예: glob(["**/*.java"]))을 실행할 수 있습니다. 셸과 달리 하위 패키지로 하위에 포함되지 않는 하위 glob을 지원합니다. 이를 위해 파일 시스템에 액세스해야 할 수 있으며, 이는 속도가 느릴 수 있으므로 최대한 병렬로 효율적으로 실행되도록 모든 종류의 기술을 구현합니다.

Globving은 다음 클래스에서 구현됩니다.

  • 빠르고 행복하며 스카이프레임을 인식하지 못하는 글로버 LegacyGlobber
  • SkyframeHybridGlobber - 스카이프레임을 사용하고 '스카이프레임 재시작'을 피하기 위해 기존 globber로 되돌리는 버전(아래에서 설명)

Package 클래스 자체에는 WORKSPACE 파일을 파싱하는 데만 사용되고 실제 패키지에는 적절하지 않은 멤버가 일부 포함되어 있습니다. 이 문제는 설계상의 결함입니다. 일반 패키지를 설명하는 객체에는 다른 것을 설명하는 필드가 포함되면 안 되기 때문입니다. 예를 들면 다음과 같습니다.

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

WORKSPACE 파일을 파싱하는 것과 일반 패키지를 파싱하는 것 사이의 구분이 더 바람직하여 Package가 두 가지 요구사항을 모두 충족할 필요가 없습니다. 안타깝게도 이 두 가지는 매우 밀접하게 관련되어 있기 때문에 확인하기가 어렵습니다.

라벨, 대상, 규칙

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

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

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

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

일종의 규칙 (예: "C++ 라이브러리")을 '규칙 클래스'라고 합니다. 규칙 클래스는 Starlark (rule() 함수) 또는 자바('네이티브 규칙'이라고 함)로 구현될 수 있습니다. 장기적으로 모든 언어별 규칙은 Starlark에서 구현되지만 일부 기존 규칙 계열 (예: 자바 또는 C++)은 당분간 계속 자바로 제공됩니다.

Starlark 규칙 클래스는 load() 문을 사용하여 BUILD 파일의 시작 부분에서 가져와야 하지만 자바 규칙 클래스는 ConfiguredRuleClassProvider로 등록하여 Bazel이 이를 인지하고 있습니다.

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

  1. 속성 (예: srcs, deps): 유형, 기본값, 제약 조건 등
  2. 구성 전환 및 각 속성에 연결된 요소(있는 경우)
  3. 규칙 구현
  4. 대개의 경우 규칙에서 만드는 전환 정보 제공자는

용어 참고사항: 코드베이스에서는 규칙 클래스로 만든 대상을 의미하기 위해 '규칙'을 사용하는 경우가 많습니다. 하지만 Starlark 및 사용자 대상 문서에서 '규칙'은 규칙 클래스 자체를 참조하는 데만 사용되어야 합니다. 타겟은 '타겟'일 뿐입니다. 또한 RuleClass의 이름에 '클래스'가 있더라도 규칙 클래스와 해당 유형의 대상 간에는 자바 상속 관계가 없습니다.

스카이프레임

Bazel의 기반이 되는 평가 프레임워크는 Skyframe이라고 합니다. 모델에서는 빌드 중에 빌드해야 하는 모든 항목이 방향성 비순환 그래프로 구성되며 가장자리는 데이터에서 모든 종속 항목(즉, 구성되어야 하는 다른 데이터 요소)을 가리킵니다.

그래프의 노드를 SkyValue이라고 하고 노드 이름을 SkyKey라고 합니다. 둘 다 변경할 수 없으며 변경 불가능한 객체만 연결할 수 있어야 합니다. 이 불변 요소는 대부분 항상 유지되지만, 그렇지 않은 경우(예: BuildConfigurationValueSkyKey의 구성원인 개별 옵션 클래스 BuildOptions의 경우) 이를 변경하지 않거나 외부에서 관찰할 수 없는 방식으로 변경하려고 매우 노력합니다. 이렇게 하면 스카이프레임 내에서 계산된 모든 항목 (예: 구성된 타겟)도 변경할 수 없어야 합니다.

스카이프레임 그래프를 관찰하는 가장 편리한 방법은 bazel dump --skyframe=detailed 그래프를 실행하는 것입니다. 이는 그래프를 한 줄에 하나씩 SkyValue 실행합니다. 크기가 상당히 클 수 있으므로 작은 빌드에는 이 방법을 사용하는 것이 가장 좋습니다.

스카이프레임은 com.google.devtools.build.skyframe 패키지에 상주합니다. 비슷한 이름의 패키지 com.google.devtools.build.lib.skyframe에는 스카이프레임을 기반으로 하는 Bazel 구현이 포함되어 있습니다. Skyframe에 관한 자세한 내용은 여기를 참고하세요.

SkyValue를 생성하려면 다음 단계를 따르세요.

  1. 연결된 SkyFunction 실행
  2. SkyFunction가 작업을 실행하는 데 필요한 종속 항목 (예: SkyValue) 선언 SkyFunction.Environment.getValue()의 다양한 오버로드를 호출하면 됩니다.
  3. 종속 항목을 사용할 수 없는 경우 getValue()에서 null을 반환하여 신호를 보냅니다. 이 경우 SkyFunction는 null을 반환하여 Skyframe에 제어 권한을 부여할 것으로 예상됩니다. 그런 다음 Skyframe은 아직 평가되지 않은 종속 항목을 평가하고 SkyFunction를 다시 호출하여 (1)로 돌아갑니다.
  4. 결과 SkyValue 생성

결과적으로 (3)에서 모든 종속 항목을 사용할 수 없다면 함수를 완전히 다시 시작해야 하므로 계산을 다시 실행해야 합니다. 이는 분명히 비효율적입니다. Google에서는 여러 가지 방법으로 이 문제를 해결합니다.

  1. 그룹에서 SkyFunction의 종속 항목을 선언하여 함수에 종속 항목이 10개 있으면 10번이 아니라 한 번만 다시 시작하면 됩니다.
  2. 한 함수를 여러 번 다시 시작할 필요가 없도록 SkyFunction를 분할합니다. 이렇게 하면 SkyFunction 내부에 있을 수 있는 데이터를 Skyframe에 삽입하여 메모리 사용량이 늘어나는 부작용이 있습니다.
  3. 상태 (예: ActionExecutionFunction.stateMap에서 실행 중인 작업 상태)를 유지하기 위해 Skyframe 뒷면의 캐시를 사용합니다. 이 경우 결국에는 가독성에 도움이 되지 않는 연속 전달 스타일 (예: 작업 실행)으로 코드가 작성됩니다.

물론 이는 모두 스카이프레임의 한계에 대한 해결책일 뿐입니다. 이는 대부분 자바가 경량 스레드를 지원하지 않고 종종 수십만 개의 스카이프레임 노드가 있기 때문입니다.

스타라크

Starlark는 Bazel을 구성하고 확장하는 데 사용하는 도메인별 언어입니다. 이는 Python의 훨씬 제한된 유형인 제어 흐름에 대한 더 많은 제한사항 및 무엇보다도 동시 읽기를 사용 설정하는 강력한 불변성 보장이 포함된 제한된 하위 집합으로 생각됩니다. 튜링이 완전하지는 않으므로 일부 사용자는 전체가 아닌 특정 언어 내에서 일반적인 프로그래밍 작업을 수행하지 못하게 됩니다.

Starlark는 com.google.devtools.build.lib.syntax 패키지에 구현됩니다. 여기에 독립적인 Go 구현도 있습니다. Bazel에 사용되는 자바 구현은 현재 인터프리터입니다.

Starlark는 4가지 컨텍스트에서 사용됩니다.

  1. The BUILD language. This is where new rules are defined. Starlark code running in this context only has access to the contents of the BUILD file itself and Starlark files loaded by it.
  2. 규칙 정의. 새 규칙 (예: 새 언어 지원)은 이렇게 정의됩니다. 이 컨텍스트에서 실행되는 Starlark 코드는 직접 종속 항목에서 제공하는 구성 및 데이터에 액세스할 수 있습니다 (자세한 내용은 뒷부분 참고).
  3. WORKSPACE 파일. 여기에서 외부 저장소 (기본 소스 트리에 없는 코드)가 정의됩니다.
  4. 저장소 규칙 정의. 여기에서 새 외부 저장소 유형이 정의됩니다. 이 컨텍스트에서 실행되는 Starlark 코드는 Bazel이 실행 중인 머신에서 임의 코드를 실행하고 작업공간 외부에 도달할 수 있습니다.

BUILD 및 .bzl 파일에 사용할 수 있는 방언은 다른 내용을 표현하기 때문에 약간 다릅니다. 차이점 목록은 여기에서 확인할 수 있습니다.

Starlark에 대한 자세한 내용은 여기를 참조하세요.

로드/분석 단계

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

이는 '로드/분석 단계'라고 합니다. 2가지 개별 부분으로 분할할 수 있는데, 이전에는 이 부분이 직렬화되어 있었지만 이제 시간이 겹쳐질 수 있습니다.

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

명령줄에서 요청된 구성된 타겟의 전이적 클로저에 있는 각 구성된 대상은 상향식으로 분석되어야 합니다. 즉, 리프 노드가 먼저 그런 다음 명령줄의 리프 노드가 분석됩니다. 구성된 단일 타겟 분석에 대한 입력은 다음과 같습니다.

  1. 구성. 규칙을 작성하는 방법(예: 타겟 플랫폼, 사용자가 C++ 컴파일러로 전달되기를 바라는 명령줄 옵션 등도 포함)
  2. 직접 종속 항목. 전이 정보 제공자는 분석 중인 규칙에 사용할 수 있습니다. 클래스 경로의 모든 .jar 파일이나 C++ 바이너리에 연결해야 하는 모든 .o 파일과 같이 구성된 타겟의 전이적 클로저에 관한 정보의 '롤업'을 제공하기 때문에 이와 같이 호출됩니다.
  3. 대상 자체. 대상이 있는 패키지를 로드한 결과입니다. 규칙의 경우 이는 일반적으로 중요한 속성입니다.
  4. 구성된 대상의 구현입니다. 규칙의 경우 Starlark 또는 자바에 있을 수 있습니다. 규칙이 아닌 모든 대상은 자바로 구현됩니다.

구성된 대상을 분석한 결과는 다음과 같습니다.

  1. 종속 대상을 구성하는 전이 정보 제공자는
  2. 만들 수 있는 아티팩트와 아티팩트를 생성하는 작업

자바 규칙에 제공되는 API는 RuleContext이며 Starlark 규칙의 ctx 인수와 같습니다. API는 더 강력하지만 동시에 시간 또는 공간 복잡도가 2차 (또는 그 이상)인 코드를 작성하거나 Bazel 서버가 자바 예외와 충돌하도록 하거나 (예: Options 인스턴스를 실수로 수정 또는 구성된 대상을 변경 가능하게 하는) Bad ThingsTM를 더 쉽게 수행할 수 있습니다.

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

구성

구성은 어떤 플랫폼, 어떤 명령줄 옵션을 사용하는지 등 타겟을 빌드하는 '방법'입니다.

같은 빌드의 여러 구성에 대해 동일한 대상을 빌드할 수 있습니다. 이는 예를 들어 빌드 중에 실행되는 도구와 타겟 코드에 같은 코드를 사용하고 크로스 컴파일하거나 여러 Android CPU 앱 (여러 CPU 아키텍처용 네이티브 코드를 포함하는 앱)을 빌드할 때 유용합니다.

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

이로 인해 예를 들어 테스트 타겟에만 영향을 미치지만 요청된 테스트 실행 수가 변경되는 경우 전체 빌드를 재분석해야 하는 등의 문제가 발생할 수 있습니다 (이러한 구성이 실제로는 아니더라도 미리 준비할 수는 있음).

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

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

  1. 종속 항목 에지 이러한 전환은 Attribute.Builder.cfg()에 지정되며, Rule(전환이 발생하는 위치)와 BuildOptions(원본 구성)에서 하나 이상의 BuildOptions(출력 구성)로의 함수입니다.
  2. 구성된 대상으로 수신되는 에지에 사용합니다. 이는 RuleClass.Builder.cfg()에 지정되어 있습니다.

관련 클래스는 TransitionFactoryConfigurationTransition입니다.

구성 전환이 사용됩니다. 예를 들면 다음과 같습니다.

  1. 특정 종속 항목이 빌드 중에 사용된다고 선언하려면 실행 아키텍처에서 빌드되도록 해야 함
  2. 여러 아키텍처를 위해 특정 종속 항목을 빌드해야 한다고 선언하는 경우 (예: 지방 Android APK의 네이티브 코드)

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

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

전환 정보 제공업체

전이 정보 제공자는 구성된 타겟에 종속되는 다른 구성된 대상에 대해 알릴 수 있는 방법 (및 _only _way)입니다. 이름에 'transitive'가 있는 이유는 일반적으로 구성된 대상의 전이적 폐쇄의 롤업이기 때문입니다.

일반적으로 자바 전이 정보 제공자와 Starlark 제공자 간에 1:1 서신이 있습니다 (단, FileProvider, FilesToRunProvider, RunfilesProvider의 조합인 DefaultInfo는 예외임). 이는 API가 자바의 직접 음역보다 Starlark처럼 보였기 때문입니다. 키는 다음 중 하나입니다.

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

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

구성된 대상

구성된 타겟은 RuleConfiguredTargetFactory으로 구현됩니다. 자바에 구현된 각 규칙 클래스에는 서브클래스가 있습니다. Starlark 구성 대상은 StarlarkRuleConfiguredTargetUtil.buildRule()를 통해 생성됩니다 .

구성된 타겟 팩토리는 RuleConfiguredTargetBuilder를 사용하여 반환 값을 구성해야 합니다. 다음 항목으로 구성됩니다.

  1. 이 규칙이 나타내는 파일 집합의 옅은 컨셉인 filesToBuild가 표시됩니다. 구성된 타겟이 명령줄 또는 src의 src에 있을 때 생성되는 파일입니다.
  2. 실행 파일, 일반, 데이터
  3. 출력 그룹 이러한 파일은 규칙을 작성할 수 있는 다양한 '기타 파일 세트'입니다. BUILD에서 파일 그룹 규칙의 output_group 속성을 사용하고 자바에서 OutputGroupInfo 공급자를 사용하여 액세스할 수 있습니다.

실행 파일

일부 바이너리는 실행할 데이터 파일이 필요합니다. 눈에 띄는 예는 입력 파일이 필요한 테스트입니다. Bazel에서 "runfiles"라는 개념으로 표현됩니다. 'runfiles 트리'는 특정 바이너리의 데이터 파일 디렉터리 트리입니다. 파일 시스템에서 심볼릭 트리로 생성되며 심볼릭 링크는 출력 트리 소스의 파일을 가리킵니다.

runfile 집합은 Runfiles 인스턴스로 표시됩니다. 이는 개념적으로 runfile 트리의 파일 경로에서 이 파일을 나타내는 Artifact 인스턴스로의 매핑입니다. 단일 Map보다 다음과 같은 두 가지 이유로 인해 조금 더 복잡합니다.

  • 대부분의 경우 파일의 runfiles 경로는 execpath와 동일합니다. 이를 사용하여 RAM을 절약합니다.
  • runfile 트리에는 다양한 종류의 기존 항목이 있으며, 이는 표현해야 합니다.

실행 파일은 RunfilesProvider를 사용하여 수집됩니다. 이 클래스의 인스턴스는 구성된 타겟 (예: 라이브러리)과 전이적 닫힘 요구를 나타내며 중첩된 집합처럼 수집됩니다 (실제로는 표지 아래의 중첩된 세트를 사용하여 구현됨). 각 대상은 종속 항목의 실행 파일을 통합하고 일부 자체를 추가한 후 종속 항목 그래프에서 결과 집합을 보냅니다. RunfilesProvider 인스턴스에는 Runfiles 인스턴스가 두 개 있습니다. 하나는 'data' 속성을 통해 규칙이 종속되는 인스턴스가고 다른 하나는 다른 모든 종류의 수신 종속 항목을 위한 인스턴스입니다. 타겟이 달리 데이터 속성을 사용하면 다른 실행 파일을 표시할 때가 있기 때문입니다. 이는 아직 제거되지 않은 바람직하지 않은 레거시 동작입니다.

바이너리의 실행 파일은 RunfilesSupport의 인스턴스로 표시됩니다. 매핑인 Runfiles와 달리 RunfilesSupport는 실제로 빌드될 수 있다는 점에서 Runfiles과 다릅니다. 이를 위해서는 다음 추가 구성요소가 필요합니다.

  • 입력 runfile 매니페스트. 다음은 runfile 트리의 직렬화된 설명입니다. 이 파일은 runfiles 트리 콘텐츠의 프록시로 사용되며 Bazel은 매니페스트 파일의 콘텐츠가 변경된 경우에만 runfile 트리가 변경된다고 가정합니다.
  • 출력 runfiles 매니페스트 이는 특히 Windows에서 실행 파일 트리를 처리하는 런타임 라이브러리에서 사용하며, 심볼릭 링크는 지원하지 않는 경우도 있습니다.
  • Runfile 미들맨 실행 파일 트리가 존재하려면 심볼릭 링크 트리와 심볼릭 링크가 가리키는 아티팩트를 빌드해야 합니다. 종속 항목 가장자리의 수를 줄이기 위해 runfile 미들맨을 사용하여 이러한 모든 것을 나타낼 수 있습니다.
  • RunfilesSupport 객체가 나타내는 실행 파일이 있는 바이너리를 실행하기 위한 명령줄 인수

측면

측면은 종속 항목 그래프에 계산을 전파하는 방법입니다. Bazel 사용자는 여기에 설명되어 있습니다. 좋은 신호는 프로토콜 버퍼입니다. proto_library 규칙은 특정 언어를 알 수 없지만, 모든 프로그래밍 언어에서 프로토콜 버퍼 메시지 (프로토콜 버퍼의 '기본 단위')의 구현을 빌드하는 것은 proto_library 규칙과 결합되어야 합니다. 그래야 같은 언어의 타겟 두 개가 같은 프로토콜 버퍼에 종속되면 한 번만 빌드됩니다.

구성된 타겟과 마찬가지로 이러한 타겟은 Skyframe에서 SkyValue으로 표현되고 구성되는 방식이 구성된 방식과 매우 유사합니다. 여기에는 RuleContext라는 액세스 권한이 있는 ConfiguredAspectFactory라는 팩토리 클래스가 있지만 구성된 타겟 팩토리와 달리 연결된 타겟 및 제공자도 알고 있습니다.

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

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

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

측면의 측면의 복잡성은 AspectCollection 클래스에서 캡처됩니다.

플랫폼 및 도구 모음

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

플랫폼은 제약 조건 설정(예: 'CPU 아키텍처'의 개념)에서 제약 조건 값(예: x86_64와 같은 특정 CPU)에 대한 키-값 매핑으로 설명합니다. @platforms 저장소에는 가장 흔히 사용되는 제약 조건 설정과 값이 있습니다.

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

이를 위해 도구 모음은 지원하는 실행 및 대상 플랫폼 제약조건으로 주석을 답니다. 이를 위해 도구 모음의 정의는 다음 두 부분으로 나뉩니다.

  1. 도구 모음이 지원하는 실행 및 대상 제약 조건의 집합을 설명하고 도구 모음의 종류 (예: C++ 또는 자바)를 설명하는 toolchain() 규칙 (후자의 경우 toolchain_type() 규칙으로 표시됨)
  2. 실제 도구 모음을 설명하는 언어별 규칙 (예: cc_toolchain())

이렇게 하는 이유는 도구 모음 확인을 하기 위해 모든 도구 모음의 제약 조건을 알아야 하고 언어별 *_toolchain() 규칙에는 그보다 훨씬 많은 정보가 포함되기 때문에 로드 시간이 더 많이 소요됩니다.

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

  1. WORKSPACE 파일에서 register_execution_platforms() 함수를 사용합니다.
  2. 명령줄에서 --extra_execution_platforms 명령줄 옵션 사용

사용 가능한 실행 플랫폼 세트는 RegisteredExecutionPlatformsFunction로 계산됩니다 .

구성된 타겟의 타겟 플랫폼은 PlatformOptions.computeTargetPlatform()로 결정됩니다. 플랫폼 목록은 결국 여러 대상 플랫폼을 지원하기를 원하지만 아직 구현되지 않았기 때문에 플랫폼 목록입니다.

구성된 대상에 사용할 도구 모음 집합은 ToolchainResolutionFunction에 따라 결정됩니다. 다음과 같은 함수입니다.

  • 등록된 도구 모음 집합 (WORKSPACE 파일 및 구성)
  • 원하는 실행 및 대상 플랫폼 (구성)
  • 구성된 대상 (UnloadedToolchainContextKey)에서)에 필요한 도구 모음 유형 집합
  • UnloadedToolchainContextKey에 구성된 타겟 (exec_compatible_with 속성)과 구성(--experimental_add_exec_constraints_to_targets)의 실행 플랫폼 제약조건 세트

결과는 UnloadedToolchainContext입니다. 이는 기본적으로 도구 모음 유형 (ToolchainTypeInfo 인스턴스로 표시됨)에서 선택한 도구 모음의 라벨입니다. 도구 모음 자체가 아닌 라벨만 포함되어 있기 때문에 '로드되지 않음'이라고 합니다.

그러면 도구 모음은 실제로 ResolvedToolchainContext.load()를 사용하여 로드되며 도구 모음을 요청한 구성된 대상의 구현에 의해 사용됩니다.

또한 하나의 구성 및 타겟 구성이 --cpu 같은 다양한 구성 플래그로 표현되므로 이를 사용하는 기존 시스템도 있습니다. 위 시스템으로 점진적으로 전환 중입니다. 사용자가 기존 구성 값을 사용하는 경우를 처리하기 위해 Google은 플랫폼 매핑을 구현하여 레거시 플래그와 새로운 스타일의 플랫폼 제약조건 간에 변환합니다. 이들의 코드는 PlatformMappingFunction로 되어 있고 Starlark가 아닌 '작은 언어'를 사용합니다.

제약 조건

대상이 일부 플랫폼과만 호환되는 것으로 지정하려는 경우가 있습니다. 안타깝게도 Bazel은 다음과 같은 여러 메커니즘을 보유하고 있습니다.

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

규칙별 제약조건은 대부분 자바 규칙용으로 Google 내에서 사용됩니다. 이러한 제약 조건은 곧 출시될 예정이며 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/...을 사용하는 명령줄 구문과 다릅니다.
  • 패키지 그룹은 자체 타겟 및 구성된 타겟 유형 (PackageGroupPackageGroupConfiguredTarget)으로 구현됩니다. 원한다면 간단한 규칙으로 대체할 수 있습니다.
  • 공개 상태 라벨 목록에서 종속 항목으로의 변환은 DependencyResolver.visitTargetVisibility 및 기타 몇 가지 위치에서 실행됩니다.
  • 실제 확인은 CommonPrerequisiteValidator.validateDirectPrerequisiteVisibility()에서 실행됩니다.

중첩 세트

일반적으로 구성된 타겟은 종속 항목에서 파일 집합을 집계하고, 자체 항목을 추가하고, 집계 세트를 전이 정보 제공자로 래핑하여 종속된 구성된 대상이 동일하게 수행할 수 있도록 합니다. 예:

  • 빌드에 사용되는 C++ 헤더 파일
  • cc_library의 전이적 폐쇄를 나타내는 객체 파일
  • 자바 규칙이 컴파일하거나 실행할 수 있도록 클래스 경로에 있어야 하는 .jar 파일 집합
  • Python 규칙의 전이적 클로저에 있는 Python 파일 세트

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

이 문제를 해결하기 위해 NestedSet라는 개념을 생각해 냈습니다. 다른 NestedSet 인스턴스 및 자체의 인스턴스로 구성된 데이터 구조이므로 집합의 방향성 비순환 그래프를 형성합니다. 변경할 수 없으며 구성원들이 반복될 수 있습니다. Google에서는 여러 반복 순서 (NestedSet.Order)를 정의합니다. 선주문, 순서 추가, 토폴로지(노드가 항상 상위 항목 다음에 옴)하고 상관없지만 매번 동일해야 합니다.

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

아티팩트 및 작업

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

아티팩트는 두 가지 종류로, 소스 아티팩트 (Bazel이 실행을 시작하기 전에 사용 가능한 아티팩트)와 파생 아티팩트 (빌드해야 하는 아티팩트)가 있습니다. 파생된 아티팩트는 그 자체로 여러 종류일 수 있습니다.

  1. **일반 아티팩트. **이러한 파일은 체크섬을 계산하여 바로가기에 의해 최신 상태로 확인되며, mtime은 바로가기로 사용됩니다. ctime이 변경되지 않았다면 파일을 체크섬하지 않습니다.
  2. 확인되지 않은 심볼릭 링크 아티팩트. readlink()를 호출하여 최신 상태인지 확인합니다. 일반 아티팩트와 달리 버벅거림은 심볼릭 링크일 수 있습니다. 일반적으로 어떤 파일을 일종의 아카이브로 압축하는 경우에 사용됩니다.
  3. 트리 아티팩트. 이 파일은 단일 파일이 아니라 디렉터리 트리입니다. 파일에 포함된 파일 집합과 콘텐츠를 확인하여 최신 상태를 확인합니다. TreeArtifact로 표시됩니다.
  4. 상수 메타데이터 아티팩트. 이러한 아티팩트를 변경해도 재빌드는 트리거되지 않습니다. 이는 빌드 스탬프 정보에만 사용됩니다. 현재 시간이 변경되었다는 이유만으로는 재빌드를 원하지 않습니다.

소스 아티팩트가 트리 아티팩트이거나 확인되지 않은 심볼릭 링크 아티팩트일 수 없는 기본적인 이유는 없습니다. 아직 구현하지 않았기 때문입니다. 그러나 BUILD 파일에서 소스 디렉터리를 참조하는 것은 오래전에 알려진 잘못된 오류 문제 중 하나이며 BAZEL_TRACK_SOURCE_DIRECTORIES=1 JVM을 사용 설정한 종류의 작업이 있습니다.

주목할 만한 Artifact 종류는 중개자입니다. 이러한 인스턴스는 MiddlemanAction의 출력인 Artifact 인스턴스로 표시됩니다. 몇 가지 특별한 경우에 사용됩니다.

  • 합산 중간자는 아티팩트를 함께 그룹화하는 데 사용됩니다. 따라서 많은 작업이 동일한 입력 세트를 사용하는 경우 N*M 종속 항목 가장자리가 없고 N+M만 중첩됩니다 (중첩된 세트로 교체됨).
  • 종속 항목 중간 사용자를 예약하면 작업이 다른 항목보다 먼저 실행되도록 합니다. 대부분 린트뿐 아니라 C++ 컴파일에도 사용됩니다 (설명은 CcCompilationContext.createMiddleman() 참고).
  • 실행 파일 트리는 출력 매니페스트 및 runfile 트리에서 참조하는 모든 단일 아티팩트에 별도로 의존하지 않아도 되도록 실행 파일 트리가 있는지 확인하는 데 사용됩니다.

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

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

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

결국에는 모든 것을 SpawnAction로 이동하고자 합니다. JavaCompileAction는 상당히 가깝지만 C++은 .d 파일 파싱 및 스캔 포함으로 인해 특별한 경우가 있습니다.

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

  • 파생된 아티팩트에는 자체 SkyValue가 없습니다. 대신 Artifact.getGeneratingActionKey()는 키를 생성하는 작업의 키를 찾는 데 사용됩니다.
  • 중첩된 세트에는 자체 Skyframe 키가 있습니다.

공유 작업

일부 작업은 구성된 여러 타겟에 의해 생성됩니다. Starlark 규칙은 파생 작업을 구성 및 패키지에 의해 결정되는 디렉터리에만 배치할 수 있기 때문에 더 제한됩니다. 그러나 동일한 패키지의 규칙은 충돌할 수 있습니다. 하지만 자바에서 구현된 규칙은 어디에나 파생될 수 있습니다.

이는 잘못된 기능으로 간주됩니다. 하지만 예를 들어 소스 파일이 어떤 식으로 처리되어야 하고 해당 파일이 여러 규칙 (핸드웨이브-핸드웨이브)에 의해 참조되는 경우 실행 시간을 크게 줄일 수 있기 때문에 이를 제거하기가 매우 어렵습니다. 이로 인해 RAM이 손실될 수 있습니다. 공유 작업의 각 인스턴스는 메모리에 별도로 저장해야 합니다.

두 개의 작업이 동일한 출력 파일을 생성하는 경우 정확히 동일해야 합니다. 입력과 출력이 동일하고 같은 명령줄을 실행해야 합니다. 이 등가 관계는 Actions.canBeShared()에서 구현되며 모든 작업을 확인하여 분석 단계와 실행 단계 간에 확인됩니다. 이는 SkyframeActionExecutor.findAndStoreArtifactConflicts()에 구현되며 Bazel에서 빌드의 '전역' 뷰가 필요한 몇 가지 위치 중 하나입니다.

실행 단계

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

분석 단계 후 Bazel이 가장 먼저 해야 할 일은 어떤 아티팩트를 빌드해야 하는지 결정하는 것입니다. 이를 위한 로직은 TopLevelArtifactHelper에 인코딩되어 있습니다. 대략적으로 말하자면 명령줄에서 구성된 타겟의 filesToBuild이고, 명시적으로 이 표현을 표현하기 위한 특수 출력 그룹의 콘텐츠입니다. 이 타겟이 명령줄에 있으면 이러한 아티팩트를 빌드합니다.

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

  • 패키지가 패키지 경로 항목에서 다른 경로로 이동할 때 작업 명령줄을 변경합니다 (흔히 발생함).
  • 작업이 로컬에서 실행되는 경우와 원격으로 실행될 경우 서로 다른 명령줄이 생깁니다.
  • 사용 중인 도구와 관련된 명령줄 변환이 필요합니다(자바 클래스 경로와 C++ 포함 경로와 같은 차이점 고려).
  • 작업의 명령줄을 변경하면 작업의 캐시 항목이 무효화됩니다.
  • --package_path가 서서히 지원 중단됨

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

작업 실행에는 비용이 많이 들기 때문에 스카이프레임 뒤에 들어갈 수 있는 몇 가지 캐싱 레이어가 있습니다.

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

오프라인 액션 캐시

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

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

이름과 달리 추출된 아티팩트의 경로에서 이를 내보낸 작업으로의 맵입니다. 이 작업은 다음과 같이 설명합니다.

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

캐시가 많이 사용되는 것을 방지하기 위해 전이 해시를 사용하여 아직 개발 중인 고도로 실험적인 '하향식 작업 캐시'도 있습니다.

입력 검색 및 입력 잘라내기

일부 작업은 단순히 입력 세트를 사용하는 것보다 더 복잡합니다. 작업의 입력 집합에 대한 변경사항은 두 가지 형식으로 제공됩니다.

  • 작업 실행 전에 새로운 입력을 탐색하거나 일부 입력이 실제로 필요하지 않다고 판단할 수 있습니다. 표준 예는 C++로, C++ 파일에서 전이적 클로저로부터 어떤 헤더 파일을 사용하는지에 관해 교육용으로 추측하는 것이 좋습니다. 현재 모든 파일을 원격 실행자에게 보내는 데 주의를 기울입니다. 따라서 모든 헤더 파일을 등록하지 않아도 됩니다. 가령 '인터터블이 포함되어 있는 경우' 소스화된 라인을 명시해야 합니다.
  • 작업 중에 일부 파일이 사용되지 않았음을 인식할 수 있습니다. C++에서는 이를 '.d 파일'이라고 합니다. 컴파일러는 사후에 사용된 헤더 파일을 알려주며, Make보다 점진적인 증분에 따른 당황을 피하기 위해 Bazel이 이 사실을 사용합니다. 이 검사는 컴파일러를 사용하므로 포함 스캐너보다 더 나은 예상값을 제공합니다.

Actions on 메서드를 사용하여 구현됩니다.

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

작업 캐시가 새 작업 인스턴스에 대한 조회를 반환하면 (예: 서버 재시작 후 생성됨) Bazel은 입력 자체가 입력 검색 및 제거의 결과를 반영하도록 updateInputs() 자체를 호출합니다.

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

작업을 실행하는 다양한 방법: 전략/ActionContext

일부 작업은 서로 다른 방식으로 실행할 수 있습니다. 예를 들어 명령줄은 로컬에서 실행할 수 있지만 다양한 종류의 샌드박스에서 또는 원격으로 실행할 수 있습니다. 이를 반영하는 개념은 ActionContext (또는 Strategy)라고 합니다. 이름을 바꾸면 절반밖에 남지 않았기 때문입니다...

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

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

전략에서는 다른 전략을 호출하여 자유롭게 작업을 수행할 수 있습니다. 예를 들어 로컬에서 또는 원격으로 작업을 시작한 다음 가장 먼저 완료되는 동적 전략에 이 전략을 사용할 수 있습니다.

한 가지 주목할 만한 전략은 영구적인 작업자 프로세스(WorkerSpawnStrategy)를 구현하는 전략입니다. 일부 도구는 시작 시간이 길기 때문에 작업마다 처음부터 새로 시작하는 대신 작업 간에 재사용해야 합니다 (Bazel이 개별 요청 간에 식별 가능한 상태를 수행하지 않는다는 약속을 지니므로 잠재적 정확성 문제를 나타냄).

도구가 변경되면 작업자 프로세스를 다시 시작해야 합니다. 작업자의 재사용 가능 여부는 WorkerFilesHash를 사용하여 사용되는 도구의 체크섬을 계산하여 결정됩니다. 이 도구는 작업의 어느 부분이 도구의 일부를 나타내는지, 입력을 나타내는지를 알아야 합니다. 이는 작업의 생성자에 따라 결정됩니다. Spawn.getToolFiles()Spawn의 실행 파일은 도구의 일부로 계산됩니다.

전략 (또는 액션 컨텍스트)에 대한 추가 정보:

  • 작업 실행을 위한 다양한 전략에 대한 정보는 여기에서 확인할 수 있습니다.
  • 동적 전략에 관한 정보는 로컬 및 원격에서 어느 작업이 먼저 실행되는지 확인하는 작업을 여기에서 확인할 수 있습니다.
  • 로컬에서 작업을 실행하는 복잡한 방법에 관한 정보는 여기에서 확인할 수 있습니다.

로컬 리소스 관리자

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

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

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

출력 디렉터리의 구조

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

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

특정 구성과 연결된 디렉터리의 이름은 어떻게 결정되나요? 2개의 바람직한 속성이 있습니다.

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

지금까지 구성 자르기 문제와 유사한 이 문제를 해결할 수 있는 원칙적인 방법은 떠오르지 않았습니다. 옵션에 대한 자세한 설명은 여기에서 확인할 수 있습니다. 주된 문제점은 Starlark 규칙 (저자가 일반적으로 Bazel에 친숙하지 않음)과 측면으로, '동일한' 출력 파일을 생성할 수 있는 또 다른 차원의 공간을 추가합니다.

현재 접근 방식은 구성의 경로 세그먼트가 다양한 접미사가 추가된 <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를 사용하여 테스트를 실행하여 확인할 수 있습니다. 각 독점 테스트는 별도의 '기본' 빌드 후 테스트 실행을 요청하는 별도의 Skyframe 호출로 실행됩니다. 이는 SkyframeExecutor.runExclusiveTest()에서 구현됩니다.

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

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

실행된 테스트의 결과는 TestAttempt, TestResult 또는 TestingCompleteEvent와 같은 다양한 이벤트를 관찰하여 이벤트 버스에서 확인할 수 있습니다. 이러한 결과는 빌드 이벤트 프로토콜에 덤프되며 AggregatingTestListener에 의해 콘솔에 전송됩니다.

노출 범위 수집

테스트 범위는 LCOV 형식의 테스트에 의해 bazel-testlogs/$PACKAGE/$TARGET/coverage.dat 파일에 보고됩니다 .

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

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

collect_coverage.sh의 삽입은 테스트 전략에 의해 실행되며 collect_coverage.sh이 테스트 입력에 있어야 합니다. 이는 암시적 속성 :coverage_support를 통해 구현됩니다. 이 속성은 구성 플래그 --coverage_support 값으로 결정됩니다(TestConfiguration.TestOptions.coverageSupport 참고).

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

또 다른 핵심 개념은 기준 적용 범위입니다. 실행 중인 코드가 없는 경우 라이브러리, 바이너리 또는 테스트의 적용 범위입니다. 이 경우 바이너리의 테스트 범위를 계산하려는 경우 모든 테스트의 범위를 병합하는 것만으로는 충분하지 않습니다. 바이너리에 테스트에 연결되지 않은 코드가 있을 수 있기 때문입니다. 따라서 Google에서는 커버리지 라인 없이 커버리지를 수집하는 파일만 포함하는 모든 바이너리에 대한 커버리지 파일을 내보냅니다. 타겟의 기준 범위 파일은 bazel-testlogs/$PACKAGE/$TARGET/baseline_coverage.dat에 있습니다 . Bazel에 --nobuild_tests_only 플래그를 전달하면 테스트 외에도 바이너리 및 라이브러리에도 생성됩니다.

현재 기준 범위가 손상되었습니다.

각 규칙의 범위 수집을 위해 두 가지 파일 그룹 즉 계측 파일 집합과 계측 메타데이터 파일 집합을 추적합니다.

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

계측 메타데이터 파일 세트는 테스트에서 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.Callback에 전달하여 QueryFunction를 반환하려고 합니다.

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

몇 가지 쿼리 출력 형식 (proto)의 미묘한 요구사항은 Bazel이 패키지 로드에서 제공하는 정보를 _내보내야 출력을 출력하고 특정 대상이 변경되었는지 확인할 수 있다는 것입니다. 따라서 속성 값을 직렬화할 수 있어야 합니다. 따라서 복잡한 Starlark 값이 있는 속성이 없는 속성 유형은 거의 없습니다. 일반적인 해결 방법은 라벨을 사용하고 해당 라벨이 있는 규칙에 복잡한 정보를 연결하는 것입니다. 아주 만족스럽지 않은 해결 방법이며, 이 요구사항을 없애면 매우 좋을 것입니다.

모듈 시스템

Bazel은 모듈을 추가하여 확장할 수 있습니다. 각 모듈은 BlazeModule를 서브클래스로 지정해야 하며(이름이 Bazel이라고 불렸을 때 Bazel의 기록을 활용함) 명령어를 실행하는 동안 다양한 이벤트에 관한 정보를 가져옵니다.

대부분 Bazel의 일부 버전 (예: Google에서 사용하는 버전)에만 필요한 다양한 '비핵심' 기능을 구현하는 데 사용됩니다.

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

BlazeModule에서 제공하는 확장 프로그램 세트는 다소 복잡합니다. 좋은 디자인 원칙의 예로 사용하지 마세요.

이벤트 버스

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

  • 빌드할 빌드 대상 목록이 결정되었습니다(TargetParsingCompleteEvent).
  • 최상위 구성이 결정되었습니다(BuildConfigurationEvent).
  • 대상이 빌드되었는지, 성공적으로 빌드되었는지 여부(TargetCompleteEvent)
  • 테스트 실행됨(TestAttempt, TestSummary)

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

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

외부 저장소

Bazel은 원래 모노레포 (빌드해야 하는 모든 것이 포함된 단일 소스 트리)에 사용되도록 설계되었지만, Bazel은 이것이 반드시 필요한 것은 아닌 세상에 살고 있습니다. '외부 저장소'는 이러한 두 세계를 연결하는 데 사용되는 추상화로, 빌드에 필요하지만 기본 소스 트리에 없는 코드를 나타냅니다.

WORKSPACE 파일

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

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

@foo라는 저장소를 사용할 수 있습니다. 더 복잡해지는 경우, Starlark 파일에 새 저장소 규칙을 정의할 수 있습니다. 그러면 새 Starlark 코드를 로드하는 데 사용하여 새 저장소 규칙을 정의할 수 있습니다.

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

저장소를 가져오는 중

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

저장소를 가져오는 단계는 다음과 같습니다.

  1. PackageLookupFunction는 저장소가 필요하다는 것을 인식하고 RepositoryNameSkyKey로 만들어 RepositoryLoaderFunction를 호출합니다.
  2. RepositoryLoaderFunction는 명확하지 않은 이유로 요청을 RepositoryDelegatorFunction에 전달합니다. 코드에는 Skyframe이 다시 시작될 때 다시 다운로드하지 않도록 되어 있지만, 그 이유는 명확하지 않습니다.
  3. RepositoryDelegatorFunction는 요청된 저장소를 찾을 때까지 WORKSPACE 파일의 청크를 반복하여 반복하여 가져올 저장소 규칙을 찾습니다.
  4. 저장소 가져오기를 구현하는 적절한 RepositoryFunction가 있습니다. 저장소의 Starlark 구현이나 자바로 구현된 저장소에 대한 하드 코딩 맵입니다.

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

  1. 다운로드한 파일에 대한 체크섬(RepositoryCache)으로 키가 지정된 캐시가 있습니다. 이를 위해 체크섬을 WORKSPACE 파일에서 사용할 수 있어야 하지만 밀폐에는 적합합니다. 이는 실행 중인 작업공간 또는 출력 기반에 관계없이 동일한 워크스테이션의 모든 Bazel 서버 인스턴스에서 공유됩니다.
  2. '마커 파일'은 각 저장소마다 가져오는 데 사용된 규칙의 체크섬이 포함된 $OUTPUT_BASE/external로 작성됩니다. Bazel 서버가 다시 시작되지만 체크섬이 변경되지 않으면 다시 가져오지 않습니다. 이는 RepositoryDelegatorFunction.DigestWriter에서 구현됩니다.
  3. --distdir 명령줄 옵션은 다운로드할 아티팩트를 찾는 데 사용되는 다른 캐시를 지정합니다. 이 방법은 Bazel이 인터넷에서 임의의 항목을 가져오지 않아야 하는 엔터프라이즈 설정에 유용합니다. 이는 DownloadManager에서 구현합니다 .

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

관리 디렉터리

간혹 외부 저장소가 작업공간 루트 아래에서 파일을 수정해야 하는 경우가 있습니다(예: 다운로드한 패키지를 소스 트리의 하위 디렉터리에 보관하는 패키지 관리자). Bazel이 소스 파일이 사용자에 의해서만 수정되고 패키지가 작업공간 루트 아래의 모든 디렉터리를 참조할 수 있다고 가정한다는 경우는 없습니다. 이러한 종류의 외부 저장소를 작동하기 위해 Bazel은 다음 두 가지 작업을 수행합니다.

  1. Bazel이 도달할 수 없는 작업공간의 하위 디렉터리를 사용자가 지정할 수 있습니다. .bazelignore이라는 파일에 나열되고 기능은 BlacklistedPackagePrefixesFunction에 구현됩니다.
  2. 작업공간의 하위 디렉터리에서 작업공간의 하위 디렉터리로의 매핑을 ManagedDirectoriesKnowledge로 인코딩하고, 이를 참조하는 FileStateValue는 일반 외부 저장소에서와 동일한 방식으로 처리합니다.

저장소 매핑

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

따라서 Bazel은 한 바이너리 저장소가 @guava//의 한 Guava 저장소 (예: @guava1//)와 다른 Guava 저장소 (예: @guava2//)의 저장소를 참조할 수 있도록 외부 저장소 라벨을 다시 매핑할 수 있게 해줍니다.

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

매핑은 WORKSPACE 파일에 개별 저장소 정의의 repo_mapping 속성으로 지정됩니다. 그런 다음 SkyFrame에 WorkspaceFileValue의 구성원으로 표시되며, 이는 다음으로 연결됩니다.

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

JNI 비트

Bazel 서버는 대부분 자바로 작성되어 있습니다. 예외적으로 자바가 자체적으로 구현할 수 없는 부분이나 구현 시 자바가 단독으로 처리할 수 없는 부분은 예외입니다. 이는 주로 파일 시스템과의 상호작용, 프로세스 제어, 기타 다양한 저수준 항목의 작업으로 제한됩니다.

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

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

콘솔 출력

콘솔 출력을 내보내는 것은 간단한 작업처럼 보일 수 있지만, 여러 프로세스 (때로는 원격으로 실행)와 세밀한 캐싱, 좋은 색상과 풍부한 단말기 출력을 얻고자 하는 요구사항 때문에 장기간 실행되는 서버를 보유한다는 것이 그리 간단하지 않습니다.

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

출력은 기본적으로 ANSI 이스케이프 시퀀스로 인쇄됩니다. 원하지 않는 경우(--color=no) AnsiStrippingOutputStream에 의해 삭제됩니다. 또한 System.outSystem.err는 이러한 출력 스트림으로 리디렉션됩니다. 따라서 System.err.println()를 사용하여 디버깅 정보를 출력하고 클라이언트의 터미널 출력(서버의 출력과 다름)을 받게 됩니다. 프로세스가 바이너리 출력 (예: bazel query --output=proto)을 생성하면 stdout의 munging이 발생하지 않는다는 점에 주의해야 합니다.

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

일부 EventHandler 구현은 수신한 이벤트를 저장합니다. 다양한 종류의 캐시된 처리로 인해 UI에 정보를 재생하는 데 사용됩니다(예: 캐시된 구성 대상에서 방출한 경고).

일부 EventHandler는 결국 이벤트 버스로 이동하는 이벤트 게시를 허용합니다 (일반 Event는 거기에 _표시되지 _않습니다). 다음은 ExtendedEventHandler의 구현이며 주요 목적은 캐시된 EventBus 이벤트를 재생하는 것입니다. 이러한 EventBus 이벤트는 모두 Postable를 구현하지만 EventBus에 게시된 모든 항목이 반드시 이 인터페이스를 구현하는 것은 아닙니다. ExtendedEventHandler에서 캐시되는 이벤트만 구현하는 것이 좋으며 대부분의 경우 유용합니다.

터미널 출력은 대부분 UiEventHandler를 통해 내보내며, 이는 Bazel이 하는 모든 화려한 출력 형식 지정과 진행 상황을 보고합니다. 다음과 같은 두 가지 입력이 있습니다.

  • 이벤트 버스
  • 신고자를 통해 이벤트 스트림으로 이동되는 이벤트 스트림

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

Bazel 프로파일링

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

Chrome 프로파일러 형식의 프로필을 방출하며 Chrome에서 가장 잘 표시됩니다. 작업 모델은 데이터 스택입니다. 작업 스택은 작업을 시작하고 서로 종료할 수 있으며, 작업 스택이 서로 깔끔하게 중첩되어 있어야 합니다. 각 자바 스레드는 자체 작업 스택을 가져옵니다. TODO: 작업 및 지속적 전달 스타일에서는 어떻게 작동하나요?

프로파일러는 각각 BlazeRuntime.initProfiler()BlazeRuntime.afterCommand()에서 시작 및 중지되며 모든 프로필을 프로파일링할 수 있도록 최대한 오래 활성화하려고 시도합니다. 프로필에 항목을 추가하려면 Profiler.instance().profile()를 호출합니다. 닫을 때 작업 끝을 나타내는 Closeable가 반환됩니다. 이 문은 try-with-resources 문과 함께 사용하는 것이 가장 좋습니다.

MemoryProfiler에서 기본 메모리 프로파일링도 수행합니다. 또한 항상 켜져 있으며 대부분 최대 힙 크기 및 GC 동작을 기록합니다.

Bazel 테스트

Bazel에는 두 가지 주요 테스트, 즉 Bazel을 '블랙박스'로 관찰하는 테스트와 분석 단계만 실행하는 테스트가 있습니다. 이전 통합 테스트와 후속 단위 테스트라고 부르는 테스트는 통합 수준이 낮은 통합 테스트와 비슷하다고 볼 수 있습니다. 또한 실제 단위 테스트가 필요한 경우도 있습니다.

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

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

AbstractBlackBoxTest도 Windows에서 작동한다는 이점이 있지만 대부분의 통합 테스트는 bash로 작성됩니다.

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