第1章 条件判断与循环:CMake的控制流核心
各位同学,欢迎来到CMake与NDK集成的第一课。今天我们要聊的,是CMake里最基础也最常用的东西——条件判断和循环。说白了,就是让CMake脚本能“动脑子”,根据不同的情况做不同的事。
我刚开始用CMake的时候,总觉得它就是个配置工具,写几个set()、add_library()就完事了。后来项目越来越复杂,要支持不同Android ABI、不同API级别、还要区分Debug和Release……嗯,那时候我才意识到,没有控制流的CMake脚本,就像没有方向盘的车。
1.1 if()/elseif()/else()/endif()——让CMake学会做决定
条件判断是CMake里最常用的控制结构。它的语法很简单,但坑也不少。先看个基本例子:
if(ANDROID_ABI STREQUAL "arm64-v8a")
message("当前编译64位ARM架构")
elseif(ANDROID_ABI STREQUAL "armeabi-v7a")
message("当前编译32位ARM架构")
else()
message("其他架构: ${ANDROID_ABI}")
endif()
这里有个细节要注意:if()和endif()必须成对出现。我见过不少新手漏掉endif(),结果CMake报错半天找不到原因。
核心要点:CMake的if语句不要求缩进,但强烈建议缩进。你想想看,不缩进的代码,三个月后你自己都看不懂。
1.1.1 变量真假判断
CMake里判断一个变量是“真”还是“假”,规则有点特别。我个人习惯用if(var)来判断变量是否被定义且非空:
set(USE_OPENGL ON)
if(USE_OPENGL)
target_link_libraries(myapp GLESv2)
endif()
# 注意:OFF、NO、FALSE、0、空字符串、NOTFOUND 都算假
set(DEBUG_MODE OFF)
if(DEBUG_MODE)
message("这行不会打印")
endif()
我曾经踩过的坑:有一次我写if(ANDROID)来判断是否在Android平台,结果发现这个变量在某些NDK版本里根本没定义。后来我改成if(DEFINED ANDROID)才解决问题。记住:先判断变量是否存在,再判断它的值。
1.1.2 字符串比较
比较字符串用STREQUAL、STRLESS、STRGREATER这些操作符。注意大小写敏感:
set(TARGET_ARCH "arm64")
if(TARGET_ARCH STREQUAL "ARM64")
message("不会匹配,因为大小写不同")
elseif(TARGET_ARCH STREQUAL "arm64")
message("这才对")
endif()
1.1.3 数值比较
数值比较用EQUAL、LESS、GREATER等。注意CMake里所有变量都是字符串,但比较时会自动转成数值:
set(API_LEVEL 29)
if(API_LEVEL GREATER_EQUAL 30)
message("Android 11及以上")
else()
message("Android 10及以下")
endif()
1.2 逻辑运算符:AND、OR、NOT
单个条件不够用?那就组合起来。CMake支持AND、OR、NOT三个逻辑运算符:
if(ANDROID_ABI STREQUAL "arm64-v8a" AND ANDROID_PLATFORM GREATER_EQUAL 29)
message("64位架构 + Android 10以上")
endif()
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE "Release")
endif()
小技巧:多个条件组合时,建议用括号明确优先级。虽然CMake的优先级规则还算直观,但加括号能让代码更清晰。
1.3 while()循环——用得少但很关键
说实话,我在CMake里用while()的次数不多。大多数场景用foreach()就够了。但有些情况,比如处理不确定长度的列表,while()就派上用场了:
set(SOURCES "a.cpp" "b.cpp" "c.cpp")
list(LENGTH SOURCES len)
set(index 0)
while(index LESS len)
list(GET SOURCES ${index} file)
message("处理文件: ${file}")
math(EXPR index "${index} + 1")
endwhile()
注意while()和endwhile()也要成对。我建议你尽量用foreach()代替while(),因为foreach()更不容易出错。
1.4 foreach()循环——最常用的循环
foreach()是CMake里最实用的循环结构。它有好几种用法,我一个个说。
1.4.1 遍历列表
set(ABIS "arm64-v8a" "armeabi-v7a" "x86_64")
foreach(abi ${ABIS})
message("当前ABI: ${abi}")
endforeach()
1.4.2 遍历范围
# 从1到10
foreach(i RANGE 1 10)
message("迭代次数: ${i}")
endforeach()
# 从0到10,步长2
foreach(i RANGE 0 10 2)
message("偶数: ${i}")
endforeach()
1.4.3 遍历多个列表(zip模式)
这个功能我特别喜欢。CMake 3.17以后支持ZIP_LISTS,可以同时遍历多个列表:
set(NAMES "Alice" "Bob" "Charlie")
set(AGES 25 30 35)
foreach(name age IN ZIP_LISTS NAMES AGES)
message("${name} 今年 ${age} 岁")
endforeach()
实战经验:我在做NDK多ABI编译时,经常用foreach()循环处理不同架构的源文件。比如为每个ABI设置不同的编译选项,然后分别编译。
1.5 比较运算符全表
为了方便查阅,我把CMake里常用的比较运算符整理成了一张表:
| 运算符 | 用途 | 示例 |
|---|---|---|
| STREQUAL | 字符串相等 | if(A STREQUAL "hello") |
| STRLESS | 字符串小于 | if(A STRLESS B) |
| STRGREATER | 字符串大于 | if(A STRGREATER B) |
| EQUAL | 数值相等 | if(A EQUAL 10) |
| LESS | 数值小于 | if(A LESS 10) |
| GREATER | 数值大于 | if(A GREATER 10) |
| LESS_EQUAL | 数值小于等于 | if(A LESS_EQUAL 10) |
| GREATER_EQUAL | 数值大于等于 | if(A GREATER_EQUAL 10) |
| DEFINED | 变量是否定义 | if(DEFINED var) |
| EXISTS | 文件或目录是否存在 | if(EXISTS /path/to/file) |
1.6 知识体系总览
为了让你对本章内容有个整体认识,我画了一张图:
1.7 实战中的避坑指南
最后,分享几个我在项目中积累的经验:
- 变量作用域:CMake的变量默认是全局的,但函数内的变量是局部的。如果你在
function()里用set(),外面是拿不到的。需要用set(var PARENT_SCOPE)才能传递出去。 - 空字符串判断:判断一个字符串是否为空,用
if(NOT var)或者if(var STREQUAL "")。但要注意,如果变量未定义,if(NOT var)也会为真。 - 循环中的性能:CMake的循环性能一般,不要在循环里做大量计算。如果需要在编译时生成大量文件,建议用
add_custom_command()配合foreach()生成规则,而不是在CMake配置阶段执行。
我的个人习惯:写CMake脚本时,我会在if()和endif()后面加注释,标明对应的条件。比如endif() # ANDROID_ABI STREQUAL "arm64-v8a"。这样嵌套多层时,一眼就能看出每个endif()对应哪个if()。
好了,这一章的内容就到这里。条件判断和循环是CMake脚本的骨架,掌握了它们,你就能写出灵活、可配置的构建脚本。下一章我们会深入变量和列表操作,到时候你会看到,这些控制流结构会发挥更大的作用。
公众号:蓝海资料掘金营,微信deep3321