CMake 系列教程(三):变量、条件与控制流
让你的构建脚本"聪明"起来
一、变量基础
1.1 定义与使用
cmake
# 定义普通变量
set(MY_NAME "CMake")
set(MY_VERSION 3)
# 使用变量:${变量名}
message(STATUS "Project: ${MY_NAME}, Version: ${MY_VERSION}")
# -- Project: CMake, Version: 3
变量在 CMake 中本质是字符串,没有类型区分。
1.2 列表
CMake 通过分号 ; 分隔实现列表:
cmake
# 两种等价写法
set(SOURCES a.cpp b.cpp c.cpp) # 空格分隔,自动转为分号
set(SOURCES "a.cpp;b.cpp;c.cpp") # 显式分号
# 结果相同:SOURCES = "a.cpp;b.cpp;c.cpp"
# 使用列表
add_executable(myapp ${SOURCES})
# 展开为:add_executable(myapp a.cpp b.cpp c.cpp)
列表操作
cmake
# 追加元素
list(APPEND SOURCES d.cpp e.cpp)
# SOURCES = "a.cpp;b.cpp;c.cpp;d.cpp;e.cpp"
# 在开头插入
list(INSERT SOURCES 0 main.cpp)
# 删除元素
list(REMOVE_ITEM SOURCES c.cpp)
# 获取长度
list(LENGTH SOURCES COUNT)
message(STATUS "Source count: ${COUNT}")
# 排序
list(SORT SOURCES)
1.3 变量作用域
CMake 变量遵循函数作用域规则:
cmake
set(X "top-level")
function(my_func)
message(STATUS "Inside func, X = ${X}") # top-level(可读取外部变量)
set(X "inside-func") # 仅在函数内修改,不影响外部
message(STATUS "After set, X = ${X}") # inside-func
endfunction()
my_func()
message(STATUS "After func, X = ${X}") # top-level(函数内的修改未传播)
从函数内部修改外部变量 需要用 PARENT_SCOPE:
cmake
function(my_func)
set(X "modified" PARENT_SCOPE) # 修改调用者的 X
endfunction()
⚠️
add_subdirectory引入的子CMakeLists.txt也是一个新作用域,子目录修改的变量不会影响父目录(除非用PARENT_SCOPE)。
二、缓存变量
2.1 普通变量 vs 缓存变量
CMake 有两套独立的变量系统:
| 特性 | 普通变量 | 缓存变量 |
|---|---|---|
| 作用域 | 函数/目录作用域 | 全局持久 |
| 存储位置 | 内存 | CMakeCache.txt |
| 生命周期 | 配置阶段结束即消失 | 跨多次配置保留 |
| 设置方式 | set(VAR value) |
set(VAR value CACHE TYPE "") |
| 优先级 | 高于缓存变量 | 低于普通变量 |
cmake
# 缓存变量
set(BUILD_TESTS ON CACHE BOOL "Whether to build tests")
# 第一次配置:写入 CMakeCache.txt
# 后续配置:不覆盖已有缓存值(除非 FORCE)
2.2 缓存变量类型
| 类型 | 用途 | 在 cmake-gui 中的表现 |
|---|---|---|
BOOL |
开关 | 复选框 |
STRING |
字符串 | 文本框 |
FILEPATH |
文件路径 | 文件选择器 |
PATH |
目录路径 | 目录选择器 |
2.3 修改缓存变量
bash
# 命令行方式
cmake -B build -DBUILD_TESTS=OFF
# 交互式方式
ccmake build/ # 终端 TUI
cmake-gui build/ # 图形界面(Windows)
2.4 option 命令
option 是 BOOL 类型缓存变量的语法糖:
cmake
# 等价写法
option(BUILD_TESTS "Build test programs" ON)
# set(BUILD_TESTS ON CACHE BOOL "Build test programs")
💡
option一定要在project()之后调用,否则ON/OFF可能与缓存中的已有值冲突。
三、条件判断
3.1 基本语法
cmake
if(CONDITION)
# ...
elseif(ANOTHER_CONDITION)
# ...
else()
# ...
endif()
3.2 常用条件表达式
布尔判断
cmake
# 以下值为"假":OFF, NO, FALSE, 0, N, IGNORE, NOTFOUND, 空字符串, 以 -NOTFOUND 结尾
# 其余为"真"
if(BUILD_TESTS)
message(STATUS "Tests enabled")
endif()
比较
cmake
# 数值比较
if(${PROJECT_VERSION_MAJOR} GREATER 2)
# 字符串比较
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
# 版本比较
if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.20")
| 操作符 | 含义 |
|---|---|
EQUAL / LESS / GREATER |
数值比较 |
STREQUAL / STRLESS / STRGREATER |
字符串比较 |
VERSION_EQUAL / VERSION_GREATER / VERSION_LESS |
版本号比较 |
逻辑组合
cmake
if(UNIX AND NOT APPLE)
# Linux 环境
endif()
if(WIN32 OR CYGWIN)
# Windows 环境
endif()
平台判断
cmake
if(WIN32) # Windows(含 64 位)
if(UNIX) # Linux / macOS / BSD
if(APPLE) # macOS / iOS
if(MSVC) # Microsoft Visual C++
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
# 64 位
endif()
文件系统
cmake
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/config.h")
message(STATUS "config.h found")
endif()
if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include")
message(STATUS "include directory exists")
endif()
3.3 常见陷阱
cmake
set(VAR "OFF")
# ❌ 错误:永远为真,因为 "OFF" 是非空字符串
if(${VAR})
# ✅ 正确:展开后变成 if(OFF),进行布尔判断
if(${VAR})
# ✅ 更推荐:使用变量名,让 if 自动求值
if(VAR)
💡 最佳实践 :在
if()中直接写变量名(不加${}),让 CMake 自动处理布尔语义。仅当需要字符串比较时才用${}。
四、循环
4.1 foreach
cmake
# 遍历列表
set(LANGS C CXX CUDA)
foreach(lang IN LISTS LANGS)
message(STATUS "Language: ${lang}")
endforeach()
# 遍历值
foreach(i RANGE 1 5) # 1, 2, 3, 4, 5
message(STATUS "i = ${i}")
endforeach()
foreach(i RANGE 0 10 3) # 0, 3, 6, 9(步长为 3)
message(STATUS "i = ${i}")
endforeach()
# 同时遍历多个列表
set(NAMES alpha beta gamma)
set(VALUES 1 2 3)
foreach(name val IN ZIP_LISTS NAMES VALUES)
message(STATUS "${name} = ${val}")
endforeach()
# alpha = 1, beta = 2, gamma = 3
4.2 while
cmake
set(COUNT 0)
while(COUNT LESS 5)
math(EXPR COUNT "${COUNT} + 1")
message(STATUS "Count: ${COUNT}")
endwhile()
4.3 循环控制
cmake
foreach(i RANGE 1 10)
if(i EQUAL 5)
continue() # 跳过本次迭代
endif()
if(i EQUAL 8)
break() # 跳出循环
endif()
message(STATUS "i = ${i}")
endforeach()
# 输出:1, 2, 3, 4, 6, 7
五、函数与宏
5.1 function
cmake
function(add_my_library name)
# ARGN:所有额外参数
# ARGC:参数总数
# ARGV:所有参数列表
# ARGV0, ARGV1, ...:按位置访问
message(STATUS "Creating library: ${name}")
message(STATUS "Sources: ${ARGN}")
add_library(${name} STATIC ${ARGN})
target_include_directories(${name} PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}
)
target_compile_features(${name} PUBLIC cxx_std_17)
endfunction()
# 调用
add_my_library(math math/add.cpp math/sub.cpp)
# 创建一个名为 math 的静态库
函数内部是独立作用域,修改的变量默认不传播到外部。
5.2 macro
cmake
macro(my_macro arg)
# 宏是文本替换,不做作用域隔离
message(STATUS "Macro arg: ${arg}")
endmacro()
函数 vs 宏:
| 特性 | function | macro |
|---|---|---|
| 作用域 | 独立 | 调用者作用域 |
| 参数传递 | 值传递(副本) | 文本替换 |
return() |
跳出函数 | 跳出包含宏的整个函数 |
| 推荐度 | ✅ 优先使用 | 仅当需要修改调用者变量时 |
⚠️ 强烈建议 :除非有特殊需求,一律使用 function,避免宏的隐式作用域问题。
六、configure_file:生成配置头文件
6.1 问题场景
代码中需要用到版本号、构建类型等信息,但不能硬编码------这些值在 CMake 配置阶段才能确定。
6.2 解决方案
config.h.in(模板文件):
c
#pragma once
#define PROJECT_VERSION "@PROJECT_VERSION@"
#define PROJECT_NAME "@PROJECT_NAME@"
#cmakedefine ENABLE_LOGGING
#cmakedefine01 HAVE_OPENSSL
// 使用 configure 变量
#define DATA_DIR "@CMAKE_INSTALL_PREFIX@/share/@PROJECT_NAME@"
CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 2.1.0 LANGUAGES CXX)
option(ENABLE_LOGGING "Enable logging" ON)
# 查找 OpenSSL(可选)
find_package(OpenSSL)
# 生成 config.h
configure_file(
${CMAKE_CURRENT_SOURCE_DIR}/config.h.in
${CMAKE_CURRENT_BINARY_DIR}/config.h
@ONLY # 只替换 @VAR@ 形式,不替换 ${VAR} 形式
)
# 使用生成的头文件
add_executable(myapp main.cpp)
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_BINARY_DIR} # 包含生成的 config.h
)
生成的 config.h(假设 ENABLE_LOGGING=ON, OpenSSL 已安装):
c
#pragma once
#define PROJECT_VERSION "2.1.0"
#define PROJECT_NAME "MyApp"
#define ENABLE_LOGGING
#define HAVE_OPENSSL 1
#define DATA_DIR "/usr/local/share/MyApp"
6.3 #cmakedefine 规则
| 模板写法 | 变量为真 | 变量为假 |
|---|---|---|
#cmakedefine VAR |
#define VAR |
/* #undef VAR */ |
#cmakedefine01 VAR |
#define VAR 1 |
#define VAR 0 |
七、实用模式
7.1 多配置构建类型判断
cmake
# 兼容单配置和多配置生成器
get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG)
if(isMultiConfig)
# Visual Studio / Ninja Multi-Config
message(STATUS "Multi-config generator")
else()
# Makefile / Ninja (单配置)
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE)
endif()
endif()
7.2 平台适配编译选项
cmake
function(set_default_compile_options target)
target_compile_features(${target} PUBLIC cxx_std_17)
if(MSVC)
target_compile_options(${target} PRIVATE /W4 /utf-8)
else()
target_compile_options(${target} PRIVATE
-Wall -Wextra -Wpedantic -Werror
)
endif()
endfunction()
# 使用
add_executable(myapp main.cpp)
set_default_compile_options(myapp)
7.3 条件编译源文件
cmake
set(APP_SOURCES main.cpp app.cpp)
if(WIN32)
list(APPEND APP_SOURCES platform/win.cpp)
elseif(UNIX AND NOT APPLE)
list(APPEND APP_SOURCES platform/linux.cpp)
elseif(APPLE)
list(APPEND APP_SOURCES platform/macos.cpp)
endif()
add_executable(myapp ${APP_SOURCES})
小结
| 知识点 | 要点 |
|---|---|
| 变量 | 字符串本质,${} 引用,函数作用域 |
| 列表 | 分号分隔,list() 操作 |
| 缓存变量 | CACHE 类型,CMakeCache.txt,option |
| 条件 | if/elseif/else/endif,推荐变量名不加 ${} |
| 循环 | foreach 为主,RANGE、ZIP_LISTS |
| 函数 | 独立作用域,优先于宏 |
configure_file |
模板生成配置头文件,#cmakedefine |
📖 下一期预告 :《CMake 系列教程(四):依赖管理》------ 从
find_package到FetchContent,解决 C/C++ 项目最头疼的第三方库集成问题。