CMake 从入门到精通:完整教程 / 第 8 章:命令与控制流
第 8 章:命令与控制流
8.1 条件语句
8.1.1 if 命令
# 基本条件
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
message("调试模式")
endif()
# if-else
if(WIN32)
message("Windows 平台")
elseif(APPLE)
message("macOS 平台")
elseif(UNIX)
message("Unix/Linux 平台")
else()
message("未知平台")
endif()
8.1.2 条件操作
set(MY_VAR 42)
# 比较操作
if(MY_VAR EQUAL 42) # 等于
if(MY_VAR LESS 100) # 小于
if(MY_VAR GREATER 0) # 大于
if(MY_VAR LESS_EQUAL 42) # 小于等于
if(MY_VAR GREATER_EQUAL 42) # 大于等于
# 字符串比较
if(MY_STR STREQUAL "hello") # 字符串相等
if(MY_STR MATCHES "^hello") # 正则匹配
if(MY_STR VERSION_LESS "2.0") # 版本比较
# 逻辑操作
if(A AND B) # 逻辑与
if(A OR B) # 逻辑或
if(NOT A) # 逻辑非
# 复合条件
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND CMAKE_CXX_COMPILER_VERSION VERSION_GREATER "10.0")
message("GCC 10+")
endif()
# 存在性检查
if(DEFINED MY_VAR) # 变量已定义
if(DEFINED CACHE{MY_VAR}) # 缓存变量已定义
if(DEFINED ENV{MY_VAR}) # 环境变量已定义
if(EXISTS "/path/to/file") # 文件/目录存在
if(IS_DIRECTORY "/path") # 是目录
if(IS_SYMLINK "/path") # 是符号链接
if(IS_ABSOLUTE "/path") # 是绝对路径
# 目标检查
if(TARGET mylib) # 目标存在
# 策略检查
if(POLICY CMP0144) # 策略存在
# 列表检查
if("item" IN_LIST MY_LIST) # 列表包含元素(CMake 3.3+)
8.1.3 常量真值与假值
# 假值
if(FALSE) # 假
if(OFF) # 假
if(NO) # 假
if(0) # 假
if(N) # 假
if("") # 假(空字符串)
if("NOTFOUND") # 假(以 NOTFOUND 结尾)
# 真值
if(TRUE) # 真
if(ON) # 真
if(YES) # 真
if(1) # 真
if("something") # 真(非空字符串)
⚠️ 注意:当变量名不是已知关键字时,CMake 会先将其视为变量引用。例如
if(MY_VAR)检查的是变量MY_VAR的值,而非字符串 “MY_VAR”。
8.2 循环
8.2.1 foreach 循环
# 基本遍历
foreach(item IN LISTS MY_LIST)
message("项目: ${item}")
endforeach()
# 直接列出项目
foreach(item a b c d e)
message("字母: ${item}")
endforeach()
# 范围
foreach(i RANGE 1 10) # 1 到 10(包含)
message("数字: ${i}")
endforeach()
foreach(i RANGE 0 100 10) # 0 到 100,步长 10
message("步长: ${i}")
endforeach()
# 遍历多个列表
foreach(item IN LISTS LIST1 LIST2 ZIP_LISTS LIST3 LIST4)
message("item: ${item}")
endforeach()
# ZIP_LISTS(CMake 3.17+)
set(A 1 2 3)
set(B x y z)
foreach(a b IN ZIP_LISTS A B)
message("${a} -> ${b}")
endforeach()
# 输出: 1 -> x, 2 -> y, 3 -> z
8.2.2 while 循环
set(i 0)
while(i LESS 10)
message("i = ${i}")
math(EXPR i "${i} + 1")
endwhile()
8.2.3 break 和 continue
foreach(i RANGE 0 100)
if(i EQUAL 5)
continue() # 跳过本次迭代
endif()
if(i GREATER 10)
break() # 退出循环
endif()
message("i = ${i}")
endforeach()
8.3 函数(function)
8.3.1 定义和调用
# 定义函数
function(my_function)
message("函数被调用了!")
endfunction()
# 调用
my_function()
8.3.2 参数传递
# 位置参数
function(my_func arg1 arg2 arg3)
message("参数: ${arg1}, ${arg2}, ${arg3}")
endfunction()
my_func("hello" "world" "!") # 参数: hello, world, !
# 可变参数
function(my_func required_arg)
message("必需参数: ${required_arg}")
message("ARGC: ${ARGC}") # 总参数数量
message("ARGV: ${ARGV}") # 所有参数列表
message("ARGN: ${ARGN}") # 额外参数(不含命名参数)
foreach(extra IN LISTS ARGN)
message("额外参数: ${extra}")
endforeach()
endfunction()
my_func("hello" "extra1" "extra2" "extra3")
8.3.3 cmake_parse_arguments
function(my_func)
# 定义参数规范
set(options ENABLE_DEBUG VERBOSE) # 布尔选项
set(oneValueArgs NAME VERSION OUTPUT_DIR) # 单值参数
set(multiValueArgs SOURCES INCLUDE_DIRS DEPENDENCIES) # 多值参数
# 解析参数
cmake_parse_arguments(
MY # 前缀
"${options}" # 布尔选项
"${oneValueArgs}" # 单值参数
"${multiValueArgs}" # 多值参数
${ARGN} # 传入的参数
)
# 使用解析结果
message("名称: ${MY_NAME}")
message("版本: ${MY_VERSION}")
message("源文件: ${MY_SOURCES}")
message("包含目录: ${MY_INCLUDE_DIRS}")
message("依赖: ${MY_DEPENDENCIES}")
if(MY_ENABLE_DEBUG)
message("调试模式已启用")
endif()
if(MY_VERBOSE)
message("详细输出已启用")
endif()
# 检查未解析的参数
if(MY_UNPARSED_ARGUMENTS)
message(WARNING "未解析的参数: ${MY_UNPARSED_ARGUMENTS}")
endif()
endfunction()
# 调用
my_func(
NAME "MyLibrary"
VERSION "2.0"
SOURCES src/a.cpp src/b.cpp
INCLUDE_DIRS include/ third_party/include
DEPENDENCIES fmt spdlog
ENABLE_DEBUG
VERBOSE
)
8.3.4 返回值
function(compute_result input)
math(EXPR result "${input} * 2")
# 通过 PARENT_SCOPE 返回
set(RESULT ${result} PARENT_SCOPE)
endfunction()
compute_result(21)
message("结果: ${RESULT}") # 42
8.3.5 函数作用域
set(OUTER_VAR "outer")
function(test_scope)
# 可以读取外部变量
message("外部变量: ${OUTER_VAR}")
# 内部变量不会影响外部
set(OUTER_VAR "modified in function")
message("函数内: ${OUTER_VAR}") # modified in function
endfunction()
test_scope()
message("函数外: ${OUTER_VAR}") # outer(未改变!)
# 要修改外部变量,使用 PARENT_SCOPE
function(test_scope_v2)
set(OUTER_VAR "modified" PARENT_SCOPE)
endfunction()
test_scope_v2()
message("现在: ${OUTER_VAR}") # modified
8.4 宏(macro)
8.4.1 基本宏
# 定义宏
macro(my_macro)
message("宏被调用了!")
endmacro()
# 调用
my_macro()
8.4.2 宏 vs 函数
| 特性 | function | macro |
|---|---|---|
| 作用域 | 独立作用域 | 调用者的作用域(文本替换) |
| 变量设置 | 默认不影响外部 | 直接修改调用者的变量 |
return() | 退出函数 | 退出调用者范围 |
${ARGN} | 值列表 | 值列表 |
| 参数展开 | 按值传递 | 文本替换 |
# 函数示例
function(my_func arg)
set(arg "modified")
message("函数内: ${arg}")
endfunction()
set(myvar "original")
my_func(${myvar})
message("函数外: ${myvar}") # original(不变)
# 宏示例
macro(my_macro arg)
set(arg "modified")
message("宏内: ${arg}")
endmacro()
set(myvar "original")
my_macro(${myvar})
message("宏外: ${myvar}") # original(不变,但这是宏的特殊行为)
⚠️ 推荐:优先使用
function而非macro。宏的文本替换特性可能导致意外的副作用。
8.4.3 宏的适用场景
# 宏适合用于需要影响调用者作用域的场景
macro(set_default var value)
if(NOT DEFINED ${var})
set(${var} ${value})
endif()
endmacro()
set_default(CMAKE_BUILD_TYPE "Release")
set_default(BUILD_TESTS ON)
8.5 return
function(check_condition)
if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
message(WARNING "仅支持 GCC")
return() # 提前退出
endif()
# 继续处理...
endfunction()
# 在宏中使用 return 会退出调用者的作用域
macro(early_return)
message("在宏中")
return() # 退出调用者!
endmacro()
8.6 file 命令
8.6.1 文件操作
# 读取文件
file(READ "config.txt" content)
file(STRINGS "data.txt" lines) # 按行读取
file(STRINGS "data.txt" lines REGEX "^#") # 过滤行
# 写入文件
file(WRITE "output.txt" "Hello World\n")
file(APPEND "output.txt" "Additional content\n")
# 生成文件
file(GENERATE
OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/generated.h"
CONTENT "#pragma once\n#define VERSION \"${PROJECT_VERSION}\"\n"
)
# 文件操作
file(COPY "source.txt" DESTINATION "${CMAKE_BINARY_DIR}/conf")
file(RENAME "old.txt" "new.txt")
file(REMOVE "temp.txt")
file(REMOVE_RECURSE "temp_dir")
# 创建目录
file(MAKE_DIRECTORY "${CMAKE_BINARY_DIR}/output")
# 下载文件
file(DOWNLOAD
"https://example.com/data.bin"
"${CMAKE_BINARY_DIR}/data.bin"
STATUS download_status
EXPECTED_HASH SHA256=abc123...
)
8.6.2 glob 文件
# 通配符匹配
file(GLOB headers "include/*.h")
file(GLOB_RECURSE sources "src/*.cpp" "src/*.h")
# 注意:新增文件不会自动检测
# 需要重新运行 cmake
8.6.3 路径操作(CMake 3.20+)
set(filepath "/home/user/project/src/main.cpp")
cmake_path(GET filepath FILENAME name) # main.cpp
cmake_path(GET filepath STEM stem) # main
cmake_path(GET filepath EXTENSION ext) # .cpp
cmake_path(GET filepath PARENT_PATH parent) # /home/user/project/src
cmake_path(SET normalized NORMALIZE "/a/../b/./c") # /b/c
cmake_path(IS_ABSOLUTE path result) # TRUE/FALSE
cmake_path(HAS_EXTENSION path result) # TRUE/FALSE
8.7 execute_process
8.7.1 运行外部命令
# 基本执行
execute_process(
COMMAND git describe --tags
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
OUTPUT_VARIABLE GIT_TAG
OUTPUT_STRIP_TRAILING_WHITESPACE
RESULT_VARIABLE result
)
if(result EQUAL 0)
message("Git 标签: ${GIT_TAG}")
endif()
# 多个命令(管道)
execute_process(
COMMAND ${CMAKE_COMMAND} -E echo "hello world"
COMMAND ${CMAKE_COMMAND} -E md5sum
OUTPUT_VARIABLE hash
)
8.7.2 参数说明
| 参数 | 说明 |
|---|---|
COMMAND | 要执行的命令和参数 |
WORKING_DIRECTORY | 工作目录 |
RESULT_VARIABLE | 存储返回码 |
OUTPUT_VARIABLE | 存储标准输出 |
ERROR_VARIABLE | 存储标准错误 |
OUTPUT_STRIP_TRAILING_WHITESPACE | 去除输出尾部空白 |
ERROR_STRIP_TRAILING_WHITESPACE | 去除错误输出尾部空白 |
INPUT_FILE | 标准输入文件 |
OUTPUT_FILE | 标准输出文件 |
TIMEOUT | 超时(秒) |
COMMAND_ECHO | 回显命令(STDOUT/STDERR) |
8.7.3 cmake -E 内置命令
# CMake 提供的跨平台工具命令
cmake -E echo "hello" # 输出文本
cmake -E copy src.txt dst.txt # 复制文件
cmake -E rename old.txt new.txt # 重命名
cmake -E remove file.txt # 删除文件
cmake -E make_directory dir # 创建目录
cmake -E tar cf archive.tar files # 创建归档
cmake -E md5sum file.txt # 计算 MD5
cmake -E sha256sum file.txt # 计算 SHA256
cmake -E env MY_VAR=1 command # 设置环境变量执行命令
cmake -E touch file.txt # 创建/更新文件时间戳
cmake -E compare_files a.txt b.txt # 比较文件
cmake -E true # 总是成功
cmake -E false # 总是失败
8.8 自定义命令
8.8.1 add_custom_command
# 生成文件的自定义命令
add_custom_command(
OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp
COMMAND ${CMAKE_COMMAND} -E echo "const char* version = \"${PROJECT_VERSION}\";" > generated.cpp
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/version.txt
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
COMMENT "生成 version.cpp"
VERBATIM
)
# 使用生成的文件
add_executable(app main.cpp ${CMAKE_CURRENT_BINARY_DIR}/generated.cpp)
# 构建后命令
add_custom_command(TARGET app POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "构建完成!"
COMMENT "后处理步骤"
)
8.8.2 add_custom_target
# 创建不产生输出文件的自定义目标
add_custom_target(docs
COMMAND doxygen Doxyfile
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "生成文档"
)
add_custom_target(format
COMMAND clang-format -i ${sources}
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
COMMENT "格式化代码"
)
# 依赖关系
add_custom_target(run
COMMAND $<TARGET_FILE:app>
DEPENDS app
COMMENT "运行程序"
)
8.8.3 OUTPUT vs TARGET 自定义命令的区别
| 特性 | add_custom_command OUTPUT | add_custom_command TARGET | add_custom_target |
|---|---|---|---|
| 产生文件 | ✅ | ❌ | ❌ |
| 可作为依赖 | ✅ | ❌ | ✅ |
| 自动构建 | 当被依赖时 | 当目标构建时 | 需显式构建 |
| 使用场景 | 代码生成 | 构建后处理 | 文档/格式化等 |
8.9 生成器表达式(简介)
生成器表达式在生成阶段(而非配置阶段)求值:
# 条件表达式
target_compile_definitions(myapp PRIVATE
$<$<CONFIG:Debug>:DEBUG_MODE>
$<$<CONFIG:Release>:NDEBUG>
)
# 字符串表达式
set_target_properties(myapp PROPERTIES
OUTPUT_NAME "myapp$<$<CONFIG:_debug>:_d>"
)
# 路径表达式
target_include_directories(myapp PRIVATE
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
# 常用生成器表达式
$<BOOL:...> # 布尔值
$<IF:cond,true,false> # 条件
$<TARGET_FILE:tgt> # 目标文件路径
$<TARGET_PROPERTY:tgt,prop> # 获取属性
$<CONFIG> # 当前配置名
$<PLATFORM_ID> # 平台标识
$<CXX_COMPILER_ID> # 编译器标识
生成器表达式将在第 13 章中详细讲解。
8.10 常用内置命令
8.10.1 测试相关
# 添加测试
enable_testing()
add_test(NAME mytest COMMAND $<TARGET_FILE:mytest>)
# 测试属性
set_tests_properties(mytest PROPERTIES
TIMEOUT 30
LABELS "unit;fast"
)
8.10.2 安装相关
install(TARGETS myapp DESTINATION bin)
install(DIRECTORY include/ DESTINATION include)
install(FILES config.h DESTINATION include)
8.10.3 包含其他文件
# 包含 CMake 脚本文件(在当前作用域执行)
include(MyModule)
# 包含并检查
include(MyModule OPTIONAL) # 文件不存在不报错
include(MyModule RESULT_VARIABLE result)
# 添加子目录
add_subdirectory(src)
add_subdirectory(tests EXCLUDE_FROM_ALL) # 排除默认构建
# 包含 CTest
include(CTest)
8.11 业务场景
场景:代码生成器
# 定义一个代码生成函数
function(generate_enum_header input_csv output_header)
add_custom_command(
OUTPUT ${output_header}
COMMAND Python3::Interpreter
${CMAKE_SOURCE_DIR}/tools/gen_enum.py
--input ${input_csv}
--output ${output_header}
--namespace myproject
DEPENDS
${input_csv}
${CMAKE_SOURCE_DIR}/tools/gen_enum.py
COMMENT "生成枚举头文件: ${output_header}"
VERBATIM
)
endfunction()
generate_enum_header(
${CMAKE_SOURCE_DIR}/data/enums.csv
${CMAKE_CURRENT_BINARY_DIR}/generated_enums.h
)
add_library(mylib src/mylib.cpp ${CMAKE_CURRENT_BINARY_DIR}/generated_enums.h)
场景:配置检查
# 系统检查函数
function(check_system_requirements)
include(CheckCXXCompilerFlag)
check_cxx_compiler_flag("-fsanitize=address" HAS_ASAN)
if(HAS_ASAN AND ENABLE_SANITIZERS)
target_compile_options(project_defaults INTERFACE -fsanitize=address)
target_link_options(project_defaults INTERFACE -fsanitize=address)
endif()
include(CheckIncludeFileCXX)
check_include_file_cxx("optional" HAS_OPTIONAL)
if(NOT HAS_OPTIONAL)
message(FATAL_ERROR "编译器不支持 <optional>")
endif()
endfunction()
8.12 注意事项
| 问题 | 说明 |
|---|---|
| 函数 vs 宏 | 优先使用 function,避免宏的作用域问题 |
| add_custom_command OUTPUT | 确保 OUTPUT 路径在构建目录中 |
| file(GLOB) 不自动更新 | 添加新文件需重新 cmake |
| cmake_parse_arguments 前缀 | 使用唯一前缀避免变量冲突 |
| 生成器表达式调试 | 生成器表达式无法在 configure 阶段打印 |
8.13 扩展阅读
上一章:第 7 章 — 查找模块详解 | 下一章:第 9 章 — 工具链与交叉编译 →