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。
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_COMPILER和CMAKE_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变量的全貌:
13.7 实战建议
最后,我总结几条实战中总结的经验:
- 能用普通变量就别用缓存变量。缓存变量会污染CMakeCache.txt,清理起来麻烦。
- 用户可配置的选项用缓存变量,并加上合适的类型(BOOL用BOOL,路径用PATH)。这样cmake-gui里能显示漂亮的控件。
- 环境变量只用来读,不要写。除非你很清楚自己在做什么。
- 内置变量不要轻易改。比如CMAKE_C_COMPILER,最好在工具链文件里设,不要在CMakeLists.txt里直接改。我曾经在项目里直接set(CMAKE_C_COMPILER ...),结果和CMake的自动检测逻辑冲突,编译链都乱了。
- 调试时多用message打印变量。我习惯在每个CMakeLists.txt开头加一行
message(STATUS "当前目录: ${CMAKE_CURRENT_SOURCE_DIR}"),方便追踪。
好了,CMake变量这块就聊到这儿。记住:变量是CMake的"血液",搞懂了它,你的构建脚本就成功了一半。