6、CMake变量与缓存:变量作用域、缓存变量(set CACHE)、CMake预设变量、option()与条件编译
CMake的变量系统,说实话,是很多初学者最容易踩坑的地方。我自己刚接触CMake那会儿,就被变量作用域搞得晕头转向——明明在顶层设了个变量,进到子目录怎么就没了?后来才明白,CMake的变量不是全局的,它有自己的“地盘”概念。
6.1 变量作用域:每个目录都有自己的小世界
CMake的变量作用域,说白了就是“父传子,子不传父”。每个add_subdirectory()调用,都会创建一个新的作用域。子目录能看见父目录的变量,但反过来不行。
# 顶层 CMakeLists.txt
set(MY_VAR "hello")
add_subdirectory(sub)
# sub/CMakeLists.txt
message("MY_VAR = ${MY_VAR}") # 输出:hello
set(MY_VAR "world") # 修改的是子作用域的副本
# 回到顶层,MY_VAR 仍然是 "hello"
嗯,这里要注意:set()默认只修改当前作用域。如果你想让变量穿透到子目录,可以用PARENT_SCOPE关键字。我在项目中遇到过这样一个坑:在子目录里改了变量,以为顶层也能用,结果编译出来全是错的。排查了半天才发现是作用域问题。
PARENT_SCOPE,并在注释里说明原因。
6.2 缓存变量:让设置“记住”
普通变量只在CMake运行期间有效。下次运行cmake ..,一切归零。缓存变量就不一样了——它会被写入CMakeCache.txt,下次构建时自动加载。
set(MY_CACHE_VAR "default" CACHE STRING "这是一个缓存变量")
缓存变量的典型用法是让用户通过cmake -D来覆盖默认值。比如:
cmake -DMY_CACHE_VAR="custom" ..
我个人建议:只有那些需要用户配置的、跨构建保留的变量,才用CACHE。内部临时变量别用缓存,否则CMakeCache.txt会变得又臭又长。
set(... CACHE ...),如果不加 FORCE,第二次调用不会覆盖第一次的值。因为缓存变量一旦设置,就“赖着不走”了。
6.3 CMake预设变量:系统帮你准备好了
CMake内置了很多变量,直接拿来用就行。我整理了一份常用的:
| 变量名 | 含义 | 示例值 |
|---|---|---|
CMAKE_SOURCE_DIR |
顶层源码目录 | /home/user/project |
CMAKE_BINARY_DIR |
构建目录 | /home/user/project/build |
CMAKE_CURRENT_SOURCE_DIR |
当前处理的源码目录 | /home/user/project/src |
CMAKE_CURRENT_BINARY_DIR |
当前处理的构建目录 | /home/user/project/build/src |
PROJECT_NAME |
当前项目名称 | MyProject |
PROJECT_SOURCE_DIR |
当前项目的源码目录 | /home/user/project |
PROJECT_BINARY_DIR |
当前项目的构建目录 | /home/user/project/build |
CMAKE_BUILD_TYPE |
构建类型 | Debug, Release |
CMAKE_CXX_COMPILER |
C++编译器路径 | /usr/bin/g++ |
CMAKE_CXX_STANDARD |
C++标准版本 | 17, 20 |
你想想看,如果没有这些预设变量,每次都要自己写路径,那得多痛苦。我个人最常用的是CMAKE_CURRENT_SOURCE_DIR和CMAKE_CURRENT_BINARY_DIR,尤其是在处理子项目时,它们能帮你精确定位文件位置。
6.4 option()与条件编译:让构建灵活起来
option()本质上就是一个布尔型的缓存变量。它的典型用法是让用户选择是否启用某个功能:
option(USE_OPENCV "是否启用OpenCV支持" ON)
if(USE_OPENCV)
find_package(OpenCV REQUIRED)
target_link_libraries(myapp PRIVATE ${OpenCV_LIBS})
target_compile_definitions(myapp PRIVATE USE_OPENCV=1)
endif()
用户可以通过-DUSE_OPENCV=OFF来关闭它。我在项目中经常用这种方式做功能开关——比如调试日志、性能分析、实验性特性等。
- 功能开关:用
option()+target_compile_definitions()在代码中控制#ifdef - 平台判断:用
WIN32,APPLE,UNIX等预设变量 - 编译器判断:用
CMAKE_CXX_COMPILER_ID区分 GCC、Clang、MSVC
举个例子,我曾经需要同时支持Linux和Windows的串口通信:
if(WIN32)
target_sources(myapp PRIVATE serial_win.cpp)
else()
target_sources(myapp PRINARY serial_posix.cpp)
endif()
这样代码里就不用写一堆#ifdef _WIN32了,构建系统层面就帮你分好了。
知识体系总览
下面这张图总结了CMake变量系统的核心脉络:
总结一下:普通变量管内部逻辑,缓存变量管用户配置,预设变量管环境适配。再加上option()做条件编译,你就能写出灵活又健壮的CMake脚本了。嗯,这些技巧我在实际项目中反复用过,确实能省不少事。