CMake 是一个开源、跨平台的构建系统生成器(Build-system Generator)。
CMake 是构建系统生成器,而不是构建系统,CMake 支持生成不同构建系统所支持的工程文件,如 Visual Studio,XCode,Makefile 等。
本教程作为 CMake 的简明教程,不会事无巨细的讲述 CMake 的每一个语法,而是以实用为目的,介绍 CMake 的基础语法和常用指令。
虽然只是简明教程,但通过本教程,你仍然可以掌握 CMake 的脉络,熟练应用 CMake 于项目中。
一、Modern CMake
CMake 距今已有 20 多年的历史,CMake 从 3.0 开始引入 Target 概念,有了 Target 和 Property 的定义,CMake 也就更加地现代化。
我们将引入 Target 概念之前(也就是 3.0 之前)的 CMake 称之为老式 CMake,之后的称之为现代 CMake(Modern CMake)。
现代 CMake 是围绕 Target 和 Property 来定义的,在现代 CMake 中不应该出现诸如下面的指令:
- add_compile_options
- include_directories
- link_directories
- link_libraries
因为这些指令都是目录级别的,在该目录(含子目录)上定义的所有目标都会继承这些属性,这样会导致出现很多隐藏依赖和多余属性的情况。
我们最好直接针对 Target 进行操作,如:
1 | add_executable(hello main.cpp) |
本文讲述的知识点只适用于现代 CMake,让我们脱掉沉重的历史包袱,轻装上阵吧!
二、基础概念
所有的构建系统都需要通过某个入口点来定义项目(如 Visual Studio 的 .sln 文件),CMake 作为构建系统生成器也不例外,CMake 使用的是 CMakeLists.txt
的文件,该文件以 UTF-8 编码(也支持 UTF-8 BOM 文件头),其中存储了符合 CMake 语言规范的脚本代码。
2.1 项目结构
CMake 没有强制规定 CMakeLists.txt 文件的位置以及项目的目录结构,但目前大多数项目都会采用相似的目录结构。
如果项目名称为 my_project,且该项目包含一个名为 lib 的库和一个名为 app 的程序,则目录结构通常如下面所示:
1 | - my_project |
当然,上面的名称并不是一成不变的,可以根据自己的喜好来定义,例如 my_project 可以是任意的项目名,如果不喜欢复数,可以将 tests 改成test,如果没有 python 代码,也可以移除 python 目录,cmake 目录则用于存放 CMake 辅助脚本。
从上面的目录结构可以看到,CMakeLists.txt 文件分散在各个子目录中,但在 include 目录中没有 CMakeList.txt 文件,这样是为了防止暴露不必要的文件给库的使用者,因为 include 目录中存放的是库的头文件,在安装时通常都会将该目录拷贝到指定位置(如Linux系统的 /usr/include)。
extern 目录用于存放第三方依赖库的源码,这些库可以通过 git submodule
的形式来管理,也可以直接将源码拷贝到此,并提交到项目 git 中。但无论使用哪种方式,依赖库最好能支持 CMake,这样可以方便的使用 add_subdirectory
命令将项目添加到工程中(如果你对 add_subdirectory 命令的具体用法还不了解,这没关系,现在你只需要知道该命令可以添加任何包含 CMakeLists.txt 的目录到项目中即可)。
2.2 一个简单的示例
在学习 CMake 之前,我们先将 CMake 玩起来。我们先从一个简单的示例开始,了解 CMake 的基本玩法。
该示例是只包含一个 main.cpp 文件,我们期望编译该文件能生成 hello_cmake 程序。
目录结构如下:
1 | - hello_cmake |
main.cpp 文件的内容非常简单:
1 |
|
CMakeLists.txt 内容如下:
1 | # 设置 CMake 的最低版本 |
完成上面步骤,我们就可以使用 CMake GUI 或命令行(当然你需要提前安装 CMake,这不在本文的介绍范围之内)就可以生成相应的工程了。
通过 CMake 命令行生成 Visual Studio 工程的命令如下:
1 | cmake.exe -G "Visual Studio 15 2017" -S .\hello_cmake -B .\hello_cmake\build |
2.3 源码外构建
我们通常会将构建目录指定到一个单独的子目录内,这个目录名称的通常是 build
。如果不这样做,CMake 生成的工程文件和临时缓存文件会污染源码目录。这种方式有个学名叫“源码外构建” (out-of-source build)。
使用源码外构建时,我们通常还会将 build 目录添加到 .gitignore 文件中。
2.4 工作流程
编写 CMake 脚本的基本流程如下:
- 在脚本第一行使用 cmake_minimum_required 指定运行当前脚本所需的 CMake 最低版本。
- 使用 project 指定项目名称。
- 使用 add_executable 或 add_library 创建目标。
- 为目标设置包含目录、链接库等属性(可选)。
- 安装(可选)。
编写完 CMake 脚本以后,就可以使用 CMake GUI 或命令行来生成对应的工程文件了。以 Visual Studio 为例,对于有 my_lib 库 和 app 应用程序的项目,CMake 会生成如下图所示的 5 个项目:
下面介绍 CMake 自动生成的一些项目的作用:
- 编译 ALL_BUILD 项目会自动编译除 INSTALL 项目外的所有项目。
- 编译 INSTALL 项目会执行 CMake 脚本中指定的安装操作。
- 编译 ZERO_CHECK 项目会再次执行 CMake 脚本,重新生成项目。因此若 CMake 脚本有更新,既可以使用 CMake 工具来重新生成项目,也可以是重新编译 ZERO_CHECK 项目。
2.5 注释
在 CMake 中使用 #
来声明单行注释,这是我们使用最多的注释方法。虽然也支持使用 #[[ ]]
来声明多行注释(也称块注释),但是使用的比较少,例如:
1 | #[[ |
2.6 CMake最低版本
cmake_minimum_required 是我们接触到第一个 CMake 指令,该指令用于指定编译该脚本所需的最低 CMake 版本。
你可以在 CMakeList.txt 文件的第一行都使用该指令来指定运行当前脚本需要的最低 CMake 版本,但我们通常只需要在主 CMakeList.txt (何为主 CMakeList.txt?见下面的“项目名称”小节) 中的第一行使用该指令即可。
1 | cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR]) |
如果运行 CMake 的版本低于
我们始终应该选择一个比编译器晚发布的 CMake 版本,因为只有这样,CMake 才能支持新的编译器选项。但最低版本不应低于 3.0,实际项目中通常最低版本不会低于 3.16(该版本于2020年09月15日发布),本教程也是以此为标准进行讲解的。
2.7 项目名称
使用 project 指定项目名称。
项目名称区别于目标(Target)名称,以 Visual Studio 为例,project 指定的名称对应“解决方案名称”,而 add_executable 或 add_library 等指定的名称才对应具体项目名和生成的“目标文件名”。
设置项目名称后,CMake 会自动定义一些变量(变量的具体用法会在稍后的“3.1 变量”小节进行介绍)。为了方便介绍各个变量的含义,假设我们是通过如下命令来运行 CMake 的:
1 | cmake.exe -G "Visual Studio 15 2017" -S D:\hello_cmake -B D:\hello_cmake\build |
下面列举了一些 CMake 自动定义的变量:
- PROJECT_NAME
项目名称,如 hello_cmake - CMAKE_PROJECT_NAME
如果 CMakeLists.txt 位于项目的顶级目录,还会定义 CMAKE_PROJECT_NAME 变量,值与 PROJECT_NAME 一致。 - PROJECT_SOURCE_DIR
项目的根目录(绝对路径),即-S
参数指定的目录,如 D:\hello_cmake - <PROJECT-NAME>_SOURCE_DIR
值与 PROJECT_SOURCE_DIR 相同,只是变量名不同,如 hello_cmake_SOURCE_DIR - PROJECT_BINARY_DIR
项目的构建目录(绝对路径),即-B
参数指定的目录,如 D:\hello_cmake\build - <PROJECT-NAME>_BINARY_DIR
值与 PROJECT_BINARY_DIR 相同,只是变量名不同,如 hello_cmake_BINARY_DIR
主CMakeLists.txt
主 CMakeLists.txt 即项目根目录下的 CMakeLists.txt 文件。可以通过检查 CMAKE_PROJECT_NAME 与 PROJECT_NAME 变量是否相同来判断当前的 CMakeLists.txt 文件是否为主 CMakeLists.txt。
1 | if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) |
2.8 目标类型
既然现代 CMake 是围绕目标(Target)工作的,Target 如此重要,那我们首先就需要创建一个 Target。
在 C/C++ 开发中,常见的 Target 类型有:可执行文件、静态库、动态库,CMake 还额外提供了一个 MODULE 类型。
下面列举了不同类型的目标的创建方式。
可执行文件
使用 add_executable 指令可以创建可执行文件类型的目标。
1 | add_executable(my_exe main.cpp) |
动态库和静态库
通过为 add_library 指令指定不同的参数,可以创建动态库和静态库。
1 | add_library(<name> [<type>] [EXCLUDE_FROM_ALL] <sources>...) |
1 | # 动态库 |
我们也可以在 add_library 中不指定类型参数,改为通过设置 BUILD_SHARED_LIBS 变量来切换静态库和动态库。下面示例在脚本中设置了 BUILD_SHARED_LIBS 变量值为 ON (ON / OFF 对应 CMake 中的开/关):
1 | cmake_minimum_required(VERSION 3.16) |
也可以通过命令行参数进行指定 BUILD_SHARED_LIBS 变量:
1 | cmake.exe -G "Visual Studio 15 2017" -DBUILD_SHARED_LIBS=ON -S .\hello_cmake -B .\hello_cmake\build |
亦可以在 GUI 界面上设置 BUILD_SHARED_LIBS 变量,如:
三、基础语法
3.1 变量
3.1.1 变量的定义
在 CMake 中使用 set 和 unset
命令来定义和取消定义变量。
CMake 的变量没有类型一说,因为其变量值始终是字符串类型。
基于上述原因,在 CMake 中不能直接使用 +
、-
、*
、\
等操作符对变量进行数学运算,需要使用 math 指令,也不能直接使用 >
、<
、==
等操作符对变量进行逻辑运算,需要使用 LESS、EQUAL 进行判断,详见下面的“条件判断”章节。
CMake 的变量名是大小写敏感的,而且其变量名不像其他语言那样有各种限制,它可以包含任何字符,如空格、问号等,但如果变量名中包含#
(该符号用于行注释),则需要使用 \#
进行转义。
下面的语句都是合法的:
1 | set(abc 123) |
虽然 CMake 允许变量名为任意字符串,但我们仍然建议在变量名称中仅包含字母、数字、-
和 _
,而且字母为大写字母,如:
1 | # 建议的命名方式 |
CMake 还会保留一些标识符,我们在定义变量时尽量不要使用这些名称(你执意要用,CMake 也不会报错):
- 以
CMAKE_
开头的。 - 以
_CMAKE_
开头的。 - 以
_<cmake command>
开头的,如 _file,完整的 command 列表见:cmake-commands。
在定义完变量以后,就可以通过 ${variable}
的形式进行引用了,如:
1 | set(QT_VERSION "5.15.2") |
在 CMake 中,还可以通过引用变量的方式来定义新的变量:
1 | set(a "xyz") |
环境变量的引用方式有所不同,在下面的环境变量章节会详细介绍,而且在 if 条件中可以省略 ${}
,直接使用变量名,如 if(QT_VERSION)
。
CMake 还允许使用未定义的变量,未定义的变量的值为空字符串。
CMake 也允许重复定义变量,变量的值采用最后定义的值。
3.1.2 调试输出
message
为了方便调试脚本,我们可以使用 messsage 指令来输出变量和调试信息。
1 | message([<mode>] "message text" ...) |
<mode>
关键字是可选的,它用于指定消息的类型,消息类型会影响 CMake 对该消息的处理方式。
常用的消息类型有:
- FATAL_ERROR
致命错误(红色),只有该类型会导致脚本终止执行。 - WARNING
警告(红色),脚本继续执行。 - NOTICE(默认)
需要特别关注的消息(红色),脚本继续执行。 - STATUS
普通输出,正常颜色,脚本也会继续执行。
1 | message("Current version is 1.0.0.1") |
cmake_print_variables
如果只是单纯地想打印变量的值,使用 messsage 显得有些繁琐,我们可以使用 cmake_print_variables 函数来打印变量的值,该函数以 variable=value
格式输出每个变量的值,方便进行观察。
由于该函数由 CMake 的 CMakePrintHelpers 模块提供,因此在使用之前,需要先 include(CMakePrintHelpers)
:
1 | include(CMakePrintHelpers) |
3.1.3 列表
列表就是简单地包含一系列值,使用空格分割每个值:
1 | set(MY_LIST a b "c" 1 2 ${MY_NAME} 3) |
也可以使用 ;
来代替空格:
1 | set(MY_LIST "a;b;c;1;2;${MY_NAME};3") |
list 命令
list 命令提供了众多针对列表的操作,如获取元素个数、查找、添加、删除、排序等。
1 | # 获取元素个数 |
下面仅简单地演示 2 个列表功能的用法:
1 | set(NAME_LIST jack jim jeff tom) |
遍历列表
可以使用 foreach 来遍历列表,下面示例演示了如何使用 foreach 遍历输出 MY_LIST 列表中每个元素,其中 _ITEM 变量的作用域仅限 foreach 代码块。
1 | set(MY_LIST hello world) |
我们还可以使用 while 来遍历列表,详见下面的“循环”章节。
3.1.4 双引号的作用
学习到这里,也许你会感到困扰,在定义变量时,为什么有时候使用双引号把值包围起来,有时候又不使用呢?
我们已经学习完了列表的相关知识,现在就可以解释加不加引号的区别了。
在定义变量时,若变量值中包含空格,此时不使用双引号包裹,则等同于定义列表;使用双引号包裹,则等同于定义字符串变量。
1 | # 定义的MY_VAR为列表,包含2个元素:hello、world |
对于列表类型的变量,在使用时是否使用双引号包裹也会有区别:有双引号包裹时,会将数组元素以分号作为分隔符进行拼接,否则会直接拼接各元素。
1 | set(MY_LIST hello world) |
3.1.5 三种不同的变量
CMake 中的变量分为普通变量、缓存变量和环境变量,三者都可以通过 set
指令进行定义。
普通变量
下面使用 set
定义的变量就是普通变量。
1 | set(MY_NAME "jack") |
也可以在 CMake 命令行中通过 -D
参数定义普通变量,如下面示例定义了 BUILD_SHARED_LIBS 和 TEST 变量:
1 | cmake.exe -G "Visual Studio 15 2017" -DBUILD_SHARED_LIBS=ON -DTEST=123 -S .\hello_cmake -B .\hello_cmake\build |
缓存变量
缓存变量也是通过 set
指令定义的,但需要添加额外的参数:
1 | set(<variable> <value>... CACHE <type> <docstring> [FORCE]) |
下面示例定义了一个名为 LIB_VERSION 的缓存变量:
1 | cmake_minimum_required(VERSION 3.16) |
使用 CMake GUI 程序执行该脚本,可以看到界面上多出了一个名为 LIB_VERSION 的文本输入框,而且输入框有默认值 1.0.0.1。
将输入框中的文本修改为 1.0.0.2,再次执行该脚本,可以发现调试输出的内容是 LIB_VERSION = 1.0.0.2
,而且无论我们执行多少次脚本,始终输出的都是该内容。
这是因为 CMake 会将缓存变量及其值存储到 “构建目录\CMakeCache.txt” 文件中,下次运行脚本时,会优先从该文件中加载变量,该文件内容格式大致如下:
1 | # build\CMakeCache.txt 文件 |
其中,<type>
用于指定变量的输入类型。请注意,<type>
指定的不是变量的类型,因为 CMake 的所有变量都是字符串类型。<type>
指定的是输入类型,仅用于帮助 CMake GUI 程序显示不同的用户输入控件,如文本输入框、复选框、文件选择对话框等。
<type>
的取值必须是下面列表中的一个:
- BOOL
开关ON或OFF,在 CMake GUI 上提供一个复选框。 - PATH
文件夹的路径,在 CMake GUI 上提供一个文本输入框和一个按钮来打开文件夹选择对话框。 - FILEPATH
文件的路径,在 CMake GUI 上提供一个文本输入框和一个按钮来打开文件选择对话框。 - STRING
文本字符串,在 CMake GUI 上提供一个文本输入框。 - STRINGS
文本字符串,但在 CMake GUI 上会提供一个下拉列表选择框。 - INTERNAL
虽然也是文本字符串,但不会显示在 CMake GUI 上,因此用户无法在界面上修改该变量的值。
如果我们不希望某些缓存变量直接展示 CMake GUI 上,可以使用 mark_as_advanced 指令将缓存变量设置为高级状态,这样除非用户打开了 “Show Advanced” 选项,否则高级变量不会显示在 CMake GUI 中。在脚本模式下,高级/非高级状态是无效的。
1 | set(DEBUG_LIBNAME_SUFFIX "-d" CACHE STRING "Optional suffix to append to the library name for a debug build") |
option
虽然 option 也可用于定义缓存变量,但其只能定义“开/关”类型的变量。
1 | option(ENABLE_TEST "enable test or not" ON) |
如果未指定初始值,默认为 OFF。
环境变量
环境变量的定义方式如下:
1 | set(ENV{<variable>} [<value>]) |
环境变量在引用时,需要在前面添加 ENV
标识,如 $ENV{<variable>}
示例:
1 | set(ENV{USER_NAME} "jack") |
CMake 程序在启动时,会加载系统的环境变量,同时还会设置一些内置的环境变量,内置的环境变量见 cmake-env-variables。
CMake 虽然会加载系统的环境变量,我们也可以修改该环境变量,但该修改操作不会影响到系统的环境变量。
定义或加载的环境变量只会作用于当前的 CMake 进程,而且对当前进程所运行的所有脚本都可见,但不会影响到其他 CMake 进程和系统中的其他进程。
3.1.6 变量作用域
在上面介绍三种不同的变量时,我一直在竭力避免讨论一个话题,那就是变量的作用域。事实上,上面三种变量还有个不同之处就是作用域的不同。
缓存变量和环境变量都是全局的,它们可以跨文件、目录、函数进行读写,因此作用域主要针对普通变量而言。
CMake 中作用域分为“目录级别作用域”和“函数作用域”(也可以使用 block() 来显式的创建一个作用域,但这种使用方式非常少)。
函数有自己的作用域,在函数中定义的变量只能在函数体中使用,函数体外无法访问。除非在定义变量时添加 PARENT_SCOPE
来将变量作用域设置为上一级目录。
include
使用 include 指令加载和执行其他 CMake 脚本文件(名称通常为 *.cmake
),如:
1 | # utils.cmake |
当然,在
*.cmake
文件中也可以使用 include 指令加载执行其他 cmake 文件。
假设在 A(CMakeLists.txt或*.cmake)中调用 include(B.cmake)
,则 A 和 B 之间可以相互读写彼此的变量(包含普通变量、缓存变量和环境变量)。
include 会在 CMAKE_MODULE_PATH 变量指定的目录列表中搜索指定文件。
add_subdirectory
使用 add_subdirectory 指令可以添加一个子目录到项目构建中,但是被添加的子目录中必须包含 CMakeLists.txt 文件。
假设在 A (CMakeLists.txt文件) 中调用 add_subdirectory(lib)
添加了 lib 子目录,则 lib 目录中的 CMakeLists.txt 可以读写 A 中的普通变量,但 A 不能读写 lib 目录 CMakeLists.txt 的普通变量。但可以在定义变量时添加 PARENT_SCOPE
选项来突破该限制,将变量作用域设置为上一级作用域,即父目录的作用域,如:
1 | # lib\CMakeLists.txt 文件 |
1 | add_subdirectory(lib) |
3.2 数学运算
在前面章节已经提到了 CMake 中的值都是以字符串类型存储的,不能直接使用数学运算符符进行运算,需要使用 math 指令进行数学运算。
math 的语法如下:
1 | math(EXPR <variable> "<expression>" [OUTPUT_FORMAT <format>]) |
其中,<variable>
变量如果没有定义,math 会自动定义该变量;
OUTPUT_FORMAT 选项用于指定计算结果的进制(十六进制或十进制):
- HEXADECIMAL 十六进制
- DECIMAL 十进制(默认)
math 支持如下运算符:
1 | + - * / % | & ^ ~ << >> (...) |
示例:
1 | # value 等于 "300" |
3.3 条件判断
在 CMake 中,使用 if 进行条件判断,语法如下:
1 | if(<condition>) |
与众多语言中的 if 一样,当括号中的条件为真时,才执行指定的 commands。
示例:
1 | set(a 9) |
else() 和 endif() 括号中的内容可以为空,但如果需要指定,则就必须与 if 中的条件完全一致,如下面示例所示:
1 | set(a 9) |
我们通常不在 else() 和 endif() 中指定条件,因为这样太繁琐了。
3.3.1 真值与假值
何为真,何为假,人生真真假假,难以分辨,程序的真假却清清楚楚。
下列常量始终为真(不区分大小写):
- 1 和其他非零数字(包含浮点型),如 1、2、3.14
- ON
- YES
- TRUE
- Y
下列常量始终为假(不区分大小写):
- 0
- OFF
- NO
- FALSE
- N
- IGNORE
- NOTFOUND
- 空字符串
- 被引号包裹的字符串(除始终为真的字符串外,如 TRUE、Y 等)
3.3.2 逻辑运算符
在 CMake 中,逻辑运算符的与、或、非分别使用 AND、OR、NOT 表示。
1 | if(NOT <condition>) |
3.3.3 关系运算符
由于 CMake 变量都是以字符串类型存储的,因此即便是数字也不能直接使用 >
、<
、==
这样的运算符来直接比较。
针对数值类型,CMake 支持的关系运算符如下:
- LESS 小于
- LESS_EQUAL 小于等于
- GREATER 大于
- GREATER_EQUAL 大于等于
- EQUAL 等于
CMake 还支持字符串比较,即从左到右依次比较字符串中的每个字符,出现不相同时立即返回,类似于 C 语言中的 strcmp 函数。
- STRLESS
- STRLESS_EQUAL
- STRGREATER
- STRGREATER_EQUAL
- STREQUAL
3.3.4 存在性校验
CMake 提供了一些判断变量是否定义、目标是否创建、元素是否存在于列表中、文件/目录是否存在等方法。
1 | # 给定名称是否是可以调用的指令 |
3.4 循环
CMake 有两种循环方式:
二者都可以使用 break() 提前退出循环和 continue() 跳过本次循环。
3.4.1 foreach
下面是 foreach 的基本语法形式,这种形式在之前的“遍历列表”章节已经使用到了:
1 | foreach(<loop_var> <items>) |
示例:
1 | set(MY_LIST 1 2 3 4 5 6 7) |
foreach 还支持下面两种语法形式,这两种形式都不需要指定列表 <items>
参数,作用类似于 C 语言中的 for 语句:
1 | # 循环 [0 ~ <stop>],步长为 1 |
示例:
1 | foreach(_I RANGE 3) |
3.4.2 while
while 的语法形式如下,其中 <condition>
为真时(参考 if 条件判断章节),执行代码块中的 commands 命令。
1 | while(<condition>) |
下面示例演示了如何使用 while 来遍历列表:
1 | set(MY_LIST 1 2 3) |
上面示例依次输出如下内容:
1 | element at 0 = 1 |
3.5 函数和宏
3.5.1 函数
使用 function 定义函数:
1 | function(<name> [<arg1> ...]) |
函数有自己的作用域,而宏没有自己的作用域,在函数体里面定义的普通变量默认只能在函数体中被访问,除非在定义时指定了 PARENT_SCOPE
选项,或者改为定义缓存变量、环境变量。
函数在被调用时,函数名是不区分大小写的,如我们定义了名为 foo
的函数,就可以使用 foo()
、Foo()
、FOO()
等形式来调用,但我们还是建议保持与函数定义时的名称一致。
参数
关于函数的参数,我们可以在定义函数时就指定各个参数的名称,如:
1 | function(my_func NAME AGE) |
在调用函数时,调用参数(实参)的个数可以超过定义的参数个数(形参),但不能少于定义的参数个数,否则会报错。超出的参数,可以通过下面的形式获取:
- 使用
ARGV0
,ARGV1
,ARGV2
,...
变量获取函数的每个参数。 - 使用
ARGV
变量获取函数的参数列表,通过ARGN
变量获取参数的个数。
返回值
使用 return()
可以从函数体中提前返回,但不能直接使用 return() 带出返回值,需要借用 set(<variable> <value> PARENT_SCOPE)
方式,来间接的带出返回值。
下面示例演示了函数的定义、调用、参数的获取以及返回值的用法。
1 | function(my_func NAME AGE) |
但在实际项目中,除需要传入不定个数的参数情况外,我们通常在定义函数时,就约定好了参数名称和返回参数的名称,如下面示例:
1 | function(my_func NAME AGE OUT_RET) |
3.5.2 宏
使用 macro 定义宏:
1 | macro(<name> [<arg1> ...]) |
CMake 中的宏和 C 语言中的宏一样,是在调用处进行语句替换后再执行,因此在宏中使用 return()
时要格外小心,可能会终止整个脚本的执行。
建议优先使用函数。
3.6 字符串操作
字符串操作由 string 指令提供,详见官方文档。
3.7 内置变量
本节列举了在项目中经常用到的 CMake 内置变量。
CMAKE_SOURCE_DIR
始终存储的是项目的根目录。CMAKE_BINARY_DIR
始终存储的是项目的根构建目录。PROJECT_SOURCE_DIR
与 CMAKE_SOURCE_DIR 一样,也始终存储的是项目的根目录,但该变量需要使用 project 创建项目以后,才会被定义。PROJECT_BINARY_DIR
与 CMAKE_BINARY_DIR 一样,也始终存储的是项目的构建目录,但该变量需要使用 project 创建项目以后,才会被定义。CMAKE_CURRENT_SOURCE_DIR
存储的是当前正在执行脚本所在的目录。CMAKE_CURRENT_BINARY_DIR
一个工程中可能包括多个项目,每个项目的构建目录不同,该变量存储的是当前项目的构建目录。CMAKE_CURRENT_LIST_FILE
当前脚本代码所在文件的完整路径。CMAKE_CURRENT_LIST_LINE
当前脚本代码所在行数。CMAKE_MODULE_PATH
通过设置改变变量,可以控制 CMake 查找.cmake
文件的路径,在使用include
时就可以直接使用文件名了。如:1
2
3set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")
include(utils)需要注意的是,当在 include 中仅指定了文件名时,不能再添加
.cmake
扩展名,否则会导致查找不到相应文件。WIN32
在 Windows 系统上,定义该变量,值为 1。APPLE
在 Apple 系统上,定义该变量,值为 1。UNIX
在类 UNIX 系统上,定义该变量,值为 1。CMAKE_SYSTEM_NAME
当前构建所选定目标系统,但该变量需要使用 project 创建项目以后,才会被定义。该变量常见的值有:Android、iOS、Linux、FreeBSD、MSYS、Windows、Darwin,完整的列表见:CMAKE_SYSTEM_NAME。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15if (CMAKE_SYSTEM_NAME STREQUAL "Linux")
message(STATUS "current platform: Linux ")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
message(STATUS "current platform: Windows")
elseif (CMAKE_SYSTEM_NAME STREQUAL "FreeBSD")
message(STATUS "current platform: FreeBSD")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
message(STATUS "current platform: macOS")
elseif (CMAKE_SYSTEM_NAME STREQUAL "Android")
message(STATUS "current platform: Android")
elseif (CMAKE_SYSTEM_NAME STREQUAL "iOS")
message(STATUS "current platform: iOS")
else ()
message(STATUS "other platform: ${CMAKE_SYSTEM_NAME}")
endif()MSVC
当编译器是 Microsoft Visual C++ 的某个版本或模拟 Visual C++ cl 命令行语法的其他编译器时设置为 true。MSVC_VERSION
正在使用的 Microsoft Visual C/C++ 版本(如果有)。如果正在使用模拟 Visual C++ 的编译器,则此变量将设置为 _MSC_VER 预处理器定义所给定的模拟工具集版本。
四、目标的属性
4.1 属性调试
在介绍如何设置目标的属性之前,我们先学习一下如何调试输出目标属性,方便在开发中检查属性设置是否出错。
使用 CMakePrintHelpers 模块提供 cmake_print_properties 函数可以打印输出目标的属性,该函数的原型如下:
1 | cmake_print_properties(<TARGETS [<target1> ...] | |
以打印输出目标的“包含目录”属性为例:
1 | add_library(hello_cmake main.cpp) |
输出:
1 | Properties for TARGET hello_cmake: |
4.2 包含目录
使用 target_include_directories 指定目标包含一个或多个目录。指定的目录路径可以是绝对路径也可以是相对路径,如果是相对路径,则该路径是相对于当前脚本文件的。
1 | target_include_directories(<target> [SYSTEM] [AFTER|BEFORE] |
这就相当于在 Visual Studio 中设置“附加包含目录”。
可以针对一个目标重复调用 target_include_directories,会按照调用顺序依次附加包含目录,也可以使用 BEFORE 选项,将本次设置的包含目录插入到最前面。
我们需要特别花精力理解 INTERFACE、PUBLIC、PRIVATE 三者的区别,这三者的区别,我们先按下不表,稍后介绍。
在使用 PUBLIC、PRIVATE 设置包含目录时,会自动设置 INCLUDE_DIRECTORIES
属性;在使用 INTERFACE 设置包含目录时,会自动设置 INTERFACE_INCLUDE_DIRECTORIES
属性。
示例:
1 | target_include_directories(hello_cmake |
4.3 预编译宏
使用 target_compile_definitions 设置目标的预编译宏。
1 | target_compile_definitions(<target> |
可以针对一个目标重复调用 target_compile_definitions,附加多个预编译宏。
使用 PUBLIC、PRIVATE 设置包含目录时,会自动设置 COMPILE_DEFINITIONS
属性;使用 INTERFACE 设置包含目录时,会自动设置 INTERFACE_COMPILE_DEFINITIONS
属性。
示例:
1 | # 定义2个预编译宏 |
4.4 依赖库
使用 target_link_libraries 指令设置目标的依赖库,该指令有很多原型,但常用的原型有:
1 | target_link_libraries(<target> |
item 可以是如下几种类型:
- lib 文件的绝对路径或相对路径,CMake 不会校验文件是否存在
- 其他的 CMake 目标。
- 表达式生成器(见“生成器表达式”章节)。
- 以
-
开头的链接标志,但从 CMake 3.13 版本开始,可以直接使用target_link_options()
替代。
可以针对一个目标重复调用 target_link_libraries,附加多个依赖库。
<PRIVATE|PUBLIC|INTERFACE>
可以省略,如果省略,则默认为 PUBLIC。
使用 PUBLIC、PRIVATE 设置依赖库时,会自动设置 LINK_LIBRARIES
属性;使用 INTERFACE 设置依赖库时,会自动设置 INTERFACE_LINK_LIBRARIES
属性。
4.5 INTERFACE、PUBLIC 和 PRIVATE
INTERFACE、PUBLIC 和 PRIVATE 用于指定属性的可见性传递方案。
目标类型 | 可见性传递 | 自身是否应用该属性 | 使用者是否应用该属性 |
---|---|---|---|
可执行文件 | INTERFACE | 否 | 否(可执行文件不存在使用者) |
可执行文件 | PUBLIC | 是 | 否(可执行文件不存在使用者) |
可执行文件 | PRIVATE | 是 | 否(可执行文件不存在使用者) |
库(动态或静态) | INTERFACE | 否 | 是 |
库(动态或静态) | PUBLIC | 是 | 是 |
库(动态或静态) | PRIVATE | 是 | 否 |
4.6 编译和链接选项
通过 target_compile_options 指令设置编译选项。
1 | target_compile_options(<target> [BEFORE] |
通过 target_link_options 指令设置链接选项。
1 | target_link_options(<target> [BEFORE] |
这两个指令的用法与前面介绍的 target_include_directories 类似。
示例:
1 | # 编译选项 |
4.7 其他属性
CMake 为目标还提供了其他属性,详见 target-properties ,总计约有 400 多个,涵盖了开发中会用到的绝大多数属性,例如其中以 VS_ 开头的属性是专门为 Visual Studio 所准备的。
针对这些属性,需要使用 set_target_properties 指令进行设置。
例如,使用 OUTPUT_NAME 和 DEBUG_OUTPUT_NAME 设置目标的输出文件名。
1 | set_target_properties(zoe PROPERTIES |