15、CMake条件与循环:if/elseif/else/endif、foreach、while、break/continue

条件判断和循环,是任何编程语言的基础设施。CMake 虽然是个构建系统,但它内置的脚本语言足够强大,能处理各种复杂的构建逻辑。我个人觉得,很多开发者把 CMake 当成简单的「文件列表收集器」,其实浪费了它的能力。

今天我们就来聊聊 CMake 里的条件与循环。说白了,就是让 CMake 根据不同的情况做不同的事,或者重复做同一件事。

15.1 条件判断:if/elseif/else/endif

条件判断在 CMake 里用得非常多。比如判断编译器类型、判断操作系统、判断某个变量是否被定义。我刚开始用 CMake 时,总觉得它的 if 语法有点怪——它不像 C 语言那样用括号,而是用关键字。

基本语法长这样:

if(<condition>)
  # 条件为真时执行
elseif(<condition>)
  # 另一个条件
else()
  # 都不满足时执行
endif()

注意,elseifendif 后面都要跟括号。这个括号里可以写变量名、常量、逻辑表达式。

15.1.1 常用条件表达式

我整理了几种最常见的用法,你在项目中大概率都会遇到:

表达式 说明 示例
if(DEFINED VAR) 判断变量是否被定义 检查用户是否设置了某个选项
if(VAR) 判断变量是否为真(非空、非0、非OFF、非NO等) 检查开关选项是否开启
if(NOT VAR) 取反 检查某个条件不成立
if(VAR1 STREQUAL VAR2) 字符串相等比较 比较编译器名称
if(VAR1 GREATER VAR2) 数值比较(大于) 比较版本号
if(EXISTS path) 判断文件或目录是否存在 检查依赖库是否存在
if(TARGET name) 判断某个目标是否已定义 检查库是否已添加
小技巧: 我个人习惯用 if(DEFINED VAR) 而不是 if(VAR),因为前者能区分「变量未定义」和「变量被设为空字符串」这两种情况。在复杂项目中,这个区别很重要。

15.1.2 实际项目中的条件判断

来看一个我实际用过的例子。当时在做嵌入式交叉编译,需要根据编译器类型决定链接参数:

if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
  message(STATUS "使用 GCC 编译器")
  set(COMPILER_FLAGS "-Wall -Wextra")
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
  message(STATUS "使用 Clang 编译器")
  set(COMPILER_FLAGS "-Wall -Weverything")
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
  message(STATUS "使用 MSVC 编译器")
  set(COMPILER_FLAGS "/W4")
else()
  message(WARNING "未知编译器: ${CMAKE_C_COMPILER_ID}")
  set(COMPILER_FLAGS "")
endif()

嗯,这里要注意:STREQUAL 是字符串比较,大小写敏感。如果你要忽略大小写,可以用 MATCHES 配合正则表达式。

15.1.3 逻辑运算符:AND、OR、NOT

条件可以组合使用。比如判断「既是 ARM 架构,又是 GCC 编译器」:

if(CMAKE_SYSTEM_PROCESSOR MATCHES "arm" AND CMAKE_C_COMPILER_ID STREQUAL "GNU")
  message("ARM + GCC 组合,启用优化")
  add_compile_options(-mcpu=cortex-a7 -mfpu=neon)
endif()
避坑指南: 我曾经在项目里写过 if(VAR1 AND VAR2),结果发现 VAR1VAR2 都是字符串,CMake 把它们当成布尔值判断。如果字符串是 "FALSE" 或 "NO",会被当成假。所以建议显式比较:if(VAR1 STREQUAL "ON" AND VAR2 STREQUAL "ON")

15.2 循环:foreach

foreach 是 CMake 里最常用的循环。它用来遍历列表、范围或者文件集合。

15.2.1 遍历列表

这是最基础的形式。比如我经常用它来批量添加源文件:

set(SOURCES
  main.c
  utils.c
  protocol.c
  driver.c
)

foreach(SRC ${SOURCES})
  message("处理源文件: ${SRC}")
  # 可以在这里做文件检查、编译选项设置等
endforeach()

你想想看,如果没有 foreach,你得写四遍 add_library 或者 target_sources,多累啊。

15.2.2 遍历范围

有时候你需要生成一系列编号的文件或目标。比如生成 1 到 10 的测试用例:

foreach(N RANGE 1 10)
  add_executable(test_${N} test_${N}.c)
  target_link_libraries(test_${N} PRIVATE mylib)
endforeach()

RANGE 还可以指定步长:RANGE 0 10 2 会生成 0, 2, 4, 6, 8, 10。

15.2.3 遍历 ZIP 模式

