为工具链创建 Conan 包

在学习了如何为需要打包应用程序的工具创建配方后,我们将展示一个如何创建打包预编译工具链或编译器的配方的示例,用于构建其他包。

在“如何使用 Conan 交叉编译应用程序:主机和构建上下文”教程部分,我们讨论了使用 Conan 交叉编译应用程序的基础知识,重点关注“构建”和“主机”上下文。我们学习了如何配置 Conan 以使用构建机器和目标主机不同的配置文件,使我们能够从 Ubuntu Linux 机器交叉编译 Raspberry Pi 等平台的应用程序。

但是,在那一部分,我们假设交叉编译工具链或编译器作为构建环境的一部分存在,并通过 Conan 配置文件设置。现在,我们将更进一步,演示如何为这样的工具链创建 Conan 包。然后,此包可用作其他 Conan 配方中的tool_require,简化设置交叉编译环境的过程。

请先克隆源代码以重新创建此项目。您可以在 GitHub 上的examples2 存储库中找到它们。

$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/examples/cross_build/toolchain_packages/toolchain

在这里,您将找到一个 Conan 配方(以及test_package)来打包 ARM 工具链,用于交叉编译到 Linux ARM 的 32 位和 64 位。为了简化一点,我们假设我们可以从 Linux x86_64 交叉构建到 Linux ARM,包括 32 位和 64 位。如果您正在寻找其他示例,可以探索此处提供的另一个从 MacOs 到 Linux 的交叉构建示例这里

.
├── conanfile.py
└── test_package
    ├── CMakeLists.txt
    ├── conanfile.py
    └── test_package.cpp

让我们检查配方并浏览最相关的部分

conanfile.py
import os
from conan import ConanFile
from conan.tools.files import get, copy, download
from conan.errors import ConanInvalidConfiguration
from conan.tools.scm import Version

class ArmToolchainPackage(ConanFile):
    name = "arm-toolchain"
    version = "13.2"
    ...
    settings = "os", "arch"
    package_type = "application"

    def _archs32(self):
        return ["armv6", "armv7", "armv7hf"]

    def _archs64(self):
        return ["armv8", "armv8.3"]

    def _get_toolchain(self, target_arch):
        if target_arch in self._archs32():
            return ("arm-none-linux-gnueabihf",
                    "df0f4927a67d1fd366ff81e40bd8c385a9324fbdde60437a512d106215f257b3")
        else:
            return ("aarch64-none-linux-gnu",
                    "12fcdf13a7430655229b20438a49e8566e26551ba08759922cdaf4695b0d4e23")

    def validate(self):
        if self.settings.arch != "x86_64" or self.settings.os != "Linux":
            raise ConanInvalidConfiguration(f"This toolchain is not compatible with {self.settings.os}-{self.settings.arch}. "
                                            "It can only run on Linux-x86_64.")

        valid_archs = self._archs32() + self._archs64()
        if self.settings_target.os != "Linux" or self.settings_target.arch not in valid_archs:
            raise ConanInvalidConfiguration(f"This toolchain only supports building for Linux-{valid_archs.join(',')}. "
                                        f"{self.settings_target.os}-{self.settings_target.arch} is not supported.")

        if self.settings_target.compiler != "gcc":
            raise ConanInvalidConfiguration(f"The compiler is set to '{self.settings_target.compiler}', but this "
                                            "toolchain only supports building with gcc.")

        if Version(self.settings_target.compiler.version) >= Version("14") or Version(self.settings_target.compiler.version) < Version("13"):
            raise ConanInvalidConfiguration(f"Invalid gcc version '{self.settings_target.compiler.version}'. "
                                                "Only 13.X versions are supported for the compiler.")

    def source(self):
        download(self, "https://developer.arm.com/GetEula?Id=37988a7c-c40e-4b78-9fd1-62c20b507aa8", "LICENSE", verify=False)

    def build(self):
        toolchain, sha = self._get_toolchain(self.settings_target.arch)
        get(self, f"https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x86_64-{toolchain}.tar.xz",
            sha256=sha, strip_root=True)

    def package_id(self):
        self.info.settings_target = self.settings_target
        # We only want the ``arch`` setting
        self.info.settings_target.rm_safe("os")
        self.info.settings_target.rm_safe("compiler")
        self.info.settings_target.rm_safe("build_type")

    def package(self):
        toolchain, _ = self._get_toolchain(self.settings_target.arch)
        dirs_to_copy = [toolchain, "bin", "include", "lib", "libexec"]
        for dir_name in dirs_to_copy:
            copy(self, pattern=f"{dir_name}/*", src=self.build_folder, dst=self.package_folder, keep_path=True)
        copy(self, "LICENSE", src=self.build_folder, dst=os.path.join(self.package_folder, "licenses"), keep_path=False)

    def package_info(self):
        toolchain, _ = self._get_toolchain(self.settings_target.arch)
        self.cpp_info.bindirs.append(os.path.join(self.package_folder, toolchain, "bin"))

        self.conf_info.define("tools.build:compiler_executables", {
            "c":   f"{toolchain}-gcc",
            "cpp": f"{toolchain}-g++",
            "asm": f"{toolchain}-as"
        })

