第二十三章:工具链和交叉编译

Posted by lili on

在考虑构建软件和涉及的工具过程时,开发人员通常会考虑到编译器和链接器。虽然这些是开发人员接触到的主要工具,但还有许多其他工具、库和支持文件也对该过程起到贡献作用。粗略地说,这些更广泛的工具和其他文件的集合被统称为工具链。

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

一旦选择了工具链,CMake会在内部进行相当多的处理,以测试工具链以确定其支持的特性、设置各种属性和变量等。即使是对于使用默认工具链的传统构建,这也是如此,而不仅仅是用于交叉编译的构建。这些测试的结果可以在CMake为给定的构建目录第一次运行时的输出中看到。对于GCC的示例可能看起来像这样:

 --
 The C compiler identification is GNU 9.3.0
--
 The CXX compiler identification is GNU 9.3.0
--
 Detecting C compiler ABI info
--
 Detecting C compiler ABI info - done
--
 Check for working C compiler: /usr/bin/cc - skipped
--
 Detecting C compile features
--
 Detecting C compile features - done
--
 Detecting CXX compiler ABI info
--
 Detecting CXX compiler ABI info - done
--
 Check for working CXX compiler: /usr/bin/c++ - skipped
--
 Detecting CXX compile features
--
 Detecting CXX compile features - done

这些处理的大部分通常发生在第一次调用project()命令时,工具链测试的结果会被缓存起来。当enable_language()命令启用先前未启用的语言时,也会触发这样的处理,就像另一个调用project()并添加先前未启用的语言一样。一旦启用了一种语言,其缓存的详细信息将始终被使用,而不是重新测试工具链,即使是在后续的CMake运行中也是如此。这至少有两个重要的后果:

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

  • 不同的工具链不能直接在同一个项目中混合使用。CMake基本上将一个项目视为始终使用单个工具链。为了使用多个工具链,必须将项目结构化为执行构建的部分作为外部子构建(这是一种在第29章“ExternalProject”和第34.1节“Superbuild Structure”中讨论的技术)。

23.1. 工具链文件

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

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

CMake 3.21或更高版本还支持--toolchain命令行选项或回退到CMAKE_TOOLCHAIN_FILE环境变量,但最终结果是相同的(即设置CMAKE_TOOLCHAIN_FILE缓存变量)。

cmake --toolchain myToolchain.cmake path/to/source
# Set once for the current shell
export CMAKE_TOOLCHAIN_FILE=myToolchain.cmake

# No toolchain specified, uses the environment variable
cmake path/to/source

可以使用完整的绝对路径,或者像上面的示例中一样使用相对路径,CMake首先查找相对于构建目录顶部,如果在那里找不到,则相对于源目录顶部查找。必须在第一次运行CMake时为构建目录指定该工具链文件,不能后期添加或更改为指向不同工具链。由于变量本身是缓存的,因此在后续的任何CMake运行中都不需要重新指定它。

工具链文件由第一次调用project()命令读取,可能会多次读取。在后续运行中读取的次数也可能与第一次运行不同。当首次启用语言时,CMake可能会在内部设置临时子项目来测试工具链时,也会读取工具链文件。鉴于这些因素,工具链文件应支持被多次包含,并且不应包含假设仅由主项目读取的逻辑。

开发人员应该将工具链文件保持最小化,只设置所需的内容,并尽可能少地假设项目的功能。工具链文件理想情况下应该与项目完全解耦,甚至应该可以在不同项目中重复使用,因为它们只应描述工具链,而不是它们如何与特定项目交互。

工具链文件的内容可能会有所不同,但总体上它们可能需要做的主要事情只有几个:

  • 描述目标系统的基本细节。
  • 提供工具的路径(通常仅限于编译器)。
  • 设置工具的默认标志(通常只是针对编译器和可能是链接器)。
  • 在交叉编译的情况下设置目标平台根文件系统的位置。

工具链文件中经常包含其他逻辑,尤其是用于影响各种find_…()命令行为的逻辑(参见第25章“查找内容”)。虽然有些情况下这样的逻辑可能是合适的,但可以提出这样的论点,即在大多数情况下,这样的逻辑应该是项目的一部分。只有项目知道自己想要找什么,因此工具链不应假设项目想要做什么。

23.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_SYSTEM_NAME视为如果直接在目标平台上运行CMake时,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_PROCESSOR值做出的错误决策可能导致难以检测或诊断的微妙问题。

