第21章:工具链与交叉编译

考虑构建软件和相关工具的过程时,开发人员通常会考虑编译器和链接器。这些是开发人员接触到的主要工具,但是还有许多其他工具、库和文件也对这个过程有贡献。简单来说,这些工具和其他文件统称为工具链。

对于桌面或传统服务器的应用程序,通常不需要太深入地考虑工具链。大多数情况下,决定使用主流平台工具链的哪个版本是非常复杂的。CMake通常不需要太多帮助就能找到工具链,开发人员可以继续编写软件。然而,对于移动或嵌入式开发,情况就不同了,工具链通常需要由开发人员以某种方式指定。这像指定不同的目标系统名称那样简单,也可以像指定单个工具和目标根文件系统的路径那样复杂。还可能需要设置特殊的标志,使工具生成支持芯片组、具有所需性能特征等的二进制文件。

选择了工具链,CMake就会在内部执行相当多的处理,测试工具链,确定它所支持的特性,设置各种属性和变量等等。即使使用默认工具链的构建也是如此,不仅仅是交叉编译的构建。这些测试的结果可以在CMake的输出中看到,第一次运行一个给定的构建目录,macOS的例子如下所示(为简便起见,显示的C和CXX编译器路径已经折叠):

-- The C compiler identification is AppleClang 9.0.0.9000039
-- The CXX compiler identification is AppleClang 9.0.0.9000039
-- Check for working C compiler: /Applications/Xcode.app/.../cc
-- Check for working C compiler: /Applications/Xcode.app/.../cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /Applications/Xcode.app/.../c++
-- Check for working CXX compiler: /Applications/Xcode.app/.../c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done

通常在调用第一个project()命令,并缓存工具链测试结果时进行大量处理。enable_language()命令在启用前,未启用的语言时也会触发这样的处理,另一个project()调用添加以前未启用的语言也会触发这样的处理。当启用一种语言,它的缓存信息则总会使用,而不是重新测试工具链,甚至在随后的CMake运行时也是如此。这至少有两个结果:

  • 构建目录一旦配置了特定的工具链,就不能(安全地)更改它。某些情况下,CMake可能会检测到工具链已经修改,并丢弃之前的结果,但这只会丢弃缓存的与工具链直接相关的细节。基于CMake所知道的缓存工具链信息之外的任何其他缓存都不会重置。因此,更改工具链之前,应该完全清除构建目录(仅仅删除CMakeCache.txt文件可能不够,其他细节可能缓存在不同的位置)。

  • 不同的工具链不能混合在一个项目内。CMake从根本上认为一个项目始终使用单一的工具链。为了使用多个工具链,必须构造项目来执行外部子构建的部分(在27.1节“外部项目”和28.1节“超级构建结构”中进行讨论)。

21.1. 工具链文件

如果默认的工具链不合适,推荐使用工具链文件来指定工具链的信息。这是一个普通的CMake脚本,通常包含很多set(…)命令。这些CMake变量用来描述目标平台的变量、各种工具链组件的位置等等。工具链文件的名称通过特殊的缓存变量CMAKE_TOOLCHAIN_FILE传递给CMake,如下所示:

cmake -DCMAKE_TOOLCHAIN_FILE=myToolchain.cmake path/to/source

可以使用绝对路径,也可以使用相对路径,如上面的例子中所示,CMake首先查找与构建目录相关的路径,如果没有找到,则查找与源目录相关的路径。这个工具链文件必须在第一次运行CMake的构建目录时指定,不能在之后添加或更改指向不同的工具链。因为变量本身是缓存的,所以没有必要为后续的CMake运行重新指定它。

每次调用project()命令都会读取工具链文件,这通常是一个实现细节,开发人员不需要考虑太多,但可能会导致一些微妙的意外行为。如果工具链文件设置或修改项目本身操纵的变量,或者工具链文件错误地假定为整个项目只处理一次,那么项目开发时使用的project()命令,会损坏的工具链的设置,或没有让变量进行任何改变。因此,开发人员应该确保工具链是最小化,只设置他们需要做的事情,对项目要做的事情尽可能少做假设。理想情况下,工具链文件应该完全从项目中解耦,甚至应该在不同的项目之间重用,因为它们应该只描述工具链,而不是如何与项目进行交互。

工具链文件的内容可以不同,但总体上需要做如下的几个事情:

  • 描述目标系统的基本信息。

  • 提供工具的路径(通常是编译器的路径)。

  • 设置工具的默认标志(通常只针对编译器,也可能是链接器)。

  • 交叉编译的情况下设置目标平台文件系统根目录的位置。

