chapter02 - cmake language

Comments

  1. 使用 #
  2. 使用 #[=[#]=]这种块注释是可以嵌套的

如果 #[=[ 前面还有 #,那么块注释开始标志本身被注释了。后续的块注释结束标志被认为是普通的单行注释。

##[=[
message("I'm not commented")
#]=]

Command

命令名是大小写不敏感的,但是一般用小写。

命令不是表达式,没有返回值。

commands 分类:

唯一的数据类型:string

message 命令会将所有的参数无 separator 拼接起来打印。

参数

第一种:bracket arguments

用来表示字符串字面量,尊重换行和空格。由 [[ 开启。其中双左括号中可以有零个或多个任意数量的 = 符号。比如

message([[multiline
bracket
argument
]])

message([==[
  because we used two equal-signs "=="
  following is still a single argument:
  { "petsArray" = [["mouse","cat"]] }
]==])

因为第二个字符串中包含了 ]] 本身,所以再使用 [] 就不合适了(会导致字符串提前结束!)。换用不同的开始和结束标志可以帮助转义。这一点和 shell 的 Here Document 一样。

第二种:quoted arguments

可以用 ${Var} 完成字符串插值。

message("1. escape sequence: \" \n in a quoted argument")
message("2. multi...
line")
message("3. and a variable reference: ${CMAKE_VERSION}")

没想到第二条 message 真的能换行,和 bracket arguments 相比只有转义的区别。

第三种:unquoted arguments

空白字符和分号可以分割参数:

而且圆括号比如成对才能出现在裸参数中。

message(a\ single\ argument)
message(two arguments)
message(three;separated;arguments)
message(${CMAKE_VERSION})  # a variable reference
message(()()())            # matching parentheses

打印结果:

a single argument
twoarguments
threeseparatedarguments
3.28.1
()()()

如果圆括号少了一个就会出错。

变量

和 命令 不同,变量名是大小写敏感的。

set 命令用来设置变量,第一个参数是变量名。变量名不是什么特殊的数据,只是一个字符串!可以用 set("xxx" yyy) 来写。

set(MyString1 "Text1")
set([[My String2]] "Text2")
set("My String 3" "Text3")
message(${MyString1})
message(${My\ String2})
message(${My\ String\ 3})

打印结果:

Text1
Text2
Text3

set 相对的是 unset 命令。

变量的展开是从内向外递归进行的,已展开的变量如果携带 ${} 等特殊字符将会继续影响本次展开!

不同类型的变量有不同的展开方式:

变量的展开在命令调用之前:

message("${CMAKE_CXX_COMPILE_FEATURES}")
message(${CMAKE_CXX_COMPILE_FEATURES})

这两条命令的结果不同。因为这个参数是列表,在第二条命令中会展开为多个参数,被 message 拼接起来打印。而在第一条命令中是一个参数,因而其中的分号被保留。

列表变量的存储方式就是加分号!

环境变量

设置环境变量的方式比较特殊:

set(ENV{CXX} "clang++")

环境变量也是一种特殊的变量,用字符串区别就行。展开只需要一个 $。只是对于普通变量来说,展开除了 $ 还得有花括号包裹。

环境变量是在配置阶段固定的。所以在生成阶段是无法再次修改的。

Cache 变量

在非脚本模式中,如果 CMakeCache.txt 存在会加载。通过 -D 选项也能传入。

变量作用域

Directory Scope 和 Function Scope。

set 函数提供 PARENT_SCOPE 选项来设置父作用域。注意修改父作用域的变量值不会影响到当前作用域!

List 变量

变量就是靠 ; 和 空白字符 分割的。

如果一个列表被 message 直接打印,那么它会展开为多个参数,然后直接被拼接在一起。

控制流程

if()
elseif()
else()
endif()

while()
endwhile()

foreach()
endforeach()

break()
continue()

条件

NOT <cond>
<cond> AND <cond>
<cond> OR <cond>

可以加括号保证计算顺序。

由于历史因素,未被括号包裹的参数会被 if 视为变量,进行变量展开之后再评估。也就是说,if 区别对待了变量和字符串

对于一个被引号包裹的字符串,只有以下为真,其他都是假:

  1. ON, Y, YES, TRUE(大小写不敏感)
  2. 非 0 的数字(可以是浮点数)

对于一个变量引用,只有以下为假,其他为真:

  1. OFF, NO, FALSE, N, IGNORE, NOTFOUND
  2. 以 -NOTFOUND 结尾的字符串
  3. 空字符串

实测对未被引号包裹的字符串评估顺序如下:

  1. 如果字符串本身是真,则为真
  2. 否则,若存在一个同名变量,则评估这个变量
  3. 否则按照普通字符串来评估它

while 的条件和 if 相同。

foreach RANGE 操作的默认 start 是 0,同时是包含 stop 的(和 C++ 不同)。

foreach LISTS 操作是将列表拼接,而 ZIP_LISTS 是将列表 zip。

  1. 如果只给一个变量,zip 之后对于变量 VAR 将生成 VAR_0VAR_1 等和 zip 列表数量相等的迭代变量。
  2. 如果传入多个变量,将会以给定的变量命名。

宏和函数

隐含参数如图(也给有若干具名参数)。ARGN 是只包含匿名参数。

对比:CMAKE 本身的命令行的参数用 CMAKE_ARGV1, CMAKE_ARGV2, … 和 CMAKE_ARGC 来访问。不像过程的访问这么丰富。

过程有两种:1. 宏 2. 函数

宏的替换方式比较魔幻,书上说的不是很清楚。这里是官方文档: https://cmake.org/cmake/help/latest/command/macro.html 我的大概理解如下:

  1. 只对参数的展开做替换,而不是像 C 语言一样直接查找替换,比如 ${ARG0} 会被展开成传给宏的值,但是 ARG0 这样的单纯出现的字符串不会被展开。
  2. 宏参数不是变量。所以 ARGC 可能是不存在的(除非真的有同名变量),而 ${ARGC} 会被展开成字符串。
cmake_minimum_required(VERSION 3.20.0)

macro(Test myVar)
  set(myVar "new value")
  message("argument: ${myVar}")
endmacro()

set(myVar "first value")
message("myVar is now: ${myVar}")
Test("called value")
message("myVar is now: ${myVar}")

打印结果:

myVar is now: first value
argument: called value
myVar is now: new value

函数

function 中可以使用 return() 提前返回。而宏本身是直接替换,在宏中使用 return() 将会导致宏调用处所在的函数被退出。

如果工程比较复杂,可以像 python 一样定义一个 main 宏,然后在文件最后调用。

宏和函数是大小写不敏感的。以下来自官方文档:

The function invocation is case-insensitive. A function defined as

function(foo)
  <commands>
endfunction()

can be invoked through any of

foo()
Foo()
FOO()
cmake_language(CALL foo)

宏也是一样。

实用命令

message

FATAL_ERROR 可以终止配置,SEND_ERROR 阻碍生成但是会继续配置。

CMAKE_MESSAGE_CONTEXT 是一个会在消息前的列表变量。可以在进入一个新的函数的时候向其中 APPEND 内容,这样 message 就能在有 --log-context 选项时打印出更有效的信息。

同理,CMAKE_MESSAGE_INDENT 是用来打印缩进的(不是数字类型,而是需要字符串,比如 " "),一般是给空白字符。

# zip.cmake
set(L1 "one;two;three;four")
set(L2 "1;2;3;4;5")
foreach(num IN ZIP_LISTS L1 L2)
    list(APPEND CMAKE_MESSAGE_CONTEXT j)
    message(STATUS "num_0=${num_0}, num_1=${num_1}")
endforeach()

# command line
cmake -P zip.cmake --log-context

# output
-- [j] num_0=one, num_1=1
-- [j.j] num_0=two, num_1=2
-- [j.j.j] num_0=three, num_1=3
-- [j.j.j.j] num_0=four, num_1=4
-- [j.j.j.j.j] num_0=, num_1=5

将上面的 CMAKE_MESSAGE_CONTEXT 换成 CMAKE_MESSAGE_INDENT,则打印结果为:

-- jnum_0=one, num_1=1
-- jjnum_0=two, num_1=2
-- jjjnum_0=three, num_1=3
-- jjjjnum_0=four, num_1=4
-- jjjjjnum_0=, num_1=5

这段程序足以体现 CMAKE_MESSAGE_CONTEXTCMAKE_MESSAGE_INDENT 的区别。

include

在脚本模式中默认的路径是相对于 current working directory 的,如果想要相对脚本,需要使用 CMAKE_CURRENT_LIST_DIR

在工程中还是从 CMakeLists.txt 开始寻址的。

include 可以有 OPTIONAL,这样找不到文件也不会报错。这个时候想知道是否成功就需要使用 RESULT_VARIABLE 的语法(找到则得到路径,找不到则变量会被赋值为 NOTFOUND)。

如果给的不是文件名,则 cmake 会认为它是模块名。cmake 有一堆内置模块,而对于其他模块 <module>,cmake 会在 CMAKE_MODULE_PATH 中找 <module>.cmake(默认为空)。

include_guard() 命令有 DIRECTORYGLOBAL 两种参数,放在被包含文件的开头使用。

include 不会创建新的 scope!和 C 语言一样相当于把整个文件内容拼接进来。

file

读写文件,还支持下载。

GENERATE 的文件是相对于 build 目录的。而 WRITE 是相对于源码目录的。

execute_process

  • 可以有多个 COMMAND(chaining)
  • 可以设置工作路径(一定要正确设置)
  • 可以设置 TIMEOUT
  • 可以通过 OUTPUT_VARIABLEERROR_VARIABLE 获取输出
  • 可以通过 RESULTS_VARIABLE 来获取每个命令的 exit codes
  • RESULT_VARIABLE 获取最后一条命令的 exit code。
execute_process(
  COMMAND timeout 0.01 yes
  COMMAND wc)

输出为:

 331776  331776  663552

The execute_process() command is a newer more powerful version of exec_program() , but the old command has been kept for compatibility. Both commands run while CMake is processing the project prior to build system generation.