预设与工具链:CMakePresets.json,工具链文件,交叉编译入门

说实话,CMake 的项目配置,早期是个挺让人头疼的事。你想想看,同一个项目,要在 Windows 上编译,要在 Linux 上编译,还要给嵌入式 ARM 板子交叉编译。每次换环境,都得重新敲一堆 -D 参数,或者改 CMakeLists.txt 里的逻辑。我个人习惯是,能自动化的事情绝不手动重复。所以 CMakePresets.json 这个东西一出来,我就觉得,嗯,这才是现代构建该有的样子。

CMakePresets.json:统一配置的利器

CMakePresets 是 CMake 3.19 引入的特性。它的核心作用,说白了就是把所有构建配置集中到一个 JSON 文件里。你不需要再记那些长长的命令行参数了。

一个典型的 CMakePresets.json 文件长这样:

{
  "version": 3,
  "configurePresets": [
    {
      "name": "default",
      "displayName": "默认配置",
      "description": "使用 Ninja 生成器,Debug 模式",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/default",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Debug",
        "CMAKE_CXX_STANDARD": "17"
      }
    },
    {
      "name": "release",
      "displayName": "发布配置",
      "description": "Release 模式,优化全开",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/release",
      "cacheVariables": {
        "CMAKE_BUILD_TYPE": "Release",
        "CMAKE_CXX_FLAGS": "-O3 -march=native"
      }
    }
  ],
  "buildPresets": [
    {
      "name": "default",
      "configurePreset": "default"
    },
    {
      "name": "release",
      "configurePreset": "release"
    }
  ]
}

有了这个文件,你只需要执行:

cmake --preset=release
cmake --build --preset=release

搞定。我在项目中遇到过团队里有人总记不住 -DCMAKE_BUILD_TYPE=Release 怎么拼,用了 Presets 之后,这种问题再也没出现过。

小提示: CMakePresets.json 支持 inherit 字段。你可以定义一个基础 preset,然后其他 preset 继承它,只覆盖需要改的字段。这样能减少大量重复配置。

工具链文件(Toolchain):跨平台编译的基石

工具链文件,是 CMake 里一个非常强大但容易被忽视的功能。它本质上是一个 CMake 脚本文件,在项目配置的早期阶段被加载,用来告诉 CMake:你要用什么编译器、链接器、以及各种工具的位置。

为什么要用工具链文件?因为有时候你没法直接靠 -DCMAKE_C_COMPILER=xxx 搞定一切。比如交叉编译时,不光编译器要换,系统根目录、链接器、归档器、甚至 pkg-config 的路径都得换。工具链文件就是干这个的。

一个典型的 ARM 交叉编译工具链文件 arm-linux-gnueabihf.cmake

# 目标系统名称
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)

# 指定交叉编译器
set(CMAKE_C_COMPILER   arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)

# 指定系统根目录(sysroot)
set(CMAKE_SYSROOT /opt/arm-linux-gnueabihf/sysroot)

# 查找程序时,只在 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 -DCMAKE_TOOLCHAIN_FILE=arm-linux-gnueabihf.cmake ..
注意: 工具链文件必须在第一次 cmake 配置时指定。之后如果切换工具链,必须删除 CMakeCache.txtCMakeFiles 目录,重新配置。我曾经因为忘了删缓存,折腾了整整一个下午才发现问题。

交叉编译入门:从原理到实践

交叉编译,简单说就是在一台机器(宿主机)上编译出能在另一台机器(目标机)上运行的程序。比如你在 x86 的笔记本上编译 ARM 架构的 Linux 程序。

为什么会需要交叉编译?因为目标机往往资源有限——跑个 Linux 系统都勉强,更别说跑 GCC 了。你想想看,一个嵌入式设备可能只有 64MB 内存,而编译 Chromium 需要 16GB 内存。所以,我们得在强大的宿主机上完成编译工作。