CMAKE_SYSTEM_VERSION变量的含义取决于CMAKE_SYSTEM_NAME设置。例如,对于系统名称为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值的逻辑,建议项目谨慎行事。

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

  • 在CMake 3.13及更早版本中,所有Apple平台都使用Darwin作为CMAKE_SYSTEM_NAME,即使是对iOS、tvOS或watchOS。然后通过CMAKE_OSX_SYSROOT变量选择实际的目标系统,该变量选择要用于构建的基础SDK。根据所选择的SDK确定目标设备,但开发人员仍然可以在构建时选择设备或模拟器。这是一个复杂的主题,在第24.5节“构建设置”中详细介绍。为了更好地区分不同的平台并使它们与其他平台处理方式更一致,CMake 3.14添加了专用的CMAKE_SYSTEM_NAME值iOS、tvOS和watchOS的支持。

  • 对于Apple平台,CMAKE_SYSTEM_PROCESSOR和CMAKE_SYSTEM_VERSION通常没有特别的含义,通常保持未设置状态。

  • 对于目标Android平台,通常不会设置CMAKE_SYSTEM_PROCESSOR。这将在下面的第23.7节“Android”中进一步讨论。

此外,某些项目生成器支持其自己的本机平台名称。对于这样的生成器,可以通过几种不同的方法之一指定本机平台名称,而不是设置CMAKE_SYSTEM_NAME变量。最简单和直接的方法是在cmake命令行上使用-A选项指定本机平台名称以及生成器详细信息。例如,可以这样指示Visual Studio生成器目标x64平台:

cmake -G "Visual Studio 2019" -A x64

所选平台将通过CMAKE_GENERATOR_PLATFORM CMake变量对项目可用。另外,开发人员可以选择使用工具链文件并直接设置CMAKE_GENERATOR_PLATFORM CMake变量(项目不应该自己设置这个CMake变量)。如果使用的是CMake 3.15或更高版本,则可以通过CMAKE_GENERATOR_PLATFORM环境变量提供平台。

23.3. 工具选择

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

从CMake 3.19开始,CMAKE_<LANG>COMPILER可以是一个列表。列表中的第一个项目是要使用的编译器,就像上面描述的那样。列表中的其余项目是必须存在的编译器选项,以便编译器能够正常工作。不要通过该变量添加非必需的选项。还要注意,CMAKE<LANG>_COMPILER在第一次CMake运行后不应更改。

大多数语言还支持通过设置环境变量而不是设置CMAKE_<LANG>COMPILER来设置编译器。这些通常遵循常见的约定,例如C编译器的CC,C++编译器的CXX,Fortran编译器的FC等。这些环境变量仅在第一次在构建目录中运行CMake时生效,且仅在对应的CMAKE<LANG>_COMPILER变量没有被工具链文件或CMake命令行设置时生效。

一些生成器支持自己单独的工具集规范,其工作方式与上述方法不同。可以使用cmake命令行上的-T选项选择这些工具集,或者如果使用CMake3.15或更高版本,则可以通过设置CMAKE_GENERATOR_TOOLSET环境变量来选择。它们也可以在工具链文件中通过设置CMAKE_GENERATOR_TOOLSET CMake变量选择(项目不应该自己设置这个变量)。可用的工具集和支持的语法因生成器而异,但以下示例展示了一些可能性。

构建32位可执行文件,但使用64位编译器和链接器工具:

cmake -G "Visual Studio 2019" -A Win32 -T host=x64 ...

使用Visual Studio内置的LLVM发行版的clang-cl编译器:

cmake -G "Visual Studio 2019" -T ClangCL ...

对于某些生成器,安装了多个实例的构建工具可能意味着开发人员需要指定要使用的实例。一个典型的例子是开发人员首先尝试使用Visual Studio的预览版本,然后稍后安装发布版本而不删除预览版本。在使用CMake 3.11或更高版本时,可以在工具链文件中设置CMAKE_GENERATOR_INSTANCE变量来控制将使用的特定实例。使用CMake 3.15或更高版本时,也可以设置CMAKE_GENERATOR_INSTANCE环境变量。少数生成器支持此功能,目前只有Visual Studio 2017或更高版本支持。

