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,就知道不需要检测线程库、不需要找动态链接器——这些在裸机上根本不存在。
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构建流程中的位置和作用:
7.8 避坑指南
- 忘记设置CMAKE_SYSTEM_NAME:CMake默认检测为本机系统,然后发现编译器是arm-none-eabi-gcc,直接报错退出。
- 编译器路径写错:写了个不存在的路径,CMake报错说找不到编译器。排查了半小时才发现是路径少了个斜杠。
- 只设了C编译器没设C++编译器:项目里混用C和C++时,C++文件编译失败。
嗯,其实工具链文件没那么玄乎。你只要记住:告诉CMake你在为谁编译、用什么编译,剩下的CMake会帮你搞定。刚开始写的时候,照着上面的模板改改就能用。等用熟了,再慢慢加一些高级配置。
我个人建议:第一个工具链文件不要贪多。先把 CMAKE_SYSTEM_NAME 和 CMAKE_C_COMPILER 这两个核心变量设对,能编译通过再说。其他的变量,用到的时候再加也不迟。
公众号:蓝海资料掘金营,微信deep3321