使用 CMake 进行交叉编译

交叉编译软件是指在一种系统上构建,但打算在另一种系统上运行的软件。用于构建软件的系统称为“构建主机”,软件构建的目标系统称为“目标系统”或“目标平台”。目标系统通常运行不同的操作系统(或者根本不运行)和/或运行在不同的硬件上。一个典型的用例是为嵌入式设备(如网络交换机、移动电话或发动机控制单元)开发软件。在这些情况下,目标平台没有或无法运行所需的软件开发环境。

CMake 完全支持交叉编译,从 Linux 到 Windows 的交叉编译,到超级计算机的交叉编译,再到没有操作系统的(OS)小型嵌入式设备的交叉编译。

交叉编译对 CMake 有几个影响

  • CMake 无法自动检测目标平台。

  • CMake 无法在默认系统目录中找到库和头文件。

  • 交叉编译期间构建的可执行文件无法执行。

交叉编译支持并不意味着所有基于 CMake 的项目都能神奇地开箱即用地进行交叉编译(有些项目可以),而是说 CMake 在构建平台和目标平台的信息之间做了区分,并为用户提供了解决交叉编译问题的机制,而无需其他要求,如运行虚拟机等。

为了支持特定软件项目的交叉编译,必须通过工具链文件告诉 CMake 目标平台。CMakeLists.txt 可能需要进行调整。它知道构建平台可能与目标平台具有不同的属性,并且必须处理编译后的可执行文件尝试在构建主机上执行的情况。

工具链文件

为了使用 CMake 进行交叉编译,必须创建一个描述目标平台的 CMake 文件,称为“工具链文件”。此文件告诉 CMake 它需要了解目标平台的所有信息。以下是一个在 Linux 下使用 MinGW 交叉编译器进行 Windows 交叉编译的示例;内容将在之后逐行解释。

# the name of the target operating system
set(CMAKE_SYSTEM_NAME Windows)

# which compilers to use for C and C++
set(CMAKE_C_COMPILER   i586-mingw32msvc-gcc)
set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)

# where is the target environment located
set(CMAKE_FIND_ROOT_PATH  /usr/i586-mingw32msvc
    /home/alex/mingw-install)

# adjust the default behavior of the FIND_XXX() commands:
# search programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)

# search headers and libraries in the target environment
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

假设此文件以 TC-mingw.cmake 的名称保存在您的主目录中,您可以通过设置 CMAKE_TOOLCHAIN_FILE 变量来指示 CMake 使用此文件。

~/src$ cd build
~/src/build$ cmake -DCMAKE_TOOLCHAIN_FILE=~/TC-mingw.cmake ..
...

CMAKE_TOOLCHAIN_FILE 仅需要在最初的 CMake 运行时指定;之后,将从 CMake 缓存中重用结果。您无需为要构建的每个软件包编写单独的工具链文件。工具链文件是针对每个目标平台的;即,如果您为同一个目标平台构建多个软件包,您只需编写一个工具链文件,该文件可用于所有软件包。

工具链文件中的设置意味着什么?我们将逐一检查它们。由于 CMake 无法猜测目标操作系统或硬件,因此您必须设置以下 CMake 变量

CMAKE_SYSTEM_NAME

此变量是必需的;它设置目标系统的名称,即与在目标系统上运行 CMake 时 CMAKE_SYSTEM_NAME 的值相同。典型的示例是“Linux”和“Windows”。它用于构建平台文件的名称,如 Linux.cmake 或 Windows-gcc.cmake。如果您的目标是一个没有操作系统的嵌入式系统,将 CMAKE_SYSTEM_NAME 设置为“Generic”。以这种方式预设 CMAKE_SYSTEM_NAME 而不是检测它,会自动导致 CMake 将构建视为交叉编译构建,并且 CMake 变量 CMAKE_CROSSCOMPILING 将设置为 TRUE。 CMAKE_CROSSCOMPILING 是在 CMake 文件中用于确定当前构建是交叉编译构建还是非交叉编译构建的变量。

CMAKE_SYSTEM_VERSION

设置目标系统的版本。

CMAKE_SYSTEM_PROCESSOR

此变量是可选的;它设置目标系统的处理器或硬件名称。它在 CMake 中用于一个目的,即加载 ${CMAKE_SYSTEM_NAME}-COMPILER_ID-${CMAKE_SYSTEM_PROCESSOR}.cmake 文件。此文件可用于修改设置,如目标的编译器标志。只有在使用交叉编译器时,每个目标都需要特殊的构建设置,您才需要设置此变量。该值可以选择自由设置,因此它可以是 i386、IntelPXA255 或 MyControlBoardRev42 等。

在 CMake 代码中,CMAKE_SYSTEM_XXX 变量始终描述目标平台。 WIN32UNIXAPPLE 这些简短的变量也是如此。这些变量可用于测试目标的属性。如果有必要测试构建主机系统,则有一组相应的变量:CMAKE_HOST_SYSTEMCMAKE_HOST_SYSTEM_NAMECMAKE_HOST_SYSTEM_VERSIONCMAKE_HOST_SYSTEM_PROCESSOR;以及简短形式:CMAKE_HOST_WIN32CMAKE_HOST_UNIXCMAKE_HOST_APPLE.