验证工具链包:设置、设置构建和设置目标

您可能还记得,validate() 方法用于指示包与某些配置不兼容。如前所述,我们将此包的使用限制在用于交叉编译到Linux ARM目标的Linux x86_64平台上,支持 32 位和 64 位架构。让我们检查一下我们如何将此信息整合到validate()方法中,并讨论涉及的各种类型的设置。

验证构建平台

...

settings = "os", "arch"

...

def validate(self):

    if self.settings.arch != "x86_64" or self.settings.os != "Linux":
        raise ConanInvalidConfiguration(f"This toolchain is not compatible with {self.settings.os}-{self.settings.arch}. "
                                        "It can only run on Linux-x86_64.")
    ...

首先,必须承认只有osarch设置被声明。这些设置表示将为工具链编译包的机器,因此我们只需要验证它们是否对应于Linuxx86_64,因为这些是工具链二进制文件的目标平台。

需要注意的是,对于这个作为tool_requires使用的包,这些设置与host配置文件无关,而是与build配置文件相关。Conan 在使用--build-require参数创建包时会识别这种区别。这将使settingssettings_build在包创建的上下文中相等。

验证目标平台

在涉及交叉编译的场景中,关于目标平台(工具链的编译器生成的执行文件将在其中运行)的验证必须引用settings_target。这些设置来自host配置文件中的信息。例如,如果为 Raspberry Pi 编译,则这些信息将存储在settings_target中。同样,由于在包创建期间使用了--build-require标志,Conan 意识到settings_target应该使用host配置文件信息填充。

def validate(self):
    ...

    valid_archs = self._archs32() + self._archs64()
    if self.settings_target.os != "Linux" or self.settings_target.arch not in valid_archs:
        raise ConanInvalidConfiguration(f"This toolchain only supports building for Linux-{valid_archs.join(',')}. "
                                       f"{self.settings_target.os}-{self.settings_target.arch} is not supported.")

    if self.settings_target.compiler != "gcc":
        raise ConanInvalidConfiguration(f"The compiler is set to '{self.settings_target.compiler}', but this "
                                        "toolchain only supports building with gcc.")

    if Version(self.settings_target.compiler.version) >= Version("14") or Version(self.settings_target.compiler.version) < Version("13"):
        raise ConanInvalidConfiguration(f"Invalid gcc version '{self.settings_target.compiler.version}'. "
                                        "Only 13.X versions are supported for the compiler.")

如您所见,进行了一些验证以确保生成的二进制文件执行环境的操作系统和架构的有效性。此外,它还验证编译器的名称和版本是否与host上下文的预期一致。

在此,该图显示了两个配置文件以及为处于build上下文中的arm-toolchain配方选择的设置。

digraph context_diagram { subgraph cluster_build_context { label = "build context"; fontname = Helvetica; labeljust = "l"; style=filled; color=lightblue; "arm-toolchain/13.2" [shape=box, style=filled, color=lightblue, fontname=Helvetica] "settings" [shape=box, style=filled, fillcolor=lightblue, fontname=Helvetica] "settings_target" [shape=box, style=filled, fillcolor=pink, fontname=Helvetica] } subgraph cluster_build_profile { label="build profile"; labeljust = "l"; fontname = Helvetica; color=white "build_profile" [shape=record, label="[settings]\larch=x86_64\lbuild_type=Release\lcompiler=gcc\lcompiler.cppstd=gnu14\lcompiler.version=7\los=Linux\l", style=filled, color=lightblue, fontname=Helvetica] } subgraph cluster_host_profile { label = "host profile"; labeljust = "l"; fontname = Helvetica color = white; "host_profile" [shape=record, label="[settings]\larch=armv8\lbuild_type=Release\lcompiler=gcc\lcompiler.cppstd=gnu14\lcompiler.version=13\los=Linux\l", style=filled, color=pink, fontname=Helvetica] } "build_profile" -> "settings" "host_profile" -> "settings_target" }

