第6章:控制流

大多数CMake项目在特定的情况下会使用一些特定的步骤。例如,项目可能希望仅在特定编译器或在为特定平台构建时使用特定的编译器标记。在其他情况下,项目可能需要迭代一组值,或者不断重复某些步骤,直到满足某个条件。CMake以大多数软件开发人员熟悉的方式,对这些控制提供了很好的支持。if()命令提供了if-then-else行为,循环通过foreach()while()命令提供的。这三个命令与大多数编程语言的行为相同,同时也添加了CMake的特性。

6.1. if()

if()命令的形式如下(可以提供多个elseif()子句):

if(expression1)
 # commands ...
elseif(expression2)
 # commands ...
else()
 # commands ...
endif()

早期版本的CMake要求将expression1作为else()endif()的参数,自从CMake 2.8.0之后就没有这个要求了。尽管遇到使用这种旧形式的项目和示例代码仍然很常见,因为它读起来有些混乱,所以不推荐新项目使用这种形式。新项目应该保留else()endif()参数为空。

if()elseif()命令中的表达式可以采取不同的形式。CMake提供了传统的布尔逻辑以及各种其他方式,如测试文件系统、版本比较和检查存在性。

6.1.1. 表达式

表达式中最基本的形式中只一个常量:

if(value)

CMake的真假逻辑比大多数编程语言复杂一些。对于未引用的常数,规则如下:

  • 如果value是一个没有引号的常数,值为1、ON、YES、TRUE、Y或一个非零数,将视为TRUE。不区分大小写。

  • 如果value是一个不带引号的常量,值为0、OFF、NO、FALSE、N、IGNORE、NOTFOUND、空字符串或以-NOTFOUND结尾的字符串,则将其视为FALSE。同样,不区分大小写。

  • 如果以上两种情况都不适用,则将视为变量名(或者可能是字符串),并按照接下来的描述进行计算。

下面的例子中,为了说明只展示了if(…)部分,省略了相应的分支操作和endif():

# Examples of unquoted constants
if(YES)
if(0)
if(TRUE)

# These are also treated as unquoted constants because the
# variable evaluation occurs before if() sees the values
set(A YES)
set(B 0)
if(${A}) # Evaluates to true
if(${B}) # Evaluates to false

# Does not match any of the true or false constants, so proceed
# to testing as a variable name in the fall through case below
if(someLetters)

# Quoted value, so bypass the true/false constant matching
# and fall through to testing as a variable name or string
if("someLetters")

CMake参考文档中的if使用,形式如下所示:

if(<variable|string>)

实践中的如果表达式是:

  • 变量(可能未定义)的无引号名称。

  • 可引用字符串。

当使用无引号的变量名时,将该变量的值会与假常量进行比较。如果这些值都不匹配,则表达式的结果为真。未定义的变量将计算为一个空字符串,该字符串与一个False常量匹配,因此将产生一个False结果。

# Common pattern, often used with variables defined
# by commands such as option(enableSomething "...")
if(enableSomething)
 # ...
endif()

然而,当if表达式是引用字符串时,行为就更复杂了:

  • 不管字符串的值是多少,在CMake 3.1或更高版本中,一个带引号的字符串总是计算为False(但这可以设置,参见第12章)。

  • 在CMake 3.1之前,如果字符串的值与现有变量的名称匹配,那么引用字符串将替换为该变量的名称(未引用),进行重复测试。

以上两种情况都可能让开发人员感到奇怪(至少CMake 3.1的行为是可预测的)。3.1之前的行为会导致字符串替换,当字符串值碰巧与变量名匹配时,这个变量名可能是在很远的地方定义的。引用值有潜在混淆的可能时,建议避免在if(something)中使用引号。当然,有更好的比较表达式,可以更健壮地方式处理字符串,后面的6.1.3节中会有介绍。

6.1.2. 逻辑操作符

CMake支持ANDORNOT逻辑操作符,可以使用括号控制优先级。

# Logical operators
if(NOT expression)
if(expression1 AND expression2)
if(expression1 OR expression2)

# Example with parentheses
if(NOT (expression1 AND (expression2 OR expression3)))

按照通常的约定,首先计算括号内的表达式,从最里面的括号开始。

6.1.3. 比较测试

