用 CMake 进行编程
控制流程
CMake 有一个 if
语句,尽管经过多次版本迭代它已经变得非常复杂。这里有一些全大写的变量你可以在 if
语句中使用,并且你既可以直接引用也可以利用 ${}
来对他进行解析( if
语句在历史上比变量拓展出现的更早 )。这是一个 if
语句的例子:
if(variable)
# If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
else()
# If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()
# If variable does not expand to one of the above, CMake will expand it then try again
如果你在这里使用 ${variable}
可能会有一些奇怪,因为看起来它好像 variable
被展开 ( expansion ) 了两次。在 CMake 3.1+ 版本中加入了一个新的特性 ( CMP0054 ) ,CMake 不会再展开已经被引号括起来的展开变量。也就是说,如果你的 CMake 版本大于 3.1
,那么你可以这么写:
if("${variable}")
# True if variable is not false-like
else()
# Note that undefined variables would be `""` thus false
endif()
这里还有一些关键字可以设置,例如:
一元的:
NOT
,TARGET
,EXISTS
(文件),DEFINED
, 等。二元的:
STREQUAL
,AND
,OR
,MATCHES
( 正则表达式 ),VERSION_LESS
,VERSION_LESS_EQUAL
( CMake 3.7+ ), 等。括号可以用来分组
generator-expressions
generator-expressions 语句十分强大,不过有点奇怪和专业 ( specialized ) 。大多数 CMake 命令在配置的时候执行,包括我们上面看到的 if
语句。但是如果你想要他们在构建或者安装的时候运行呢,应该怎么写? 生成器表达式就是为此而生1。它们在目标属性中被评估( evaluate ):
最简单的生成器表达式是信息表达式,其形式为 $<KEYWORD>
;它会评估和当前配置相关的一系列信息。信息表达式的另一个形式是 $<KEYWORD:value>
,其中 KEYWORD
是一个控制评估的关键字,而 value
则是被评估的对象( 这里的 value
中也允许使用信息表达式,如下面的 ${CMAKE_CURRENT_SOURCE_DIR}/include
)。如果 KEYWORD
是一个可以被评估为0或1的生成器表达式或者变量,如果(KEYWORD
被评估)为1则 value
会在这里被保留下来,而反之则不会。你可以使用嵌套的生成器表达式,你也可以使用变量来使得自己更容易理解嵌套的变量。一些表达式也可以有多个值,值之间通过逗号分隔2。
如果你有一个只想在配置阶段的 DEBUG 模式下开启的编译标志( flag ),你可以这样做:
target_compile_options(MyTarget PRIVATE "$<$<CONFIG:Debug>:--my-flag>")
译者注:这里有点迷惑性,这里其实包含了两种 generator-expression,分别是 configuration-expression 和 conditional-expression,前者使用的形式是 $<CONFIG:cfgs>
,这里的 cfgs 是一个 List,如果 CONFIG 满足 cfgs 列表中的任何一个值,这个表达式会被评估(evaluate)为 1,否则为 0。后者使用的形式是 $<condition:true_string>
,如果 condition 值为 1,则表达式被评估为 true_string,否则为空值。因此这里表达的含义是,如果这里是一个 DEBUG 的 configuration,就设置 --my-flag。可参见官方文档。
这是一个相比与指定一些形如 *_DEBUG
这样的变量更加新颖并且更加优雅的方式,并且这对所有支持生成器表达式的设置都通用。需要注意的是,你永远不要在配置阶段(configuration phase)使用配置有关值(configure time value),因为在使用像 IDE 这种多配置生成器时你没法在配置阶段获取到这些值,只有在构建阶段使用生成器表达式或者形如 *_<CONFIG>
的变量才能获得。
一些生成器表达式的其他用途:
- 限制某个项目的语言,例如可以限制其语言为 CXX 来避免它和 CUDA 等语言混在一起,或者可以通过封装它来使得他对不同的语言有不同的表现。
- 获得与属性相关的配置,例如文件的位置。
- 为构建和安装生成不同的位置。
最后一个是常见的。你几乎会在所有支持安装的软件包中看到如下代码:
译者注:表示在目标对于直接 BUILD 使用的目标包含的头文件目录为 ${CMAKE_CURRENT_SOURCE_DIR}/include
,而安装的目标包含的头文件目录为 include
,是一个相对位置(同时需要 install 对应的头文件才可以)。
target_include_directories(
MyTarget
PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
宏定义与函数
你可以轻松地定义你自己的 CMake function
或 macro
。函数和宏只有作用域上存在区别,宏没有作用域的限制。所以说,如果你想让函数中定义的变量对外部可见,你需要使用 PARENT_SCOPE
来改变其作用域。如果是在嵌套函数中,这会变得异常繁琐,因为你必须在想要变量对外的可见的所有函数中添加 PARENT_SCOPE
标志。但是这样也有好处,函数不会像宏那样对外“泄漏”所有的变量。接下来用函数举一个例子:
下面是一个简单的函数的例子:
function(SIMPLE REQUIRED_ARG)
message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGN}")
set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE)
endfunction()
simple(This Foo Bar)
message("Output: ${This}")
输出如下:
-- Simple arguments: This, followed by Foo;Bar
Output: From SIMPLE
如果你想要有一个指定的参数,你应该在列表中明确的列出,除此之外的所有参数都会被存储在 ARGN
这个变量中( ARGV
中存储了所有的变量,包括你明确列出的 )。CMake 的函数没有返回值,你可以通过设定变量值的形式来达到同样地目的。在上面的例子中,你可以通过指定变量名来设置一个变量的值。
参数的控制
你应该已经在很多 CMake 函数中见到过,CMake 拥有一个变量命名系统。你可以通过 cmake_parse_arguments
函数来对变量进行命名与解析。如果你想在低于 3.5 版本的CMake 系统中使用它,你应该包含 CMakeParseArguments 模块,此函数在 CMake 3.5 之前一直存在与上述模块中。这是使用它的一个例子:
function(COMPLEX)
cmake_parse_arguments(
COMPLEX_PREFIX
"SINGLE;ANOTHER"
"ONE_VALUE;ALSO_ONE_VALUE"
"MULTI_VALUES"
${ARGN}
)
endfunction()
complex(SINGLE ONE_VALUE value MULTI_VALUES some other values)
在调用这个函数后,会生成以下变量:
COMPLEX_PREFIX_SINGLE = TRUE
COMPLEX_PREFIX_ANOTHER = FALSE
COMPLEX_PREFIX_ONE_VALUE = "value"
COMPLEX_PREFIX_ALSO_ONE_VALUE = <UNDEFINED>
COMPLEX_PREFIX_MULTI_VALUES = "some;other;values"
如果你查看了官方文档,你会发现可以通过 set 来避免在 list 中使用分号,你可以根据个人喜好来确定使用哪种结构。你可以在上面列出的位置参数中混用这两种写法。此外,其他剩余的参数(因此参数的指定是可选的)都会被保存在 COMPLEX_PREFIX_UNPARSED_ARGUMENTS
变量中。
1. 他们看起来像是在构建或安装时被评估的,但实际上他们只对每个构建中的配置进行评估。 ↩
2. CMake 官方文档中将表达式分为信息表达式,逻辑表达式和输出表达式。 ↩