12、CMake变量与缓存:变量的定义与作用域,缓存变量(set CACHE)的使用,CMake预设变量(CMAKE_C_COMPILER等)
好,咱们今天聊聊CMake里的变量。说实话,我刚开始用CMake那会儿,觉得变量不就是个存东西的盒子嘛,有啥好讲的?结果被坑了几次之后才发现,这里面的门道还真不少。尤其是缓存变量,搞不明白的话,你改个编译器路径能折腾半天。
咱们先理清一个概念:CMake里的变量,说白了就是字符串。不管你是存路径、存开关、存版本号,底层全是字符串。但它的作用域和传递方式,跟C语言里的全局变量、局部变量有点像,又不完全一样。
12.1 变量的定义与作用域
定义一个变量,最直接的方式就是set()命令。比如:
set(MY_VAR "hello world")
set(MY_LIST a b c d) # 这其实是个列表,但底层还是字符串
这里有个细节:set()定义的变量,默认只在当前CMakeLists.txt文件以及它add_subdirectory()引入的子目录里可见。父目录看不到子目录的变量,但子目录能看到父目录的。嗯,这就是所谓的「向下可见,向上不可见」。
我在项目里遇到过一个问题:顶层CMakeLists.txt里定义了一个PROJECT_ROOT变量,结果子目录里的CMakeLists.txt死活读不到。后来才发现,是因为我在子目录里又set()了一遍,把父目录的值给覆盖了。所以记住:子目录可以继承父目录的变量,但如果你在子目录里重新set,那就变成子目录自己的了。
如果你想强制让变量在父子目录间共享,可以用PARENT_SCOPE:
set(MY_VAR "shared" PARENT_SCOPE)
这会把变量设置到父作用域里。说白了就是「往上捅」。
12.2 缓存变量:set CACHE 的使用
好,接下来是重头戏——缓存变量。你想想看,普通变量只在CMake运行期间存在,运行完就没了。但有些东西,比如编译器路径、安装前缀、编译选项,你希望这次配置完,下次再跑CMake的时候还能记住。这时候就需要缓存变量了。
语法很简单:
set(MY_CACHE_VAR "default_value" CACHE STRING "Description of this variable")
注意这个CACHE关键字。它告诉CMake:把这个变量存到CMakeCache.txt文件里。下次你再运行cmake,它会先读缓存,所以变量值就保留下来了。
这里有个坑:如果缓存里已经有这个变量了,set(... CACHE ...)不会覆盖它。除非你加FORCE:
set(MY_CACHE_VAR "new_value" CACHE STRING "Description" FORCE)
我曾经犯过一个错误:在CMakeLists.txt里用set(... CACHE ...)设置了一个默认值,然后用户通过-D参数传了一个新值。结果因为缓存里已经存了旧值,用户传的新值根本没生效。排查了半天才发现是缓存没清。所以我的习惯是:对于用户可配置的变量,用option()或者set(... CACHE ...),但一定要在文档里说清楚:如果改了没生效,先删CMakeCache.txt。
缓存变量的类型也很重要。常用的有:
| 类型 | 说明 | 示例 |
|---|---|---|
| STRING | 普通字符串 | set(TOOLCHAIN "gcc" CACHE STRING "编译器") |
| BOOL | 布尔值,ON/OFF | set(ENABLE_DEBUG ON CACHE BOOL "调试开关") |
| FILEPATH | 文件路径,会弹出文件选择对话框 | set(MY_CONFIG "config.ini" CACHE FILEPATH "配置文件") |
| PATH | 目录路径 | set(INSTALL_DIR "/usr/local" CACHE PATH "安装目录") |
| INTERNAL | 内部变量,不显示在cmake-gui中 | set(HIDDEN_VAR "secret" CACHE INTERNAL "隐藏") |
我个人建议:凡是需要用户通过cmake -D或者cmake-gui修改的变量,都用CACHE定义。这样用户体验好,也符合CMake的设计哲学。
12.3 CMake预设变量
CMake自带了一堆预设变量,说白了就是它帮你定义好的、可以直接用的变量。这些变量大部分都是缓存变量,存在CMakeCache.txt里。
最常用的几个:
- CMAKE_C_COMPILER:C编译器路径。比如
/usr/bin/gcc或/usr/bin/clang。 - CMAKE_CXX_COMPILER:C++编译器路径。
- CMAKE_BUILD_TYPE:构建类型,比如
Debug、Release、MinSizeRel。 - CMAKE_INSTALL_PREFIX:安装路径,默认是
/usr/local。 - CMAKE_SOURCE_DIR:顶层CMakeLists.txt所在的目录。
- CMAKE_BINARY_DIR:构建目录,也就是你运行
cmake命令的目录。 - CMAKE_CURRENT_SOURCE_DIR:当前正在处理的CMakeLists.txt所在的目录。
- CMAKE_CURRENT_BINARY_DIR:当前CMakeLists.txt对应的构建目录。
举个例子,嵌入式项目里经常要指定交叉编译器:
cmake -DCMAKE_C_COMPILER=/usr/bin/arm-none-eabi-gcc -DCMAKE_CXX_COMPILER=/usr/bin/arm-none-eabi-g++ ..
这样CMake就会用你指定的编译器,而不是系统默认的gcc。我在做STM32项目时,每次都要这么传,后来干脆写了个工具链文件,用CMAKE_TOOLCHAIN_FILE变量指定,省事多了。
还有一个我经常用的:CMAKE_MODULE_PATH。这个变量告诉CMake去哪里找自定义的.cmake模块文件。比如你写了一个FindMyLib.cmake,放在cmake/modules/目录下,就可以这样:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake/modules")
find_package(MyLib)
嗯,这里要注意:CMAKE_MODULE_PATH是个列表,你可以追加多个路径。
12.4 知识体系总览
说了这么多,咱们用一张图来总结一下CMake变量的整个体系。这样你脑子里能有个全局印象。
核心要点:
- 普通变量用
set(),作用域向下传递,向上需用PARENT_SCOPE - 缓存变量用
set(... CACHE ...),存到CMakeCache.txt,跨次构建保留 - 预设变量是CMake帮你定义好的,直接使用即可,大部分也是缓存变量
- 用户配置用
option()或set(... CACHE BOOL ...),别用普通变量
我的小技巧:
调试变量值时,可以用message()打印出来:message(STATUS "MY_VAR = ${MY_VAR}")。这样你能看到变量到底有没有被正确设置。我每次写新的CMakeLists.txt,都会先加几行message,确认变量传递没问题再往下写。
避坑指南:
我曾经在项目里用set(CMAKE_C_FLAGS "-O2" CACHE STRING "C flags"),结果发现每次重新配置时,之前用户通过-D传的优化选项都被覆盖了。原因就是set(... CACHE ...)不带FORCE时,如果缓存里已经有值,它不会覆盖。但如果你在CMakeLists.txt里第一次定义这个变量,它就会写入缓存,覆盖用户通过-D传入的值。正确的做法是:用set(... CACHE ...)提供默认值,但不要放在会重复执行的地方。或者干脆用if(NOT DEFINED ...)包裹一下。
好了,关于CMake变量和缓存,咱们就聊到这儿。记住一句话:变量是CMake的血液,缓存是CMake的记忆。搞懂了这两样,你的CMake水平就能上一个台阶。
公众号:蓝海资料掘金营,微信deep3321