工具链文件中包含其他逻辑很常见,特别是各种find_…()命令。虽然在某些情况下,这样的逻辑是合适的,但也有这样的论点,即在大多数情况下,这样的逻辑应该是项目的一部分。只有项目知道它要寻找什么,所以工具链不应该假设项目想要做什么。

21.2. 定义目标系统

描述目标系统信息的基本变量:

  • CMAKE_SYSTEM_NAME

  • CMAKE_SYSTEM_PROCESSOR

  • CMAKE_SYSTEM_VERSION

其中,CMAKE_SYSTEM_NAME是最重要的。它定义了目标平台的类型,而CMAKE_HOST_SYSTEM_NAME则定义了执行构建的平台。CMake本身总是设置CMAKE_HOST_SYSTEM_NAME,而CMAKE_SYSTEM_NAME可以(通常是)由工具链文件设置。如果CMake能够直接在目标平台上运行,可以将CMAKE_SYSTEM_NAME设置为CMAKE_HOST_SYSTEM_NAME。因此,典型的值包括Linux、Windows、QNX、Android或Darwin,但对于某些情况(例如:嵌入式设备),可以使用Generic系统名代替。某些情况下,平台名称也有一些变体,比如WindowsStore和WindowsPhone。如果在工具链文件中设置了CMAKE_SYSTEM_NAME,那么CMake也会将CMAKE_CROSSCOMPILING变量设置为true,即使它的值与CMAKE_HOST_SYSTEM_NAME相同。如果没有设置CMAKE_SYSTEM_NAME,它将被赋予与自动检测的CMAKE_HOST_SYSTEM_NAME相同的值。

CMAKE_SYSTEM_PROCESSOR旨在描述目标平台的硬件架构。如果未指定,将赋予与CMAKE_HOST_SYSTEM_PROCESSOR相同的值,该值由CMake自动填充。交叉编译场景中,或者在相同系统类型的64位主机上为32位平台构建时,这将导致CMAKE_SYSTEM_PROCESSOR设置不正确。因此,建议设置CMAKE_SYSTEM_PROCESSOR,如果架构与主机不匹配,那么这个项目就构建的有问题。基于错误的CMAKE_SYSTEM_PROCESSOR值,错误决策可能会导致难以检测或诊断的问题。

根据CMAKE_SYSTEM_NAME的设置,CMAKE_SYSTEM_VERSION变量有不同的含义。例如,系统名为WindowsStore、WindowsPhone或WindowsCE,系统版本将用于定义使用哪个Windows SDK。值可能是8.1或10.0,或者非常特定的版本,如10.0.10240.0。如果CMAKE_SYSTEM_NAME设置为Android,那么CMAKE_SYSTEM_VERSION通常会解释为默认的Android API版本,并且必须是正整数。对于其他系统名,经常会看到CMAKE_SYSTEM_VERSION设置为1,或者根本没有设置。CMake文档的工具链部分提供了CMAKE_SYSTEM_VERSION不同用法的示例,但是变量的含义和允许值集并不总是明确定义的。因此,建议项目在实现时,如若要有依赖于CMAKE_SYSTEM_VERSION的逻辑时,需要谨慎。

通常,这三个CMAKESYSTEM…变量完全描述了目标系统信息,但也有例外:

  • 所有的Apple平台都使用Darwin作为CMAKE_SYSTEM_NAME,甚至iOS、tvOS或watchOS也是如此。CMAKE_SYSTEM_PROCESSOR和CMAKE_SYSTEM_VERSION对于苹果平台也没有特别的意义,并且通常未设置。通过使用不同的变量CMAKE_OSX_SYSROOT来指定目标系统,该变量选择要用于构建的基本SDK。根据选择的SDK确定目标设备,开发人员可以在构建时选择设备或模拟器。这是一个复杂的主题,将在22.5节“构建设置”中详细介绍。

  • CMAKE_SYSTEM_PROCESSOR变量通常针对Android平台时不设置。这将在下面的21.6.3节“Android”中进一步讨论。

21.3. 选择工具

构建中使用的所有工具中,从开发人员的角度来看,编译器可能是最重要的。编译器的路径由CMAKE_<LANG>_COMPILER变量控制,可以在工具链文件或命令行中设置该变量来手动控制所使用的编译器,也可以忽略该变量允许CMake自动选择一个。如果可执行文件的名称是手动提供的,而没有路径,CMake将使用find_program()搜索它(在23.3节“查找程序”中介绍)。如果提供了编译器的完整路径,则将直接使用它。如果没有手动指定编译器,CMake将根据目标平台和生成器的内部默认设置选择一个编译器。

