13、CMake变量:set与unset、缓存变量、环境变量、内置变量

CMake里的变量,说白了就是构建系统的"记忆单元"。你告诉它编译器在哪、要不要开优化、链接哪个库……这些信息全得靠变量来存。我刚开始用CMake时,总觉得变量这玩意儿太简单了,不就是赋值取值嘛。结果后来被缓存变量坑过一次——改了半天源码,构建目录里死活不生效,气得我差点砸键盘。

嗯,咱们今天就把CMake变量这摊事儿彻底捋清楚。从最基础的set/unset,到缓存变量、环境变量,再到那些内置的"系统变量"(比如CMAKE_C_COMPILER),一个一个来。

13.1 set与unset:最基础的变量操作

先看最常用的set命令。它的语法其实很简单:

set(变量名 值...)

注意那个"值...",意思是你可以给一个变量赋多个值,CMake会自动把它们拼成一个列表(用分号分隔)。举个例子:

set(MY_SOURCES main.c utils.c driver.c)
# 现在 MY_SOURCES 的值是 "main.c;utils.c;driver.c"

我个人习惯用set来定义源文件列表、编译选项这些"局部"信息。比如:

set(APP_NAME "my_firmware")
set(APP_VERSION 2.1.0)
set(CMAKE_C_STANDARD 11)

要删除一个变量,用unset:

unset(MY_SOURCES)

这里有个小坑——unset之后,变量就变成"未定义"状态,不是空字符串。如果你用if(MY_SOURCES)判断,它会走FALSE分支。我在项目中遇到过有人用set(MY_SOURCES "")来"清空"变量,结果if判断还是TRUE,因为变量存在只是值为空。所以想彻底清除,老老实实用unset。

注意:set和unset默认操作的是"普通变量",只在当前CMakeLists.txt及其子目录中有效。想跨目录共享?得用缓存变量或者全局属性。

13.2 缓存变量:跨目录的"持久化"存储

缓存变量是CMake里最容易让人迷惑的东西之一。它的作用是在整个构建树中共享变量值,而且会写入CMakeCache.txt文件。下次你再运行cmake,这些值还在。

语法长这样:

set(变量名 值 CACHE 类型 "描述信息" [FORCE])

其中"类型"可以是BOOL、FILEPATH、PATH、STRING、INTERNAL等。举个例子:

set(MY_CROSS_COMPILER "arm-none-eabi-gcc" CACHE STRING "交叉编译器路径")
set(ENABLE_DEBUG ON CACHE BOOL "是否开启调试输出")

为什么要用缓存变量?我举个真实场景:你写了一个库,用户需要指定安装路径。如果用普通变量,用户每次都得重新设。用缓存变量,用户第一次通过-D选项指定后,CMakeCache.txt就记住了,下次直接复用。

FORCE参数是个双刃剑。加了FORCE,每次cmake运行都会覆盖缓存里的值。不加的话,如果缓存里已经有值了,set会被忽略。我曾经在项目里用FORCE覆盖了用户手动设置的路径,结果用户编译报错找了我半天……从那以后,我只有在绝对确定时才用FORCE。

经验之谈:缓存变量最适合用来做"用户可配置的选项"。比如选择编译器、开关功能、指定路径。普通变量则适合内部临时数据。

13.3 环境变量:读取宿主机的"上下文"

CMake里读取环境变量用$ENV{变量名},设置环境变量用set(ENV{变量名} 值)。注意:set(ENV{...})只影响当前cmake进程及其子进程,不会修改宿主机真正的环境变量。

我经常用环境变量来传递一些"运行时上下文"。比如:

# 读取PATH环境变量
message("当前PATH: $ENV{PATH}")

# 设置临时环境变量,给自定义命令用
set(ENV{TOOLCHAIN_PATH} "/opt/gcc-arm-none-eabi/bin")
add_custom_command(
  OUTPUT firmware.hex
  COMMAND $ENV{TOOLCHAIN_PATH}/arm-none-eabi-objcopy -O ihex firmware.elf firmware.hex
)

