前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
13 用 CMake 交叉编译
交叉编译一个软件意味着该软件是在一个系统上构建,但打算在另一个系统上运行。用于构建软件的系统将被称为“构建主机”,为其构建软件的系统将被称为“目标系统”或“目标平台”。目标系统通常运行不同的操作系统(或根本不运行操作系统)和/或运行在不同的硬件上。一个典型的用例是在嵌入式设备(如网络交换机、移动电话或引擎控制单元)中开发的软件。在这些情况下,目标平台没有或不能运行所需的软件开发环境。
CMake 完全支持交叉编译,从 Linux 到 Windows 的交叉编译,从超级计算机的交叉编译到没有操作系统(OS)的小型嵌入式设备的交叉编译。
交叉编译对 CMake 有几个影响:
- CMake 无法自动检测目标平台。
- CMake 无法在默认系统目录中找到库和头文件。
- 交叉编译期间生成的可执行文件不能被执行。
交叉编译支持并不意味着所有基于 CMake 的项目都可以神奇地进行开箱即用的交叉编译(有些是这样),而是 CMake 分离了关于构建平台和目标平台的信息,并为用户提供了解决交叉编译问题的机制,而不需要额外的要求,比如运行虚拟机等。
为了支持特定软件项目的交叉编译,必须通过工具链文件将目标平台的信息告知 CMake 。 CMakeLists.txt 可能必须进行调整,因为我们意识到构建平台可能与目标平台具有不同的属性,并且它必须处理编译后的可执行文件试图在构建主机上执行的情况。
13.1 工具链文件
为了使用 CMake 进行交叉编译,必须创建一个描述目标平台的 CMake 文件,称为“工具链文件”,该文件告诉 CMake 它需要知道的关于目标平台的所有信息。下面是一个在 Linux 上使用 Windows MinGW 交叉编译器的例子,内容将在后面逐行解释。
# 目标操作系统名称
set(CMAKE_SYSTEM_NAME Windows)
# C 和 C++ 使用哪个编译器
set(CMAKE_C_COMPILER i586-mingw32msvc-gcc)
set(CMAKE_CXX_COMPILER i586-mingw32msvc-g++)
# 目标环境所在位置
set(CMAKE_FIND_ROOT_PATH /usr/i586-mingw32msvc
/home/alex/mingw-install)
# 调整 FIND_XXX() 命令的默认行为:在主机环境中查找程序
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
# 在目标环境中寻找头文件和库
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 变量:
- 这个变量是必须的。它设置目标系统的名称,即,如果 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 中用于一个目的,加载 C M A K E S Y S T E M N A M E − C O M P I L E R I D − {CMAKE_SYSTEM_NAME}- COMPILER_ID- CMAKESYSTEMNAME−COMPILERID−{CMAKE_SYSTEM_PROCESSOR}.cmake 文件。该文件可用于修改目标的编译器标志等设置。只有在使用交叉编译器,其中每个目标都需要特殊的构建设置时,才需要设置此变量。该值可以自由选择,例如,它可以是 i386 、 IntelPXA255 或 MyControlBoardRev42 。
在 CMake 代码中, CMAKE_SYSTEM_XXX 变量总是描述目标平台。对于短的 WIN32 、 UNIX 和 APPLE 变量也是如此。这些变量可用于测试目标的属性。如果有必要测试构建主机系统,那么有一组相应的变量: CMAKE_HOST_SYSTEM , CMAKE_HOST_SYSTEM_NAME , CMAKE_HOST_SYSTEM_VERSION , CMAKE_HOST_SYSTEM_PROCESSOR 以及缩写形式: CMAKE_HOST_WIN32 , CMAKE_HOST_UNIX 和 CMAKE_HOST_APPLE 。
因为 CMake 无法猜测目标系统,所以它无法猜测应该使用哪个编译器。通过设置以下变量,可以定义在目标系统中使用什么编译器。
- 这将以完整路径或只是文件名来指定 C 编译器可执行文件。如果指定了完整路径,那么在搜索 C++ 编译器和其他工具( binutils 、 linker 等)时,该路径将是首选路径。如果编译器是一个带有前缀名称的 GNU 交叉编译器(例如" arm-elf-gcc “), CMake 将检测到这一点并自动找到相应的 C++ 编译器(例如” arm-elf-c++ ")。还可以通过 CC 环境变量设置编译器。直接在工具链文件中设置 CMAKE_C_COMPILER 的好处是,关于目标系统的信息完全包含在这个文件中,它不依赖于环境变量。
- 这将以完整路径或只是文件名来指定 C++ 编译器可执行文件。它的处理方式与 CMAKE_C_COMPILER 相同。如果工具链是一个 GNU 工具链,它应该只(需要)设置 CMAKE_C_COMPILER , CMake 应该会自动找到相应的 C++ 编译器。与 CMAKE_C_COMPILER 一样,对于 C++ 编译器可以通过 CXX 环境变量来设置。
13.2 查找外部库、程序和其他文件
大多数重要的项目都使用外部库或工具。为此, CMake 提供了 find_program 、 find_library 、 find_file 、 find_path 和 find_package 命令。它们在常见位置的文件系统中搜索这些文件并返回结果。 find_package 有点不同,因为它实际上并不自己搜索,而是执行 Find<*>.cmake 模块,它们通常依次调用 find_program 、 find_library 、 find_file 和 find_path 命令。
交叉编译时,这些命令变得更加复杂。例如,当在 Linux 系统上交叉编译到 Windows 时,获取 /usr/lib/libjpeg.so 作为命令 find_package(JPEG) 的结果将是无用的,因为这将是主机系统而不是目标系统的 JPEG 库。在某些情况下,你希望找到针对目标平台的文件;在其他情况下,你将希望为构建主机查找文件。下面的变量为你提供了更改 CMake 中典型 find 命令工作方式的灵活性的方法,以便你可以根据需要查找构建主机和目标文件。
find_program 命令通常用于查找将在构建期间执行的程序,因此它仍然应该在主机文件系统中搜索,而不是在目标平台的环境中搜索。 find_library 通常用于查找随后用于链接的库,因此该命令应该只在目标环境中搜索。对于 find_file 和 find_path ,就不那么明显了。在许多情况下,它们用于搜索头文件,因此默认情况下,它们应该只在目标环境中搜索。可以设置以下 CMake 变量来调整交叉编译的 find 命令的行为。
-
这是包含目标环境的目录列表。这里列出的每个目录都将作为每个 find 命令的每个搜索目录的前缀。假设你的目标环境安装在 /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.so 。
默认情况下, CMAKE_FIND_ROOT_PATH 为空。如果设置了,首先将搜索以 CMAKE_FIND_ROOT_PATH 中给出的路径为前缀的目录,然后将搜索相同目录的无前缀版本。
通过设置这个变量,你基本上是在 CMake 中的所有 find 命令中添加了一组新的搜索前缀,但是对于某些 find 命令,你可能不希望搜索目标或主机目录。通过在调用时传入以下三个选项: NO_CMAKE_FIND_ROOT_PATH 、 ONLY_CMAKE_FIND_ROOT_PATH 或 CMAKE_FIND_ROOT_PATH_BOTH ,可以控制每个查找命令调用的工作方式。你还可以使用以下三个变量来控制 find 命令的工作方式。
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_process 或 add_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_path 和 find_file 。在大多数情况下,这用于查找包含目录,因此应该搜索目标环境。在大多数情况下,应该将其设置为 ONLY 。如果你还需要在构建主机的文件系统中查找文件(例如,将在构建期间处理的一些数据文件)。你可能需要使用 NO_CMAKE_FIND_ROOT_PATH 、 ONLY_CMAKE_FIND_ROOT_PATH 和 CMAKE_FIND_ROOT_PATH_BOTH 选项来调整这些 find_path 和 find_file 调用的行为。
有了如上所述的工具链文件, CMake 现在知道如何处理目标平台和交叉编译器。现在我们应该能够为目标平台构建软件。对于复杂的项目,必须考虑更多的问题。
13.3 系统检查
大多数可移植软件项目都有一组系统检查测试,用于确定(目标)系统的属性。使用 CMake 检查系统特性的最简单方法是测试变量。为此,CMake提供了变量 UNIX 、 WIN32 和 APPLE 。交叉编译时,这些变量应用于目标平台,为了测试构建主机平台,有相应的变量 CMAKE_HOST_UNIX 、 CMAKE_HOST_WIN32 和 CMAKE_HOST_APPLE 。
如果这个粒度太粗,则可以测试针对目标系统的变量 CMAKE_SYSTEM_NAME 、 CMAKE_SYSTEM 、 CMAKE_SYSTEM_VERSION 和 CMAKE_SYSTEM_PROCESSOR ,以及对应的针对构建主机的变量 CMAKE_HOST_SYSTEM_NAME 、 CMAKE_HOST_SYSTEM 、 CMAKE_HOST_SYSTEM_VERSION 和 CMAKE_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()
13.3.1 使用编译检查
在 CMake 中,有诸如 check_include_files 和 check_c_source_runs 这样的宏,它们用于测试平台的属性。大多数宏在内部使用 try_compile 或 try_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_VARIABLE 或 OUTPUT_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-<变量的名称> ,例如在本例中是 cmTryCompileExec-SHARED_LIBRARY_PATH_TYPE 。然后,你可以尝试在实际的目标平台上手动运行此可执行文件并检查结果。
一旦有了这些结果,就必须将它们放入 CMake 缓存中。这可以通过使用 ccmake 或 cmake-gui 来完成,并可以直接在缓存中编辑变量。如果 CMakeCache.txt 被删除,则不可能在另一个构建目录中重用这些更改。
推荐的方法是使用 CMake 创建的 TryRunResults.cmake 文件。你应该将它复制到一个安全的位置(即,如果构建目录被删除,它将不会被删除的位置),并给它一个有用的名称,例如 TryRunResults-MyProject-eldk-ppc.cmake 。必须编辑此文件的内容,以便 set 命令将所需的变量设置为目标系统的适当值。这个文件可以使用 cmake 的 -C 选项来预加载 CMake 缓存:
src/build/ $ cmake -C ~/TryRunResults-MyProject-eldk-ppc.cmake .
不必再次使用其他 CMake 选项,因为它们现在位于缓存中。这样你就可以在多个构建树中使用 MyProjectTryRunResults-eldk-ppc.cmake ,它可以与你的项目一起分发,以便其他用户更容易交叉编译它。
13.4 运行项目中构建的可执行文件
在某些情况下,有必要在构建过程中调用之前在同一构建中构建的可执行文件,这通常是代码生成器和类似工具的情况。这在交叉编译时不起作用,因为可执行程序是为目标平台构建的,不能在构建主机上运行(不使用虚拟机、兼容性层、模拟器等)。在 CMake 中,这些程序是使用 add_executable 创建的,并使用 add_custom_command 或 add_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
13.5 交叉编译 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 创建一个工具链文件来完成的。在本例中,工具链文件如下所示:
# 目标操作系统的名称
set(CMAKE_SYSTEM_NAME Linux)
# 使用哪个 C 和 C++ 编译器
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++)
# 目标环境的位置
set(CMAKE_FIND_ROOT_PATH /home/alex/eldk-mips/mips_4KC
/home/alex/eldk-mips-extra-install )
# 调整 FIND_XXX() 命令的默认行为:
# 在目标环境中寻找头文件和库,
# 在主机环境中寻找程序
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 ,以指定目标环境的库和文件的位置。还设置了 find 模式,以便只在目标环境中搜索库和头文件,而只在宿主环境中搜索程序。现在我们将交叉编译第 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 是否可用,并设置工具库的版本号。它们被配置到 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 头文件,并成功生成 Makefiles 。调用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 。
13.6 用于微控制器的交叉编译
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")
CMAKE_SYSTEM_NAME 应该被设置为没有操作系统的目标的系统名称“ Generic ”。“ 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 文件没有大的什么的区别。重要的一点是,语言“ C ”是使用 project 命令显式启用的。如果没有这样做, 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 之前,它可以用来为特定的目标硬件和项目设置编译器和链接器标志。因此,需要稍微复杂一点的工具链文件:
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")
# 这里是目标环境位置
set(CMAKE_FIND_ROOT_PATH "c:/Program Files/SDCC"
"c:/ds80c400-install" )
# 调整 FIND_XXX() 命令的默认行为:
# 在目标环境中寻找头文件和库,
# 在主机环境中寻找程序
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 Makefiles ”生成器可以用于 MinGW 的 GNU make ,或其他 Windows 版本的 GNU make ,都是可用的。至少需要 3.78 版本,或者 Unix 下的“ Unix Makefiles ”生成器。此外,可以使用任何基于 makefile 的 IDE 项目生成器,例如 Eclipse 、 CodeBlocks 或 KDevelop3 生成器。
13.7 交叉编译一个复杂项目 - VTK
构建一个复杂的项目是一个多步骤的过程。在这种情况下,复杂意味着项目使用运行可执行程序的测试,并构建稍后在构建中用于生成代码(或类似的东西)的可执行程序。 VTK ( Visualization Toolkit )就是这样一个项目,它使用几个 try_run 测试并创建几个代码生成器。当在项目上运行 CMake 时,每个 try_run 命令都会产生一条错误消息;最后在构建目录中会有一个 TryRunResults.cmake 的文件。你需要遍历该文件的所有项并填写适当的值。如果你不确定正确的结果,你还可以尝试在真正的目标平台上执行测试二进制文件,它们保存在二进制目录中。
VTK 包含几个代码生成器,其中一个是 ProcessShader 。这些代码生成器使用 add_executable 添加。 get_target_property(LOCATION) 可以用于获取最终二进制文件的位置,然后在 add_custom_command 或 add_custom_target 命令中使用这些二进制文件。因为在构建过程中不能执行交叉编译的可执行文件,所以 add_executable 调用被 if (NOT CMAKE_CROSSCOMPILING) 命令包围,并且使用带 IMPORTED 选项的 add_executable 命令将可执行目标导入到项目中。这些导入语句在 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 文件的错误消息结束。你应该将该文件保存到一个安全的位置,否则它将在下次运行 CMake 时被覆盖。
cp TryRunResults.cmake ../TryRunResults-VTK-BlueGeneL-gcc.cmake
ccmake -C ../TryRunResults-VTK-BlueGeneL-gcc.cmake .
...
make
在第二次运行 ccmake 时,可以跳过所有其他参数,因为它们现在位于缓存中。可以将 CMake 指向包含 CMakeCache.txt 的构建目录,这样 CMake 会确定这是构建目录。
13.8 一些建议和技巧
13.8.1 处理 try_run 测试
为了使交叉编译项目更容易,请尽量避免使用 try_run 测试,而使用其他方法进行测试。如果无法避免 try_run 测试,请尝试仅使用运行的退出码,而不要使用进程的输出。这样就不需要在交叉编译时为 try_run 测试同时设置退出代码和 stdout 和 stderr 变量。这会允许省略 try_run 的 OUTPUT_VARIABLE 或 RUN_OUTPUT_VARIABLE 选项。
如果你已经这样做了,创建并完成了对于目标平台的一个正确的 TryRunResults.cmake 文件,你可以考虑将该文件添加到项目的源代码中,以便其他人可以重用它。这些文件是针对每个目标、每个工具链的。
13.8.2 目标平台和工具链问题
如果你的工具链不能在没有特殊参数的情况下构建一个简单的程序,例如链接器脚本文件或内存布局文件, CMake 最初执行的测试将失败。为了使它工作, CMake 模块的 CMakeForceeCompiler提供了以下宏:
CMAKE_FORCE_SYSTEM(name version processor),
CMAKE_FORCE_C_COMPILER(compiler compiler_id sizeof_void_p)
CMAKE_FORCE_CXX_COMPILER(compiler compiler_id).
可以在工具链文件中使用这些宏,这样就可以预先设置所需的变量并避免 CMake 测试。
13.8.3 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 在大多数情况下是错误的,它可能具有与构建主机不同的文件系统布局。