大多数语言还支持通过指定环境变量来设置编译器,而不是必须设置CMAKE_<LANG>_COMPILER。它们通常遵循一些常见的约定,比如C编译器使用CC, C++编译器使用CXX, Fortran编译器使用FC等等。这些环境变量只有在CMake第一次在构建目录中运行时才会起作用,并且只有在相应的CMAKE_<LANG>_COMPILER变量没有使用工具链文件或CMake命令行设置时才会起作用。

当有了编译器,CMake就可以识别并尝试确定它的版本。该编译器信息分别通过CMAKE_<LANG>_COMPILER_IDCMAKE_<LANG>_COMPILER_VERSION变量提供。编译器ID是一个简短的字符串,用于区分不同的编译器,通用的值是GNU、Clang、AppleClang、MSVC、Intel等等。CMake文档为CMAKE_<LANG>_COMPILER_ID提供了支持ID的完整列表。如果能够确定编译器的版本,它通常会使用major.minor.patch.tweak(不是所有的版本组件都需要出现)。

除了CMAKE_<LANG>_COMPILER_IDCMAKE_<LANG>_COMPILER_VERSION外,还支持类似的不带CMAKE_ 前缀部分的生成器表达式。变量或生成器表达式都可以仅针对特定编译器或编译器版本,有条件地添加内容。例如,GCC 7引入新选项-fcode-hoisting,下面展示了在C++编译时的两种添加方法:

add_library(foo ...)

# Conditionally add -fcode-hoisting option using variables
if(CXX_COMPILER_ID STREQUAL GNU AND
 NOT CXX_COMPILER_VERSION VERSION_LESS 7)
 target_compile_options(foo PRIVATE -fcode-hoisting)
endif()

# Same thing using generator expressions instead
target_compile_options(foo PRIVATE
 $<$<AND:$<CXX_COMPILER_ID:GNU>,
 $<VERSION_GREATER_EQUAL:$<CXX_COMPILER_VERSION>,7>>:-fcode-hoisting>
)

编译器ID是识别使用编译器最健壮的方法。CMake 3.0之前,Apple Clang编译器和Clang一样,都有编译器ID Clang。从CMake 3.0开始,苹果的编译器编译器ID AppleClang,这样就可以与Clang区别开来。添加策略CMP0025是为了允许那些需要使用旧的行为的项目。

当确定了编译器的路径,CMake就能够为编译器和链接器找出一组默认标志。这些在项目中是可见的,如CMAKE_<LANG>_FLAGS, CMAKE_<LANG>_FLAGS_<CONFIG>, CMAKE_<TARGETTYPE>_LINKER_FLAGSCMAKE_<TARGETTYPE>_LINKER_FLAGS_<CONFIG>变量,这些在14.3节“编译器和链接器变量”中已经介绍过了。开发人员可以使用相同名称,但附加了_INIT的变量,将自己项目使用的标志添加到这些变量的默认值集中。这些_INIT变量只用于设置初始值的默认值。

一个常见的错误是在工具链文件中设置非…INIT变量(例如,设置CMAKE_<LANG>_FLAGS而不是CMAKE_<LANG>_FLAGS_INIT)。这将导致丢弃或隐藏开发人员对缓存中的这些变量所做的任何更改。因为在每次project()调用时也会重新读取工具链文件,所以可以会丢弃项目本身对这些变量所做的更改。使用…_INIT设置变量,而不是开始只用初始默认值,之后再用非…_INIT的变量对其进行修改的方式。

举个例子,考虑一个工具链文件,开发人员可能会使用特殊的编译器标记来设置构建,以便进行调试(这是一种有用的方法,可以跨多个项目重用一些复杂的开发人员专用逻辑,而不必将其添加到每个项目中)。以下选择了GNU编译器并启用警告标志:

set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)
set(extraOpts "-Wall -Wextra")
set(CMAKE_C_FLAGS_DEBUG_INIT ${extraOpts})
set(CMAKE_CXX_FLAGS_DEBUG_INIT ${extraOpts})

不幸的是,CMake在将开发人员指定的…_INIT选项与它通常提供的默认选项组合的方式上存在一些不同。大多数情况下,CMake会给…INIT变量指定的选项添加更多选项,但是对于一些平台/编译器组合(特别是较老的或不太常用的),开发人员指定的…_INIT值可以丢弃。这源于这些变量的历史,过去只用于内部,总是单方面设置…_INIT值。从CMake 3.7开始,…_INIT变量设计为通用变量,对于常用的编译器,行为修改为追加而不是替换。对于非常旧的或不再维护的编译器来说,行为将保持不变。

