Cpp构建和编译笔记——6.CMake基本语法
CMake 和 make,shell 脚本一样,本质是一种 DSL 语言。在了解 CMake 的基本概念和用法之后,作为一种编程语言,还是得从最基本的变量,流程控制(for 循环,if 条件),函数等开始学习。在最开始,我们强调一点——CMake 作为一门语言是区分大小写的!只是具体到通常使用的内置命令/自定义函数/自定义宏,不区分大小写。
不得不说,CMake 这一类文本化的语言语法都非常反人类,而且CMake 的官方文档写的真是垃圾中的垃圾。
本文的主要内容
- 调试输出(IO)
- 变量
- 字符串操作
- 列表操作
- 数学表达式
调试输出
在 CMakeLists 中,有着和 echo 类似的用于向控制台输出信息的 message 命令
1 | message([<mode>] "message text") |
其中的 mode 通常为
NOTICE
: 缺省时默认的选项,表示正常输出到控制台的重要提示信息WARNING
: 表示输出到控制台的警告信息,但不会中断 CMake 的运行STATUS
: 表示正常输出到控制台的一般提示性信息,和 CMake 自动输出的提示信息一样,每一条自动以--
开头,通常不需要关注FATAL_ERROR
: 表示致命错误,CMake 通常不会执行到此,如果执行到了这条语句,就会输出这里的信息并停止生成构建系统
message 命令会在生成构建系统时输出信息,而不是在编译阶段输出信息,例如正常输出
1 | # message("hello,cmake") |
报错输出
1 | # message(FATAL_ERROR "hello,cmake") |
我们可以在 CMakeLists.txt 中利用 message 命令输出各种 CMake 变量的具体信息,有助于我们了解当前 CMake 的状况,还可以在 CMake 执行到关键部分时,输出相应的提示信息。
变量
变量介绍
CMake 把变量分成普通变量、缓存变量和环境变量三类:
- 普通变量的含义是在多次生成构建系统的过程中,CMake 并没有记住这个变量,而是每一次构建时重新处理,普通变量还有作用域,并不是全局有效的,例如自定义函数中会有独立的变量作用域
- 缓存变量会被 CMake 通过缓存文件 CMakeCache.txt 记下来,在多次生成构建系统时可以重复地、全局地使用
- 环境变量就是当前 CMake 进程中获取的环境变量,我们可以获取并使用,也可以进行临时性的修改(不建议)
注意这里的 CMake 变量和 C++预处理的宏不是一回事,CMake
不会把自己的变量传递给编译器,如果希望给编译器传递相关的宏,需要使用target_compile_definitions
之类的命令
在 CMake 这种 DSL 语言中,变量和字符串总是容易混淆的东西,并没有建立一个完整的类型系统,因此语法非常反人类:
- 关于字符串和字符串列表:
- 对于不含空格的单个字符串,加不加引号对于 CMake 来说都一样
- 对于含有空格的情况,空格在不加引号时会被视作分隔符,在引号内则不会,例如
A B C
被视作三个字符串,"A B C"
被视作一个字符串 - 无论加不加引号,
;
都会被视作字符串列表中的分隔符 - 单个字符串被视作只有一个元素的字符串列表,因此使用 CMake 的列表操作也是可以的
- 字符的处理:
转义字符加双斜杠,例如
"ABC\\nDEF"
;特殊字符需要加单斜杠,如"ABC\"D"
,在 CMake 中的路径分隔符应当使用/
表示
- 变量的名称:
几乎可以由任何文本组成,建议简单使用字母、数字、
-
和_
组成,CMake 内部的习惯是纯大写加下划线 - 变量的值: 值在本质上都是字符串或字符串列表
- 其中的
0-9
字符可以被解释为数字 - 其中的 TRUE/FALSE,ON/OFF,YES/NO,Y/N 可以被解释为布尔变量,此时不区分大小写,建议使用 ON/OFF
- 由于访问变量的本质是字符串的展开替换,
${var}
不同于"${var}"
,可能被拆成多个传递,建议把访问后的值加引号,避免值在解析时被错误地拆开
- 其中的
${var}
不同于"${var}"
(其中的 var
是一个列表),在某些情形下会因为解析逻辑不同,得到不一样的处理结果,例如
1 | set(specialStr "aaa;bbb") |
普通变量
我们可以在 CMakeLists
中使用set()
命令定义一个普通变量,赋予它一个值(字符串或字符串数组),例如
1 | set(Var "value") |
通常使用${Var}
访问变量,以字符串替换的形式获取变量的值。
1 | message("Var=${Var}") |
对于字符串列表,可以用很多种等价的定义形式,其中的;
被用作字符串的分隔符。
1 | # 对字符串用分号分隔代表列表 |
在变量列表被整体访问时也会得到使用;
进行分隔的整体,例如
1 | message("Var_A=${Var_A}") |
下面这种形式不是列表,空格被保存在了 Var_B 的值中,不视作字符串列表的分隔符
1 | set(Var_B "v1 v2 v3") |
可以使用unset
命令撤销变量的定义,或者可以把变量修改为空字符串,效果一样(CMake
对于未定义变量的解析结果就是空字符串)
1 | unset(Var) |
CMake 非常反人类的语法是,如果 Var
这个变量没有定义,那么直接读取${Var}
也不会报错,会正常地返回一个空字符串。
特别注意,在后面的 IF 语句等结构中,IF
后面可以直接使用变量名Var
,而不是用${Var}
,这个语法糖是因为历史原因,IF
语句的出现比${}
还要早。
可以使用下面的语句来判断是否定义了某个普通变量,如果没有则使用默认值来定义
1 | if(NOT DEFINED ABC) |
普通变量具有独立的作用域,父作用域的普通变量在子作用域中可以访问,但是在子作用域中的修改不会反馈到父作用域中(可以通过 PARENT_SCOPE 选项反馈到上一层作用域),详见函数的普通变量。
缓存变量
缓存变量介绍
CMake 第一次构建时会生成 CMakeCache.txt,基于 CMakeCache.txt 存储缓存变量。 此后的构造则会偷懒跳过很多步骤,直接从 CMakeCache.txt 获取缓存变量。(如果缓存错误,可以直接修改或删除 CMakeCache.txt,也可以删除整个 build 文件夹,让 CMake 一切重新开始)
缓存变量的常见类型:
- BOOL: 布尔值 ON/OFF
- FILEPATH: 文件路径
- PATH: 目录路径
- STRING: 字符串
缓存变量的创建和修改:
- 在生成构建系统的 cmake
命令中,附加的
-D
可以直接定义或修改缓存变量,或者使用-U
撤销缓存变量,包括最常见的两个缓存变量的设置CMAKE_BUILD_TYPE
编译类型(Debug/Release 等)CMAKE_INSTALL_PREFIX
安装目录前缀
- 在 CMakeLists 中
- 使用
set(...CACHE...)
定义缓存变量- 注意只能在缓存变量不存在时定义(相当于提供缓存变量的默认值)
- 如果缓存变量已经存在于 CMakeCache.txt 中,则这条命令没有修改能力,被直接忽略
- 在 CMakeLists
中使用
set(...CACHE...FORCE)
强制定义或修改缓存变量,此时无论缓存变量是否存在,都会生效
- 使用
- 简单粗暴的方式:直接编辑 CMakeCache.txt 来定义或修改缓存变量(这也是有效的,只需要保证正确的数据格式)
缓存变量的获取:(全局可见)
- 使用
${Var}
获取名为 Var 的普通变量/缓存变量,如果存在恰好同名的普通变量,会优先于缓存变量被读取(因为普通变量是局部的,访问优先级更高) - 使用
if(DEFINED Var)
也一样,如果定义了名为 Var 的普通变量或缓存变量都可以 - 使用
$CACHE{Var}
强制获取名为 Var 的缓存变量,不会被同名的普通变量替代
缓存变量的意义: cmake
可以记住第一次通过命令行-D
选项定义的变量和值(通过命令行定义的变量自动是缓存变量),使得第一次生成之后不需要重复输入繁琐的命令,再次生成时不需要重复定义。
再次生成时使用命令行-D
选项可以赋值或修改已存在的缓存变量,赋值和修改都在
CMakeCache,txt 文件中进行。保证 CMakeLists
中的直接赋值比命令行-D
有着更低的修改权限(从机制上避免出现虽然用-D
修改,又因为
CMake 执行 CMakeLists
中的命令,把-D
的修改自动抹掉的情况),但是仍然提供了缓存变量修改的最高权限——通过
CMakeLists 中的 FORCE 修改。
缓存变量还可以进一步分为:
- 外部缓存变量(EXTERNAL cache entries)
- CMake 创建的外部缓存变量
- 用户创建的外部缓存变量
- 内部缓存变量(INTERNAL cache entries)
可以直观地在 CMakeCache.txt 文件中发现,确实是按照上述结构进行的存储, 例如 CMAKE_BUILD_TYPE 就是 CMake 创建的外部缓存变量。
不太了解它们之间的区别,可能只是来源?或者对于外部的缓存变量,CMake 可能更加关注,比如有没有使用等。
命令行方式
在命令行中,使用-D
选项创建缓存变量的具体语法和例子如下,可以附带缓存变量的类型
1 | cmake -Bbuild -D <var>:<type>=<value>, <var>=<value> |
我们可以在 CMakeCache.txt 中看到自定义的条目,例如
1 | //No help, variable specified on the command line. |
可以使用-U
选项删除缓存文件中的缓存变量,支持按照匹配模式批量删除(但是得留意
CMakeLists 是不是又自动加进来了)
1 | cmake .. -U <globbing_expr> |
CMakeLists 方式
在 CMakeLists
中使用set(...CACHE...)
创建缓存变量的语法为
1 | set(<variable> <value> ... CACHE <type> <docstring> [FORCE]) |
其中的CACHE
是必要的,<docstring>
是缓存变量的描述语句,<type>
代表缓存变量的类型。
例如
1 | set(MY_CACHE_VALUE "value" CACHE STRING "Value Created by Me") |
在 CMakeLists
中使用set(...CACHE...FORCE)
命令强制修改缓存变量的值,例如
1 | set(MY_CACHE_VALUE "value" CACHE STRING "Value Created by Me 2" FORCE) |
还有一个很常用的 option 命令,它是定义 BOOL 类型的缓存变量的语法糖
1 | option(<variable> "<help_text>" value) |
环境变量
cmake 可以使用或修改当前进程中是环境变量,环境变量的修改是临时性的,只在当前的 cmake 进程中有效,建议不要乱用环境变量。
设置环境变量
1 | set(ENV{DEMO_VAR} "hello") |
获取环境变量
1 | $ENV{DEMO_VAR} |
和缓存变量不同,无法在缺省 ENV 关键词的情况下获取到环境变量的值。
撤销环境变量
1 | unset(ENV{DEMO_VAR}) |
例如获取一个系统预设的环境变量 HOME,得到的结果为
1 | if(DEFINED ENV{HOME}) |
常见变量
下面提供一些常见的变量。(很多是缓存变量,可以在 CMakeCache.txt 找到)
版本号
版本号格式为major[.minor[.patch[.tweak]]]
,一共包括四个数字,不足的部分末尾补
0。
可以在 project 命令中指定项目的版本,在 CMake 中,可以使用如下变量获取当前的版本号
PROJECT_VERSION
完整的版本号PROJECT_VERSION_MAJOR
主版本号,第一个数字PROJECT_VERSION_MINOR
第二个数字PROJECT_VERSION_PATCH
第三个数字PROJECT_VERSION_TWEAK
第四个数字
目录相关变量
CMAKE_MODULE_PATH
: cmake 查找.cmake
模块的目录,可以使得 include 命令不需要添加搜索目录CMAKE_INSTALL_PREFIX
: cmake 安装位置前缀CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT
: 布尔变量,表明当前的安装位置前缀是否被设置,还是仍然为默认值PROJECT_NAME
:当前项目名称,CMAKE_PROJECT_NAME
: 根项目名称;PROJECT_BINARY_DIR
,<projectname>_BINARY_DIR
,CMAKE_BINARY_DIR
: 项目的编译目录,通常为生成时指定的/build
子目录,三者的细微区别由前缀体现。PROJECT_SOURCE_DIR
,<projectname>_SOURCE_DIR
,CMAKE_SOURCE_DIR
: 项目的源文件目录,通常为 project 命令的 CMakeLists.txt 所在目录,三者的细微区别由前缀体现。(建议不要使用 CMAKE_SOURCE_DIR)CMAKE_CURRENT_SOURCE_DIR
,CMAKE_CURRENT_LIST_DIR
:正在处理的CMakeLists.txt
所在目录,两者可能略有区别,建议使用后者,尤其在依赖管理时的目录
注:上文中不同前缀对应的细微区别如下
CMAKE_
通常指的是根项目的属性,建议不要直接使用,因为这使得根项目无法作为子项目存在。(根项目指的是启动 CMake 时的 CMakeLists.txt 的第一个项目)PROJECT_
当前项目的属性,如果只有一个项目,则与CMAKE_
相同;如果存在子项目则不同。(当前项目指的是最近一次调用的project(...)
)<projectname>_
指定某个具体的项目的属性
特性相关变量
CMAKE_CXX_COMPILER_ID
: 编译器的 ID,例如"MSVC","GNU","Clang"CMAKE_GENERATOR
: 构建系统CMAKE_BUILD_TYPE
: 构建模式,debug/release 等CMAKE_CXX_STANDARD
: c++标准,例如 20 代表 c++20CMAKE_CXX_STANDARD_REQUIRED
: 布尔变量,是否严格要求满足 c++标准CMAKE_DEBUG_POSTFIX
: debug 模式下会给生成的库赋予额外的后缀,便于区分,例如set(CMAKE_DEBUG_POSTFIX "_d")
鉴于 MSVC 和 Linux 上的构建系统有太多不一样,CMake 直接定义了如下变量,可以直接判断并进入不同的处理分支
1 | if(MSVC) |
输出位置相关变量
见前文。
1 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${PROJECT_SOURCE_DIR}/bin") |
编译选项相关变量
见前文。
1 | set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -Wall -Wextra -Wfatal-errors -Wshadow -Wno-unused-parameter -O0") |
字符串操作
cmake
支持对字符串的简单操作。在这里我们使用<string>
表示字符串值,使用<string-var>
表示值为字符串的变量名称。
以如下的长字符串为例
1 | set(S |
字符串访问与查找
LENGTH: 获取字符串的长度,结果存在 out-var 中
1 | string(LENGTH <string> <out-var>) |
注意这里不是给一个字符串变量,而是需要给一个字符串值,例如
1 | set(S2 "abc e ") |
FIND: 在字符串中查找指定的子串,返回子字符串开头在原字符串中的索引,默认查找第一次出现的,也可以反向查找最后一次出现的,没有找到会返回-1
1 | string(FIND <string> <substring> <out-var> [...]) |
例如
1 | string(FIND ${S} "in" S_index) |
SUBSTRING: 子字符串提取,指定字串的开始索引和长度,结果存入 out-var
1 | string(SUBSTRING <string> <begin> <length> <out-var>) |
例如
1 | string(SUBSTRING ${S} 0 8 S_HEAD) |
字符串增加
APPEND: 在字符串变量的尾部添加字符串
1 | string(APPEND <string-var> [<input>...]) |
例如
1 | set(S2 "Hello") |
PREPEND: 在字符串变量的头部添加字符串
1 | string(PREPEND <string-var> [<input>...]) |
例如
1 | set(S2 "Hello") |
字符串替换
REPLACE:
将输入字符串<input>
中所有出现的<match-string>
替换为<replace_string>
,并将修改后的结果存储在<output_var>
中。
1 | string(REPLACE <match-string> <replace-string> <out-var> <input>...) |
例如
1 | set(S2 "Hello,world!") |
字符串正则表达式替换
速成一下简单的正则表达式语法
^
: 匹配输入开头$
: 匹配输入结束.
: 匹配任意单个字符\<char>
: 匹配单字符<char>
。使用它来匹配特殊的正则表达式字符,例如\.
表示点,\\
表示反斜杠,\a
表示a
[ ]
: 匹配任何在括号内的字符[^ ]
: 匹配任何不在括号内的字符-
: 用在方括号内,指定字符的范围,例如[a-f]
表示[abcdef]
,[0-3]
表示[0123]
,[+*/-]
表示数学运算符。*
: 匹配前面模式的零次或多次+
: 匹配前面模式的一次或多次?
: 匹配前面模式的零次或一次|
: 匹配|
两侧的模式()
: 保存匹配的子表达式(模式)
REGEX MATCH:
字符串正则匹配,将所有输入字符串<input>
在匹配之前都连接在一起,然后根据正则表达式<regular_expression>
匹配一次,将匹配的结果存储在<output_variable>
1 | string(REGEX MATCH <regular_expression> <output_variable> <input> [<input>...]) |
例如可以匹配任何含有 in 的单词,但是注意到只会匹配一次
1 | string(REGEX MATCH "[A-Za-z]*in[A-Za-z]*" S_out_var ${S}) |
REGEX MATCHALL: 字符串正则匹配,和上面的区别就是匹配所有的项,结果以一个列表的形式返回
1 | string(REGEX MATCHALL <regular_expression> <output_variable> <input> [<input>...]) |
例如可以匹配任何含有 in 的所有单词
1 | string(REGEX MATCHALL "[A-Za-z]*in[A-Za-z]*" S_out_var ${S}) |
REGEX REPLACE:
字符串正则替换,将所有输入字符串<input>
在匹配之前都连接在一起,然后尽可能匹配<regular_expression>
并替换为
<replacement_expression>
,将结果存储在<output_variable>
。
1 | string(REGEX REPLACE <regular_expression> <replacement_expression> <output_variable> <input> [<input>...]) |
例如把所有匹配到的含有 in 的单词,替换成 hello
1 | string(REGEX REPLACE "[A-Za-z]*in[A-Za-z]*" "hello" S_out_var ${S}) |
字符串大小写转换
TOUPPER,TOLOWER: 修改字符串的大小写形式,结果存入 out-var
1 | string(TOUPPER <string> <out-var>) |
例如
1 | set(S2 "aBc") |
字符串比较
对两个字符串按照字典序列比较,注意对大小写敏感,结果 true 或 false 存入最后的变量中
1 | string(COMPARE LESS <string1> <string2> <output_variable>) |
列表操作
cmake 支持对值为字符串列表的变量(通常是文件名列表,或目录列表)使用简单的操作,这部分内容主要参考这篇博客:【CMake 语法】(7) CMake 列表操作
列表访问与查询
LENGTH: 获取列表的长度,会把 list 的长度赋值给 out-var
1 | list(LENGTH <list> <out-var>) |
例如
1 | list(LENGTH A Alen) |
GET: 获取列表指定索引的元素,索引从 0 开始,0 代表第一个元素,还支持反向索引,-1 代表最后一个元素
1 | list(GET <list> <element index> [<index> ...] <out-var>) |
这里既可以只用一个索引,得到的单个值保存在 out-var,也可以使用多个索引,得到的值列表保存在 out-var,例如
1 | set(A a b c d e) |
FIND: 在列表中查找指定元素,返回列表指定元素的索引,如果未找到返回 -1
1 | list(FIND <list> <value> <out-var>) |
例如
1 | set(A a b c d e) |
列表增加
APPEND: 在列表尾部增加元素
1 | list(APPEND <list> <values> |
例如
1 | set(A a b c d e) |
PREPEND: 在列表的头部添加元素
1 | list(PREPEND <list> [<element>...]) |
例如
1 | set(A a b c d e) |
IINSERT: 在列表指定索引位置插入元素
1 | list(INSERT <list> <index> [<element>...]) |
例如
1 | set(A a b c d e) |
列表删除
REMOVE_ITEM: 按照值删除,可以同时删除多个值
1 | list(REMOVE_ITEM <list> <value>...) |
REMOVE_AT: 按照索引删除,可以同时删除多个索引
1 | list(REMOVE_AT <list> <index>...) |
REMOVE_DUPLICATES: 列表去重,保持相对顺序
1 | list(REMOVE_DUPLICATES <list>) |
POP_BACK,POP_FRONT: 以栈的形式删除列表的尾部或头部的若干元素(默认只删除一个,但是如果后接 m 个变量,则一次性删除 m 个,并且把值赋给它们)
1 | list(POP_BACK <list> [<out-var>...]) |
数学表达式
既然是把 cmake 作为一门语言,那么基本的数学运算也搞一搞吧。
1 | math(EXPR <var> "<expression>" [OUTPUT_FORMAT <format>]) |
计算的表达式 expression 需要以字符串的形式给出,支持数字和运算符(C 语言风格),计算的结果会保存到 var 中,甚至可以加入一些输出格式要求,例如
1 | math(EXPR value "3+2") |
甚至可以加上流程控制,达到更复杂的运算
1 | set(value 0) |
当然,即使 cmake 作为一门 DSL 语言啥都不缺,但肯定没有人会把复杂的编程内容通过 cmake 语言实现。