工具链

<ph type="x-smartling-placeholder"></ph> 报告问题 <ph type="x-smartling-placeholder"></ph> 查看来源 每晚 · 7.2 条 · 7.1敬上 · 7.0 · 6.5 · 6.4

本页面介绍了工具链框架,规则作者可以借助该框架 将其规则逻辑与基于平台的工具选择分离开来。时间是 建议阅读规则平台 然后才能继续。本页面介绍为什么需要使用工具链,以及如何 定义和使用它们,以及 Bazel 如何根据上下文选择合适的工具链 平台限制。

设计初衷

我们先来看一下工具链旨在解决的问题。假设您 编写规则来支持编程语言。您的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.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"] 会返回 ToolchainInfo 提供方 将工具链依赖项解析到任何目标 Bazel 的位置。其中 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 则默认为必需项。

可以使用以下表单:

  • 必需的工具链类型: <ph type="x-smartling-placeholder">
      </ph>
    • 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)]
  • 可选工具链类型: <ph type="x-smartling-placeholder">
      </ph>
    • 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 和 工具链类型。ToolchainInfo(如 struct)可以存储任意字段值 对。关于向 ToolchainInfo 添加哪些字段的确切规范 都应该在工具链类型中明确记录。在此示例中,值 return 封装在 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 目标 它会看到什么配置,有哪些转换 应该用于依赖项?上面的示例使用的是字符串属性, 对于依赖于其他目标的更复杂的工具链,会发生什么情况 运行什么命令?

我们来看一个更复杂的 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. target_compatible_withexec_compatible_with 子句与 platform,如果对于其列表中的每个 constraint_value,该平台还具有 该constraint_value(显式或默认)。

    如果平台的 constraint_value 来自 constraint_setting 而非 子句引用,这些都不会影响匹配。

  2. 如果构建的目标指定了 exec_compatible_with 属性 (或其规则定义指定了 exec_compatible_with 参数), 已过滤可用执行平台列表,以移除 任何不符合执行约束条件的对象。

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

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

  5. 任何未能找到兼容的强制性工具链的执行平台 将被排除在其他平台中 第一个成为当前目标的执行平台, 工具链(如果有的话)成为目标的依赖项。

所选的执行平台用于运行目标 生成的内容。

如果可以在多个配置中构建同一目标(例如 针对不同 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