有些编译器更多地相适编译器的驱动程序,期望命令行参数来指定要编译的目标平台/架构。Clang和QNX qcc就是这种编译器。对于那些CMake认为需要这样参数的编译器,可以在工具链文件中设置 CMAKE_<LANG>_COMPILER_TARGET变量来指定目标。在支持的情况下,应该使用它,而不是尝试手动添加CMAKE_<LANG>_FLAGS_INIT标志。

另一种不太常见的情况是,编译器工具链不包括其他支持工具,如归档器或链接器。这些编译器驱动程序通常支持命令行参数,可用于指定这些工具的位置。CMake提供了CMAKE_<LANG>_COMPILER_EXTERNAL_TOOLCHAIN变量,该变量可用于指定这些程序所在的目录。

21.4. 系统根目录

很多情况下,工具链是需要的,但有时项目可能需要访问一组库、头文件等,因为可以在目标平台上找到。处理这个问题的一种常见方法是为构建提供目标平台文件系统根目录的简化版本(甚至是完整版本)。称为系统根,或者简称为sysroot。sysroot基本上就是目标平台的根文件系统,可以挂载或复制到可以通过主机的文件系统访问的路径。工具链包通常提供一个最小的sysroot,其中包含编译和链接所需的各种库。

CMake对sysroot有相当广泛的支持。工具链文件可以将CMAKE_SYSROOT变量设置为sysroot位置,仅使用该信息,CMake就可以优先在sysroot中找到库、头文件等所需之物,而不是主机上的同名文件。许多情况下,CMake还会自动向底层工具添加必要的编译器/链接器标志,使它们知道sysroot的位置。对于更复杂的情况,需要提供不同的sysroot来编译和链接(如使用统一头文件的Android NDK),使用CMake 3.9或更高版本时,工具链文件可以设置为CMAKE_SYSROOT_COMPILE和CMAKE_SYSROOT_LINK。

开发人员可以选择在主机挂载点下挂载完整的目标文件系统,并将其设置为sysroot。可以将其挂载为只读,如果不是这样,构建时不要修改它。因此,在构建项目时,可能需要将其安装到其他地方,而不是写入sysroot区域。CMake提供了CMAKE_STAGING_PREFIX变量,它可以用来设置一个分段点,任何安装命令都将安装到该分段点以下(有关此区域的讨论,请参阅25.1.2节“基本安装位置”)。这个分段区域可以是正在运行的目标系统的挂载点,可以在安装之后测试已安装的二进制文件。在快速主机上对目标系统进行交叉编译时,这种方式特别有用,否则在目标系统上构建会很慢(例如:在桌面机器上构建树莓派目标)。在23.1.2节“交叉编译控件”还讨论了CMAKE_STAGING_PREFIX如何影响CMake搜索库、头文件的方式。

21.5. 检查编译器

project()或enablelanguage()触发对编译器和语言特性的测试时,会在内部调用try_compile()命令来执行各种检查。如果提供了一个工具链文件,那么每次try_compile()调用都会读取它,因此测试项目将以类似构建的方式配置。CMake会自动传递一些相关的变量,比如`CMAKE_FLAGS`,但是工具链文件可能希望传递其他变量到测试构建中。由于主构建将首先读取工具链文件,因此工具链文件本身可以定义应该传递哪些变量来进行构建测试。这是通过向CMAKE_TRY_COMPILE_PLATFORM_VARIABLES变量添加变量名来实现的(不要在项目中设置该变量,只能在工具链文件中设置)。使用list(APPEND)而不是set(),这样CMake添加的任何变量都不会丢失。CMAKE_TRY_COMPILE_PLATFORM_VARIABLES最后是否包含重复项并不重要,重要的是所需的变量名是否存在。

try_compile()命令通常编译并链接测试代码以生成可执行文件,一些交叉编译场景中,如果运行链接器需要自定义标志或链接器脚本,或者不希望调用链接器,则会出现问题(交叉编译可能有这样的限制)。如果使用的是CMake 3.6或更高版本,可以通过将CMAKE_TRY_COMPILE_TARGET_TYPE设置为STATIC_LIBRARY来告诉命令创建一个静态库。这就避免了对链接器的需要,但是仍然需要一个归档工具。CMAKE_TRY_COMPILE_TARGET_TYPE也可以有EXECUTABLE,如果没置就为默认行为。CMake 3.6之前,目前已弃用的CMakeForceCompiler模块可以用于避免try_compile()的调用,但现在CMake严重依赖这些测试来找出支持功能的编译器,所以不推荐使用CMakeForceCompiler。