这个功能我特别喜欢。当你需要同时遍历两个列表时,可以用 ZIP_LISTS

set(NAMES "Alice" "Bob" "Charlie")
set(AGES  "25"   "30"   "35")

foreach(NAME AGE IN ZIP_LISTS NAMES AGES)
  message("${NAME} 今年 ${AGE} 岁")
endforeach()

输出:

Alice 今年 25 岁
Bob 今年 30 岁
Charlie 今年 35 岁

这在处理成对的数据时非常方便,比如源文件和对应的头文件。

15.3 循环:while

while 循环用得相对少一些,但在某些场景下很管用。比如等待某个条件满足,或者实现递归遍历。

基本语法:

set(COUNTER 0)
while(COUNTER LESS 5)
  message("计数: ${COUNTER}")
  math(EXPR COUNTER "${COUNTER} + 1")
endwhile()

注意,CMake 没有自增运算符 ++,你得用 math(EXPR ...) 来做算术运算。

实际案例: 我曾经用 while 循环实现了一个递归目录搜索,查找所有 .c 文件。虽然 CMake 有 file(GLOB_RECURSE),但有时候你需要更精细的控制,比如排除某些目录。这时候 while 配合 file(GLOB ...) 就能派上用场。

15.4 循环控制:break 和 continue

这两个关键字和 C 语言里的用法一样。break 跳出整个循环,continue 跳过当前迭代,进入下一次。

来看个例子:

foreach(FILE ${ALL_SOURCES})
  # 跳过测试文件
  if(FILE MATCHES "test_")
    continue()
  endif()

  # 如果遇到某个特殊文件,停止处理
  if(FILE STREQUAL "stop_here.c")
    message("遇到停止文件,退出循环")
    break()
  endif()

  message("处理: ${FILE}")
endforeach()

嗯,这里要注意:breakcontinue 只作用于最内层的循环。如果你有嵌套循环,它们只影响当前这一层。

15.5 知识体系总览

下面这张图总结了 CMake 条件与循环的核心结构。我把它画成了流程图,方便你理解各个部分的关系:

CMake 条件与循环知识体系 条件判断:if / elseif / else / endif 变量判断 字符串比较 数值比较 文件/目标存在 逻辑组合:AND / OR / NOT 循环:foreach / while foreach 列表 foreach 范围 foreach ZIP while 条件 循环控制:break / continue 核心原则:条件决定方向,循环控制重复 灵活组合,应对各种构建场景

15.6 综合实战:一个完整的条件+循环示例

最后,我分享一个我在实际项目中用过的片段。当时需要根据不同的平台和架构,生成不同的编译配置:

# 定义支持的平台列表
set(PLATFORMS "linux" "android" "freertos")
set(ARCHS "x86_64" "armv7" "armv8")

foreach(PLATFORM ${PLATFORMS})
  foreach(ARCH ${ARCHS})

    # 跳过不合理的组合
    if(PLATFORM STREQUAL "freertos" AND NOT ARCH MATCHES "arm")
      continue()
    endif()

    # 设置平台相关变量
    set(CONFIG_NAME "${PLATFORM}_${ARCH}")

    if(PLATFORM STREQUAL "linux")
      set(TOOLCHAIN_FILE "toolchain_linux_${ARCH}.cmake")
    elseif(PLATFORM STREQUAL "android")
      set(TOOLCHAIN_FILE "toolchain_android_${ARCH}.cmake")
    elseif(PLATFORM STREQUAL "freertos")
      set(TOOLCHAIN_FILE "toolchain_freertos_${ARCH}.cmake")
    else()
      message(FATAL_ERROR "不支持的平台: ${PLATFORM}")
    endif()

    message("生成配置: ${CONFIG_NAME} -> ${TOOLCHAIN_FILE}")

    # 这里可以调用 add_subdirectory 或 ExternalProject_Add
    # 实际项目中会在这里创建构建目录并运行 cmake

  endforeach()
endforeach()

这个例子把 foreach 嵌套、if/elseif/elsecontinue 都用上了。你想想看,如果没有这些控制结构,你得手动写多少重复代码?

个人建议: 写 CMake 条件循环时,尽量保持逻辑简单。如果嵌套超过三层,考虑拆成函数。CMake 的脚本调试起来不如 C 语言方便,太复杂的逻辑容易出 bug 还不好查。

好了,关于 CMake 的条件与循环就聊到这里。这些工具用好了,你的 CMakeLists.txt 会变得既灵活又整洁。下次遇到需要根据不同情况做不同处理,或者需要批量操作时,别忘了用上它们。


公众号:蓝海资料掘金营,微信deep3321