工具链与交叉编译:CMAKE_TOOLCHAIN_FILE、系统与处理器检测、编译器检测与选择、Sysroot与查找路径
交叉编译,说白了就是在一台机器上编译出另一台机器能跑的程序。我刚开始接触嵌入式开发时,觉得这事特别玄乎——明明我的电脑是x86的,怎么就能生成ARM的二进制文件呢?后来才明白,其实就是换了一套工具链,让编译器、链接器都变成目标平台专用的版本。
CMake对交叉编译的支持,核心就是CMAKE_TOOLCHAIN_FILE这个变量。我个人习惯把工具链配置单独写成一个文件,而不是散落在CMakeLists.txt里。这样项目切换平台时,只需要换一个文件就行,清爽得很。
CMAKE_TOOLCHAIN_FILE:交叉编译的入口
这个文件通常在命令行传入:
cmake -DCMAKE_TOOLCHAIN_FILE=/path/to/toolchain.cmake /path/to/source
工具链文件里要做什么?我总结下来就三件事:
- 指定目标系统的名称和处理器架构
- 设置编译器、链接器、归档器等工具的路径
- 配置Sysroot和查找路径
一个典型的工具链文件长这样:
# 目标系统
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 编译器
set(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)
# Sysroot
set(CMAKE_SYSROOT /opt/arm-sysroot)
# 查找策略
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY)
系统与处理器检测
CMake在配置阶段会检测当前系统。你可以在CMakeLists.txt里用这些变量:
| 变量 | 说明 | 常见值 |
|---|---|---|
| CMAKE_SYSTEM_NAME | 操作系统名称 | Linux, Windows, Darwin, Android |
| CMAKE_SYSTEM_PROCESSOR | 处理器架构 | x86_64, arm, aarch64, i686 |
| CMAKE_SYSTEM_VERSION | 系统版本号 | 如 5.10.0 (Linux内核版本) |
| CMAKE_HOST_SYSTEM_NAME | 宿主机的系统名称 | 与CMAKE_SYSTEM_NAME类似,但指编译机 |
我曾经在一个项目里遇到过这样的坑:代码里用#ifdef __linux__判断平台,但交叉编译时目标系统是Linux,宿主机也是Linux,结果预处理器宏完全一样。后来我改用CMake的CMAKE_SYSTEM_NAME来传递平台宏,才彻底解决。
你可以这样在CMakeLists.txt里做条件判断:
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
target_compile_definitions(myapp PRIVATE PLATFORM_LINUX=1)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
target_compile_definitions(myapp PRIVATE PLATFORM_WINDOWS=1)
endif()
if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm|aarch64")
target_compile_definitions(myapp PRIVATE ARCH_ARM=1)
endif()
CMAKE_SYSTEM_PROCESSOR做匹配时,建议用MATCHES而不是STREQUAL。因为不同工具链报告的处理器名称可能略有差异,比如有的写"armv7l",有的写"arm"。用正则匹配更稳妥。
编译器检测与选择
CMake检测编译器的方式,其实比你想象的要智能。它不光看路径,还会运行编译器来获取版本信息。你可以用这些变量来查看检测结果:
message(STATUS "C compiler: ${CMAKE_C_COMPILER}")
message(STATUS "C++ compiler: ${CMAKE_CXX_COMPILER}")
message(STATUS "C compiler ID: ${CMAKE_C_COMPILER_ID}")
message(STATUS "C++ compiler ID: ${CMAKE_CXX_COMPILER_ID}")
message(STATUS "C compiler version: ${CMAKE_C_COMPILER_VERSION}")
常见的编译器ID有:GNU、Clang、AppleClang、MSVC、Intel等。我建议你在项目里用这些ID做编译器相关的条件编译,而不是依赖路径判断。为什么呢?因为路径可能变,但编译器ID是CMake从编译器内部信息提取的,更可靠。
举个例子:
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
target_compile_options(myapp PRIVATE -Wall -Wextra)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
target_compile_options(myapp PRIVATE -Wall -Weverything)
endif()
嗯,这里要注意一点:CMAKE_C_COMPILER和CMAKE_CXX_COMPILER可以在工具链文件里设置,也可以在命令行传入。但如果两者冲突,CMake会报错。我个人习惯全在工具链文件里搞定,省得每次敲命令行敲到手软。
Sysroot与查找路径
Sysroot是什么?说白了就是目标系统的根文件系统。交叉编译时,你的程序需要链接目标平台上的库,比如libc、libstdc++等。这些库不在你的宿主机上,而是在Sysroot目录里。
设置Sysroot很简单:
set(CMAKE_SYSROOT /opt/arm-sysroot)
设置了Sysroot后,CMake会自动把-sysroot参数传给编译器。编译器就会去Sysroot目录下找头文件和库,而不是去宿主机的/usr/include。
但光有Sysroot还不够。你还需要告诉CMake,查找程序、库、头文件时,应该优先去Sysroot里找,还是去宿主机里找。这就是CMAKE_FIND_ROOT_PATH_MODE_*系列变量的作用:
| 变量 | 作用 | 推荐值(交叉编译) |
|---|---|---|
| CMAKE_FIND_ROOT_PATH_MODE_PROGRAM | 查找可执行程序 | NEVER(用宿主机的工具) |
| CMAKE_FIND_ROOT_PATH_MODE_LIBRARY | 查找库文件 | ONLY(只用Sysroot里的) |
| CMAKE_FIND_ROOT_PATH_MODE_INCLUDE | 查找头文件 | ONLY(只用Sysroot里的) |
| CMAKE_FIND_ROOT_PATH_MODE_PACKAGE | 查找CMake包 | ONLY(只用Sysroot里的) |
为什么程序要设成NEVER?你想想看,交叉编译时你需要的程序是cmake、make这些宿主机的工具,而不是目标平台上的程序。如果设成ONLY,CMake可能会去Sysroot里找cmake,那肯定找不到。
核心原则:编译时用的工具(编译器、链接器、cmake等)用宿主机的;链接时用的库、头文件用Sysroot里的。这个原则搞清楚了,交叉编译的配置就不会乱。
另外,CMAKE_FIND_ROOT_PATH可以设置多个路径。如果你有多个Sysroot或者额外的库目录,可以这样:
list(APPEND CMAKE_FIND_ROOT_PATH /opt/arm-sysroot /opt/extra-libs)
我曾经在一个项目里,因为Sysroot里缺了一个第三方库,就把那个库单独放在另一个目录,然后加到CMAKE_FIND_ROOT_PATH里。这样既保持了Sysroot的纯净,又解决了依赖问题。
知识体系总览
下面这张图,我把工具链和交叉编译的核心逻辑梳理了一下:
说白了,整个交叉编译的配置就是围绕这三件事展开的。你只要把工具链文件写对了,CMake就能自动帮你搞定剩下的工作。我个人建议,每个交叉编译项目都单独维护一个工具链文件,并放在项目的cmake/目录下。这样团队成员一看就知道怎么配置,省去不少沟通成本。
最后提醒一句:交叉编译环境搭建好之后,先用cmake --system-information看看检测结果,确认所有变量都符合预期。我曾经因为Sysroot路径写错了一个字母,排查了整整一个下午。嗯,这种低级错误,犯过一次就够了。