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:构建类型,比如DebugReleaseMinSizeRel
  • 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变量的整个体系。这样你脑子里能有个全局印象。

CMake变量体系总览 普通变量 set() 定义,作用域受限 缓存变量 set(... CACHE ...) 持久化 预设变量 CMake内置,如CMAKE_C_COMPILER set(VAR value) 默认当前目录及子目录可见 set(VAR value PARENT_SCOPE) 向上传递到父作用域 set(VAR val CACHE TYPE "desc") 存入CMakeCache.txt set(VAR val CACHE ... FORCE) 强制覆盖缓存中的值 option(VAR "desc" ON/OFF) BOOL类型缓存的快捷方式 CMAKE_C_COMPILER C编译器路径 CMAKE_BUILD_TYPE Debug/Release等 CMAKE_INSTALL_PREFIX 安装路径 核心原则:普通变量临时用,缓存变量持久化,预设变量直接查

核心要点:

  • 普通变量用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