指定了工具链后,CMake将识别编译器并尝试确定其版本。这些编译器信息将通过CMAKE_<LANG>COMPILER_ID和CMAKE<LANG>COMPILER_VERSION变量分别提供给项目。编译器ID是一个简短的字符串,用于区分一个编译器与另一个编译器,常见的值有GNU、Clang、AppleClang、MSVC和Intel。CMake文档中关于CMAKE<LANG>_COMPILER_ID的部分提供了支持的ID的完整列表。如果可以确定编译器版本,它将具有通常的major.minor.patch.tweak形式,其中不需要所有版本组件(例如4.9将是一个有效的版本)。

除了CMAKE_<LANG>COMPILER_ID和CMAKE<LANG>_COMPILER_VERSION变量之外,还支持没有前缀CMAKE_的类似生成器表达式。变量和生成器表达式可以用于有条件地仅针对某些编译器或编译器版本添加内容。例如,GCC 7引入了一个新的-fcode-hoisting选项,下面展示了仅在可用时为C++编译添加该选项的两种方法:

# Conditionally add -fcode-hoisting option using variables
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND
	CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 7)
	target_compile_options(SomeTarget PRIVATE -fcode-hoisting)
endif()

# Same thing using generator expressions instead
set(isGNU $<CXX_COMPILER_ID:GNU>)
set(newEnough $<VERSION_GREATER_EQUAL:$<CXX_COMPILER_VERSION>,7>)
target_compile_options(SomeTarget PRIVATE
	$<$<AND:${isGNU},${newEnough}>:-fcode-hoisting>
)

编译器ID是识别所使用编译器的最可靠方式。项目可能需要注意的一个情况是,在CMake 3.0之前,苹果的Clang编译器与上游的Clang被视为相同,两者都有编译器ID为Clang。从CMake 3.0开始,苹果的编译器将编译器ID更改为AppleClang,以便与上游的Clang区分开来。添加了策略CMP0025,允许使用旧的行为,以便对需要的项目进行设置。

一旦确定了编译器的路径,CMake就能够确定编译器和链接器的适当默认标志集。这些通过以下变量对项目可用:

  • CMAKE_<LANG>_FLAGS
  • CMAKE_<LANG>FLAGS<CONFIG>
  • CMAKE_<TARGETTYPE>_LINKER_FLAGS
  • CMAKE_<TARGETTYPE>LINKER_FLAGS<CONFIG>

这些变量在第15.5节“编译器和链接器变量”中进行了介绍。开发人员可以通过使用相同名称但附加了_INIT的变量将自己的标志添加到这些默认值集中。这些…_INIT变量仅用于设置初始默认值,在CMake运行一次并将实际值保存在缓存中后,它们就不再起作用了。

一个常见的错误是在工具链文件中设置非…INIT变量(例如设置CMAKE_<LANG>FLAGS而不是CMAKE<LANG>_FLAGS_INIT)。这会导致丢弃或隐藏开发人员在缓存中对这些变量所做的任何更改,这是不希望的。工具链文件可能在后续的project()或enable_language()调用中被重新读取,从而丢弃项目本身对这些变量所做的任何更改。相反,设置…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变量,可以用来指定这些实用程序所在的目录。

23.4. 系统根目录

在许多情况下,工具链就是所需的全部,但有时项目可能需要访问更广泛的库、头文件等,就像它们在目标平台上找到的那样。处理这种情况的一种常见方法是为构建提供一个缩减版(甚至是完整版)的目标平台根文件系统。这被称为系统根目录,简称为sysroot。sysroot基本上只是将目标平台的根文件系统挂载或复制到可以通过主机文件系统访问的路径。工具链包通常提供一个包含编译和链接所需的各种库等的最小sysroot。

CMake对sysroot具有相当广泛且易于使用的支持。工具链文件可以设置CMAKE_SYSROOT变量来指定sysroot的位置,仅凭这些信息,CMake就可以优先在sysroot区域找到库、头文件等,而不是在主机上同名文件(这在”交叉编译控制”一节中有详细介绍)。在许多情况下,CMake还会自动向底层工具添加必要的编译器/链接器标志,以使它们能够识别sysroot区域。对于需要为编译和链接提供不同sysroots的更复杂的场景(例如,使用Android NDK的统一头文件),在使用CMake 3.9或更高版本时,工具链文件可以分别设置CMAKE_SYSROOT_COMPILE和CMAKE_SYSROOT_LINK。

