7. 编写第一个工具链文件:set()设置CMAKE_SYSTEM_NAME、CMAKE_C_COMPILER等核心变量

好,咱们终于要动手写第一个工具链文件了。

说实话,我刚开始学交叉编译那会儿,觉得工具链文件特别神秘。好像是个黑盒子,填几个变量就能让CMake乖乖给ARM、RISC-V生成代码。后来踩了几次坑才明白——工具链文件其实就是告诉CMake三件事:你在为谁编译、用什么编译器、目标平台长什么样。

7.1 工具链文件到底长什么样?

说白了,工具链文件就是一个.cmake文件。里面全是set()命令。没有复杂的逻辑,没有循环判断。你想想看,CMake本身已经够复杂了,工具链文件要是再搞一堆宏和函数,那不乱套了?

我个人习惯把工具链文件放在项目根目录的cmake/文件夹下。比如:

my_project/
├── CMakeLists.txt
├── cmake/
│   └── arm-none-eabi.cmake
└── src/
    └── main.c

这样结构清晰,一看就知道交叉编译相关的配置都在cmake/里。

7.2 第一个核心变量:CMAKE_SYSTEM_NAME

这个变量是工具链文件的灵魂。它告诉CMake:你现在不是在给Linux编译,也不是在给Windows编译,而是在给一个嵌入式目标编译。

常见的取值有:

CMAKE_SYSTEM_NAME 取值 含义
Generic 裸机系统(无操作系统)
Linux 嵌入式Linux
Windows Windows桌面/嵌入式
Android Android NDK交叉编译
iOS 苹果移动端

我做过一个STM32的项目,当时就设成了Generic。为什么?因为STM32跑的是裸机程序,没有操作系统。CMake看到Generic,就知道不需要检测线程库、不需要找动态链接器——这些在裸机上根本不存在。

重要经验: 如果你在给RTOS(比如FreeRTOS、RT-Thread)做交叉编译,CMAKE_SYSTEM_NAME 仍然设成 Generic。RTOS不是完整的操作系统,CMake不认它。

7.3 第二个核心变量:CMAKE_C_COMPILER

这个更直接——就是你的交叉编译器路径。我见过有人在这里栽跟头,写了个相对路径,结果换个目录编译就找不到了。

我的建议是:要么写绝对路径,要么确保编译器在PATH环境变量里

# 推荐写法:绝对路径
set(CMAKE_C_COMPILER /usr/local/gcc-arm-none-eabi-10.3-2021.10/bin/arm-none-eabi-gcc)

# 或者:编译器在PATH中,直接写名字
set(CMAKE_C_COMPILER arm-none-eabi-gcc)

嗯,这里要注意:CMAKE_C_COMPILER 设的是C编译器。如果你还要编译C++代码,记得同时设置 CMAKE_CXX_COMPILER

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

我曾经犯过一个低级错误——只设了C编译器,没设C++编译器。结果项目里有一个C++文件,CMake直接报错说找不到编译器。排查了半天才发现是这个问题。

7.4 其他常用核心变量

除了上面两个,还有几个变量我几乎每个工具链文件都会设:

  • CMAKE_ASM_COMPILER:汇编器。嵌入式开发经常要写启动代码,这个不能少。
  • CMAKE_AR:归档工具,用于生成静态库。
  • CMAKE_OBJCOPY:用于生成.bin、.hex等烧录文件。
  • CMAKE_SIZE:查看编译后的代码大小。

你看,这些工具其实都是交叉编译器工具链自带的。比如ARM GCC工具链里,它们分别是:

arm-none-eabi-gcc      # 编译器
arm-none-eabi-g++      # C++编译器
arm-none-eabi-as       # 汇编器
arm-none-eabi-ar       # 归档器
arm-none-eabi-objcopy  # 格式转换
arm-none-eabi-size     # 大小分析

7.5 第一个完整的工具链文件

好了,咱们把上面这些拼起来,写一个完整的工具链文件。以ARM Cortex-M4为例:

# cmake/arm-none-eabi.cmake

# 目标系统:裸机
set(CMAKE_SYSTEM_NAME Generic)

# 目标处理器架构
set(CMAKE_SYSTEM_PROCESSOR cortex-m4)

# 编译器
set(CMAKE_C_COMPILER   arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
set(CMAKE_ASM_COMPILER arm-none-eabi-gcc)

# 工具链其他工具
set(CMAKE_AR           arm-none-eabi-ar)
set(CMAKE_OBJCOPY      arm-none-eabi-objcopy)
set(CMAKE_OBJDUMP      arm-none-eabi-objdump)
set(CMAKE_SIZE         arm-none-eabi-size)

# 重要:告诉CMake不要尝试检测编译器
# 交叉编译环境下,很多检测会失败
set(CMAKE_TRY_COMPILE_TARGET_TYPE STATIC_LIBRARY)
小技巧: 最后一行 CMAKE_TRY_COMPILE_TARGET_TYPE 设成 STATIC_LIBRARY,可以跳过CMake的编译器运行时检测。我在做RISC-V交叉编译时,如果不加这一行,CMake会尝试编译一个可执行文件并运行它——但目标板上根本没有操作系统,怎么可能运行?加上这一行就绕过了这个坑。

7.6 怎么使用这个工具链文件?

使用方式很简单,在配置项目时通过 -DCMAKE_TOOLCHAIN_FILE 指定:

mkdir build && cd build
cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/arm-none-eabi.cmake

就这么一行命令,CMake就会读取工具链文件,用ARM GCC来编译你的代码。

我个人习惯在项目根目录放一个 build.sh 脚本,把这条命令写进去。这样团队成员不用记复杂的参数,直接 ./build.sh 就能开始编译。

7.7 核心逻辑流程图

下面这张图展示了工具链文件在CMake构建流程中的位置和作用:

工具链文件在CMake构建流程中的位置 CMakeLists.txt cmake -DCMAKE_TOOLCHAIN_FILE=... 工具链文件 (.cmake) CMAKE_SYSTEM_NAME 目标平台 CMAKE_C_COMPILER 编译器路径 其他核心变量 汇编器/归档器等 CMake读取工具链文件 → 确定交叉编译环境 → 生成构建系统

7.8 避坑指南

我曾经踩过的坑:
  • 忘记设置CMAKE_SYSTEM_NAME:CMake默认检测为本机系统,然后发现编译器是arm-none-eabi-gcc,直接报错退出。
  • 编译器路径写错:写了个不存在的路径,CMake报错说找不到编译器。排查了半小时才发现是路径少了个斜杠。
  • 只设了C编译器没设C++编译器:项目里混用C和C++时,C++文件编译失败。

嗯,其实工具链文件没那么玄乎。你只要记住:告诉CMake你在为谁编译、用什么编译,剩下的CMake会帮你搞定。刚开始写的时候,照着上面的模板改改就能用。等用熟了,再慢慢加一些高级配置。

我个人建议:第一个工具链文件不要贪多。先把 CMAKE_SYSTEM_NAMECMAKE_C_COMPILER 这两个核心变量设对,能编译通过再说。其他的变量,用到的时候再加也不迟。


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