[Bazel]自定义工具链

时间:2022-07-23
本文章向大家介绍[Bazel]自定义工具链,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。
  • 1 前言
  • 2 Non-Platform 方式
  • 3 Platform 方式
    • 3.1 平台
    • 3.2 工具链
    • 3.3 Platform + Toolchain 实现平台方式构建
  • 4 小结

1 前言

本文会讲述 Bazel 自定义工具链的两种方式,PlatformNon-Platform 方式。会存在这两种方式的原因是 Bazel 的历史问题。例如,C++ 相关规则使用 --cpu--crosstool_top 来设置一个构建目标 CPUC++ 工具链,这样就可以实现选择不同的工具链构建 C++ 项目。但是这都不能正确地表达出“平台”特征。使用这种方式不可避免地导致出现了笨拙且不准确的构建 APIs。这其中导致了对 Java 工具链基本没有涉及,Java 工具链就发展了他们自己的独立接口 --java_toolchain。因此非平台方式(Non-Platform)的自定义工具链实现并没有统一的 APIs 来规范不同语言的跨平台构建。而 Bazel 的目标是在大型、混合语言、多平台项目中脱颖而出。这就要求对这些概念有更原则的支持,包括清晰的 APIs,这些 API 绑定而不是分散语言和项目。这就是新平台(platform)和工具链(toolchain) APIs 所实现的内容。

如果没有去了解 PlatformNon-Platform 方式区别,可能会对上面说的内容有点不理解,这里通俗的来讲下这两者区别。比如我们编译 C++Java 混合的相关项目,这个项目需要在多个平台下可以运行,因此涉及到多个平台下的工具链,而 C++Java 的工具链是不一样的,非平台方式,对于 C++,我们需要通过 --crosstool_top 来指定工具链集合,--cpu 来指定具体的某设备工具链;对于 Java,则需要通过 --java_toolchain--host_java_toolchain--javabase--host_javabase 来构建 Java 相关内容。这样一个 C++Java 的混合项目,需要指定这么多的输入才能够完整编译项目。

如果用了平台方式,那就简单了。首先理解平台概念很简单,平台就是一组约束值(constraint_value)的集合,即比如一个平台可以由 OSCPU 两个约束类型来决定,又或者一个平台可以由 OSCPUGLibc_Version 来决定。则我们可以将 C++ 相关编译的平台约束绑定平台,将 Java 相关编译的平台约束也绑定平台,这样就可以将混合语言项目统一到一个平台,即一旦确定了某个平台,那么只需要在命令行执行类似如下命令即可编译混合语言项目:

$ bazel build //:my_mixed_project --platforms==//:myplatform

目前平台方式构建在 Bazel 中并不完善。这些 APIs 不足以让所有项目都使用平台。Bazel 还必须淘汰旧的 APIs。这不是很容易就完成的任务,因为项目的所有语言、工具链、依赖项和 select() 都必须支持新的 APIs。这需要一个有序的迁移顺序来保持项目正常工作。Bazel 的 C++ 相关规则已经支持平台,而 Android 相关规则不支持。你的 C++ 项目可能不关心 Android,但其他人可能会。因此,在全球范围内启用所有 C++ 平台构建方式是不安全的。已经完整支持平台构建方式的有:

  • C/C++
  • Rust
  • Go
  • Java

未来 Bazel 的目标是实现 $ bazel build //:all,即一个命令行就可以构建任何项目和目标平台。

2 Non-Platform 方式

通过上一章节的介绍,Non-Platform 方式,则是通过各项目性质采用对应的独立构建方式,比如 C++ 相关的 --crosstool_top--cpuJava 相关的 --java_toolchain--host_java_toolchain--javabase--host_javabase。这一节我们仅仅实现 C++Non-Platform 方式构建(当然完整的平台构建方式并未完善,比如 Apple、Android 都还未支持平台构建方式)。

在 Bazel 的官方文档中有一个教程已经详细地介绍了如何去配置一个 C++ 工具链,具体见 https://docs.bazel.build/versions/master/tutorial/cc-toolchain-config.html ,主要涉及的内置规则有:cc_commoncc_toolchaincc_toolchain_suite

当然这里可以进一步去做一些工程上的优化:

