4、CMake变量类型:字符串变量、列表变量、布尔变量、路径变量、缓存变量、环境变量

说实话,CMake的变量系统刚接触时有点绕。我当年第一次用CMake,就被各种变量类型搞晕过。字符串、列表、布尔、路径、缓存、环境……听起来像是一堆概念,但用熟了你会发现,其实核心就那么几种。

咱们一个一个来拆解。我会结合我踩过的坑,帮你把这些变量类型彻底搞明白。

4.1 字符串变量:最基础的单元

字符串变量是CMake里最常用的类型。说白了,你写的几乎所有值,本质上都是字符串。

set(MY_NAME "CMake实战")
set(VERSION_STR "3.28.0")

这里有个细节:引号不是必须的。但我的习惯是,只要值里包含空格或特殊字符,就一定加引号。否则CMake会把它当成多个参数。

注意: 不加引号时,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的变量查找顺序是这样的:

  1. 先找普通变量(当前作用域)
  2. 再找缓存变量
  3. 最后找环境变量

举个例子:

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里一切都是字符串,只是我们赋予了它们不同的含义。理解了这一点,后面的路就好走了。