在某些情况下,开发人员可以选择将完整的目标文件系统挂载到主机挂载点下,并将其用作sysroot。这可以以只读方式挂载,或者即使不是只读,仍然可能希望保持其未被构建修改。因此,当项目构建完成时,可能需要将其安装到其他位置,而不是写入sysroot区域。CMake提供了CMAKE_STAGING_PREFIX变量,可以用来设置一个分级点,在这个点下,任何安装命令都将安装到该点(请参阅”基础安装位置”一节对该区域的讨论)。

这个分级区域可以是运行目标系统的挂载点,安装后的二进制文件可以立即进行测试。这种安排在快速主机上进行交叉编译用于慢速构建的目标系统时尤其有用(例如,在桌面机器上为树莓派目标构建)。”交叉编译控制”一节还讨论了CMAKE_STAGING_PREFIX如何影响CMake搜索库、头文件等的方式。

23.5. 编译器检查

当 project() 或者 enable_language()调用触发编译器和语言特性的测试时,try_compile()命令会在内部调用以执行各种检查。如果提供了工具链文件,每次try_compile()调用都会读取该文件,因此测试编译将以类似于主构建的方式配置。CMake会自动传递一些相关的变量,例如CMAKE_<LANG>_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_compile()密切相关,其行为受交叉编译影响。try_run()实际上是try_compile()后跟一个尝试运行刚刚构建的可执行文件。当CMAKE_CROSSCOMPILING设置为true时,CMake会修改运行测试可执行文件的逻辑。如果设置了CMAKE_CROSSCOMPILING_EMULATOR变量,CMake会将其添加到在目标平台上运行可执行文件所使用的命令之前,并使用该命令在主机平台上运行可执行文件。如果在CMAKE_CROSSCOMPILING为true时没有设置CMAKE_CROSSCOMPILING_EMULATOR,CMake要求工具链或项目手动设置一些缓存变量。这些变量提供了如果可执行文件能够在目标平台上运行时所获得的退出代码以及stdout和stderr的输出。手动提供这些变量显然不方便且容易出错,因此在CMAKE_CROSSCOMPILING_EMULATOR无法设置的交叉编译情况下,项目通常应尽量避免调用try_run()。

对于无法避免手动定义这些变量的情况,CMake try_run()命令的文档提供了有关要设置的变量的必要详细信息。有关CMAKE_CROSSCOMPILING_EMULATOR的进一步用法也在”交叉编译和仿真器”一节中进行了讨论。

23.6. 示例

以下示例被选中以突出本章讨论的概念。CMake参考文档中的工具链部分包含了各种不同目标平台的更多示例。

23.6.1. 树莓派

为树莓派进行交叉编译是对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是一个挂载点,连接到运行中的树莓派(最好指向某个本地目录,而不是系统根目录,以便可以清除或以其他方式任意修改而不会破坏运行中的系统)。工具链文件将指定此挂载点作为一个分段区域,如下所示:

set(CMAKE_STAGING_PREFIX /mnt/rpiStage)

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

23.6.2. 在64位主机上为32位目标使用GCC

GCC允许在64位主机上构建32位二进制文件,方法是将-m32标志添加到编译器和链接器命令中。以下工具链示例仍然允许在PATH中找到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")

23.7. Android

为Android进行交叉编译可能比上面讨论的情况更复杂。有许多特定于Android的设置会影响构建,不仅仅是通常的目标平台、工具链位置和标志。一些CMake和Android NDK版本的组合也可能存在兼容性问题,因此开发人员需要仔细选择工具和工具包。

23.7.1. 历史背景

CMake自身具有针对Android的内置支持,但Android NDK也对构建的设置有自己的期望。CMake和NDK在彼此支持方面的发展基本上是相对独立且并行的。这导致了在不同阶段出现不兼容性的问题。

NDK r19版本引入的变化破坏了CMake内置的Android支持。CMake 3.16.0包含了恢复使用内置Android支持的能力的变化,但在某些情况下仍存在一些问题。使用NDK r23版本和CMake 3.21或更高版本,这些不兼容性问题最终得到了解决。对于上述内容的详细信息,读者可以参考以下链接:

  • https://gitlab.kitware.com/cmake/cmake/issues/18787
  • https://github.com/android-ndk/ndk/issues/463

使用CMake 3.21或更高版本时,用户应该能够可靠地使用NDK r23或更高版本提供的工具链文件。建议必须使用r18—r22范围内的NDK版本的开发人员尽可能使用CMake 3.20或更高版本。3.20版本包含了与这些NDK版本相关的修复。