生成 CcToolchainConfigInfo 的规则,可以优化其输入配置,使得写一个工具链配置规则即可配置所有主流的 C++ 编译器attrs = {
 "include_paths" : attr.string_list(doc = "The compiler include directories."),
 "compiler_root" : attr.string(doc = "The compiler root directory."),
 "host_os" : attr.string(default = "linux", doc = "The cross toolchain prefix."),
 "toolchain_identifier": attr.string(),
 "target_os" : attr.string(default = "linux"),
 "target_arch" : attr.string(default = "x86-64"), 
 "cc_compiler" : attr.string(default = "gcc", doc = "The compiler type."),
 "extra_features": attr.string_list(),
}
优化多工具配置生成,可以仅仅通过一个所有工具链配置文件自动生成所有工具链集合,从而方便命令行通过 --cpu 可以切换到某个工具链def generate_toolchain_suite():
    toolchains = {}
    native.filegroup(name = "empty")

 for (platform, toolchain_info) in TOOLCHAIN_SUPPORT_MATRIX.items():
        host_os = toolchain_info[TOOLCHAIN_HOST_OS]
        target_os = toolchain_info[TOOLCHAIN_TARGET_OS]
        target_arch = toolchain_info[TOOLCHAIN_TARGET_ARCH]
        compiler_root = toolchain_info[TOOLCHAIN_COMPILER_ROOT]
        include_paths = toolchain_info[TOOLCHAIN_INCLUDE_PATHS]
        toolchain_identifier = toolchain_info[TOOLCHAIN_IDENTIFIER]
        cc_compiler = toolchain_info[TOOLCHAIN_CC_COMPILER]

        base_name = "{platform}_{target_os}_{target_arch}_{cc_compiler}_{toolchain_identifier}".format(
            platform = platform,
            target_os = target_os,
            target_arch = target_arch,
            cc_compiler = cc_compiler,
            toolchain_identifier = toolchain_identifier
        )

        configuration_name = "%s_cc_toolchain_config" % base_name
        cc_name = "%s_cc_toolchain" % base_name
        toolchain_name = "%s_cc" % base_name

        my_cc_toolchain_config(
            name = configuration_name,
            include_paths = include_paths,
            compiler_root = compiler_root,
            host_os = host_os,
            toolchain_identifier = toolchain_identifier,
            target_os = target_os,
            target_arch = target_arch,
            cc_compiler = cc_compiler,
            extra_features = [],
        )

        cc_toolchain(
            name = cc_name,
            toolchain_identifier = toolchain_name,
            toolchain_config = ":%s" % configuration_name,
            all_files = ":empty",
            compiler_files = ":empty",
            dwp_files = ":empty",
            linker_files = ":empty",
            objcopy_files = ":empty",
            strip_files = ":empty",
            supports_param_files = 0,
        )

 if platform in toolchains.keys():
 print("%s already exist!" % platform)
            fail("%s already exist!" % platform)
 else:
            toolchains[platform] = cc_name

 print("toolchains = ", toolchains)
    cc_toolchain_suite(
        name = "compiler_suite",
        toolchains = toolchains
    )

TOOLCHAIN_SUPPORT_MATRIX = {
 "hisi": {
        TOOLCHAIN_HOST_OS : "linux",
        TOOLCHAIN_TARGET_OS : "linux",
        TOOLCHAIN_TARGET_ARCH : "armv7",
        TOOLCHAIN_COMPILER_ROOT : "",
        TOOLCHAIN_INCLUDE_PATHS : [],
        TOOLCHAIN_IDENTIFIER : "",
        TOOLCHAIN_CC_COMPILER : "gcc"
    },
 "ubuntu_gcc": {
        TOOLCHAIN_HOST_OS : "linux",
        TOOLCHAIN_TARGET_OS : "linux",
        TOOLCHAIN_TARGET_ARCH : "x86-64",
        TOOLCHAIN_COMPILER_ROOT : "/usr/bin/",
        TOOLCHAIN_INCLUDE_PATHS : [
 "/usr/include",
 "/usr/lib/gcc",
 "/usr/local/include"
        ],
        TOOLCHAIN_IDENTIFIER : "",
        TOOLCHAIN_CC_COMPILER : "gcc"
    },
 "ubuntu_clang": {
        TOOLCHAIN_HOST_OS : "linux",
        TOOLCHAIN_TARGET_OS : "linux",
        TOOLCHAIN_TARGET_ARCH : "x86-64",
        TOOLCHAIN_COMPILER_ROOT : "",
        TOOLCHAIN_INCLUDE_PATHS : [],
        TOOLCHAIN_IDENTIFIER : "",
        TOOLCHAIN_CC_COMPILER : "clang"
    },
 "ubuntu_arm_linux_gnueabihf" : {
        TOOLCHAIN_HOST_OS : "linux",
        TOOLCHAIN_TARGET_OS : "linux",
        TOOLCHAIN_TARGET_ARCH : "aarch64",
        TOOLCHAIN_COMPILER_ROOT : "/usr/bin/",
        TOOLCHAIN_INCLUDE_PATHS : [
 "/usr/arm-linux-gnueabihf/include/",
 "/usr/lib/gcc-cross/arm-linux-gnueabihf/7/include",
        ],
        TOOLCHAIN_IDENTIFIER : "arm-linux-gnueabihf-",
        TOOLCHAIN_CC_COMPILER : "gcc"
    }
}