虽然在编译器检查期间不会调用try_run()命令,但是try_run()与try_compile()密切相关,它的行为会受到交叉编译的影响。try_run()实际上是try_compile()之后尝试构建的可执行文件。当CMAKE_CROSSCOMPILING设置为true时,CMake可以修改其运行测试可执行文件的逻辑。如果设置了CMAKE_CROSSCOMPILING_EMULATOR变量,CMake将把它前置到命令中,否则该命令将用于在目标平台上运行可执行文件,并使用该命令在主机平台上运行可执行文件。如果CMAKE_CROSSCOMPILING为true时CMAKE_CROSSCOMPILING_EMULATOR未设置,CMake要求工具链或项目手动设置一些缓存变量。这些变量提供了退出代码以及标准输出和标准错误输出,如果可执行文件能够在目标平台上运行,则将获得这些输出。必须手动设置显然不方便,并且且容易出错,所以在CMAKE_CROSSCOMPILING_EMULATOR没有设置时,项目应避免调用try_run()做交叉编译的情况。无法避免手动定义变量的情况下,CMake文档的try_run()命令关于变量集提供了必要的信息。进一步利用CMAKE_CROSSCOMPILING_EMULATOR也在24.6节所讨论的“交叉编译和模拟器”。

21.6. 例子

选择下面的示例来突出本章讨论的概念。CMake参考文档的工具链部分包含了针对各种不同目标平台的进一步示例。

21.6.1. 树莓派 Raspberry Pi

交叉编译的树莓派是一个很好的使用方式,通常使用CMake处理交叉编译。第一步是获得编译器工具链,最常见的方法是使用像crosstool-NG这样的实用程序。这个示例的其余部分将使用/path/to/toolchain引用工具链目录结构的顶部。

树莓派工具链文件可能是这样的:

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR ARM)

set(CMAKE_C_COMPILER /path/to/toolchain/bin/armv8-rpi3-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /path/to/toolchain/bin/armv8-rpi3-linux-gnueabihf-g++)

set(CMAKE_SYSROOT /path/to/toolchain/armv8-rpi3-linux-gnueabihf/sysroot)

如果主机有一个用于运行的挂载点,可以用于测试项目构建的二进制文件。例如,假设/mnt/rpiStage是一个附加到正在运行的Raspberry Pi的挂载点(它最好指向某个本地目录,而不是系统根目录,这样就可以以任意方式删除或修改它,而不会破坏正在运行的系统)。工具链文件会将此挂载点指定为一个暂存区域,如下所示:

set(CMAKE_STAGING_PREFIX /mnt/rpiStage)

项目的二进制文件可以安装到这个区域,并直接在设备上运行(参见25.1.2节,“基本安装位置”)。

21.6.2. GCC在64位平台上构建32位目标

GCC通过在编译器和链接器命令中添加-m32标志,允许在64位主机上构建32位二进制文件。下面的工具链示例仍然允许在路径中找到GCC编译器,只在编译器和链接器使用的初始设置中添加额外的标记。个人看来,这种安排可以视为交叉编译。因此,也可以设置CMAKE_SYSTEM_NAME,因为设置它会强制CMAKE_CROSSCOMPILING的值为true。无论哪种方式,都应该设置CMAKE_SYSTEM_PROCESSOR,因为这个工具链文件的目标是专门针对与主机不同的处理器。

set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR i686)

set(CMAKE_C_COMPILER gcc)
set(CMAKE_CXX_COMPILER g++)

set(CMAKE_C_FLAGS_INIT -m32)
set(CMAKE_CXX_FLAGS_INIT -m32)

set(CMAKE_EXE_LINKER_FLAGS_INIT -m32)
set(CMAKE_SHARED_LINKER_FLAGS_INIT -m32)
set(CMAKE_MODULE_LINKER_FLAGS_INIT -m32)

确认构建确实是32位的一种方法是使用CMAKE_SIZEOF_VOID_P变量,该变量由CMake自动计算,作为其工具链设置的一部分。对于64位构建,它的值为8,而对于32位构建,它的值为4。

math(EXPR bitness "${CMAKE_SIZEOF_VOID_P} * 8")
message("${bitness}-bit build")

21.6.3. 安卓

Android的交叉编译可能比目前介绍的基本情况更加复杂,并且在如何描述目标系统方面存在一些差异。CMAKE_SYSTEM_NAME必须设置为Android,但通常不设置CMAKE_SYSTEM_PROCESSOR, CMAKE_SYSTEM_VERSION留给CMake来确定。不需要设置单个编译器和工具的路径,而是由一些Android变量控制工具链配置。使用的CMake生成器的类型也会影响可用选项,因为不同的生成器支持不同的开发环境。例如,当使用Visual Studio生成器时,CMake要求安装Nvidia Nsight Tegra Visual Studio Edition。另一方面,使用Ninja或Makefile生成器可以让开发者在使用Android NDK或独立工具链之间做出选择。