CMake将比较测试分为三个不同的类别:数字、字符串和版本号,不过语法形式都遵循相同的模式:

if(value1 OPERATOR value2)

两个操作数value1value2既可以是变量名,也可以是(用引号括起来的)具体值。如果值与已定义变量的名称相同,那将视为变量。否则,将直接当作字符串或值处理。不过,引号中的值具有歧义性。在CMake 3.1之前,与变量名匹配的字符串替换成该变量的值。CMake 3.1及以后版本的行为使用了引用值而不会替换,这正是开发人员所期望的。

这三个比较类别都支持相同的操作集,但是每个类别的OPERATOR名称不同。下表总结了支持的操作符:

$^1$仅适用于CMake 3.7及以后的版本。

数值比较与预期一样,比较左边和右边的值。但请注意,如果操作数不是数字,并且不止一个数字时,行为就不完全符合官方文档,CMake通常也不会报错。根据数字和非数字的混合,表达式的结果不可预期。

# Valid numeric expressions, all evaluating as true
if(2 GREATER 1)
if("23" EQUAL 23)
set(val 42)
if(${val} EQUAL 42)
if("${val}" EQUAL 42)

# Invalid expression that evaluates as true with at
# least some CMake versions. Do not rely on this behavior.
if("23a" EQUAL 23)