最后--crosstool_top=//toolchains/cpp:{cc_toolchain_suite的名称}--cpu={cc_toolchain的名称},即可实现交叉编译。整个实现内容这里就不贴出来了。为了简化 $ bazel build 命令,可以将默认配置项写入 .bazelrc 文件中:

build:compiler_config --crosstool_top=//toolchains/cpp:compiler_suite
build:compiler_config --cpu=ubuntu_gcc
build:compiler_config --host_crosstool_top=@bazel_tools//tools/cpp:toolchain

3 Platform 方式

3.1 平台

3.1.1 概述

Bazel 可以在各种硬件、操作系统和系统配置上构建和测试代码,使用许多不同版本的构建工具,比如链接器和编译器。为了帮助管理这种复杂性,Bazel 提出了约束(constraints )和平台(platforms)的概念。约束是构建或生产环境可能不同的维度,比如 CPU 架构、GPU 的存在或缺失,或者系统安装的编译器的版本。如第一章所述,平台是这些约束的指定选择集合,表示在某些环境中可用的特定资源。

将环境建模为平台有助于 Bazel 为构建操作自动选择适当的工具链。平台还可以与 config_setting 规则结合使用来编写可配置属性。

Bazel 认为平台可以扮演三个角色:

  • Host(主机): Bazel 本身运行的平台
  • Execution(执行): 构建工具执行构建操作以产生中间和最终输出的平台,执行平台设置一般是固定的。
  • Target(目标): 最终输出驻留在其上并在其上执行的平台,比如可能在执行平台上交叉编译目标平台输出,则目标平台是多变的。

“注:这里 Host 平台只是平台扮演一个角色的阐述,跟实际编写 Bazel 规则没有关系。toolchain 规则里也只有对执行平台和目标平台的约束设置。

Bazel 支持以下针对平台的构建场景:

  • 单平台构建(默认):主机、执行和目标平台是相同的。例如,在运行在 Intel x64 CPU 上的 Ubuntu 上构建 Linux 可执行文件。
  • 交叉编译构建:主机和执行平台是相同的,但是目标平台是不同的。例如,在 macOS 上开发一个运行在 MacBook Pro 上的 iOS 应用。
  • 多平台构建:主机、执行和目标平台都是不同的。

3.1.2 定义约束和平台

平台的可能选择空间是通过使用构建文件中的 constraint_settingconstraint_value 规则定义的。constraint_setting 创建一个新维度,可以说是一个约束值集合,constraint_value 为给定维度(constraint_setting)创建一个新值;它们一起有效地定义了枚举及其可能的值。简单来说,constraint_settingconstraint_value 就是一个单键多值的 map ,例如,下面的构建文件片段为系统的 glibc 版本引入了具有两个可能值的约束。

constraint_setting(name = "glibc_version")

constraint_value(
    name = "glibc_2_25",
    constraint_setting = ":glibc_version",
)

constraint_value(
    name = "glibc_2_26",
    constraint_setting = ":glibc_version",
)

约束及其值可以在工作区中的不同包之间定义。它们通过标签进行引用,并服从通常的可见性控制。如果可见性允许,你就可以通过定义自己的值来扩展现有的约束设置。

平台规则 `platform`[1] 引入了一个具有特定约束值选择的新平台。下面创建了一个名为 linux_x86 的平台,描述了在 glibc 版本为 2.25x86_64 体系结构上运行 Linux 操作系统的任何环境。