下载工具链的二进制文件并打包

...

def _archs32(self):
    return ["armv6", "armv7", "armv7hf"]

def _archs64(self):
    return ["armv8", "armv8.3"]

def _get_toolchain(self, target_arch):
    if target_arch in self._archs32():
        return ("arm-none-linux-gnueabihf",
                "df0f4927a67d1fd366ff81e40bd8c385a9324fbdde60437a512d106215f257b3")
    else:
        return ("aarch64-none-linux-gnu",
                "12fcdf13a7430655229b20438a49e8566e26551ba08759922cdaf4695b0d4e23")

def source(self):
    download(self, "https://developer.arm.com/GetEula?Id=37988a7c-c40e-4b78-9fd1-62c20b507aa8", "LICENSE", verify=False)

def build(self):
    toolchain, sha = self._get_toolchain(self.settings_target.arch)
    get(self, f"https://developer.arm.com/-/media/Files/downloads/gnu/13.2.rel1/binrel/arm-gnu-toolchain-13.2.rel1-x86_64-{toolchain}.tar.xz",
        sha256=sha, strip_root=True)

def package(self):
    toolchain, _ = self._get_toolchain(self.settings_target.arch)
    dirs_to_copy = [toolchain, "bin", "include", "lib", "libexec"]
    for dir_name in dirs_to_copy:
        copy(self, pattern=f"{dir_name}/*", src=self.build_folder, dst=self.package_folder, keep_path=True)
    copy(self, "LICENSE", src=self.build_folder, dst=os.path.join(self.package_folder, "licenses"), keep_path=False)

...

source()方法用于下载配方许可证,因为它位于 ARM 工具链的下载页面上。但是,这是唯一在那里执行的操作。实际的工具链二进制文件是在build()方法中获取的。这种方法是必要的,因为工具链包旨在支持 32 位和 64 位架构,需要我们下载两组不同的工具链二进制文件。包最终包含哪个二进制文件取决于settings_target架构。此条件下载过程无法在source()方法中发生,因为它缓存下载的内容

package()方法没有任何异常;它只是将下载的文件复制到包文件夹中,包括许可证。

settings_target添加到包 ID 信息

在为交叉编译场景设计的配方中,特别是那些涉及针对特定架构或操作系统的工具链的配方,以及基于目标平台二进制包可能不同,我们可能需要修改package_id()以确保 Conan 正确识别和区分基于其目标平台的二进制文件。

在这种情况下,我们扩展了package_id()方法以包含settings_target,它封装了目标平台的配置(在这种情况下,它是 32 位还是 64 位)。

def package_id(self):
    # Assign settings_target to the package ID to differentiate binaries by target platform.
    self.info.settings_target = self.settings_target

    # We only want the ``arch`` setting
    self.info.settings_target.rm_safe("os")
    self.info.settings_target.rm_safe("compiler")
    self.info.settings_target.rm_safe("build_type")

通过指定self.info.settings_target = self.settings_target,我们明确指示 Conan 在生成包 ID 时考虑目标平台的设置。在这种情况下,我们删除了oscompilerbuild_type设置,因为更改它们与选择用于构建的工具链无关,并且只保留arch设置,该设置将用于决定是否要生成 32 位或 64 位的二进制文件。

为使用者定义信息

package_info()方法中,我们定义了使用者在使用工具链时需要使用到的所有信息。

def package_info(self):
    toolchain, _ = self._get_toolchain(self.settings_target.arch)
    self.cpp_info.bindirs.append(os.path.join(self.package_folder, toolchain, "bin"))

    self.conf_info.define("tools.build:compiler_executables", {
        "c":   f"{toolchain}-gcc",
        "cpp": f"{toolchain}-g++",
        "asm": f"{toolchain}-as"
    })

