第47章:工具链文件:编写Toolchain.cmake进行交叉编译

交叉编译,说白了就是在一台电脑上编译出能在另一台不同架构的机器上跑的程序。比如你在x86的笔记本上编译ARM架构的嵌入式程序。嗯,这活儿我干了好多年了,刚开始那会儿真是踩了不少坑。

CMake里做交叉编译,核心就是工具链文件(Toolchain.cmake)。它告诉CMake:编译器在哪、目标系统是什么、链接器怎么用。没有它,CMake会默认用本机编译器,编译出来的东西根本跑不到目标板子上。

一句话总结:Toolchain.cmake 就是给CMake的一张「身份卡」,告诉它你正在为谁编译。

47.1 工具链文件长什么样?

先看一个最基础的例子。假设我们要为ARM Cortex-M4编译一个裸机程序:

# arm-none-eabi.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(CMAKE_C_COMPILER   arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

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_SYSTEM_NAME:设为 Generic 表示无操作系统。如果是Linux目标,就设 Linux
  • CMAKE_SYSTEM_PROCESSOR:目标CPU架构,比如 armaarch64mips
  • 编译器路径:直接写可执行文件名,前提是它在PATH里。也可以写绝对路径。
  • 查找模式NEVER 表示不要在本机找程序,ONLY 表示只在目标系统路径找库和头文件。

我的习惯:我会把编译器路径写成绝对路径,避免因为PATH环境变量不同导致编译失败。尤其是团队协作时,每个人的环境可能不一样。

47.2 更复杂的场景:带sysroot的交叉编译

如果你在编译Linux应用,目标板上有完整的根文件系统,那就需要指定 CMAKE_SYSROOT。我曾经在一个项目里,因为没设这个,链接时死活找不到libc,折腾了一下午。

# arm-linux-gnueabihf.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

set(TOOLCHAIN_PATH /opt/toolchains/arm-linux-gnueabihf)
set(CMAKE_SYSROOT ${TOOLCHAIN_PATH}/arm-linux-gnueabihf/sysroot)

set(CMAKE_C_COMPILER   ${TOOLCHAIN_PATH}/bin/arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PATH}/bin/arm-linux-gnueabihf-g++)

set(CMAKE_FIND_ROOT_PATH ${CMAKE_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_SYSROOT,它告诉CMake:目标系统的头文件和库都在这个目录下。CMake在查找 stdio.hlibc.so 时,就会去 ${CMAKE_SYSROOT}/usr/include${CMAKE_SYSROOT}/usr/lib 找。

注意:如果目标系统和本机系统不同(比如目标用glibc 2.28,本机用2.31),一定要设 CMAKE_SYSROOT。否则CMake可能找到本机的头文件,编译出来的二进制在目标板上跑不起来。

47.3 工具链文件的核心逻辑

我画了一张图,帮你理解工具链文件到底干了什么:

Toolchain.cmake 核心逻辑 Toolchain.cmake CMake 配置阶段 设置编译器路径 设置目标系统信息 设置查找策略 CMAKE_C_COMPILER CMAKE_SYSTEM_NAME CMAKE_FIND_ROOT_PATH 生成正确的 Makefile / Ninja 构建文件

说白了,工具链文件就是给CMake的三个核心信息:用什么编译器编译给谁用去哪里找依赖。这三个信息对了,交叉编译就成功了一大半。

47.4 使用工具链文件

写好了工具链文件,怎么用?很简单,在CMake命令行里指定:

mkdir build-arm && cd build-arm
cmake .. -DCMAKE_TOOLCHAIN_FILE=../arm-linux-gnueabihf.cmake

或者用CMake预设(Presets)的方式,我个人更推荐这个,因为可以保存常用配置:

// CMakePresets.json
{
  "version": 3,
  "configurePresets": [
    {
      "name": "arm-linux",
      "displayName": "ARM Linux 交叉编译",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build-arm",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/arm-linux-gnueabihf.cmake"
      }
    }
  ]
}

然后直接:

cmake --preset arm-linux

小技巧:我习惯在工具链文件里加一些版本检查。比如检查编译器是否存在,版本是否满足要求。这样配置阶段就能发现问题,而不是等到编译报错才去排查。

47.5 常见坑与避坑指南

交叉编译的坑,我踩过的真不少。挑几个典型的说说:

问题 原因 解决方法
编译通过,运行报错 链接了本机的库 检查 CMAKE_SYSROOT 和 CMAKE_FIND_ROOT_PATH
找不到头文件 查找模式设成了 NEVER 将 INCLUDE 模式改为 ONLY
链接器报错 编译器路径不对 用绝对路径指定编译器
CMake 检测到本机系统 CMAKE_SYSTEM_NAME 没设 显式设置目标系统名称

我曾经踩过的一个坑:有一次我忘了设 CMAKE_SYSTEM_NAME,CMake 自动检测到本机是 Linux,然后用了本机的 pkg-config 查找库。结果链接时全乱套了。从那以后,我每次写工具链文件第一行就是 set(CMAKE_SYSTEM_NAME ...)

47.6 实战:完整的工具链文件模板

下面是我个人常用的一个模板,加了版本检查和错误提示:

# my-toolchain.cmake
# 目标:ARM Cortex-A7 Linux 交叉编译

# 1. 目标系统
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 2. 工具链路径
set(TOOLCHAIN_DIR /opt/toolchains/gcc-arm-10.3-2021.07-x86_64-arm-none-linux-gnueabihf)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/arm-none-linux-gnueabihf/libc)

# 3. 编译器
set(CMAKE_C_COMPILER   ${TOOLCHAIN_DIR}/bin/arm-none-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/bin/arm-none-linux-gnueabihf-g++)

# 4. 检查编译器是否存在
if(NOT EXISTS ${CMAKE_C_COMPILER})
  message(FATAL_ERROR "找不到编译器: ${CMAKE_C_COMPILER}")
endif()

# 5. 检查编译器版本
execute_process(
  COMMAND ${CMAKE_C_COMPILER} --version
  OUTPUT_VARIABLE COMPILER_VERSION
  ERROR_QUIET
)
message(STATUS "使用编译器: ${COMPILER_VERSION}")

# 6. 查找策略
set(CMAKE_FIND_ROOT_PATH ${CMAKE_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)

# 7. 额外标志(可选)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -march=armv7-a -mfpu=neon")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=armv7-a -mfpu=neon")

这个模板我用了好几年,基本没出过问题。你根据自己的目标平台改改路径和架构标志就能用。

47.7 总结

工具链文件是CMake交叉编译的基石。你想想看,没有它,CMake就像个瞎子,不知道给谁编译、用什么编译。写好它,交叉编译就成功了一半。

核心就三点:目标系统编译器路径查找策略。把这三点搞清楚了,剩下的就是微调。嗯,多写几次就熟练了。

最后提醒一句:工具链文件写好后,先跑一次 cmake --system-information 看看CMake检测到的信息对不对。这一步能帮你省下大量调试时间。


公众号:蓝海资料掘金营,微信deep3321