platform(
    name = "linux_x86",
    constraint_values = [
        "@platforms//os:linux",
        "@platforms//cpu:x86_64",
        ":glibc_2_25",
    ],
)

注意,对于一个平台来说,同一个约束设置多个值是错误的,比如 glibc_2_25glibc_2_26 不能同时设置,因为他们都属于 glibc_version 约束。

3.1.3 通用的约束和平台

为了保持生态系统的一致性,Bazel 团队维护了一个存储库,其中包含最流行的 CPU 架构和操作系统的约束定义。这些都位于 https://github.com/bazelbuild/platforms。当然你也可以自己自定义。

Bazel 附带以下特殊的平台定义 :@local_config_platform//:host。会自动检测主机平台的值:表示 Bazel 运行的系统的平台。

3.1.4 指定平台构建

你可以使用以下命令行标志为构建指定主机和目标平台:

  • --host_platform:默认为 @bazel_tools//platforms:host_platform
  • --platforms:默认为 @bazel_tools//platforms:target_platform
  • 不指定 --platforms,默认是一个表示本地构建机器的平台,即由 @local_config_platform//:host 自动生成。

3.2 工具链

在“前言”一章节中,可以知道平台可以实现混合语言项目的构建,而如果对每一种语言实现构建,则需要配置工具链以及实现工具链的平台约束设定。这样就可以将平台与工具链联合在一起了,原理类似依赖注入。

工具链是使用 toolchain[2] 规则定义的目标,该规则将工具链实现与工具链类型相关联。工具链类型是使用 tooclhain_type() 规则定义的目标(其实用一个字符串常量也可以替代)。工具链实现是一个目标,它通过列出作为工具链一部分的文件(例如,编译器和标准库)以及使用该工具链所需的代码来表示实际的工具链。工具链实现必须返回 ToolchainInfo Provider(Provider 可以认为就是一个函数的返回值),ToolchainInfo 存放着工具链相关配置信息,对于存放什么内容没有要求,即你可以定义任何你想要存放的信息。

任何定义工具链的人都需要声明一个 toolchain_type 目标,这是一个字符串标识,用来标志工具链类别,以避免在加载了多个语言规则的工作区中出现潜在的冲突。比如 Bazel 官方提供了一个 CPP 的标识:@bazel_tools//tools/cpp:toolchain_type,而 rules_go 提供了 @io_bazel_rules_go//go:toolchain 用以区分工具链类别。

对于 C++cc_toolchain 规则即工具链实现,跟 Non-Platform 的工具链目标实现一致。当然你也可以使用任何返回 ToolchainInfo 的规则,而不仅仅是 cc_toolchain,比如可以通过 platform_common.ToolchainInfo 创建一个 ToolchainInfo,然后创建自己的工具链实现规则。

HELLOSDK = provider(
    fields = {
        "os": "The host OS the SDK was built for.",
        "arch": "The host architecture the SDK was built for.",
        "root_file": "A file in the SDK root directory",
        "libs": ("List of pre-compiled .a files for the standard library " +
                 "built for the execution platform."),
        "headers": ("List of .h files from pkg/include that may be included " +
                    "in assembly sources."),
        "srcs": ("List of source files for importable packages in the " +
                 "standard library. Internal, vendored, and tool packages " +
                 "may not be included."),
        "package_list": ("A file containing a list of importable packages " +
                         "in the standard library."),
        "hello": "The hello binary file",
    },
)

def _hello_toolchain_impl(ctx):
    return [platform_common.ToolchainInfo(
        sdk = ctx.attr.sdk,
        cflags = ctx.attr.cflags,
    )]

hello_toolchain = rule(
    _hello_toolchain_impl,
    attrs = {
        "sdk": attr.label(
            mandatory = True,
            providers = [HELLOSDK],
            cfg = "exec",
            doc = "The SDK this toolchain is based on sdk",
        ),
        "cflags": attr.string_list(),
    },
    doc = "Defines a hello toolchain based on SDK",
    provides = [platform_common.ToolchainInfo],
)

工具链实现规则可以认为是面向对象中的类,我们可以 New 很多实例出来,这里 New 出来的就是很多不同平台架构或者不同版本的工具链了。完成工具链实例创建,就可以通过 native.toolchain 绑定工具链类型、目标平台、运行平台约束了。