这里有个常见的坑:环境变量在CMake配置阶段就解析了,不是构建阶段。如果你在add_custom_command里用$ENV{...},它会在cmake运行时展开,而不是在make运行时。想延迟展开?用$ENV{变量名}的生成器表达式形式?不行,生成器表达式不支持环境变量。所以我的建议是:环境变量只用来传递"配置时"的信息,比如SDK路径、用户名、构建编号等。

13.4 内置变量:CMake自带的"系统信息"

CMate提供了大量内置变量,我挑几个最常用的说说。

变量名 含义 典型值
CMAKE_C_COMPILER C编译器路径 /usr/bin/gcc 或 arm-none-eabi-gcc
CMAKE_CXX_COMPILER C++编译器路径 /usr/bin/g++
CMAKE_SOURCE_DIR 顶层源码目录 /home/user/project
CMAKE_BINARY_DIR 构建目录 /home/user/project/build
CMAKE_CURRENT_SOURCE_DIR 当前处理的CMakeLists.txt所在目录 /home/user/project/src
CMAKE_SYSTEM_NAME 目标系统名称 Linux、Windows、Generic
CMAKE_SIZEOF_VOID_P 指针大小(字节) 4 或 8

我最常用的是CMAKE_C_COMPILERCMAKE_SYSTEM_NAME。做交叉编译时,你得在工具链文件里设置这两个变量:

# toolchain.cmake
set(CMAKE_SYSTEM_NAME Generic)
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)

然后通过-DCMAKE_TOOLCHAIN_FILE=toolchain.cmake来加载。CMake看到CMAKE_SYSTEM_NAME不是"Linux"或"Windows",就知道你在做交叉编译,会自动跳过一些宿主机的检测。

小技巧:message("CMAKE_C_COMPILER = ${CMAKE_C_COMPILER}")来调试。我每次换工具链都会先打印一下,确认没设错。

13.5 变量作用域与优先级

CMake里变量有作用域的概念。简单说:

  • 普通变量:只在当前CMakeLists.txt及其子目录(通过add_subdirectory添加的)中可见。
  • 缓存变量:全局可见,所有目录都能访问。
  • 环境变量:全局可见,但只读(除非你用set(ENV{...})临时改)。

当普通变量和缓存变量同名时,普通变量优先。这个设计是为了让你能在局部"覆盖"缓存变量的值。比如:

# 缓存里定义了 CMAKE_BUILD_TYPE=Release
# 但在某个子目录里,你想临时用 Debug
set(CMAKE_BUILD_TYPE Debug)  # 普通变量覆盖缓存变量

嗯,这里要注意:覆盖只在当前目录及其子目录有效。回到父目录,缓存变量的值又恢复了。这个机制我用来做"多配置构建"——同一个源码树,不同子目录用不同的编译选项。

13.6 知识体系总览

下面这张图帮你理清CMake变量的全貌:

CMake变量体系 普通变量 set/unset 局部作用域 子目录可见 缓存变量 set(... CACHE ...) 全局持久化 写入CMakeCache.txt 环境变量 $ENV{...} 宿主机上下文 配置时解析 内置变量 CMAKE_C_COMPILER CMAKE_SYSTEM_NAME CMAKE_SOURCE_DIR 作用域与优先级 普通变量 > 缓存变量 子目录继承父目录 FORCE覆盖缓存

13.7 实战建议

最后,我总结几条实战中总结的经验:

  1. 能用普通变量就别用缓存变量。缓存变量会污染CMakeCache.txt,清理起来麻烦。
  2. 用户可配置的选项用缓存变量,并加上合适的类型(BOOL用BOOL,路径用PATH)。这样cmake-gui里能显示漂亮的控件。
  3. 环境变量只用来读,不要写。除非你很清楚自己在做什么。
  4. 内置变量不要轻易改。比如CMAKE_C_COMPILER,最好在工具链文件里设,不要在CMakeLists.txt里直接改。我曾经在项目里直接set(CMAKE_C_COMPILER ...),结果和CMake的自动检测逻辑冲突,编译链都乱了。
  5. 调试时多用message打印变量。我习惯在每个CMakeLists.txt开头加一行message(STATUS "当前目录: ${CMAKE_CURRENT_SOURCE_DIR}"),方便追踪。

好了,CMake变量这块就聊到这儿。记住:变量是CMake的"血液",搞懂了它,你的构建脚本就成功了一半。


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