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()
注意,elseif 和 endif 后面都要跟括号。这个括号里可以写变量名、常量、逻辑表达式。
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),结果发现 VAR1 和 VAR2 都是字符串,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()
嗯,这里要注意:break 和 continue 只作用于最内层的循环。如果你有嵌套循环,它们只影响当前这一层。
15.5 知识体系总览
下面这张图总结了 CMake 条件与循环的核心结构。我把它画成了流程图,方便你理解各个部分的关系:
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/else、continue 都用上了。你想想看,如果没有这些控制结构,你得手动写多少重复代码?
好了,关于 CMake 的条件与循环就聊到这里。这些工具用好了,你的 CMakeLists.txt 会变得既灵活又整洁。下次遇到需要根据不同情况做不同处理,或者需要批量操作时,别忘了用上它们。
公众号:蓝海资料掘金营,微信deep3321