版本号比较有点像数字比较的增强版。版本号假设为major[.minor[.patch[.]的形式,每个组件都应该是正整数。比较两个版本号时,首先比较major。只有当major相等时,才会比较minor(如果存在的话),以此类推。缺失的部分视为零。下面的例子中,表达式都为True:

if(1.2 VERSION_EQUAL 1.2.0)
if(1.2 VERSION_LESS 1.2.3)
if(1.2.3 VERSION_GREATER 1.2 )
if(2.0.1 VERSION_GREATER 1.9.7)
if(1.8.2 VERSION_LESS 2 )

版本号与数字比较具有同样的健壮性。版本号的每个部分都应该是整数,如果没有这个限制,比较结果是未定义的。

对于字符串,是按字典顺序进行比较。没有对字符串的内容做任何假设,但是要注意前面描述变量/字符串替代的可能性。字符串比较是发生替换的常见情况之一。

CMake还支持根据正则表达式比较字符串:

if(value MATCHES regex)

同样遵循上面定义的变量或字符串规则,并与regex正则表达式进行比较。如果匹配,则表达式的结果为True。虽然CMake文档没有为if()命令定义支持的正则表达式语法,但在其他地方定义了正则表达式语法(例如,参见string()命令)。本质上,CMake支持基本的正则表达式语法。

括号可用于获取部分匹配值。命令将以CMAKE_MATCH_<n>的形式设置变量,其中<n>是要匹配的组。整个匹配的字符串存储在0组中。

if("Hi from ${who}" MATCHES "Hi from (Fred|Barney).*")
 message("${CMAKE_MATCH_1} says hello")
endif()

6.1.4. 文件系统的比较

CMake还包括一组可用于查询文件系统的比较。支持下列表达式:

if(EXISTS pathToFileOrDir)
if(IS_DIRECTORY pathToDir)
if(IS_SYMLINK fileName)
if(IS_ABSOLUTE path)
if(file1 IS_NEWER_THAN file2)

上面的内容都能自解释,有几点需要注意。如果文件丢失或两个文件有相同的时间戳(包括如果同一个文件file1file2),IS_NEWER_THAN操作符会返回True。因此,实际执行IS_NEWER_THAN之前,需要判断file1file2是否存在,如果其中任何一个文件丢失了,那么IS_NEWER_THAN的结果通常不是开发人员所期望的。使用IS_NEWER_THAN时也应该给出完整路径。

需要注意的另一点是,与大多数if表达式不同,没有任何文件系统操作符在没有${}的情况下执行任何变量/字符串替换(不考虑引号)。

6.1.5. 存在性测试

最后一类if表达式支持测试是否存在各种CMake实例。它们在较大、更复杂的项目中特别有用,这些项目中,某些部分可能存在,也可能不存在,或者启用了。

if(DEFINED name)
if(COMMAND name)
if(POLICY name)
if(TARGET name)
if(TEST name) # Available from CMake 3.4 onward

如果在if命令执行时存在,上面的每一个都将返回True。

DEFINED

如果指定名称的变量存在,则返回True。变量的值无关紧要,只测试存在性。这也可以用来检查是否定义了特定的环境变量:

if(DEFINED SOMEVAR) # Checks for a CMake variable
if(DEFINED ENV{SOMEVAR}) # Checks for an environment variable

COMMAND

测试指定名称的CMake命令、函数或宏是否存在。这对于在尝试使用某个东西之前,检查它是否定义。对于CMake提供的命令,最好测试CMake版本,但是对于项目提供的函数和宏(参见第8章),用命令来测试存在是非常有用的。

POLICY

测试某一特定策略是否已知。策略名通常采用CMPxxxx的形式,其中xxxx部分总是一个四位数。关于这个主题的详细信息,请参阅第12章。

TARGET

如果CMake目标已由add_executable()add_library()add_custom_target()定义,则返回True。目标可以在任何目录中定义,这个测试在复杂的项目结构中特别有用,这些复杂的项目结构引入其他外部项目,并且这些项目可能共享共同的依赖项(例如,这种if测试可以用于在尝试创建目标之前检查目标是否定义)。

TEST

如果CMake测试前已经由add_test()命令定义(在第24章中详细介绍),则返回True。

这个测试在CMake 3.5及以后可用:

if(value IN_LIST listVar)

如果变量listVar包含指定的值,该表达式将返回True,value遵循变量或字符串规则,但listVar必须是列表变量的名称。

6.1.6. 常见用例

if()的一些用法非常常见,其中许多依赖于预定义的CMake变量,特别是与编译器和目标平台相关的变量。不幸的是,这种基于错误变量的表达式很常见。例如,假设一个项目有两个C++源文件,一个用Visual Studio或与之兼容的构建器(如Intel)构建,另一个用所有其他编译器构建。这种逻辑通常是这样实现的:

if(WIN32)
 set(platformImpl source_win.cpp)
else()
 set(platformImpl source_generic.cpp)
endif()

虽然这可能适用于大多数项目,但并没有正确的表达约束。例如,考虑一个在Windows上构建但使用MinGW编译器的项目。对于这种情况,source_generic.cpp可能是更合适的源文件。以下可以更准确地实现上述目标:

if(MSVC)
 set(platformImpl source_msvc.cpp)
else()
 set(platformImpl source_generic.cpp)
endif()

另一个例子涉及到正在使用的CMake生成器的条件行为。特别是,CMake在使用Xcode生成器构建时,提供了其他生成器不支持的附加特性。项目有时会假设为macOS构建将使用Xcode生成器,但这并不是必须的(通常不是)。下面是一些不正确的用法:

if(APPLE)
 # Some Xcode-specific settings here...
else()
 # Things for other platforms here...
endif()

这看上去是正确的做法,但如果在macOS上使用不同的生成器(如Ninja或Unix makefile),那就会失败。用APPLE没有表达正确的测试条件,这里应该测试CMake生成器:

if(CMAKE_GENERATOR STREQUAL "Xcode")
 # Some Xcode-specific settings here...
else()
 # Things for other CMake generators here...
endif()

上面的例子都是测试平台,而不是约束实际涉及的实体。因为平台是最容易理解和测试的,但使用不准确的约束会不必要地限制开发人员可用的生成器选择,或者导致完全错误的行为。

另一个常见的示例(这次使用得很恰当)是基于是否设置了特定的CMake选项,从而有条件地包含目标。

option(BUILD_MYLIB "Enable building the myLib target")
if(BUILD_MYLIB)
 add_library(myLib src1.cpp src2.cpp)
endif()

更复杂的项目通常使用上述模式,有条件地包含目录或执行,基于CMake选项或缓存变量的其他任务。开发人员可以在不直接编辑CMakeLists.txt文件的情况下打开/关闭该选项或将该变量设置为非默认值。对于由持续集成系统等驱动的脚本构建特别有用,这些脚本构建可能需要启用或禁用某些构建部分。

6.2. 循环

许多CMake项目中,另一个需求是对列表或范围内的值进行操作。另外,可能需要多次操作,直到满足条件为止。CMake可以很好地涵盖了这些需求,并添加了一些功能。

6.2.1. foreach()

CMake提供foreach()命令,使项目能够遍历一组项或值。foreach()有几种不同的形式,其中最基本的是:

foreach(loopVar arg1 arg2 ...)
 # ...
endforeach()

上面的形式中,对每个argN值,将loopVar设置为该参数,执行循环。不执行变量/字符串测试,参数按照值的方式使用。不需要显式地列出每一项,参数也可以通过一个或多个列表来指定变量,常用形式为:

foreach(loopVar IN [LISTS listVar1 ...] [ITEMS item1 ...])
 # ...
endforeach()

更常规的形式中,可以使用ITEMS关键字指定单个参数,但是LISTS关键字允许指定一个或多个列表变量。使用常规形式时,必须提供一个或两个ITEMS和/或LISTS。两者都提供时,ITEMS必须出现在LISTS之后。listVarN列表变量允许是一个空列表。下面的例子有助于阐明这个更常规的用法。

set(list1 A B)
set(list2)
set(foo WillNotBeShown)
foreach(loopVar IN LISTS list1 list2 ITEMS foo bar)
 message("Iteration for: ${loopVar}")
endforeach()

上面代码的输出为:

Iteration for: A
Iteration for: B
Iteration for: foo
Iteration for: bar

foreach()命令还支持对数值的迭代:

foreach(loopVar RANGE start stop [step])

使用foreach()时,循环执行时将loopVar设置为范围中开始停止(包括)的每个值。如果提供了step选项,则在每次迭代后将此值替换为前一个值,当结果大于stop时循环停止。

RANGE形式只接受一个参数,像这样:

foreach(loopVar RANGE value)

这相当于foreach(loopVar RANGE 0 value),循环体将执行(value + 1)次,更直观的数值是循环体执行的次数,显式地指定开始和停止值会让循环更为清楚。

if()endif()命令的类似,CMake的早期版本中(即2.8.0之前),所有的foreach()命令都需要将loopVar也指定为endforeach()参数。这降低了可读性,所以不鼓励在新项目中对endforeach()指定loopVar参数。

6.2.2. while()

CMake提供的另一个循环是while():

while(condition)
 # ...
endwhile()

如果条件为True(遵循与if()语句中的表达式相同的规则),则执行循环体。重复此操作,直到条件计算为False或提前退出循环(参见下一节)。在2.8.0之前的版本中,必须在endwhile()命令中重复该条件,对于新项目不鼓励这样做。

6.2.3. 中断循环

while()foreach()循环都支持使用break()提前退出循环,或者使用continue()跳至下一个迭代。这些命令的行为就像C语言一样,都只在内部的封闭循环上操作。下面的示例演示了该行为:

foreach(outerVar IN ITEMS a b c)
 unset(s)
 foreach(innerVar IN ITEMS 1 2 3)
   # Stop inner loop once string s gets long
   list(APPEND s "${outerVar}${innerVar}")
   string(LENGTH s length)
   if(length GREATER 5)
       break() ①
   endif()

   # Do no more processing if outer var is "b"
   if(outerVar STREQUAL "b")
       continue() ②
   endif()
   message("Processing ${outerVar}-${innerVar}")
 endforeach()

 message("Accumulated list: ${s}")
endforeach()

① 提前结束innerVar的循环。 ② 结束当前innerVar的迭代,跳转至下一个迭代。

上面例子的输出:

Processing a-1
Processing a-2
Processing a-3
Accumulated list: a1;a2;a3
Accumulated list: b1;b2;b3
Processing c-1
Processing c-2
Processing c-3
Accumulated list: c1;c2;c3

6.3. 总结

if()foreach()while()命令中的变量尽可能减少字符串形式。避免使用带引号的一元表达式,最好使用字符串比较操作。强烈推荐使用3.1为最低版本来禁用旧的行为,该行为允许将引用的字符串隐式转换为变量名。

如果正则表达式匹配if(xxx MATCHES regex)时,捕获到所需的变量,通常可以使用CMAKE_MATCH_<n>在比较后获取变量。当下一个正则表达式执行后,将覆盖这些变量。

使用循环命令,可以避免模棱两可或误导的情况。如果使用foreach()RANGE形式,则指定开始值和结束值。如果在进行迭代,请考虑使用IN LISTSINITEMS的方式,会比foreach(loopVar item1 item2…)的方式更简单易懂。

Last updated