chapter02 - cmake language
Comments
- 使用
#
- 使用
#[=[
和#]=]
(这种块注释是可以嵌套的)
如果 #[=[
前面还有 #
,那么块注释开始标志本身被注释了。后续的块注释结束标志被认为是普通的单行注释。
##[=[
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 区别对待了变量和字符串。
对于一个被引号包裹的字符串,只有以下为真,其他都是假:
- ON, Y, YES, TRUE(大小写不敏感)
- 非 0 的数字(可以是浮点数)
对于一个变量引用,只有以下为假,其他为真:
- OFF, NO, FALSE, N, IGNORE, NOTFOUND
- 以 -NOTFOUND 结尾的字符串
- 空字符串
- 零
实测对未被引号包裹的字符串评估顺序如下:
- 如果字符串本身是真,则为真
- 否则,若存在一个同名变量,则评估这个变量
- 否则按照普通字符串来评估它
while 的条件和 if 相同。
foreach RANGE 操作的默认 start 是 0,同时是包含 stop 的(和 C++ 不同)。
foreach LISTS 操作是将列表拼接,而 ZIP_LISTS 是将列表 zip。
- 如果只给一个变量,zip 之后对于变量
VAR
将生成VAR_0
,VAR_1
等和 zip 列表数量相等的迭代变量。 - 如果传入多个变量,将会以给定的变量命名。
宏和函数
隐含参数如图(也给有若干具名参数)。ARGN
是只包含匿名参数。
对比:CMAKE 本身的命令行的参数用
CMAKE_ARGV1
,CMAKE_ARGV2
, … 和CMAKE_ARGC
来访问。不像过程的访问这么丰富。
过程有两种:1. 宏 2. 函数
宏
宏的替换方式比较魔幻,书上说的不是很清楚。这里是官方文档: https://cmake.org/cmake/help/latest/command/macro.html 我的大概理解如下:
- 宏只对参数的展开做替换,而不是像 C 语言一样直接查找替换,比如
${ARG0}
会被展开成传给宏的值,但是ARG0
这样的单纯出现的字符串不会被展开。 - 宏参数不是变量。所以
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_CONTEXT
和 CMAKE_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()
命令有 DIRECTORY
和 GLOBAL
两种参数,放在被包含文件的开头使用。
include 不会创建新的 scope!和 C 语言一样相当于把整个文件内容拼接进来。
file
读写文件,还支持下载。
GENERATE
的文件是相对于 build 目录的。而 WRITE
是相对于源码目录的。
execute_process
- 可以有多个
COMMAND
(chaining) - 可以设置工作路径(一定要正确设置)
- 可以设置
TIMEOUT
- 可以通过
OUTPUT_VARIABLE
和ERROR_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.