用户通过在 WORKSPACE 文件中调用 `register_toolchains`[3] 函数或者在命令行中传递 --extra_toolchains 标志来注册他们想要使用的工具链。

最后,当 Bazel 开始构建时,它会检查执行和目标平台的约束条件。然后选择与这些约束兼容的一组合适的工具链。Bazel 将向请求它们的规则提供这些工具链的 ToolchainInfo 对象。

如果想了解 Bazel 如何选择或拒绝注册的工具链,可以使用 --toolchain_resolution_debug 标志来调试。

3.3 Platform + Toolchain 实现平台方式构建

Bazel 的 C++ 规则使用平台来选择工具链,需要设置 --incompatible_enable_cc_toolchain_resolution,如果不设置,即使显示的在命令行加上--platforms也不起作用。

同样地,Platform + Toolchain 实现平台方式构建,官方文档也提供了一个样例,参见:https://docs.bazel.build/versions/master/toolchains.html 。总的步骤这里总结下:

  1. 创建 ToolchainInfo
  2. 创建 xx_toolchain,比如 C++ 已经有了内置的 cc_toolchain,则无需第一步和这一步了,即不用自己手动去实现该规则,只需要配置 cc_toolchain 即可
  3. native.toolchain 关联工具链实现,并设定 target_compatible_with,与平台绑定以及工具链类型等,这里关联相关平台约束也需要创建。
  4. 在 WORKSPACE 文件中注册所有声明的工具链,可以用register_toolchains() 或者命令行指定注册 --extra_toolchains=
  5. 通过 --platforms= 就可以通过平台方式构建了

这里同样跟 Non-Platform 方式一样,对于 C++,我们可以复用工具链的配置和 cc_toolchain 配置部分。工程上可以优化:

  • 可以根据工具链配置自动生成平台约束def generate_constraint_set_platform(): available = get_available_unique_platform_idetifier() native.constraint_setting( name = "platform", visibility = ["//visibility:public"], ) for item in available: native.constraint_value( name = item, constraint_setting = ":platform", visibility = ["//visibility:public"], )
  • 可以根据工具链配置自动声明工具链,同样地类似 Non-Platform 可以批量声明native.toolchain( name = toolchain_name, exec_compatible_with = [ "@platforms//cpu:%s" % bazel_exec_platform_info["cpu"], "@platforms//os:%s" % bazel_exec_platform_info["os"], ], target_compatible_with = [ "//platforms:%s" % platform, ], toolchain = "//toolchains/cpp:%s" % cc_name, toolchain_type = "@bazel_tools//tools/cpp:toolchain_type", )
  • 配置文件可以配置同一平台下多个工具链或不同平台下的工具链

--incompatible_enable_cc_toolchain_resolution 启动平台方式设置我们也可以将其放入 .bazelrc 全局构建配置文件中,从而省去命令行键入:

build   --incompatible_enable_cc_toolchain_resolution

如果想自己完完全全实现一个与语言相关的平台工具链规则集合,可以参考 go 语言的规则实现:https://github.com/bazelbuild/rules_go/blob/master/go/private/go_toolchain.bzl 。

最后,整个实现代码这里也不贴出来了。我们重点需要了解实现过程中的重点以及如果更好的在工程实际中应用。

4 小结

从 Bazel 的平台应用我们可以看到它的强大在于大型、混合语言、多平台的应用,能够统一平台构建,这也是 Bazel 的核心特点。平台本身就是一组约束值的集合,但是实际上可能在一个平台上会出现不同约束值的组合,当约束维度足够多的时候,就会出现约束组合爆炸增长。而且对于单语言项目,比如 C++,平台的内容其实主要是 C++ 编译器的配置和平台约束绑定,与 Non-Platform 相比,反而增加了实现复杂度,对单语言项目来说可能 Bazel 平台方式构建也不是一个推荐的选择,不过对于单语言的大型项目,实现分布式缓存和构建、非时间戳的增量构建用 Bazel 也是一个很好的选择。最后,Bazel 平台功能还没有完善,未来期待 Bazel 变得强大好用。

参考资料

[1]

platform: https://docs.bazel.build/versions/3.4.0/be/platform.html#platform

[2]

toolchain: https://docs.bazel.build/versions/master/be/platform.html#toolchain

[3]

register_toolchains: https://docs.bazel.build/versions/master/skylark/lib/globals.html#register_toolchains