在这种情况下,我们需要定义以下信息:

  • 添加可能在编译期间需要的包含工具链工具的目录。我们下载的工具链将将其工具存储在bin<toolchain_triplet>/bin中。由于self.cpp_info.bindirs默认为bin,因此我们只需要添加特定于三元组的目录。请注意,无需定义环境信息以将这些目录添加到PATH中,因为 Conan 将通过VirtualRunEnv管理此操作。

  • 我们定义了tools.build:compiler_executables配置。此配置将在几个生成器中考虑,例如CMakeToolchainMesonToolchainAutotoolsToolchain,以指向相应的编译器二进制文件。

测试 Conan 工具链包

我们还添加了一个简单的test_package来测试工具链。

test_package/conanfile.py
import os
from io import StringIO

from conan import ConanFile
from conan.tools.cmake import CMake, cmake_layout


class TestPackageConan(ConanFile):
    settings = "os", "arch", "compiler", "build_type"
    generators = "CMakeToolchain", "VirtualBuildEnv"

    def build_requirements(self):
        self.tool_requires(self.tested_reference_str)

    def layout(self):
        cmake_layout(self)

    def build(self):
        cmake = CMake(self)
        cmake.configure()
        cmake.build()

    def test(self):
        if self.settings.arch in ["armv6", "armv7", "armv7hf"]:
            toolchain = "arm-none-linux-gnueabihf"
        else:
            toolchain = "aarch64-none-linux-gnu"
        self.run(f"{toolchain}-gcc --version")
        test_file = os.path.join(self.cpp.build.bindirs[0], "test_package")
        stdout = StringIO()
        self.run(f"file {test_file}", stdout=stdout)
        if toolchain == "aarch64-none-linux-gnu":
            assert "ELF 64-bit" in stdout.getvalue()
        else:
            assert "ELF 32-bit" in stdout.getvalue()

此测试包确保工具链正常工作,构建一个最小的hello world程序,并且由此产生的二进制文件正确地针对指定的架构。

使用工具链交叉构建应用程序

详细介绍了工具链配方后,是时候继续进行包创建了。

$ conan create . -pr:b=default -pr:h=../profiles/raspberry-64 --build-require

======== Exporting recipe to the cache ========
...
======== Input profiles ========
Profile host:
[settings]
arch=armv8
build_type=Release
compiler=gcc
compiler.cppstd=gnu14
compiler.libcxx=libstdc++11
compiler.version=13
os=Linux

Profile build:
[settings]
arch=x86_64
build_type=Release
compiler=gcc
compiler.cppstd=gnu14
compiler.libcxx=libstdc++11
compiler.version=7
os=Linux
...
======== Testing the package: Executing test ========
arm-toolchain/13.2 (test package): Running test()
arm-toolchain/13.2 (test package): RUN: aarch64-none-linux-gnu-gcc --version
aarch64-none-linux-gnu-gcc (Arm GNU Toolchain 13.2.rel1 (Build arm-13.7)) 13.2.1 20231009
Copyright (C) 2023 Free Software Foundation, Inc.
...

我们为buildhost上下文使用了两个配置文件,但最重要的细节是使用–build-require参数。这告诉 Conan 该包旨在作为构建需求,将其置于构建上下文中。因此,settings与构建配置文件中的设置匹配,而settings_target与主机配置文件的设置保持一致。

工具链包准备就绪后,我们将继续构建一个实际的应用程序。这将与之前在如何使用Conan交叉编译您的应用程序:主机和构建上下文部分中交叉编译的应用程序相同。但是,这次我们将工具链包作为主机配置文件中的依赖项包含进来。这确保了工具链用于构建应用程序及其所有依赖项。

$ cd .. && cd consumer
$ conan install . -pr:b=default -pr:h=../profiles/raspberry-64 -pr:h=../profiles/arm-toolchain --build missing
$ cmake --preset conan-release
$ cmake --build --preset conan-release
$ file ./build/Release/compressor
compressor: ELF 64-bit LSB executable, ARM aarch64, version 1 (SYSV), dynamically
linked, interpreter /lib/ld-linux-aarch64.so.1, for GNU/Linux 3.7.0, with debug_info,
not stripped

我们将现有的配置文件与另一个名为arm-toolchain的配置文件组合,该配置文件仅添加了tool_requires

[tool_requires]
arm-toolchain/13.2

在此过程中,如果zlib依赖项尚未编译,它也将针对ARM 64位架构进行编译。此外,务必验证生成的可执行文件的架构,确认其与目标64位架构一致。