이 페이지에서는 규칙 작성자가 플랫폼 기반 도구 선택에서 규칙 로직을 분리하는 방법인 도구 모음 프레임워크를 설명합니다. 계속하기 전에 규칙 및 플랫폼 페이지를 읽어보는 것이 좋습니다. 이 페이지에서는 도구 모음이 필요한 이유, 도구 모음의 정의 및 사용 방법, Bazel이 플랫폼 제약조건에 따라 적절한 도구 모음을 선택하는 방법을 설명합니다.
동기
먼저 도구 모음이 해결하도록 설계된 문제를 살펴보겠습니다. 'bar' 프로그래밍 언어를 지원하는 규칙을 작성한다고 가정해 보겠습니다. bar_binary
규칙은 자체적으로 작업공간에서 다른 타겟으로 빌드된 도구인 barc
컴파일러를 사용하여 *.bar
파일을 컴파일합니다. bar_binary
타겟을 작성하는 사용자는 컴파일러의 종속 항목을 지정할 필요가 없으므로 규칙 정의에 비공개 속성으로 추가하여 암시적 종속 항목을 만듭니다.
bar_binary = rule(
implementation = _bar_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
...
"_compiler": attr.label(
default = "//bar_tools:barc_linux", # the compiler running on linux
providers = [BarcInfo],
),
},
)
//bar_tools:barc_linux
는 이제 모든 bar_binary
타겟의 종속 항목이므로 모든 bar_binary
타겟보다 먼저 빌드됩니다. 다른 속성과 마찬가지로 규칙의 구현 함수를 통해 액세스할 수 있습니다.
BarcInfo = provider(
doc = "Information about how to invoke the barc compiler.",
# In the real world, compiler_path and system_lib might hold File objects,
# but for simplicity they are strings for this example. arch_flags is a list
# of strings.
fields = ["compiler_path", "system_lib", "arch_flags"],
)
def _bar_binary_impl(ctx):
...
info = ctx.attr._compiler[BarcInfo]
command = "%s -l %s %s" % (
info.compiler_path,
info.system_lib,
" ".join(info.arch_flags),
)
...
여기서 문제는 컴파일러의 라벨이 bar_binary
로 하드코딩되지만 대상이 되는 플랫폼 및 빌드 중인 플랫폼에 따라 각각 다른 컴파일러(타겟 플랫폼과 실행 플랫폼이라고 함)가 필요할 수 있다는 점입니다. 또한 사용 가능한 모든 도구 및 플랫폼을 모르더라도 규칙 작성자가 규칙의 정의에 하드 코딩할 수 없습니다.
덜 이상적인 해결책은 _compiler
속성을 비공개가 아님으로 만들어 사용자에게 부담을 옮기는 것입니다. 그러면 개별 대상을 하드코딩하여 한 플랫폼 또는 다른 플랫폼에 맞게 빌드할 수 있습니다.
bar_binary(
name = "myprog_on_linux",
srcs = ["mysrc.bar"],
compiler = "//bar_tools:barc_linux",
)
bar_binary(
name = "myprog_on_windows",
srcs = ["mysrc.bar"],
compiler = "//bar_tools:barc_windows",
)
select
를 사용하여 플랫폼에 따라 compiler
를 선택하면 이 솔루션을 개선할 수 있습니다.
config_setting(
name = "on_linux",
constraint_values = [
"@platforms//os:linux",
],
)
config_setting(
name = "on_windows",
constraint_values = [
"@platforms//os:windows",
],
)
bar_binary(
name = "myprog",
srcs = ["mysrc.bar"],
compiler = select({
":on_linux": "//bar_tools:barc_linux",
":on_windows": "//bar_tools:barc_windows",
}),
)
하지만 이 작업은 지루하고 모든 bar_binary
사용자에게 물어봐야 합니다.
이 스타일이 작업공간 전체에서 일관되게 사용되지 않으면 단일 플랫폼에서 잘 작동하는 빌드로 이어지지만 여러 플랫폼 시나리오로 확장되면 실패합니다. 또한 기존 규칙이나 타겟을 수정하지 않고 새로운 플랫폼 및 컴파일러를 추가로 지원하는 문제도 해결되지 않습니다.
도구 모음 프레임워크는 간접 수준을 추가함으로써 이 문제를 해결합니다. 기본적으로 규칙에서 대상 계열 (도구 모음 유형)의 일부 구성원에 대한 추상 종속 항목을 선언하고 Bazel이 해당 플랫폼 제약 조건에 따라 특정 대상 (도구 모음)으로 이를 자동 확인합니다. 규칙 작성자와 대상 작성자 모두 사용 가능한 플랫폼과 도구 모음의 전체 집합을 알지 못합니다.
도구 모음을 사용하는 규칙 작성
도구 모음 프레임워크에서는 규칙이 도구에 직접 종속되는 대신 도구 모음 유형에 종속됩니다. 도구 모음 유형은 다양한 플랫폼에 동일한 역할을 하는 도구 클래스를 나타내는 간단한 대상입니다. 예를 들어 바 컴파일러를 나타내는 유형을 선언할 수 있습니다.
# By convention, toolchain_type targets are named "toolchain_type" and
# distinguished by their package path. So the full path for this would be
# //bar_tools:toolchain_type.
toolchain_type(name = "toolchain_type")
이전 섹션의 규칙 정의는 컴파일러를 속성으로 사용하는 대신 //bar_tools:toolchain_type
도구 모음을 사용한다고 선언하도록 수정되었습니다.
bar_binary = rule(
implementation = _bar_binary_impl,
attrs = {
"srcs": attr.label_list(allow_files = True),
...
# No `_compiler` attribute anymore.
},
toolchains = ["//bar_tools:toolchain_type"],
)
이제 구현 함수는 도구 모음 유형을 키로 사용하여 ctx.attr
대신 ctx.toolchains
에서 이 종속 항목에 액세스합니다.
def _bar_binary_impl(ctx):
...
info = ctx.toolchains["//bar_tools:toolchain_type"].barcinfo
# The rest is unchanged.
command = "%s -l %s %s" % (
info.compiler_path,
info.system_lib,
" ".join(info.arch_flags),
)
...
ctx.toolchains["//bar_tools:toolchain_type"]
는 도구 모음 종속 항목을 해결한 대상 Bazel의 ToolchainInfo
제공업체를 반환합니다. ToolchainInfo
객체의 필드는 기본 도구의 규칙에 의해 설정됩니다. 다음 섹션에서 이 규칙은 BarcInfo
객체를 래핑하는 barcinfo
필드가 있도록 정의됩니다.
도구 모음을 대상으로 확인하는 Bazel의 절차는 아래에 설명되어 있습니다. 해결된 도구 모음 대상만 실제로 bar_binary
대상의 종속 항목이 되고 후보 도구 모음의 전체 공간은 아닙니다.
필수 및 선택적 도구 모음
규칙이 위에서와 같이 기본 도구 모음을 사용하여 도구 모음 유형 종속 항목을 표현하는 경우 기본적으로 도구 모음 유형은 필수로 간주됩니다. Bazel이 필수 도구 모음 유형과 일치하는 도구 모음을 찾을 수 없다면 (아래의 도구 모음 해결 참고) 오류 및 분석이 중단됩니다.
대신 다음과 같이 선택적 도구 모음 유형 종속 항목을 선언할 수 있습니다.
bar_binary = rule(
...
toolchains = [
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
선택사항인 도구 모음 유형을 확인할 수 없으면 분석이 계속되고 ctx.toolchains["//bar_tools:toolchain_type"]
의 결과는 None
입니다.
config_common.toolchain_type
함수의 기본값은 기본값입니다.
다음과 같은 양식을 사용할 수 있습니다.
- 필수 도구 모음 유형:
toolchains = ["//bar_tools:toolchain_type"]
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type")]
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = True)]
- 선택적 도구 모음 유형:
toolchains = [config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False)]
bar_binary = rule(
...
toolchains = [
"//foo_tools:toolchain_type",
config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
],
)
동일한 규칙에서 양식을 혼합하여 사용할 수도 있습니다. 그러나 동일한 도구 모음 유형이 여러 번 나열되면 가장 엄격한 버전이 사용되며 필수 버전은 선택사항보다 더 엄격합니다.
도구 모음을 사용하는 측면 작성
관점은 규칙과 동일한 도구 모음 API에 액세스할 수 있습니다. 필요한 도구 모음 유형을 정의하고 컨텍스트를 통해 도구 모음에 액세스하며 도구 모음을 사용하여 새 작업을 생성하는 데 사용할 수 있습니다.
bar_aspect = aspect(
implementation = _bar_aspect_impl,
attrs = {},
toolchains = ['//bar_tools:toolchain_type'],
)
def _bar_aspect_impl(target, ctx):
toolchain = ctx.toolchains['//bar_tools:toolchain_type']
# Use the toolchain provider like in a rule.
return []
도구 모음 정의
특정 도구 모음 유형의 일부 도구 모음을 정의하려면 다음 세 가지가 필요합니다.
도구 또는 도구 모음의 종류를 나타내는 언어별 규칙입니다. 규칙에 따라 이 규칙 이름에는 접미사로 '_toolchain'이 추가됩니다.
- 참고:
\_toolchain
규칙은 빌드 작업을 만들 수 없습니다. 대신 다른 규칙에서 아티팩트를 수집하여 도구 모음을 사용하는 규칙으로 전달합니다. 이 규칙은 모든 빌드 작업을 만듭니다.
- 참고:
다양한 플랫폼용 도구 또는 도구 모음 버전을 나타내는 이 규칙 유형의 여러 대상
이러한 각 대상에 대해 일반
toolchain
규칙의 연결된 타겟으로 도구 모음 프레임워크에서 사용하는 메타데이터를 제공합니다. 이toolchain
타겟은 이 도구 모음과 연결된toolchain_type
도 나타냅니다. 즉, 지정된_toolchain
규칙은 모든toolchain_type
과 연결될 수 있으며 이_toolchain
규칙을 사용하는toolchain
인스턴스에서만 규칙이toolchain_type
와 연결됩니다.
실행 예는 bar_toolchain
규칙의 정의입니다. 이 예시에는 컴파일러만 있지만 링커와 같은 다른 도구도 그 아래에 그룹화할 수 있습니다.
def _bar_toolchain_impl(ctx):
toolchain_info = platform_common.ToolchainInfo(
barcinfo = BarcInfo(
compiler_path = ctx.attr.compiler_path,
system_lib = ctx.attr.system_lib,
arch_flags = ctx.attr.arch_flags,
),
)
return [toolchain_info]
bar_toolchain = rule(
implementation = _bar_toolchain_impl,
attrs = {
"compiler_path": attr.string(),
"system_lib": attr.string(),
"arch_flags": attr.string_list(),
},
)
이 규칙은 ToolchainInfo
제공자를 반환해야 합니다. 이 제공자는 ctx.toolchains
및 도구 모음 유형의 라벨을 사용하여 소비 규칙이 검색하는 객체가 됩니다. struct
와 같은 ToolchainInfo
는 임의의 필드-값 쌍을 보유할 수 있습니다. 도구 모음 유형에서 ToolchainInfo
에 추가되는 필드를 정확하게 명시해야 합니다. 이 예에서는 값이 BarcInfo
객체에 래핑되어 위에서 정의한 스키마를 재사용합니다. 이 스타일은 유효성 검사와 코드 재사용에 유용할 수 있습니다.
이제 특정 barc
컴파일러의 타겟을 정의할 수 있습니다.
bar_toolchain(
name = "barc_linux",
arch_flags = [
"--arch=Linux",
"--debug_everything",
],
compiler_path = "/path/to/barc/on/linux",
system_lib = "/usr/lib/libbarc.so",
)
bar_toolchain(
name = "barc_windows",
arch_flags = [
"--arch=Windows",
# Different flags, no debug support on windows.
],
compiler_path = "C:\\path\\on\\windows\\barc.exe",
system_lib = "C:\\path\\on\\windows\\barclib.dll",
)
마지막으로 2개의 bar_toolchain
대상에 대해 toolchain
정의를 만듭니다.
이러한 정의는 도구 모음 유형에 언어별 대상을 연결하고 도구 모음이 특정 플랫폼에 적합한 경우 Bazel에 알려주는 제약조건 정보를 제공합니다.
toolchain(
name = "barc_linux_toolchain",
exec_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
toolchain = ":barc_linux",
toolchain_type = ":toolchain_type",
)
toolchain(
name = "barc_windows_toolchain",
exec_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
target_compatible_with = [
"@platforms//os:windows",
"@platforms//cpu:x86_64",
],
toolchain = ":barc_windows",
toolchain_type = ":toolchain_type",
)
위의 상대 경로 구문을 사용하면 이러한 정의가 모두 동일한 패키지에 포함되어 있지만 도구 모음 유형, 언어별 도구 모음 대상, toolchain
정의 대상이 모두 별도의 패키지에 있을 수 있는 것은 아닙니다.
실제 예는 go_toolchain
를 참고하세요.
도구 모음 및 구성
규칙 작성자에게 중요한 질문은 bar_toolchain
대상이 분석될 때 어떤 구성이 표시되고 종속 항목에 어떤 전환이 사용되어야 하는지입니다. 위의 예에서는 문자열 속성을 사용하지만 Bazel 저장소의 다른 대상에 종속되는 더 복잡한 도구 모음에서는 어떻게 될까요?
bar_toolchain
의 더 복잡한 버전을 살펴보겠습니다.
def _bar_toolchain_impl(ctx):
# The implementation is mostly the same as above, so skipping.
pass
bar_toolchain = rule(
implementation = _bar_toolchain_impl,
attrs = {
"compiler": attr.label(
executable = True,
mandatory = True,
cfg = "exec",
),
"system_lib": attr.label(
mandatory = True,
cfg = "target",
),
"arch_flags": attr.string_list(),
},
)
attr.label
사용은 표준 규칙과 동일하지만 cfg
매개변수의 의미는 약간 다릅니다.
도구 모음 해결을 통해 대상('상위'라고 함)에서 도구 모음에 대한 종속 항목은 '도구 모음 전환'이라는 특수한 구성 전환을 사용합니다. 도구 모음 전환은 구성을 동일하게 유지한다는 점을 제외하고 도구 모음의 실행 플랫폼이 상위 항목의 도구 모음과 동일하다는 점을 제외하면 동일하게 유지됩니다. 그렇지 않으면 도구 모음의 도구 모음 해상도가 모든 실행 플랫폼을 선택할 수 있으며 상위 항목과 동일할 필요는 없습니다. 이렇게 하면 도구 모음의 모든 exec
종속 항목을 상위 요소의 빌드 작업에서도 실행할 수 있습니다. cfg =
"target"
를 사용하거나 'target'이 기본값이므로 cfg
을 지정하지 않는 도구 모음의 모든 종속 항목은 상위 요소와 동일한 타겟 플랫폼에 대해 빌드됩니다. 이를 통해 도구 모음 규칙은 라이브러리 (위의 system_lib
속성)와 도구 (compiler
속성)를 모두 필요로 하는 빌드 규칙에 기여할 수 있습니다. 시스템 라이브러리는 최종 아티팩트에 연결되어 있으므로 동일한 플랫폼에 대해 빌드되어야 하는 반면, 컴파일러는 빌드 중에 호출되는 도구이며 실행 플랫폼에서 실행할 수 있어야 합니다.
도구 모음 등록 및 빌드
이제 모든 구성요소가 조합되고 Bazel의 확인 절차에서 도구 모음을 사용할 수 있도록 하면 됩니다. register_toolchains()
를 사용하여 WORKSPACE
파일에 도구 모음을 등록하거나 명령줄에서 --extra_toolchains
플래그를 사용하여 도구 모음의 라벨을 전달하면 됩니다.
register_toolchains(
"//bar_tools:barc_linux_toolchain",
"//bar_tools:barc_windows_toolchain",
# Target patterns are also permitted, so you could have also written:
# "//bar_tools:all",
)
이제 도구 모음 유형에 종속된 대상을 빌드할 때 대상 및 실행 플랫폼에 따라 적절한 도구 모음이 선택됩니다.
# my_pkg/BUILD
platform(
name = "my_target_platform",
constraint_values = [
"@platforms//os:linux",
],
)
bar_binary(
name = "my_bar_binary",
...
)
bazel build //my_pkg:my_bar_binary --platforms=//my_pkg:my_target_platform
Bazel은 @platforms//os:linux
가 있는 플랫폼에서 //my_pkg:my_bar_binary
가 빌드되는 것을 확인하고 //bar_tools:barc_linux_toolchain
에 대한 //bar_tools:toolchain_type
참조를 확인합니다.
이렇게 하면 //bar_tools:barc_linux
가 빌드되지만 //bar_tools:barc_windows
는 빌드되지 않습니다.
도구 모음 해결
도구 모음을 사용하는 각 대상에 대해 Bazel의 도구 모음 확인 절차에 따라 대상의 구체적인 도구 모음 종속 항목이 결정됩니다. 이 절차는 필수 도구 모음 유형, 대상 플랫폼, 사용 가능한 실행 플랫폼 목록, 사용 가능한 도구 모음 목록을 입력으로 받습니다. 각 도구 모음 유형에 선택된 도구 모음과 현재 대상을 위해 선택된 실행 플랫폼이 출력됩니다.
사용 가능한 실행 플랫폼과 도구 모음은 register_execution_platforms
와 register_toolchains
를 통해 WORKSPACE
파일에서 수집됩니다.
명령줄에서 --extra_execution_platforms
및 --extra_toolchains
를 통해 추가 실행 플랫폼 및 도구 모음을 지정할 수도 있습니다.
호스트 플랫폼은 사용 가능한 실행 플랫폼으로 자동 포함됩니다.
사용 가능한 플랫폼 및 도구 모음은 확정성을 위해 순서가 지정된 목록으로 추적되며 목록의 이전 항목이 우선 적용됩니다.
해결 단계는 다음과 같습니다.
목록에 있는 각
constraint_value
의 플랫폼에constraint_value
(명시적 또는 기본값)도 포함된 경우target_compatible_with
또는exec_compatible_with
절은 플랫폼과 일치합니다.플랫폼에 절에서 참조되지 않는
constraint_setting
의constraint_value
가 있으면 일치에 영향을 미치지 않습니다.빌드 중인 대상이
exec_compatible_with
속성을 지정하거나 규칙의 정의에서exec_compatible_with
인수를 지정하는 경우 사용 가능한 실행 플랫폼 목록이 필터링되어 실행 제약조건과 일치하지 않는 플랫폼은 삭제됩니다.사용 가능한 각 실행 플랫폼에 대해 각 도구 모음 유형을 이 실행 플랫폼 및 대상 플랫폼과 호환되는 첫 번째 도구 모음(있는 경우)과 연결합니다.
도구 모음 유형 중 하나와 호환되는 필수 도구 모음을 찾지 못한 실행 플랫폼은 제외됩니다. 나머지 플랫폼 중 첫 번째 플랫폼은 현재 타겟의 실행 플랫폼이 되고, 연결된 도구 모음 (있는 경우)은 타겟의 종속 항목이 됩니다.
선택한 실행 플랫폼은 대상이 생성하는 모든 작업을 실행하는 데 사용됩니다.
동일한 빌드 내에서 같은 타겟을 여러 구성 (예: 여러 CPU)으로 빌드할 수 있는 경우에는 해결 절차가 각 대상 버전과 독립적으로 적용됩니다.
규칙이 실행 그룹을 사용하는 경우 각 실행 그룹은 도구 모음 해결을 별도로 수행하고 각 도구 모음에는 자체 실행 플랫폼과 도구 모음이 있습니다.
디버깅 도구 모음
기존 규칙에 도구 모음 지원을 추가하는 경우 --toolchain_resolution_debug=regex
플래그를 사용합니다. 도구 모음 확인 중에 이 플래그는 정규식 변수와 일치하는 도구 모음 유형 또는 대상 이름에 관한 상세 출력을 제공합니다. .*
를 사용하여 모든 정보를 출력할 수 있습니다. Bazel은 확인 프로세스 중에 확인하고 건너뛰는 도구 모음의 이름을 출력합니다.
도구 모음 해상도에서 어느 cquery
종속 항목이 있는지 확인하려면 다음과 같이 cquery
의 --transitions
플래그를 사용합니다.
# Find all direct dependencies of //cc:my_cc_lib. This includes explicitly
# declared dependencies, implicit dependencies, and toolchain dependencies.
$ bazel cquery 'deps(//cc:my_cc_lib, 1)'
//cc:my_cc_lib (96d6638)
@bazel_tools//tools/cpp:toolchain (96d6638)
@bazel_tools//tools/def_parser:def_parser (HOST)
//cc:my_cc_dep (96d6638)
@local_config_platform//:host (96d6638)
@bazel_tools//tools/cpp:toolchain_type (96d6638)
//:default_host_platform (96d6638)
@local_config_cc//:cc-compiler-k8 (HOST)
//cc:my_cc_lib.cc (null)
@bazel_tools//tools/cpp:grep-includes (HOST)
# Which of these are from toolchain resolution?
$ bazel cquery 'deps(//cc:my_cc_lib, 1)' --transitions=lite | grep "toolchain dependency"
[toolchain dependency]#@local_config_cc//:cc-compiler-k8#HostTransition -> b6df211