为工具链创建 Conan 包¶
在学习了如何为打包应用程序的工具需求创建 recipe 后,我们将举例说明如何创建一个打包预编译的工具链或编译器以构建其他包的 recipe。
在“使用 Conan 交叉编译应用程序:host 和 build 上下文”教程部分,我们讨论了使用 Conan 交叉编译应用程序的基础知识,重点关注“build”和“host”上下文。我们学习了如何配置 Conan 使用不同的 profile 来构建机器和目标主机机器,从而使我们能够从 Ubuntu Linux 机器交叉编译应用程序到 Raspberry Pi 等平台。
然而,在该部分中,我们假设 cross-compiling toolchain 或 compiler 已作为 build 环境的一部分存在,并通过 Conan profile 进行设置。现在,我们将更进一步,演示如何为这样的工具链创建 Conan 包。此包随后可用作其他 Conan recipe 中的 tool_require,从而简化交叉编译环境的设置过程。
请先克隆源代码以重新创建此项目。您可以在 GitHub 上的examples2 仓库中找到它们。
$ git clone https://github.com/conan-io/examples2.git
$ cd examples2/examples/cross_build/toolchain_packages/toolchain
在这里,您将找到一个 Conan recipe(以及 test_package),用于打包 ARM 工具链以交叉编译到 32 位和 64 位的 Linux ARM。为简化起见,我们假设我们可以从 Linux x86_64 交叉构建到 Linux ARM,包括 32 位和 64 位。如果您正在寻找另一个示例,可以 在此处 探索另一个 MacOs 到 Linux 交叉构建示例。
.
├── conanfile.py
└── test_package
├── CMakeLists.txt
├── conanfile.py
└── test_package.cpp
让我们查看 recipe 并浏览最相关部分
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"
})
验证工具链包:settings、settings_build 和 settings_target¶
您可能还记得,validate() 方法 用于指示包与某些配置不兼容。如前所述,我们将该包的使用限制在 Linux x86_64 平台,用于交叉编译到 Linux ARM 目标,支持 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.")
...
首先,重要的是要认识到只声明了 os 和 arch 设置。这些设置代表将为工具链编译包的机器,因此我们只需要验证它们是否对应于 Linux 和 x86_64,因为这些是工具链二进制文件预期的平台。
请注意,对于要用作 tool_requires 的此包,这些设置与 host profile 无关,而是与 build profile 相关。Conan 在使用 --build-require 参数创建包时会识别此区别。这将使得在包创建的上下文中,settings 和 settings_build 相等。
验证目标平台
在涉及交叉编译的情况下,关于目标平台(工具链编译器生成的执行文件将运行的平台)的验证必须引用 settings_target。这些设置来自 host profile 中的信息。例如,如果编译到 Raspberry Pi,那就是存储在 settings_target 中的信息。再次,Conan 知道由于在创建包时使用了 --build-require 标志,settings_target 应该用 host profile 信息填充。
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 上下文的预期一致。
此处,图示显示了两个 profile 以及为位于 build 上下文中的 arm-toolchain recipe 选择的设置。
下载工具链的二进制文件并打包¶
...
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() 方法用于下载 recipe 许可证,因为它在 ARM 工具链的下载页面上。然而,那里只执行了此操作。实际的工具链二进制文件在 build() 方法中获取。此方法是必需的,因为工具链包旨在支持 32 位和 64 位体系结构,这要求我们下载两组不同的工具链二进制文件。包最终包含哪个二进制文件取决于 settings_target 体系结构。此条件下载过程无法在 source() 方法中进行,因为它缓存下载的内容。
package() 方法没有不寻常之处;它只是将下载的文件(包括许可证)复制到包文件夹中。
将 settings_target 添加到 Package ID 信息中¶
在为交叉编译场景设计的 recipe 中,特别是那些涉及针对特定体系结构或操作系统的工具链的 recipe,并且二进制包可能根据目标平台不同,我们可能需要修改 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 时考虑目标平台的设置。在这种情况下,我们删除了 os、compiler 和 build_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,因此我们只需添加特定于 triplet 的目录。请注意,无需定义环境变量信息即可将这些目录添加到PATH,因为 Conan 将通过 VirtualRunEnv 管理此项。我们定义了
tools.build:compiler_executables配置。此配置将在多个生成器中考虑,例如 CMakeToolchain、MesonToolchain 或 AutotoolsToolchain,以指向合适的编译器二进制文件。
测试 Conan 工具链包¶
我们还添加了一个简单的 test_package 来测试工具链。
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 程序,并且使用该工具链生成的二进制文件正确地定位到指定的体系结构。
使用工具链交叉编译应用程序¶
在详细介绍了工具链 recipe 后,是时候继续进行包创建了。
$ 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.
...
重要
使用 --build-require 参数。
conan create 命令默认创建“host”上下文的包,使用“host” profile。但如果要创建的包将用作 tool_requires 的工具,那么它需要为“build”上下文构建。
--build-require 参数指定了这一点。当提供此参数时,当前 recipe 的二进制文件将为“build”上下文构建,在本例中,使用 default profile,并且它将接收 raspberry-64“host” profile 设置作为 settings_target。 arm-toolchain/13.2 包是一个可执行文件在当前“build”机器上运行,而不是在 RaspberryPI 上运行的包,但它是一个以 RaspberryPI 为目标工具。
--build-require 参数对于正确地将 arm-toolchain 包构建为构建工具是必需的。
准备好工具链包后,我们继续构建实际应用程序。这将是在 使用 Conan 交叉编译应用程序:host 和 build 上下文 部分中之前交叉编译过的同一个应用程序。但是,这次我们将工具链包作为 host profile 中的依赖项。这确保了工具链用于构建应用程序及其所有依赖项。
$ 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
我们将现有的 profile 与另一个名为 arm-toolchain 的 profile 组合,该 profile 只添加了 tool_requires。
[tool_requires]
arm-toolchain/13.2
在此过程中,zlib 依赖项也将被编译为 ARM 64 位体系结构(如果尚未编译)。此外,重要的是要验证生成的可执行文件的体系结构,确认其与目标 64 位体系结构一致。