工具链

本页介绍了工具链框架,规则作者可以使用该框架将其规则逻辑与基于平台的工具选择分离。 建议您先阅读 规则平台 页面,然后再继续。本页介绍了为什么需要工具链、如何 定义和使用工具链,以及 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 会根据适用的平台限制自动将其解析为特定目标(工具链)。规则作者和目标作者 都不需要知道可用平台和工具链的完整集。

编写使用工具链的规则

在工具链框架下,规则不再直接依赖于工具, 而是依赖于工具链类型。工具链类型是一个简单的目标 表示一类工具,这些工具为不同的 平台提供相同的角色。例如,您可以声明一个表示 bar 编译器的类型:

# 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.toolchains 下访问此依赖项,而不是 ctx.attr

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 将工具链解析为目标的过程。 below. 只有解析的工具链目标实际上 成为 bar_binary 目标的依赖项,而不是整个候选 工具链空间。

必需和可选工具链

默认情况下,当规则使用裸标签 (如上所示)表示工具链类型依赖项时,该工具链类型被视为必需。如果 Bazel 无法为必需的工具链 类型找到匹配的工具链(请参阅下面的 工具链解析),则会发生错误,并且分析会停止。

您也可以改为声明可选工具链类型依赖项,如下所示:

bar_binary = rule(
    ...
    toolchains = [
        config_common.toolchain_type("//bar_tools:toolchain_type", mandatory = False),
    ],
)

当无法解析可选工具链类型时,分析会继续,并且 ctx.toolchains["//bar_tools:toolchain_type"] 的结果为 None

The 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 []

定义工具链

如需为给定的工具链类型定义一些工具链,您需要以下三项:

  1. 表示工具或工具套件类型的语言专用规则。按照惯例,此规则的名称以“_toolchain”为后缀。

    1. 注意\_toolchain 规则无法创建任何构建操作。 相反,它会从其他规则收集工件,并将它们转发给使用工具链的 规则。该规则负责创建所有 构建操作。
  2. 此规则类型的多个目标,表示不同平台的工具或工具 套件的版本。

  3. 对于每个此类目标,都有一个通用 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 和工具链类型的标签检索的使用规则的对象。ToolchainInfostruct 一样,可以保存任意字段值 对。应在工具链类型中清楚地记录添加到 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",
)

最后,您为两个 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" 的依赖项(或未指定 cfg 的依赖项,因为“target”是默认值)都 针对与父级相同的目标平台构建。这样一来,工具链规则就可以 向需要它们的构建规则提供库(上面的 system_lib 属性)和工具( compiler 属性)。系统库 会链接到最终工件中,因此需要针对同一 平台构建,而编译器是在构建期间调用的工具,需要能够 在执行平台上运行。

注册和构建工具链

至此,所有构建块都已组装完毕,您只需让 Bazel 的解析过程可以使用工具链即可。这可以通过 注册工具链来完成,方法是在 MODULE.bazel 文件中使用 register_toolchains(),或者使用 --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",
    # or even
    # "//bar_tools/...",
)

使用目标模式注册工具链时,各个工具链的注册顺序由以下规则确定:

  • 软件包的子软件包中定义的工具链在 软件包本身中定义的工具链之前注册。
  • 在软件包中,工具链按其名称的词典顺序注册 。

现在,当您构建依赖于工具链类型的目标时,系统会根据目标平台和执行平台选择合适的 工具链。

# 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 会看到 //my_pkg:my_bar_binary 正在使用具有 @platforms//os:linux 的平台构建,因此会将 //bar_tools:toolchain_type 引用解析为 //bar_tools:barc_linux_toolchain。这最终会构建 //bar_tools:barc_linux,但不会构建 //bar_tools:barc_windows

工具链解析

对于使用工具链的每个目标,Bazel 的工具链解析过程 都会确定目标的具体工具链依赖项。该过程将一组必需的工具链类型、目标平台、可用执行平台列表和可用工具链列表作为输入。其输出 是为每种工具链类型选择的工具链以及为当前目标选择的执行 平台。

可用的执行平台和工具链通过 外部依赖关系图中的 register_execution_platformsregister_toolchains调用从 MODULE.bazel文件中收集。您还可以在命令行中通过 --extra_execution_platforms--extra_toolchains指定其他执行平台和工具链。 主机平台会自动作为可用的执行平台包含在内。 可用的平台和工具链会作为有序列表进行跟踪,以确保确定性, 并且列表中的较早项具有优先权。

可用工具链集(按优先级顺序)由 --extra_toolchainsregister_toolchains 创建:

  1. 首先添加使用 --extra_toolchains 注册的工具链。(在这些工具链中,最后一个 工具链具有最高优先级。)
  2. 使用传递外部 依赖关系图中的 register_toolchains 注册的工具链,顺序如下:(在这些工具链中,第一个 提及的工具链具有最高优先级。)
    1. 由根模块注册的工具链(例如,MODULE.bazel位于 工作区根目录);
    2. 在用户的 WORKSPACE 文件中注册的工具链,包括从该文件中调用的任何 宏;
    3. 由非根模块注册的工具链(例如,由 根模块指定的依赖项及其依赖项,依此类推);
    4. 在“WORKSPACE 后缀”中注册的工具链;这仅供 与 Bazel 安装捆绑的某些原生规则使用。

注意:伪目标(例如 :all:*/...)由 Bazel 的软件包 加载机制排序,该机制使用词典顺序。

解析步骤如下。

  1. 如果平台对于其列表中的每个 constraint_value 也具有 该 constraint_value(显式或作为默认值),则 target_compatible_withexec_compatible_with 子句 平台匹配。

    如果平台具有子句未引用的 constraint_setting 中的 constraint_value,则这些值不会影响匹配。

  2. 如果正在构建的目标指定了 exec_compatible_with属性 (或其规则定义指定了 exec_compatible_with参数), 则会过滤可用执行平台列表,以移除 任何与执行约束不匹配的平台。

  3. 过滤可用工具链列表,以移除任何 指定 target_settings 与当前配置不匹配的工具链。

  4. 对于每个可用的执行平台,您将每个工具链类型与 第一个可用的工具链(如果有)相关联,该工具链与此执行 平台和目标平台兼容。

  5. 任何未能为其工具链类型之一找到兼容的必需工具链 的执行平台都会被排除。在剩余的平台中,第 一个平台将成为当前目标的执行平台,并且其关联的 工具链(如果有)将成为目标的依赖项。

所选的执行平台用于运行目标 生成的所有操作。

如果同一目标可以在同一构建中的多个配置(例如,针对不同的 CPU)中构建,则解析过程将独立应用于目标的每个版本。

如果规则使用 执行组,则每个执行 组都会单独执行工具链解析,并且每个执行组都有自己的执行 平台和工具链。

调试工具链

如果您要向现有规则添加工具链支持,请使用 --toolchain_resolution_debug=regex 标志。在工具链解析期间,该标志 会为与 regex 变量匹配的工具链类型或目标名称提供详细输出。您 可以使用 .* 输出所有信息。Bazel 将输出在解析过程中 检查和跳过的工具链的名称。

例如,如需调试直接由 //my:target创建的所有操作的工具链选择,请执行以下操作:

$ bazel build //my:all --toolchain_resolution_debug=//my:target

如需调试所有构建目标的所有操作的工具链选择,请执行以下操作:

$ bazel build //my:all --toolchain_resolution_debug=.*

如果您想查看哪些 cquery 依赖项来自工具链 解析,请使用 cquery's --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