3、CMake变量与缓存:set()命令、变量作用域、缓存变量、CMake预设变量
好,咱们今天来聊聊CMake里最基础也最容易踩坑的东西——变量。
说实话,我刚开始用CMake那会儿,觉得变量嘛,不就是存个值吗?跟Shell脚本差不多。结果一上手就懵了:为什么我在子目录设了个变量,主CMakeLists.txt里读不到?为什么我改了变量值,重新cmake一下又变回去了?
嗯,这些坑我都踩过。今天咱们就把变量这摊事儿彻底捋清楚。
3.1 set()命令:变量的基本操作
CMake里定义变量,最常用的就是set()命令。语法很简单:
set(<variable> <value>... [PARENT_SCOPE])
举个例子:
set(MY_NAME "CMake实战")
set(MY_SOURCES main.cpp utils.cpp helper.cpp)
set(MY_FLAG ON)
这里要注意一点:set()可以一次给变量赋多个值,这些值会以分号分隔的列表形式存储。说白了,MY_SOURCES实际上存的是 "main.cpp;utils.cpp;helper.cpp"。
我在项目中经常看到有人这么写:
set(MY_SOURCES main.cpp)
set(MY_SOURCES ${MY_SOURCES} utils.cpp)
set(MY_SOURCES ${MY_SOURCES} helper.cpp)
这样写没问题,但啰嗦。我个人习惯直接一行搞定:
set(MY_SOURCES main.cpp utils.cpp helper.cpp)
set() 配合 ${} 展开。比如:
set(BASE_DIR "/home/user/project")
set(SRC_DIR "${BASE_DIR}/src")
这样 SRC_DIR 的值就是 /home/user/project/src。
3.2 变量作用域:为什么子目录的变量父目录看不到?
这是新手最容易困惑的地方。CMake的变量作用域,说白了就是:每个目录有自己的变量空间。
你想想看,你在 src/CMakeLists.txt 里设了一个变量:
# src/CMakeLists.txt
set(SUBDIR_VAR "我在子目录")
然后在顶层的 CMakeLists.txt 里想用这个变量:
# CMakeLists.txt
add_subdirectory(src)
message("SUBDIR_VAR = ${SUBDIR_VAR}") # 输出为空!
为什么会这样?因为 add_subdirectory() 会创建一个新的作用域。子目录里定义的变量,默认不会传递到父目录。
那怎么把子目录的变量传出来呢?有两种方式:
- 使用
PARENT_SCOPE:在子目录里这样写:
# src/CMakeLists.txt
set(SUBDIR_VAR "我在子目录" PARENT_SCOPE)
这样变量就会传递到父作用域。注意,PARENT_SCOPE 只向上传递一层,不会跨多级。
- 使用缓存变量:这个我们马上讲。
set() 修改了一个父目录定义的变量,以为父目录也会跟着变。结果调试了半天才发现,子目录的修改只影响子目录自己的副本。从那以后,我养成了一个习惯:凡是需要在多个目录间共享的变量,要么用 PARENT_SCOPE,要么用缓存变量。
3.3 缓存变量:让变量值持久化
普通变量只在当前CMake运行期间有效。你重新运行 cmake 命令,变量值就丢了。但缓存变量不同,它会写入 CMakeCache.txt 文件,下次运行 cmake 时还能读到。
定义缓存变量的语法:
set(<variable> <value> CACHE <type> <docstring> [FORCE])
其中 <type> 可以是 BOOL、STRING、FILEPATH、PATH 等。
举个例子:
set(MY_OPTION ON CACHE BOOL "是否启用某个功能")
set(MY_PATH "/usr/local" CACHE PATH "安装路径")
这里有个关键点:如果缓存变量已经存在,set() 不会覆盖它。也就是说,第一次运行 cmake 时变量被设为 ON,第二次运行时你想改成 OFF,但 set() 不会生效——除非你加上 FORCE。
set(MY_OPTION OFF CACHE BOOL "是否启用某个功能" FORCE)
加了 FORCE 之后,每次运行 cmake 都会强制覆盖缓存中的值。我个人建议:除非你明确知道自己在做什么,否则别轻易用 FORCE。因为这会破坏用户通过 cmake -D 设置的变量。
| 特性 | 普通变量 | 缓存变量 |
|---|---|---|
| 存储位置 | 内存 | CMakeCache.txt |
| 跨运行持久化 | 否 | 是 |
| 作用域 | 当前目录 | 全局 |
| 用户可通过 -D 修改 | 否 | 是 |
| FORCE 关键字 | 无 | 强制覆盖 |
3.4 CMake预设变量:那些CMake帮你定义好的变量
CMake内置了很多预设变量,你直接拿来用就行。我挑几个最常用的说说:
3.4.1 编译器相关
CMAKE_CXX_STANDARD:指定C++标准,比如17、20、23。CMAKE_C_STANDARD:指定C标准。CMAKE_CXX_COMPILER:C++编译器路径。CMAKE_C_COMPILER:C编译器路径。
用法示例:
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON) # 如果编译器不支持C++17,直接报错
set(CMAKE_CXX_EXTENSIONS OFF) # 禁用编译器扩展
我个人习惯在项目一开始就设好这些。你想想看,如果团队成员用的编译器版本不一样,不统一标准的话,编译结果可能千奇百怪。
3.4.2 构建类型相关
CMAKE_BUILD_TYPE:构建类型,常见值有Debug、Release、RelWithDebInfo、MinSizeRel。
用法:
# 在命令行中设置
# cmake -DCMAKE_BUILD_TYPE=Debug ..
# 或者在CMakeLists.txt中设置默认值
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "构建类型" FORCE)
endif()
这里有个小细节:CMAKE_BUILD_TYPE 只在单配置生成器(如Makefile、Ninja)中有效。对于Visual Studio这种多配置生成器,它会被忽略。
3.4.3 安装路径相关
CMAKE_INSTALL_PREFIX:安装根目录,默认是/usr/local(Linux)或C:/Program Files(Windows)。CMAKE_INSTALL_BINDIR:可执行文件安装目录。CMAKE_INSTALL_LIBDIR:库文件安装目录。
用法:
# 用户可以在命令行指定
# cmake -DCMAKE_INSTALL_PREFIX=/opt/myapp ..
# 项目中也可以设置默认值
set(CMAKE_INSTALL_PREFIX "/usr/local" CACHE PATH "安装路径")
3.4.4 其他常用预设变量
| 变量名 | 说明 |
|---|---|
CMAKE_SOURCE_DIR |
顶层CMakeLists.txt所在目录 |
CMAKE_BINARY_DIR |
构建目录(运行cmake的目录) |
CMAKE_CURRENT_SOURCE_DIR |
当前处理的CMakeLists.txt所在目录 |
CMAKE_CURRENT_BINARY_DIR |
当前处理目录对应的构建目录 |
PROJECT_NAME |
当前project()命令指定的项目名 |
PROJECT_SOURCE_DIR |
当前project()命令所在目录 |
PROJECT_BINARY_DIR |
当前project()命令对应的构建目录 |
CMAKE_CURRENT_SOURCE_DIR 和 CMAKE_CURRENT_BINARY_DIR,而不是 CMAKE_SOURCE_DIR 和 CMAKE_BINARY_DIR。为什么?因为如果你的项目被其他项目通过 add_subdirectory() 引用,CMAKE_SOURCE_DIR 指向的是别人的顶层目录,而不是你的。用 CURRENT 版本更安全。
3.5 知识体系总览
下面这张图把本章的核心逻辑串起来了:
3.6 避坑指南
最后,我把自己这些年踩过的坑总结一下:
- 别在子目录里随意修改父目录的变量——除非你用
PARENT_SCOPE,否则改了也白改。 - 缓存变量不是用来存临时数据的——它会被写入文件,如果你频繁修改,会导致
CMakeCache.txt混乱。 - 预设变量不要随意覆盖——比如
CMAKE_SOURCE_DIR是只读的,你改了也没用。 - 注意变量展开的顺序——
set(A ${B})时,B必须在之前定义过,否则A会是空字符串。
嗯,关于变量这块,今天就聊到这儿。记住一句话:普通变量管局部,缓存变量管全局,预设变量直接用。搞清楚了这些,CMake的变量体系你就拿下了。
公众号:蓝海资料掘金营,微信deep3321