交叉编译的核心三要素:

  • 交叉编译器:能生成目标架构机器码的编译器。比如 arm-linux-gnueabihf-gcc
  • 系统根目录(sysroot):目标机的根文件系统副本,包含目标机上的库和头文件。
  • 工具链文件:告诉 CMake 以上两样东西在哪。

下面我用一个完整的例子,带你走一遍交叉编译的流程。

实战:为 ARM Linux 交叉编译一个 Hello World

项目结构:

cross-hello/
├── CMakeLists.txt
├── arm-linux-gnueabihf.cmake
└── main.cpp

main.cpp

#include <iostream>

int main() {
    std::cout << "Hello from ARM!" << std::endl;
    return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.20)
project(CrossHello LANGUAGES CXX)

add_executable(hello main.cpp)

# 可选:设置输出名称前缀,方便识别
set_target_properties(hello PROPERTIES
    OUTPUT_NAME "hello_arm"
)

然后执行:

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

编译完成后,用 file 命令检查生成的可执行文件:

$ file hello_arm
hello_arm: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux-armhf.so.3, for GNU/Linux 3.2.0, not stripped

看到 ARM 字样了吗?说明编译成功了。把这个文件拷贝到 ARM 开发板上,就能直接运行。

核心要点: 交叉编译不是魔法。它只是告诉编译器:「别用你本机的头文件和库,用我给的那一套。」工具链文件就是干这个的桥梁。

结合 CMakePresets 与工具链文件

你完全可以把工具链文件路径写到 Presets 里。这样团队里每个人都能一键交叉编译,不用再记那些复杂的路径。

{
  "version": 3,
  "configurePresets": [
    {
      "name": "arm-debug",
      "displayName": "ARM Debug",
      "generator": "Ninja",
      "binaryDir": "${sourceDir}/build/arm-debug",
      "cacheVariables": {
        "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/cmake/arm-linux-gnueabihf.cmake",
        "CMAKE_BUILD_TYPE": "Debug"
      }
    }
  ]
}

然后:

cmake --preset=arm-debug
cmake --build --preset=arm-debug

简单明了。我在公司里推广这套方案后,新同事上手交叉编译的时间从半天缩短到了十分钟。

知识体系总览

下面这张图,帮你理清本章的核心逻辑:

CMake 预设与工具链知识体系 CMakePresets.json 统一管理构建配置 工具链文件(.cmake) 指定编译器、sysroot、查找策略 交叉编译 宿主机 → 目标机(ARM/RISC-V等) 交叉编译器 arm-linux-gnueabihf-gcc 系统根目录(sysroot) 目标机库与头文件 查找策略 FIND_ROOT_PATH_MODE Presets + 工具链文件 = 可复现、跨平台的构建方案

避坑指南

最后,分享几个我踩过的坑:

  • 缓存问题:切换工具链时,一定要清空 build 目录。CMake 会缓存编译器检测结果,不清空的话,它会用旧的编译器继续编译。
  • sysroot 路径:工具链文件里的 CMAKE_SYSROOT 路径不要写错。我曾经把路径写成了 /opt/arm-sysroot 但实际是 /opt/arm-sysroot/(多了一个斜杠),结果 CMake 找不到任何库,报了一堆莫名其妙的链接错误。
  • Presets 版本:CMakePresets.json 的 version 字段要写对。目前最新是 version 6,但如果你用旧版 CMake(比如 3.19),只能用 version 3。我建议至少用 CMake 3.23 以上,支持更完整的 Presets 特性。
  • 工具链文件中的变量作用域:工具链文件里设置的变量,比如 CMAKE_C_COMPILER,是全局的。不要在 CMakeLists.txt 里用 set(... CACHE ...) 去覆盖它,否则会出问题。

嗯,以上就是本章的全部内容。记住,工具链文件和 Presets 是 CMake 项目工程化的基石。把它们用好,你的构建系统会变得非常清晰和可维护。


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