23.7.2. 使用NDK

为Android构建的推荐方式是使用Android NDK提供的工具链文件。该文件的名称通常类似于android.toolchain.cmake。根据主机平台和NDK的安装方式,工具链文件可能位于多个位置。典型的安排是将文件放置在NDK的基本安装目录下的build/cmake子目录中。

工具链文件负责设置许多内容。CMAKE_SYSTEM_NAME将始终设置为Android。将使用NDK提供的Clang编译器(NDK r18取消了选择gcc工具链的能力)。CMAKE_SYSROOT将被设置为NDK内部适当的目录。架构和ABI可以保持NDK选择的默认值,但建议开发人员明确设置它们。这样可以确保非常清楚地了解正在构建的内容。可以通过将CMAKE_ANDROID_ARCH_ABI变量设置为以下值之一来实现这一点(其他可能也受支持,请查看NDK文档或工具链文件):

  • armeabi-v7a
  • arm64-v8a
  • x86
  • x86_64

如果选择armeabi-v7a,则以下两个变量也相关:

  • CMAKE_ANDROID_ARM_NEON可以设置为true以启用NEON支持,或设置为false以在不启用NEON的情况下构建。如果未设置此变量,默认情况下将启用NEON支持。
  • CMAKE_ANDROID_ARM_MODE控制要构建的处理器类型。将其设置为true以构建32位ARM处理器,否则构建将针对16位Thumb处理器。

CMake将根据上述内容和NDK提供的信息将CMAKE_SYSTEM_PROCESSOR设置为适当的值。