由于 CMake 无法猜测目标系统,因此它无法猜测应该使用哪个编译器。设置以下变量定义了目标系统要使用的编译器。

CMAKE_C_COMPILER

这将 C 编译器可执行文件指定为完整路径或仅文件名。如果指定了完整路径,则在搜索 C++ 编译器和其他工具(binutils、链接器等)时,将优先使用此路径。如果编译器是带有前缀名称的 GNU 交叉编译器(例如,“arm-elf-gcc”),CMake 将检测到这一点并自动找到相应的 C++ 编译器(即“arm-elf-c++”)。也可以通过 CC 环境变量设置编译器。在工具链文件中直接设置 CMAKE_C_COMPILER 的优点是,有关目标系统的信息完全包含在此文件中,并且不依赖于环境变量。

CMAKE_CXX_COMPILER

此选项指定 C++ 编译器可执行文件,可以是完整路径,也可以仅是文件名。其处理方式与 CMAKE_C_COMPILER 相同。如果工具链是 GNU 工具链,只需设置 CMAKE_C_COMPILER 即可;CMake 应该会自动找到相应的 C++ 编译器。与 CMAKE_C_COMPILER 一样,C++ 编译器也可以通过 CXX 环境变量设置。

查找外部库、程序和其他文件

大多数非简单项目都会使用外部库或工具。CMake 提供了 find_programfind_libraryfind_filefind_pathfind_package 命令来完成此目的。它们会在文件系统中搜索这些文件的常见位置,并返回结果。 find_package 有点不同,因为它本身不进行搜索,而是执行 Find<*>.cmake 模块,这些模块反过来会调用 find_programfind_libraryfind_filefind_path 命令。

