第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 字符串比较

比较字符串用STREQUALSTRLESSSTRGREATER这些操作符。注意大小写敏感:

set(TARGET_ARCH "arm64")
if(TARGET_ARCH STREQUAL "ARM64")
    message("不会匹配,因为大小写不同")
elseif(TARGET_ARCH STREQUAL "arm64")
    message("这才对")
endif()

1.1.3 数值比较

数值比较用EQUALLESSGREATER等。注意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支持ANDORNOT三个逻辑运算符:

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 知识体系总览

为了让你对本章内容有个整体认识,我画了一张图:

CMake控制流知识体系 条件判断 • if() / elseif() / else() / endif() • 变量真假判断(ON/OFF/空值) • 字符串比较(STREQUAL等) • 数值比较(EQUAL/LESS等) 逻辑运算符 • AND(与) • OR(或) • NOT(非) • 括号组合使用 while()循环 • 条件控制循环 • 适合不确定长度的列表 • 注意避免死循环 foreach()循环 • 遍历列表元素 • RANGE范围遍历 • ZIP_LISTS多列表遍历

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