4、CMake变量类型:字符串变量、列表变量、布尔变量、路径变量、缓存变量、环境变量
说实话,CMake的变量系统刚接触时有点绕。我当年第一次用CMake,就被各种变量类型搞晕过。字符串、列表、布尔、路径、缓存、环境……听起来像是一堆概念,但用熟了你会发现,其实核心就那么几种。
咱们一个一个来拆解。我会结合我踩过的坑,帮你把这些变量类型彻底搞明白。
4.1 字符串变量:最基础的单元
字符串变量是CMake里最常用的类型。说白了,你写的几乎所有值,本质上都是字符串。
set(MY_NAME "CMake实战")
set(VERSION_STR "3.28.0")
这里有个细节:引号不是必须的。但我的习惯是,只要值里包含空格或特殊字符,就一定加引号。否则CMake会把它当成多个参数。
set(FOO hello world) 实际上创建了一个列表,而不是单个字符串。
我在项目中遇到过一个问题:某个第三方库的路径里有个空格,我没加引号,结果编译死活过不去。排查了半天,最后发现是路径被拆成了两段。从那以后,我养成了「但凡路径必加引号」的习惯。
4.2 列表变量:用分号分隔的字符串
列表变量其实也是字符串。CMake用分号 ; 来分隔列表元素。
set(SOURCES main.cpp utils.cpp math.cpp)
# 等价于 set(SOURCES "main.cpp;utils.cpp;math.cpp")
你可能会问:那空格呢?嗯,这里要注意。用空格分隔多个值,CMake会自动转成列表。但如果你写 set(LIST "a b c"),那它就是一个字符串,不是列表。
list() 命令操作列表更安全。比如 list(APPEND SOURCES new_file.cpp),比直接字符串拼接靠谱得多。
我个人习惯在定义源文件列表时,每个文件单独一行,这样可读性更好:
set(SOURCES
main.cpp
utils.cpp
math.cpp
network.cpp
)
这样写,后续增删文件时,git diff 也清晰很多。
4.3 布尔变量:ON/OFF 的哲学
CMake里没有真正的布尔类型。所谓的布尔变量,其实就是约定用 ON 或 OFF 来表示真和假。
set(ENABLE_TESTING ON)
set(USE_OPENGL OFF)
但CMake很宽容。以下值都被视为「假」:OFF、NO、FALSE、0、空字符串、NOTFOUND。其他值都是「真」。
我曾经踩过一个坑:用 set(FLAG 0) 想表示关闭,结果 if(FLAG) 判断为真。为什么?因为 0 是字符串,不是数字。CMake把它当成非空字符串处理了。正确的写法是 set(FLAG OFF)。
ON 和 OFF,别搞特殊。这是CMake社区的约定俗成。
4.4 路径变量:处理文件系统
路径变量本质上还是字符串,但CMake提供了一些专门处理路径的命令。比如 file()、get_filename_component() 等。
set(PROJECT_ROOT "${CMAKE_CURRENT_SOURCE_DIR}")
set(OUTPUT_DIR "${PROJECT_ROOT}/build/output")
这里有个关键点:路径分隔符。CMake在Windows下也能正确处理正斜杠 /,所以建议统一用正斜杠,避免转义问题。
我建议用 file(TO_CMAKE_PATH) 来规范化路径,尤其是从外部传入的路径:
file(TO_CMAKE_PATH "${USER_INPUT_PATH}" NORMALIZED_PATH)
这样不管用户输入的是 Windows 反斜杠还是 Unix 正斜杠,都能统一处理。
4.5 缓存变量:跨文件共享的秘密
缓存变量是CMake里最有意思的类型。普通变量只在当前 CMakeLists.txt 及其子目录有效。但缓存变量会被写入 CMakeCache.txt,在整个构建过程中持久化。
set(MY_CACHE_VAR "hello" CACHE STRING "这是一个缓存变量")
缓存变量最常见的用途是:让用户在命令行修改。
cmake -DMY_CACHE_VAR="world" ..
我在项目中经常用缓存变量来控制编译选项:
set(BUILD_SHARED_LIBS ON CACHE BOOL "构建动态库")
set(CMAKE_BUILD_TYPE "Release" CACHE STRING "构建类型")
set() 覆盖,除非加 FORCE。这是很多人踩坑的地方。
举个例子:
set(MY_VAR "first" CACHE STRING "")
set(MY_VAR "second") # 不会覆盖缓存中的 "first"
如果你确实想强制更新,用 FORCE:
set(MY_VAR "second" CACHE STRING "" FORCE)
4.6 环境变量:与系统交互的桥梁
环境变量用 $ENV{} 语法读取,用 set(ENV{}) 设置。它读取的是操作系统的环境变量。
message("PATH: $ENV{PATH}")
set(ENV{MY_VAR} "hello")
但要注意:set(ENV{}) 只影响当前CMake进程,不会修改系统的环境变量。子进程会继承这些修改。
我经常用环境变量来传递敏感信息,比如API密钥:
if(DEFINED ENV{API_KEY})
target_compile_definitions(my_target PRIVATE API_KEY="$ENV{API_KEY}")
else()
message(FATAL_ERROR "请设置 API_KEY 环境变量")
endif()
这样既安全又灵活,密钥不会写死在代码里。
4.7 变量类型对比一览
| 类型 | 本质 | 作用域 | 典型用途 |
|---|---|---|---|
| 字符串 | 普通文本 | 当前作用域 | 版本号、名称、消息 |
| 列表 | 分号分隔的字符串 | 当前作用域 | 源文件列表、依赖列表 |
| 布尔 | ON/OFF 约定 | 当前作用域 | 开关选项、功能启用 |
| 路径 | 文件系统路径 | 当前作用域 | 目录、文件位置 |
| 缓存 | 持久化存储 | 全局(跨目录) | 用户可配置选项 |
| 环境 | 系统环境变量 | 进程级 | 系统配置、密钥 |
4.8 变量作用域与优先级
理解了变量类型,还得知道它们怎么「打架」。CMake的变量查找顺序是这样的:
- 先找普通变量(当前作用域)
- 再找缓存变量
- 最后找环境变量
举个例子:
set(MY_VAR "普通变量")
set(MY_VAR "缓存变量" CACHE STRING "")
message("${MY_VAR}") # 输出 "普通变量"
普通变量会「遮蔽」缓存变量。如果你想强制读取缓存变量,用 $CACHE{MY_VAR} 语法(CMake 3.13+)。
CACHE_ 前缀。
4.9 实战:一个完整的变量使用示例
咱们把今天学的串起来,写一个实际的 CMakeLists.txt:
cmake_minimum_required(VERSION 3.16)
project(VariableDemo VERSION 1.0.0)
# 字符串变量
set(PROJECT_DESCRIPTION "CMake变量类型演示项目")
# 列表变量
set(SOURCES
src/main.cpp
src/utils.cpp
src/network.cpp
)
# 布尔变量(缓存,用户可配置)
set(ENABLE_TESTS ON CACHE BOOL "启用单元测试")
# 路径变量
set(OUTPUT_DIR "${CMAKE_BINARY_DIR}/output")
# 环境变量
if(DEFINED ENV{EXTRA_FLAGS})
message(STATUS "使用额外编译标志: $ENV{EXTRA_FLAGS}")
endif()
# 根据布尔变量决定行为
if(ENABLE_TESTS)
enable_testing()
add_subdirectory(tests)
endif()
add_executable(my_app ${SOURCES})
target_include_directories(my_app PRIVATE include)
这个例子涵盖了今天讲的所有变量类型。你可以在命令行这样用:
cmake -DENABLE_TESTS=OFF -DCMAKE_BUILD_TYPE=Debug ..
嗯,到这里你应该对CMake的变量类型有了全面的认识。记住一句话:CMake里一切都是字符串,只是我们赋予了它们不同的含义。理解了这一点,后面的路就好走了。