在交叉编译时,这些命令会变得更加复杂。例如,在 Linux 系统上交叉编译到 Windows 时,如果 find_package(JPEG) 命令的结果是 /usr/lib/libjpeg.so`,那么将毫无用处,因为这将是主机系统的 JPEG 库,而不是目标系统的 JPEG 库。在某些情况下,您希望查找为目标平台设计的文件;在其他情况下,您希望查找构建主机的文件。以下变量旨在提供灵活性,以更改 CMake 中典型查找命令的工作方式,以便您可以根据需要查找构建主机文件和目标文件。

工具链将包含其自己的针对目标平台的库和头文件,这些库和头文件通常安装在通用前缀下。建议设置一个目录,用于安装为目标平台构建的所有软件,这样软件包就不会与工具链附带的库混淆。

find_program 命令通常用于查找在构建期间执行的程序,因此它应该仍然在主机文件系统中搜索,而不是在目标平台的环境中搜索。 find_library 通常用于查找用于链接目的的库,因此此命令应该只在目标环境中搜索。对于 find_pathfind_file,情况并不那么明显;在许多情况下,它们用于搜索头文件,因此默认情况下它们应该只在目标环境中搜索。以下 CMake 变量可以用来调整查找命令在交叉编译时的行为。

CMAKE_FIND_ROOT_PATH

这是一个包含目标环境的目录列表。列出的每个目录都将被附加到每个查找命令的每个搜索目录之前。假设您的目标环境安装在 /opt/eldk/ppc_74xx 下,并且您为该目标平台的安装目录是 ~/install-eldk-ppc74xx,则将 CMAKE_FIND_ROOT_PATH 设置为这两个目录。然后,find_library(JPEG_LIB jpeg) 将在 /opt/eldk/ppc_74xx/lib/opt/eldk/ppc_74xx/usr/lib~/install-eldk-ppc74xx/lib~/install-eldk-ppc74xx/usr/lib 中搜索,并应该返回 /opt/eldk/ppc_74xx/usr/lib/libjpeg.so

默认情况下,CMAKE_FIND_ROOT_PATH 为空。如果设置了该变量,则首先搜索以 CMAKE_FIND_ROOT_PATH 中给出的路径为前缀的目录,然后搜索未加前缀的相同目录。

通过设置此变量,您实际上是在为 CMake 中的所有查找命令添加一个新的搜索前缀集,但对于某些查找命令,您可能不想搜索目标或主机目录。您可以通过在调用查找命令时传入以下三个选项之一来控制每个查找命令调用的工作方式:NO_CMAKE_FIND_ROOT_PATHONLY_CMAKE_FIND_ROOT_PATHCMAKE_FIND_ROOT_PATH_BOTH。您也可以使用以下三个变量来控制查找命令的工作方式。

CMAKE_FIND_ROOT_PATH_MODE_PROGRAM

此选项设置 find_program 命令的默认行为。它可以设置为 NEVER、ONLY 或 BOTH。当设置为 NEVER 时,除非显式启用,否则 CMAKE_FIND_ROOT_PATH 不会用于 find_program 调用。如果设置为 ONLY,CMAKE_FIND_ROOT_PATH 中的前缀目录将被 find_program 使用。默认值为 BOTH,这意味着首先搜索带前缀的目录,然后搜索未加前缀的目录。

在大多数情况下,find_program 用于搜索可执行文件,然后执行该文件,例如使用 execute_processadd_custom_command。因此,在大多数情况下,需要来自构建主机的可执行文件,因此将 CMAKE_FIND_ROOT_PATH_MODE_PROGRAM 设置为 NEVER 通常是首选。

CMAKE_FIND_ROOT_PATH_MODE_LIBRARY

这与上面相同,但适用于 find_library 命令。在大多数情况下,此命令用于查找库,然后将其用于链接,因此需要目标库。在大多数情况下,它应该设置为 ONLY。

CMAKE_FIND_ROOT_PATH_MODE_INCLUDE

这与上面相同,用于 find_pathfind_file。在大多数情况下,此命令用于查找包含目录,因此应搜索目标环境。在大多数情况下,它应该设置为 ONLY。如果您还需要在构建主机的文件系统中查找文件(例如,构建过程中将处理的一些数据文件);您可能需要使用 NO_CMAKE_FIND_ROOT_PATHONLY_CMAKE_FIND_ROOT_PATHCMAKE_FIND_ROOT_PATH_BOTH 选项调整这些 find_pathfind_file 调用的行为。

使用如上所述设置的工具链文件,CMake 现在知道如何处理目标平台和交叉编译器。现在我们应该能够为目标平台构建软件。对于复杂的项目,还有更多问题需要解决。

系统检查

大多数可移植软件项目都有一组系统检查测试,用于确定(目标)系统的属性。使用 CMake 检查系统功能的最简单方法是测试变量。为此,CMake 提供了变量 UNIXWIN32APPLE。交叉编译时,这些变量适用于目标平台,用于测试构建主机平台,有对应的变量 CMAKE_HOST_UNIXCMAKE_HOST_WIN32CMAKE_HOST_APPLE

如果这种粒度太粗,可以测试变量 CMAKE_SYSTEM_NAMECMAKE_SYSTEMCMAKE_SYSTEM_VERSIONCMAKE_SYSTEM_PROCESSOR,以及它们的对应变量 CMAKE_HOST_SYSTEM_NAMECMAKE_HOST_SYSTEMCMAKE_HOST_SYSTEM_VERSIONCMAKE_HOST_SYSTEM_PROCESSOR,它们包含相同的信息,但适用于构建主机,而不适用于目标系统。

if(CMAKE_SYSTEM MATCHES Windows)
   message(STATUS "Target system is Windows")
endif()

if(CMAKE_HOST_SYSTEM MATCHES Linux)
   message(STATUS "Build host runs Linux")
endif()

使用编译检查

在 CMake 中,有一些宏,如 check_include_filescheck_c_source_runs 用于测试平台的属性。大多数这些宏在内部使用 try_compiletry_run 命令。 try_compile 命令在交叉编译时按预期工作;它尝试使用交叉编译工具链编译代码片段,这将给出预期的结果。

所有使用 try_run 的测试将不起作用,因为创建的可执行文件通常无法在构建主机上运行。在某些情况下,这可能是可能的(例如,使用虚拟机、像 Wine 这样的模拟层或与实际目标的接口),因为 CMake 不依赖于这些机制。在构建过程中依赖模拟器会引入一组新的潜在问题;它们可能对文件系统有不同的看法,使用其他换行符,需要特殊的硬件或软件等。

如果在交叉编译时调用 try_run,它将首先尝试编译软件,这与非交叉编译时的工作方式相同。如果成功,它将检查变量 CMAKE_CROSSCOMPILING 以确定是否可以执行生成的可执行文件。如果不能,它将创建两个缓存变量,然后必须由用户或通过 CMake 缓存设置。假设命令如下所示

try_run(SHARED_LIBRARY_PATH_TYPE
        SHARED_LIBRARY_PATH_INFO_COMPILED
        ${PROJECT_BINARY_DIR}/CMakeTmp
        ${PROJECT_SOURCE_DIR}/CMake/SharedLibraryPathInfo.cxx
        OUTPUT_VARIABLE OUTPUT
        ARGS "LDPATH"
        )

在这个示例中,源文件 SharedLibraryPathInfo.cxx 将被编译,如果成功,生成的执行文件将被执行。变量 SHARED_LIBRARY_PATH_INFO_COMPILED 将被设置为构建结果,即 TRUE 或 FALSE。CMake 将创建一个缓存变量 SHARED_LIBRARY_PATH_TYPE 并将其预设为 PLEASE_FILL_OUT-FAILED_TO_RUN。此变量必须设置为如果在目标上执行,执行文件的退出代码将是什么。此外,CMake 将创建一个缓存变量 SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT 并将其预设为 PLEASE_FILL_OUT-NOTFOUND。此变量应设置为执行文件打印到 stdout 和 stderr 的输出,如果它在目标上执行。此变量仅在 try_run 命令使用 RUN_OUTPUT_VARIABLEOUTPUT_VARIABLE 参数时创建。您必须为这些变量填写适当的值。为了帮助您完成此操作,CMake 会尽力为您提供有用的信息。为了实现这一点,CMake 会创建一个文件 ${CMAKE_BINARY_DIR}/TryRunResults.cmake,您可以在此处看到一个示例。

# SHARED_LIBRARY_PATH_TYPE
#   indicates whether the executable would have been able to run
#   on its target platform. If so, set SHARED_LIBRARY_PATH_TYPE
#   to the exit code (in many cases 0 for success), otherwise
#   enter "FAILED_TO_RUN".
# SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
#   contains the text the executable would have printed on
#   stdout and stderr. If the executable would not have been
#   able to run, set SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
#   empty. Otherwise check if the output is evaluated by the
#   calling CMake code. If so, check what the source file would
#   have printed when called with the given arguments.
# The SHARED_LIBRARY_PATH_INFO_COMPILED variable holds the build
# result for this TRY_RUN().
#
# Source file: ~/src/SharedLibraryPathInfo.cxx
# Executable : ~/build/cmTryCompileExec-SHARED_LIBRARY_PATH_TYPE
# Run arguments:  LDPATH
#    Called from: [1]   ~/src/CMakeLists.cmake

set(SHARED_LIBRARY_PATH_TYPE
    "0"
    CACHE STRING "Result from TRY_RUN" FORCE)

set(SHARED_LIBRARY_PATH_TYPE__TRYRUN_OUTPUT
    ""
    CACHE STRING "Output from TRY_RUN" FORCE)

您可以找到 CMake 无法确定的所有变量,它们从哪个 CMake 文件调用、源文件、可执行文件的参数以及可执行文件的路径。CMake 还将把可执行文件复制到构建目录中;它们的名称为 cmTryCompileExec-<name of the variable>,例如在本例中为 cmTryCompileExec-SHARED_LIBRARY_PATH_TYPE。然后,您可以尝试在实际的目标平台上手动运行此可执行文件并检查结果。

一旦您获得了这些结果,就必须将它们放入 CMake 缓存中。这可以通过使用 ccmake 或 cmake-gui 来完成,并直接在缓存中编辑变量。在另一个构建目录中或如果删除了 CMakeCache.txt,则无法重用这些更改。

建议的方法是使用 CMake 创建的 TryRunResults.cmake 文件。您应该将其复制到安全的位置(即在删除构建目录时不会被删除的位置),并为其提供一个有用的名称,例如 TryRunResults-MyProject-eldk-ppc.cmake。必须编辑此文件的内容,以便 set 命令将必需的变量设置为目标系统上适当的值。然后,可以使用 -C 选项来使用此文件预加载 CMake 缓存。

src/build/ $ cmake -C ~/TryRunResults-MyProject-eldk-ppc.cmake .

您不必再次使用其他 CMake 选项,因为它们现在已在缓存中。这样,您可以在多个构建树中使用 MyProjectTryRunResults-eldk-ppc.cmake,并且它可以与您的项目一起分发,以便其他用户更容易地对其进行交叉编译。

运行项目中构建的可执行文件

在某些情况下,有必要在构建过程中调用一个在同一构建中早先构建的可执行文件;这通常是代码生成器和类似工具的情况。在交叉编译时,这不起作用,因为可执行文件是为目标平台构建的,不能在构建主机上运行(不使用虚拟机、兼容性层、模拟器等)。使用 CMake,这些程序使用 add_executable 创建,并使用 add_custom_commandadd_custom_target 执行。以下三个选项可用于支持 CMake 的这些可执行文件。旧版本的 CMake 代码可能看起来像这样

add_executable(mygen gen.c)
get_target_property(mygenLocation mygen LOCATION)
add_custom_command(
  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
  COMMAND ${mygenLocation}
       -o "${CMAKE_CURRENT_BINARY_DIR}/generated.h" )

现在我们将展示如何修改此文件以使其在交叉编译时有效。基本思想是只有在为构建主机进行本机构建时才构建可执行文件,然后将其作为可执行文件目标导出到 CMake 脚本文件中。然后,在交叉编译时包含此文件,并将可执行文件目标加载到 mygen。将创建与原始目标同名的导入目标。 add_custom_command 识别目标名称为可执行文件,因此对于 add_custom_command 中的命令,只需使用目标名称即可;无需使用 LOCATION 属性来获取可执行文件的路径。

if(CMAKE_CROSSCOMPILING)
   find_package(MyGen)
else()
   add_executable(mygen gen.c)
   export(TARGETS mygen FILE
          "${CMAKE_BINARY_DIR}/MyGenConfig.cmake")
endif()

add_custom_command(
  OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
  COMMAND mygen -o "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
  )

使用这样修改的 CMakeLists.txt,可以对项目进行交叉编译。首先,必须执行本机构建才能创建必要的 mygen 可执行文件。之后,可以开始交叉编译构建。本机构建的构建目录必须作为 MyGen 包的位置提供给交叉编译构建,以便 find_package(MyGen) 可以找到它。

mkdir build-native; cd build-native
cmake ..
make
cd ..
mkdir build-cross; cd build-cross
cmake -DCMAKE_TOOLCHAIN_FILE=MyToolchain.cmake \
      -DMyGen_DIR=~/src/build-native/ ..
make

交叉编译 Hello World

现在让我们实际开始交叉编译。第一步是安装一个交叉编译工具链。如果已经安装,您可以跳过下一段。

有许多不同的方法和项目处理 Linux 的交叉编译,从针对基于 Linux 的 PDA 的自由软件项目到商业嵌入式 Linux 供应商。大多数这些项目都附带自己的构建和使用相应工具链的方法。任何这些工具链都可以在 CMake 中使用;唯一的要求是它在普通文件系统中工作,并且不需要“沙盒”环境,例如 Scratchbox 项目。

一个易于使用的工具链,具有相对完整的目标环境,是嵌入式 Linux 开发工具包(http://www.denx.de/wiki/DULG/ELDK)。它支持 ARM、PowerPC 和 MIPS 作为目标平台。ELDK 可以从 ftp://ftp.sunet.se/pub/Linux/distributions/eldk/ 下载。最简单的方法是下载 ISO,挂载它们,然后安装它们。

mkdir mount-iso/
sudo mount -tiso9660 mips-2007-01-21.iso mount-iso/ -o loop
cd mount-iso/
./install -d /home/alex/eldk-mips/
...
Preparing...           ########################################### [100%]
   1:appWeb-mips_4KCle ########################################### [100%]
Done
ls /opt/eldk-mips/
bin  eldk_init  etc  mips_4KC  mips_4KCle  usr  var  version

ELDK(和其他工具链)可以安装在任何地方,无论是在主目录还是系统范围内,如果有多个用户使用它们。在这个示例中,工具链现在将位于 /home/alex/eldk-mips/usr/bin/ 中,目标环境位于 /home/alex/eldk-mips/mips_4KC/ 中。

现在已经安装了交叉编译工具链,必须设置 CMake 以使用它。如前所述,这是通过为 CMake 创建一个工具链文件来完成的。在这个示例中,工具链文件看起来像这样

# the name of the target operating system
set(CMAKE_SYSTEM_NAME Linux)

# which C and C++ compiler to use
set(CMAKE_C_COMPILER /home/alex/eldk-mips/usr/bin/mips_4KC-gcc)
set(CMAKE_CXX_COMPILER
    /home/alex/eldk-mips/usr/bin/mips_4KC-g++)

# location of the target environment
set(CMAKE_FIND_ROOT_PATH /home/alex/eldk-mips/mips_4KC
                          /home/alex/eldk-mips-extra-install )

# adjust the default behavior of the FIND_XXX() commands:
# search for headers and libraries in the target environment,
# search for programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

工具链文件可以位于任何位置,但最好将它们放在一个中心位置,以便可以在多个项目中重用它们。我们将此文件保存为 ~/Toolchains/Toolchain-eldk-mips4K.cmake。上面提到的变量在此处设置:CMAKE_SYSTEM_NAME、C/C++ 编译器以及 CMAKE_FIND_ROOT_PATH,以指定目标环境的库和头文件所在的路径。查找模式也已设置,以便仅在目标环境中搜索库和头文件,而仅在主机环境中搜索程序。现在我们将从第 2 章交叉编译 hello world 项目。

project(Hello)
add_executable(Hello Hello.c)

运行 CMake,这次告诉它使用上面的工具链文件

mkdir Hello-eldk-mips
cd Hello-eldk-mips
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-eldk-mips4K.cmake ..
make VERBOSE=1

这应该会给你一个可以在目标平台上运行的可执行文件。由于 VERBOSE=1 选项,您应该会看到使用了交叉编译器。现在我们将通过添加系统检查和安装规则来使示例更加复杂。我们将构建并安装一个名为 Tools 的共享库,然后构建链接到 Tools 库的 Hello 应用程序。

include(CheckIncludeFiles)
check_include_files(stdio.h HAVE_STDIO_H)

set(VERSION_MAJOR 2)
set(VERSION_MINOR 6)
set(VERSION_PATCH 0)

configure_file(config.h.in ${CMAKE_BINARY_DIR}/config.h)

add_library(Tools SHARED tools.cxx)
set_target_properties(Tools PROPERTIES
    VERSION ${VERSION_MAJOR}.${VERSION_MINOR}.${VERSION_PATCH}
    SOVERSION ${VERSION_MAJOR})

install(FILES tools.h DESTINATION include)
install(TARGETS Tools DESTINATION lib)

在正常的 CMakeLists.txt 中没有区别;交叉编译不需要任何特殊先决条件。CMakeLists.txt 检查头文件 stdio.h 是否可用,并设置 Tools 库的版本号。这些配置到 config.h 中,然后在 tools.cxx 中使用。版本号也用于设置 Tools 库的版本号。库和头文件分别安装到 ${CMAKE_INSTALL_PREFIX}/lib 和 ${CMAKE_INSTALL_PREFIX}/include 中。运行 CMake 会给出此结果

mkdir build-eldk-mips
cd build-eldk-mips
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-eldk-mips4K.cmake \
      -DCMAKE_INSTALL_PREFIX=~/eldk-mips-extra-install ..
-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /home/alex/eldk-mips/usr/bin/mips_4KC-gcc
-- Check for working C compiler:
   /home/alex/eldk-mips/usr/bin/mips_4KC-gcc -- works
-- Check size of void*
-- Check size of void* - done
-- Check for working CXX compiler: /home/alex/eldk-mips/usr/bin/mips_4KC-g++
-- Check for working CXX compiler:
   /home/alex/eldk-mips/usr/bin/mips_4KC-g++ -- works
-- Looking for include files HAVE_STDIO_H
-- Looking for include files HAVE_STDIO_H - found
-- Configuring done
-- Generating done
-- Build files have been written to: /home/alex/src/tests/Tools/build-mips
make install
Scanning dependencies of target Tools
[100%] Building CXX object CMakeFiles/Tools.dir/tools.o
Linking CXX shared library libTools.so
[100%] Built target Tools
Install the project...
-- Install configuration: ""
-- Installing /home/alex/eldk-mips-extra-install/include/tools.h
-- Installing /home/alex/eldk-mips-extra-install/lib/libTools.so

如上输出所示,CMake 检测到了正确的编译器,找到了目标平台的 stdio.h 头文件,并成功生成了 Makefile。make 命令被调用,然后成功地构建并安装了库到指定的安装目录。现在我们可以构建一个使用 Tools 库并执行一些系统检查的可执行文件。

project(HelloTools)

find_package(ZLIB REQUIRED)

find_library(TOOLS_LIBRARY Tools)
find_path(TOOLS_INCLUDE_DIR tools.h)

if(NOT TOOLS_LIBRARY OR NOT TOOLS_INCLUDE_DIR)
  message FATAL_ERROR "Tools library not found")
endif()

set(CMAKE_INCLUDE_CURRENT_DIR TRUE)
set(CMAKE_INCLUDE_DIRECTORIES_PROJECT_BEFORE TRUE)
include_directories("${TOOLS_INCLUDE_DIR}"
                    "${ZLIB_INCLUDE_DIR}")

add_executable(HelloTools main.cpp)
target_link_libraries(HelloTools ${TOOLS_LIBRARY}
                      ${ZLIB_LIBRARIES})
set_target_properties(HelloTools PROPERTIES
                      INSTALL_RPATH_USE_LINK_PATH TRUE)

install(TARGETS HelloTools DESTINATION bin)

构建过程与库的构建方式相同;需要使用工具链文件,然后它应该可以正常工作。

cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-eldk-mips4K.cmake \
      -DCMAKE_INSTALL_PREFIX=~/eldk-mips-extra-install ..
-- The C compiler identification is GNU
-- The CXX compiler identification is GNU
-- Check for working C compiler: /home/alex/denx-mips/usr/bin/mips_4KC-gcc
-- Check for working C compiler:
   /home/alex/denx-mips/usr/bin/mips_4KC-gcc -- works
-- Check size of void*
-- Check size of void* - done
-- Check for working CXX compiler: /home/alex/denx-mips/usr/bin/mips_4KC-g++
-- Check for working CXX compiler:
   /home/alex/denx-mips/usr/bin/mips_4KC-g++ -- works
-- Found ZLIB: /home/alex/denx-mips/mips_4KC/usr/lib/libz.so
-- Found Tools library: /home/alex/denx-mips-extra-install/lib/libTools.so
-- Configuring done
-- Generating done
-- Build files have been written to:
   /home/alex/src/tests/HelloTools/build-eldk-mips
make
[100%] Building CXX object CMakeFiles/HelloTools.dir/main.o
Linking CXX executable HelloTools
[100%] Built target HelloTools

显然,CMake 找到了正确的 zlib 和 libTools.so,它们在之前的步骤中已经被安装了。

为微控制器交叉编译

CMake 不仅可以用于交叉编译到具有操作系统的目标,也可以在开发中使用它,用于具有小型微控制器且根本没有操作系统的深度嵌入式设备。例如,我们将使用 Small Devices C Compiler (http://sdcc.sourceforge.net),它在 Windows、Linux 和 Mac OS X 下运行,支持 8 位和 16 位微控制器。为了驱动构建,我们将在 Windows 下使用 MS NMake。与之前一样,第一步是编写一个工具链文件,以便 CMake 了解目标平台。对于 sdcc,它应该看起来像这样。

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER "c:/Program Files/SDCC/bin/sdcc.exe")

对于没有操作系统的目标,系统名称应使用“Generic”,作为 CMAKE_SYSTEM_NAME。用于“Generic”的 CMake 平台文件不会设置任何特定功能。它假设目标平台不支持共享库,因此所有属性将取决于编译器和 CMAKE_SYSTEM_PROCESSOR。上面的工具链文件没有设置与 FIND 相关的变量。只要 CMake 命令中没有使用任何 find 命令,就可以这样做。在许多针对小型微控制器的项目中,情况就是这样。CMakeLists.txt 应该看起来像这样。

project(Blink C)

add_library(blink blink.c)

add_executable(hello main.c)
target_link_libraries(hello blink)

其他 CMakeLists.txt 文件没有重大差异。一个重要的一点是,使用 project 命令显式地启用语言“C”。如果没有这样做,CMake 也会尝试启用对 C++ 的支持,这将失败,因为 sdcc 只有对 C 的支持。运行 CMake 并构建项目应该可以像往常一样工作。

cmake -G"NMake Makefiles" \
      -DCMAKE_TOOLCHAIN_FILE=c:/Toolchains/Toolchain-sdcc.cmake ..
-- The C compiler identification is SDCC
-- Check for working C compiler: c:/program files/sdcc/bin/sdcc.exe
-- Check for working C compiler: c:/program files/sdcc/bin/sdcc.exe -- works
-- Check size of void*
-- Check size of void* - done
-- Configuring done
-- Generating done
-- Build files have been written to: C:/src/tests/blink/build

nmake
Microsoft (R) Program Maintenance Utility Version 7.10.3077
Copyright (C) Microsoft Corporation.  All rights reserved.

Scanning dependencies of target blink
[ 50%] Building C object CMakeFiles/blink.dir/blink.rel
Linking C static library blink.lib
[ 50%] Built target blink
Scanning dependencies of target hello
[100%] Building C object CMakeFiles/hello.dir/main.rel
Linking C executable hello.ihx
[100%] Built target hello

这是一个使用 NMake 和 sdcc 的简单示例,使用 sdcc 的默认设置。当然,也可能存在更复杂的项目布局。对于这类项目,最好设置一个安装目录,用于安装可重用库,以便在多个项目中更容易使用它们。通常需要为 sdcc 选择正确的目标平台;并非所有人都使用 i8051,它是 sdcc 的默认值。推荐的方式是通过设置 CMAKE_SYSTEM_PROCESSOR 来实现。

这将导致 CMake 搜索并加载平台文件 Platform/Generic-SDCC-C-${CMAKE_SYSTEM_PROCESSOR}.cmake。由于这种情况发生在加载 Platform/Generic-SDCC-C.cmake 之前,它可以用来设置特定目标硬件和项目的编译器和链接器标志。因此,需要一个稍微复杂一些的工具链文件。

get_filename_component(_ownDir
                       "${CMAKE_CURRENT_LIST_FILE}" PATH)
set(CMAKE_MODULE_PATH "${_ownDir}/Modules" ${CMAKE_MODULE_PATH})

set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER "c:/Program Files/SDCC/bin/sdcc.exe")
set(CMAKE_SYSTEM_PROCESSOR "Test_DS80C400_Rev_1")

# here is the target environment located
set(CMAKE_FIND_ROOT_PATH  "c:/Program Files/SDCC"
                          "c:/ds80c400-install" )

# adjust the default behavior of the FIND_XXX() commands:
# search for headers and libraries in the target environment
# search for programs in the host environment
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

该工具链文件包含一些新的设置;它也是你可能需要的最复杂的工具链文件。 CMAKE_SYSTEM_PROCESSOR 设置为 Test_DS80C400_Rev_1,这是特定目标硬件的标识符。它的效果是 CMake 将尝试加载 Platform/Generic-SDCC-C-Test_DS80C400_Rev_1.cmake。由于该文件不存在于 CMake 系统模块目录中,因此必须调整 CMake 变量 CMAKE_MODULE_PATH 以便找到该文件。如果该工具链文件保存到 c:/Toolchains/sdcc-ds400.cmake,则特定硬件文件应该保存到 c:/Toolchains/Modules/Platform/。下面是一个示例。

set(CMAKE_C_FLAGS_INIT "-mds390 --use-accelerator")
set(CMAKE_EXE_LINKER_FLAGS_INIT "")

这将选择 DS80C390 作为目标平台,并将 –use-accelerator 参数添加到默认编译标志。在本例中,使用的是“NMake Makefiles”生成器。同样,例如,可以为 MinGW 中的 GNU make 使用“MinGW Makefiles”生成器,或者可以使用其他 Windows 版本的 GNU make。至少需要 3.78 版本,或者在 UNIX 下使用“Unix Makefiles”生成器。此外,还可以使用任何基于 Makefile 的 IDE 项目生成器;例如,Eclipse、CodeBlocks 或 KDevelop3 生成器。

交叉编译复杂项目 - VTK

构建一个复杂项目是一个多步骤的过程。在这里,“复杂”意味着该项目使用运行可执行文件的测试,并且它构建了在构建后期用于生成代码(或类似内容)的可执行文件。VTK 就是这样的一个项目,它使用多个 try_run 测试,并创建多个代码生成器。在项目上运行 CMake 时,每个 try_run 命令都会产生一条错误消息;最后,将在构建目录中生成一个 TryRunResults.cmake 文件。你需要遍历该文件的所有条目并填写相应的值。如果你不确定正确的结果,也可以尝试在保存二进制文件的真实目标平台上执行测试二进制文件。

VTK 包含多个代码生成器,其中一个是 ProcessShader。这些代码生成器是使用 add_executable 添加的; get_target_property(LOCATION) 用于获取生成的二进制文件的位置,这些位置然后在 add_custom_commandadd_custom_target 命令中使用。由于交叉编译的可执行文件不能在构建期间执行,因此 add_executable 调用被 if (NOT CMAKE_CROSSCOMPILING) 命令包围,并且可执行目标使用 add_executable 命令(使用 IMPORTED 选项)导入项目中。这些导入语句位于 VTKCompileToolsConfig.cmake 文件中,不需要手动创建,而是由 VTK 的原生构建创建的。

为了交叉编译 VTK,你需要

  • 安装一个工具链并为 CMake 创建一个工具链文件。

  • 为构建主机原生构建 VTK。

  • 为目标平台运行 CMake。

  • 完成 TryRunResults.cmake

  • 使用原生构建中的 VTKCompileToolsConfig.cmake 文件。

  • 构建。

所以首先,使用标准过程为构建主机构建一个原生的 VTK。

cd VTK
mkdir build-native; cd build-native
ccmake ..
make

确保使用 ccmake 启用了所有必需的选项;例如,如果你需要为目标平台进行 Python 封装,则必须在 build-native/ 中启用 Python 封装。构建完成后,将在 build-native/ 中生成一个 VTKCompileToolsConfig.cmake 文件。如果成功,我们可以继续为目标平台交叉编译项目,在本例中,我们为 IBM BlueGene 超级计算机交叉编译。

cd VTK
mkdir build-bgl-gcc
cd build-bgl-gcc
cmake -DCMAKE_TOOLCHAIN_FILE=~/Toolchains/Toolchain-BlueGeneL-gcc.cmake \
      -DVTKCompileTools_DIR=~/VTK/build-native/ ..

每个 try_run 都会以错误消息结束,同时会生成一个 TryRunResults.cmake file 文件,您需要根据上述描述完成该文件。 您应该将该文件保存到安全的位置,否则它将在下次运行 CMake 时被覆盖。

cp TryRunResults.cmake ../TryRunResults-VTK-BlueGeneL-gcc.cmake
ccmake -C ../TryRunResults-VTK-BlueGeneL-gcc.cmake .
...
make

在第二次运行 ccmake 时,所有其他参数都可以跳过,因为它们现在已在缓存中。 可以将 CMake 指向包含 CMakeCache.txt 的构建目录,这样 CMake 会识别出这是构建目录。

一些技巧和窍门

处理 try_run 测试

为了使您的项目更容易进行交叉编译,请尽量避免使用 try_run 测试,并使用其他方法来代替测试。 如果您无法避免使用 try_run 测试,请尽量只使用运行的退出代码,而不是进程的输出。 这样,在交叉编译时,就不需要为 try_run 测试设置退出代码和标准输出和标准错误变量。 这允许省略 try_runOUTPUT_VARIABLERUN_OUTPUT_VARIABLE 选项。

如果您已经完成了这些操作,并为目标平台创建并完成了正确的 TryRunResults.cmake 文件,您可能需要考虑将此文件添加到项目的源代码中,以便其他人可以重复使用。 这些文件是针对每个目标、每个工具链的。

目标平台和工具链问题

如果您的工具链无法在没有特殊参数(例如链接器脚本文件或内存布局文件)的情况下构建一个简单的程序,那么 CMake 最初进行的测试将失败。 为了使其正常工作,CMake 模块 CMakeForceCompiler 提供了以下宏

CMAKE_FORCE_SYSTEM(name version processor),
CMAKE_FORCE_C_COMPILER(compiler compiler_id sizeof_void_p)
CMAKE_FORCE_CXX_COMPILER(compiler compiler_id).

这些宏可以在工具链文件中使用,以便预设所需的变量并避免 CMake 测试。

UNIX 下的 RPATH 处理

对于原生构建,CMake 默认情况下使用 RPATH 构建可执行文件和库。 在构建树中,RPATH 被设置为使可执行文件可以在构建树中运行;也就是说,RPATH 指向构建树。 安装项目时,CMake 会再次链接可执行文件,这次使用安装树的 RPATH,默认情况下该 RPATH 为空。

交叉编译时,您可能希望以不同的方式设置 RPATH 处理。 由于可执行文件无法在构建主机上运行,因此从一开始就使用安装 RPATH 构建它更有意义。 有几个 CMake 变量和目标属性可以用来调整 RPATH 处理。

set(CMAKE_BUILD_WITH_INSTALL_RPATH TRUE)
set(CMAKE_INSTALL_RPATH "<whatever you need>")

使用这两个设置,目标将使用安装 RPATH 而不是构建 RPATH 构建,这避免了在安装时再次链接它们。 如果您的项目不需要 RPATH 支持,您不需要设置 CMAKE_INSTALL_RPATH;它默认情况下为空。

CMAKE_INSTALL_RPATH_USE_LINK_PATH 设置为 TRUE 对原生构建很有用,因为它会自动收集目标链接的所有库的 RPATH。 对于交叉编译,它应该保持默认设置 FALSE,因为在目标上,自动生成的 RPATH 在大多数情况下都是错误的;它可能具有与构建主机不同的文件系统布局。