NDK和独立工具链

使用Ninja或Makefile生成器时,CMake会使用一系列步骤来决定是使用NDK还是独立的工具链。这些步骤在CMake工具链文档中有清楚的详细说明,但它可以帮助进一步分解这些步骤(使用第一个匹配):

直接指定开发环境

  • 如果设置了CMAKE_ANDROID_NDK变量,则会使用该位置的NDK。

  • 如果设置了CMAKE_ANDROID_STANDALONE_TOOLCHAIN变量,将使用该位置的独立工具链。这个位置必须有一个sysroot子目录。

设置CMAKE_SYSROOT

  • 如果CMAKE_SYSROOT设置为<ndk>/platforms/android-<api>/arch-<arch>,就好像CMAKE_ANDROID_NDK被置为该路径的<ndk>部分。如果工具链文件没有显式提供,默认的Android API级别将填充路径的<api>部分(见下面)。

  • 如果CMAKE_SYSROOT设置为<somedir>/sysroot形式的目录,就好像CMAKE_ANDROID_STANDALONE_TOOLCHAIN设置为<someDir>

替代CMake变量

  • 如果ANDROID_NDK设置了,会当作CMAKE_ANDROID_NDK设置了一样。新项目不应该依赖于这个变量,而应该直接使用更规范的CMAKE_ANDROID_NDK变量。

  • 类似地,如果ANDROID_STANDALONE_TOOLCHAIN设置了,会当作CMAKE_ANDROID_STANDALONE_TOOLCHAIN设置了。新的项目不应该依赖于这个变量,而应该直接使用更规范的CMAKE_ANDROID_STANDALONE_TOOLCHAIN变量。

环境变量

  • 如果设置了ANDROID_NDK_ROOT或ANDROID_NDK环境变量,它会作为CMAKE_ANDROID_NDK变量的值。

  • 如果设置了ANDROID_STANDALONE_TOOLCHAIN环境变量,它会作为CMAKE_ANDROID_STANDALONE_TOOLCHAIN变量的值。

NDK为开发者提供了(比独立工具链)更多的灵活性。独立的工具链只针对单一的架构和API级别,而NDK可能包含对多个工具链都支持,从而支持一系列的架构、API级别等等。请注意,根据NDK路线图显示,在r19发布前后,独立工具链正在被淘汰,除了Clang工具链和一个STL实现之外,所有的工具链都将移除。以下是对NDK和独立工具链相关的变量的介绍:

CMAKE_SYSTEM_VERSION

使用NDK时,可以将其设置为Android API级别,也可以由CMake填充。当未设置时,CMake首先检查是否设置了CMAKE_ANDROID_API变量,如果可用就使用。否则,如果设置了CMAKE_SYSROOT, CMake将尝试从NDK目录结构中检测API级别。如果再失败,将使用NDK支持的最新API级别。对于独立的工具链,CMAKE_SYSTEM_VERSION的值总是由工具链自动确定。

CMAKE_ANDROID_ARCH_ABI

这个变量指定了Android ABI。对于NDK构建,如果没有设置,对于r16以下的NDK版本,默认为armeabi,或者对于以后的版本,默认为最老的arm ABI。CMAKE_ANDROID_ARCH_ABI可以赋予其他值,只要NDK有必要的架构支持(例如:arm64-v8a, armeabi-v7a, armeabi-v6, mips, mips64, x86或x86_64)。这个变量在使用独立工具链时自动设置。CMAKE_ANDROID_ARCH将有CMAKE_ANDROID_ARCH_ABI替代,从而提供相对更为通用的架构值:arm、arm64、mips、mips64、x86或x86_64。

CMAKE_ANDROID_ARM_MODE

当CMAKE_ANDROID_ARCH_ABI设置为armeabi*架构时,开发人员可以在32位ARM或16位Thumb处理器之间进行选择。如果CMAKE_ANDROID_ARM_MODE设置为true值,将选中ARM处理器,否则设置为false或完全不设置,Thumb将是目标处理器。这可以通过NDK或独立的工具链来设置。

CMAKE_ANDROID_ARM_NEON

当CMAKE_ANDROID_ARCH_ABI设置为armeabi-v7a时,CMAKE_ANDROID_ARM_NEON可以设置为真值来启用NEON支持。这可以通过使用NDK或独立的工具链来设置。

CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION

这个NDK特定变量可以用来指定从NDK中选择的工具链。如果给定,值必须采用下列形式之一:

  • X.Y - GCC 版本 X.Y

  • clangX.Y - Clang 版本 X.Y

  • clang - 最新可用的Clang版本

如果未设置此变量,则使用NDK中可用的最新GCC版本。请注意,NDK文档(r16)声明在NDK中不再支持GCC,并且NDK路线图计划在r18中完全删除GCC,因此强烈建议使用Clang工具链。

CMAKE_ANDROID_STL_TYPE

使用独立的工具链时,可以通过给出其中一个支持的值来选择多种STL实现:

  • none

  • system

  • gabi++_static

  • gabi++_shared

  • gnustl_static

  • gnustl_shared

  • c++_static

  • c++_shared

  • stlport_static

  • stlport_shared

如果没有给出,默认值是gnustlstatic。但请注意,与`gnustl STL实现紧密相连的GCC工具链在NDK r18中是不可用的,而且在旧的NDK中只支持C++11。stlport_`实现甚至更旧、更原始,甚至不支持C++11。none选项根本不支持C++, system选项只有new和delete操作,没有STL。

NDK r16文档指出,c++_staticc++_shared STL类型将是未来NDK版本中唯一可用的类型,NDK路线图显示这将出现在r18中。因此,建议项目使用一个c++_* STL实现(LLVM C++标准库实现),并使用Clang工具链。

每个CMake目标都有自己的ANDROID_STL_TYPE属性,CMAKE_ANDROID_STL_TYPE变量用于提供该属性的初始值。大多数情况下,整个构建过程中使用相同的STL是可取的,因此使用变量而不是设置单个目标属性可能更简单、更健壮。

NDK构建的工具链文件的最小示例如下:

set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_ANDROID_NDK /path/to/android-ndk)

使用带有最新GCC工具链的NDK中的API级别也最新。它将不支持neon的armeabi体系结构(Thumb处理器),并将使用gnustl_static STL实现。一个更现实的例子设置了更多的变量:

set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_SYSTEM_VERSION 26) # API level
set(CMAKE_ANDROID_NDK /path/to/android-ndk)
set(CMAKE_ANDROID_ARCH_ABI arm64-v8a)
set(CMAKE_ANDROID_NDK_TOOLCHAIN_VERSION clang)
set(CMAKE_ANDROID_STL_TYPE c++_shared)

上面使用了最新的Clang工具链和一个共享的运行时STL,支持近期的C++标准。

相比之下,一个独立的工具链文件通常会非常简单,因为许多配置决定是由工具链本身决定的:

set(CMAKE_SYSTEM_NAME Android)
set(CMAKE_ANDROID_STANDALONE_TOOLCHAIN /path/to/android-toolchain)

某些工具可能强制使用自己的内部工具链文件,这可能使开发人员更难指定上述任何设置。Android Studio就是这样一个例子,它提供了自己的工具链文件,覆盖了CMake的大部分逻辑。gradle构建用于创建一个外部CMake构建,使用Ninja生成器和通过Android SDK管理器提供的NDK。虽然对工具链文件的直接访问是不启用的,但gradle构建确实提供了一系列的gradle变量,这些变量会翻译成CMake中的等价物。开发人员应该参考该工具的文档,以确定是否/如何使用不同的CMake版本,以及如何影响CMake构建的行为。这似乎是Android Studio的一个活跃开发领域。

对于使用ndk-build而不是gradle的开发者来说,CMake 3.7引入了导出Android的能力。可以使用export()作为CMake构建的一部分,也可以使用install()作为安装步骤的一部分。构建期间的导出非常简单:

export(TARGETS target1 [target2...] ANDROID_MK fileName)

文件名通常为带路径的Android.mk,把它放到ndk-build需要的位置。每个命名的目标都将包括在生成的文件中,以及相关的使用要求,如包含标志、编译器定义等。如果项目需要支持成为父ndk构建的一部分,这就是它通常想要做的。如果CMake要打包项目,并且想让自己更容易合并到任何ndk构建中,install()命令提供了所需的功能(参见25.3节“安装导出”)。

Visual Studio生成器

使用Visual Studio生成器时,CMake要求安装Nvidia Nsight Tegra Visual Studio Edition。最终的项目将驱动整个构建,而不是形成一个更大的gradle或ndk构建结构的一部分。支持是在CMake 3.1中首次添加的,但是很多选项直到CMake 3.4才添加。生成器通常在CMake上设置如下命令行:

cmake -G "Visual Studio 12 2013 Tegra-Android" \
 -DCMAKE_TOOLCHAIN_FILE=/some/path/toolchain.cmake \
 /path/to/source

最小的工具链文件只需要将CMAKE_SYSTEM_NAME设置为Android,但就像NDK和独立的工具链一样,可以设置更多的变量来影响目标架构等等。在很多情况下,为Visual Studio构建设置的变量与NDK情况不同,不过通常是相关的。

NDK和独立的工具链构建会设置CMAKE_ANDROID_ARCH_ABI并允许CMAKE_ANDROID_ARCH的派生,Visual Studio的工具链文件会直接设置CMAKE_ANDROID_ARCH。Visual Studio情况下的允许值也不同:armv7-a、armv7-a-hard、arm64-v8a、x86和x86_64。

类似地,Visual Studio构建的工具链文件将设置CMAKE_ANDROID_API,而不是使用CMAKE_SYSTEM_VERSION来指定目标设备的Android API级别,而CMAKE_ANDROID_API作为ANDROID_API目标属性的默认值。此外,可以设置CMAKE_ANDROID_API_MIN来指定用于构建本机代码的API版本(它遵循相同的模式,并充当ANDROID_API_MIN目标属性的默认值)。这有点类似于Apple平台的情况,其中用于构建的SDK可以单独指定到目标设备的最低操作系统级别(参见22.5节“构建设置”)。

CMAKE_ANDROID_STL_TYPE变量可以设置并接受NDK的值,但是不支持c++_staticc++_shared。它会用作ANDROID_STL_TYPE目标属性的默认值。

由于这种方式驱动了整个构建,所以必须设置比CMake构建本地代码更多的内容。还有许多其他目标属性与构建原生代码没有关联的构建部分相关,比如JAR依赖项、Java源的设置等。其中一些目标属性还与CMake变量的默认值的定义相关。这些目标属性都有ANDROID_…的名称,而CMake的默认变量有CMAKE_ANDROID_…这些信息超出了本文的讨论范围,因此感兴趣的读者应该参考CMake文档来了解支持的属性和变量的详细信息,然后根据项目的非原生部分来设置它们。

21.7. 推荐

工具链文件乍一看可能有点吓人,但这大多来自于其中放置了太多逻辑和许多示例和项目。工具链文件应该尽可能的少,以支持所需的工具,并且它们通常应该在不同的项目之间被重用。特定于项目的逻辑应该使用在项目自己的CMakeLists.txt文件中。

编写工具链文件时,开发人员应该确保内容不会只会执行一次。CMake可能会处理工具链文件多次,这取决于项目做了什么(例如:多次调用project()或enable_language())。工具链文件也可以用于临时构建,作为try_compile()调用的一部分,所以不应该对使用上下文做任何假设。

避免使用已弃用的CMakeForceCompiler模块在构建中设置编译器。这个模块在使用旧的CMake版本时很流行,但新版本严重依赖于测试工具链,并会找出它所支持的特性。CMakeForceCompiler模块主要用于CMake未知的编译器,但是在最近的CMake版本中使用这样的编译器很可能会带来不小的限制。建议与CMake开发人员合作,为此类编译器添加所需的支持。

注意,不要丢弃或错误处理,可能在处理工具链文件时已经设置的变量的内容。常见的错误是修改像CMAKE_<LANG>_FLAGS,而不是CMAKE_<LANG>_FLAGS_INIT变量,这可能会丢弃开发人员手动设置的值,或者在多次处理工具链文件时与需要填充的值不同。

当以Android平台为目标时,最好使用带有NDK和Ninja或Makefile生成器的简单工具链文件。这种组合CMake支持的,并且最容易使用。工具链文件可以非常简单,而且最近版本的IDE工具(如Android Studio)也开始使用这种方法。当开发人员使用他们自己的工具链文件时,不要使用流行的taka-no-me工具链文件,因为它过于复杂并且存在很多已知的问题。较新的CMake版本支持简单的工具链文件,这些工具链文件工作起来很流畅,而且工作量很小。

项目应该避免对任何逻辑使用CMAKE_CROSSCOMPILING变量。这个变量可能会引起误解,因为即使目标和主机平台相同,也可以将其设置为true,或者在目标和主机平台不同时设置为false。项目作者应该意识到,一些多配置生成器(例如Xcode)允许在建造的时选择目标平台,所以CMake逻辑基于是否交叉编译,需要仔细地处理(该项目可能产生的)不同情况。

工具链文件通常包含命令来修改CMake搜索程序、库和其他文件的位置。参见第23章,寻找与此领域相关的实践。

Last updated