强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 函数

特性functionmacro
作用域独立作用域调用者的作用域(文本替换)
变量设置默认不影响外部直接修改调用者的变量
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 OUTPUTadd_custom_command TARGETadd_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 章 — 工具链与交叉编译 →