Android API级别由名为ANDROID_PLATFORM的变量控制(注意没有任何CMAKE_前缀)。如果未设置ANDROID_PLATFORM,则API级别将设置为NDK支持的最低级别。注意,这个最低版本可能不适合所选择的架构和ABI,因此不建议依赖它。相反,应将API级别指定为一个数字,以确保所使用的API是明确定义的。建议使用API级别23或更高版本,以避免与Android PackageManager的本机库加载可能存在的可靠性问题(参见https://github.com/KeepSafe/ReLinker)。还可以使用特殊字符串latest,它会选择NDK支持的最新API级别,但这不够清晰,也不够可追踪。

CMAKE_ANDROID_STL_TYPE指定要使用的C++ STL实现。早期的NDK版本支持一系列不同的选项,但现在的项目应该只使用c++_shared或c++_static。如果应用程序只包含一个共享库,则只能使用后者。如果应用程序根本不使用C++ STL,则可以使用none作为值,但这可能不常见。

CMAKE_ANDROID_RTTI和CMAKE_ANDROID_EXCEPTIONS控制是否启用rtti和exceptions。它们是布尔变量,其默认值由STL实现的选择确定。注意,r23版本的NDK工具链文件存在一个错误,即在某些情况下可能无法正确设置CMAKE_ANDROID_EXCEPTIONS,如果没有直接由开发人员设置。因此,建议始终设置此变量,以确保获得预期的行为。

从上面可以看出,在读取NDK工具链文件之前通常需要设置许多变量。可以通过如下方式在cmake命令行上指定它们:

cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/ndk/build/cmake/android.toolchain.cmake \
-DCMAKE_ANDROID_ARCH_ABI=arm64-v8a \
-DANDROID_PLATFORM=24 \
-DCMAKE_ANDROID_STL_TYPE=c++_shared \
-DCMAKE_ANDROID_RTTI=YES \
-DCMAKE_ANDROID_EXCEPTIONS=YES \
...

或者,也可以使用包装的工具链文件:

my_android_toolchain.cmake:

set(CMAKE_ANDROID_ARCH_ABI arm64-v8a)
set(ANDROID_PLATFORM 24)
set(CMAKE_ANDROID_STL_TYPE c++_shared)
set(CMAKE_ANDROID_RTTI YES)
set(CMAKE_ANDROID_EXCEPTIONS YES)
include(/path/to/ndk/build/cmake/android.toolchain.cmake)
cmake -DCMAKE_TOOLCHAIN_FILE=my_android_toolchain.cmake ...

CMake会在第一次运行时将计算出的信息缓存到构建目录中。如果需要更改本节中提到的任何变量,则应首先删除构建目录中的内容。如果不这样做,编译器标志可能不会更新以反映更改变量中的最新值。

23.7.3. Android Studio

某些工具可能强制使用其自己的内部工具链文件,这可能会使开发人员更难指定上述任何设置。Android Studio就是这样的一个例子,它强制使用特定的工具链文件,覆盖了CMake自身逻辑的大部分内容。Gradle构建设置为创建一个使用Ninja生成器和通过Android SDK管理器提供的NDK的外部CMake构建。虽然不允许直接访问工具链文件,但Gradle构建提供了一系列Gradle变量,这些变量被转换为它们的CMake等效项。开发人员应查阅该工具的文档,了解如何使用不同的CMake版本以及如何影响CMake构建的行为。

23.7.4. ndk-build

对于使用ndk-build(本质上只是GNU make的包装器)而不是Gradle的开发人员,CMake 3.7添加了在CMake构建中导出Android.mk文件的能力,可以使用export()导出或作为安装步骤的一部分使用install()。在构建期间进行导出很简单:

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

fileName通常是Android.mk,需要添加一些路径前缀将其放置在ndk-build所需的位置。每个命名目标将包含在生成的文件中,以及相关的使用要求,如包含标志、编译器定义等。这通常是项目希望做的事情,如果需要支持成为父ndk-build的一部分。对于CMake项目将打包并希望使其易于纳入任何ndk-build的情况,install()命令提供了所需的功能(参见27.3节,“安装导出”)。

23.7.5. Visual Studio生成器

Ninja和Makefiles生成器与NDK工具链文件集成良好。当使用Visual Studio生成器时,CMake 3.19及更高版本也支持NDK。对于直接通过Visual Studio安装程序安装的NDK版本,设置应该相对顺利,但这些版本通常很旧,因此有限的实用性。如果使用更近期的NDK版本安装在Visual Studio之外,则Ninja和Makefiles生成器可能会提供更好的结果。

对于较早的CMake版本,Visual Studio中的Android支持需要使用Nvidia Nsight Tegra Visual Studio Edition。这种方法在相当多的CMake版本中基本上没有得到维护,因此其可靠性可能存疑。建议开发人员改用NDK。

23.8. 推荐实践

工具链文件一开始可能会显得有点令人生畏,但这主要是因为许多示例和项目在其中放置了过多的逻辑。工具链文件应尽可能简化,以支持所需的工具,并且它们通常应该能够在不同项目中重复使用。特定于项目的逻辑应该放在项目自己的CMakeLists.txt文件中。

在编写工具链文件时,开发人员应确保其内容不要假设只会执行一次。根据项目的操作方式(例如,对project()或enable_language()的多次调用),CMake可能会多次处理工具链文件。工具链文件还可能被用于”旁边”作为try_compile()调用的一部分进行临时构建,因此它们不应该对其使用环境做出任何假设。

避免使用已废弃的CMakeForceCompiler模块来设置构建中要使用的编译器。这个模块在使用旧版CMake时很受欢迎,但是新版本主要依赖于测试工具链并确定它支持的特性。CMakeForceCompiler模块主要用于CMake不知道的编译器的情况,但是在最新版本的CMake中使用这样的编译器可能会导致非常限制性的问题。建议与CMake开发人员合作,为这些编译器添加所需的支持。

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

在针对Android平台时,建议使用NDK及其提供的工具链文件,或者简单地包装该工具链文件。为了确保完全可靠的构建,建议使用NDK r23或更高版本和CMake 3.21或更高版本。早期版本之间存在不兼容性,并且设置更加复杂。避免曾经流行的taka-no-me工具链文件,它在在线示例中经常被引用,但过于复杂,存在已知问题,并且多年来未进行维护。还要使用Android API 23或更高版本,以避免Android PackageManager的本地库加载器存在的已知问题。

项目通常应避免在其逻辑中使用CMAKE_CROSSCOMPILING变量。该变量可能会产生误导,因为即使目标和主机平台相同,它也可以设置为true,或者在它们不同的情况下设置为false。其值的不一致性使其不可靠。

项目作者还应意识到,某些多配置生成器(例如Xcode)允许在构建时选择目标平台,因此基于是否进行交叉编译的CMake逻辑需要非常小心地编写,以处理项目可能生成的不同情况。

工具链文件通常包含修改CMake搜索程序、库和其他文件位置的命令。有关与此领域相关的推荐实践,请参阅第25章“查找事物”。