CMake 是一个开源、跨平台的构建系统生成器(Build-system Generator)。
本文是 Modern CMake 简明教程系列的中篇,上篇请移步 《Modern CMake 简明教程(上)》
本教程默认 CMake 最低版本为 3.16,即 cmake_minimum_required(VERSION 3.16)
。
一、生成器表达式
不知你是否思考过这样一个问题:我们在编译项目时,通常有不同的配置,如 Debug 和 Release,如何在不同的配置中定义不同的预编译宏、包含不同的目录、依赖不同的库呢?
其实要解决这个问题,并不困难,只需要使用到 CMake 中的 生成器表达式。我们在开源项目的 CMake 脚本中经常看到的 $<...>
这样的表达式就是生成器表达式,但通常都会嵌套使用,如 $<$<...>:...>
。
生成器表达式通常有下面几种形式:
$<condition:true_string>
$<IF:condition,true_string,false_string>
$<BOOL:string>
- 特定的语法形式,如
$<CONFIG:cfgs>
表示当前配置存在于 cfgs 列表中时,表达式结果为 1,否则为 0。
前三种形式比较简单,最后一种形式虽然有多种类型,但我们通常只需要记忆几种常用的,有需要时查阅官方文档。
1.1 $<condition:true_string>
condition 只允许为 0 或 1,其他任何值都会报错。
当 condition 为 1 时,表达式返回 true_string;
当 condition 为 0 时,表达式返回空字符串。
如:
1 | set(ENABLE_JSONCPP 1) |
1.2 $<IF:condition,true_string,false_string>
condition 只允许为 0 或 1,其他任何值都会报错。
当 condition 为 1 时,表达式返回 true_string;
当 condition 为 0 时,表达式返回 false_string。
如下面示例,当设置了 ENABLE_JSONCPP 为 1 时,包含 jsoncpp 头文件,否则包含 rapidjson 头文件:
1 | set(ENABLE_JSONCPP 1) |
1.3 $<BOOL:string>
前面 2 种形式的 condition 都只允许为 0 或 1,这种限制未免有些呆板,如果 ENABLE_JSONCPP 的值不是 1 ,而是 ON 时,该怎么办呢?
此时可以使用 $<BOOL:string>
将其转换成 0 或 1。
何时转换成 0,何时转换成 1?可以参考前面的《Modern CMake 简明教程(上)》 的“条件判断”章节,在该章节中介绍的所有为假的情况都会转换成 0,其他情况则转换成 1。
该表达式的作用注定了其单独使用意义不大,通常都是和其他表达式一起使用,如将上面示例中的 ENABLE_JSONCPP 设置为 ON:
1 | set(ENABLE_JSONCPP ON) |
1.4 逻辑运算
运算符表达式也支持与、或、非三种逻辑运算,语法如下:
1 | $<AND:conditions> |
其中,condition 都只允许是 0 或 1,conditions(复数形式)表示可以是由逗号分割的多个条件列表。
1.5 其他常见表达式条件
CMake 还支持很多类型的生成器表达式,下面列举了一些常见的表达式,这些表达式通常都是用来作为上面介绍的表达式中的条件。
1 | # 字符串比较,string1 与 string2 相等则为 1,反之为 0 |
1.6 转义字符
在生成器表达式中如果需要使用特殊字符,可以使用其转移字符。
1 | # > |
二、安装
侠义的“安装”是将目标编译生成的文件拷贝到指定位置,CMake 中的安装包含但不限于拷贝文件,还可以执行脚本、修改权限等操作。
上面这句话中,涉及到了2个术语,有必要解释一下。
“目标编译生成的文件”在 CMake 中有个学名,叫 Output Artifacts,直译为输出工件,我更愿意将其翻译为“输出品”,不同类型的目标有不同的输出品,例如在 Windows 平台上,可执行程序输出 .exe 文件,动态库输出 .dll 文件(也可能包含 .lib 文件),静态库输出 .lib 文件…..
请牢记 Artifacts 这个单词,在后面的很多定义中都会出现该词。
“指定位置”,顾名思义,我们可以指定一个安装位置。CMake 会从 CMAKE_INSTALL_PREFIX 变量中读取安装位置,该变量有默认值,我们也可以修改该变量来改变安装位置。
在不同的操作系统上,CMAKE_INSTALL_PREFIX 的默认值不同:
- Windows 系统:
C:/Program Files/${PROJECT_NAME}
写入该目录需要管理员权限,因此如果安装失败,请检查是否具有管理员权限。 - UNIX 系统:
/usr/local
CMAKE_INSTALL_PREFIX 指定的位置是安装目录的根目录,不同类型的输出品会存放在其不同的子目录中,如 .lib 文件存放在 lib 目录,.dll 和 .exe 文件存放在 bin 目录,头文件存放在 include 目录,这些子目录的具体名称可以通过 GNUInstallDirs 提供的若干变量来获取,如 CMAKE_INSTALL_BINDIR
、CMAKE_INSTALL_LIBDIR
、CMAKE_INSTALL_INCLUDEDIR
等。
CMake 定义了下列常用的输出品种类(artifact-kind),这些种类在后面的 install 选项中会使用到:
- ARCHIVE
这种类型的输出品包含下列文件(默认位于 lib 目录):- 静态库,Windows 上是 .lib 文件,Linux 上是 .a 文件,但在 macOS 上标记为 FRAMEWORK 的除外。
- 动态库的导入库,如 .lib 文件。
- 在macOS系统上,为启用 ENABLE_EXPORTS 的共享库所创建的链接器导入文件(但标记为 FRAMEWORK 的情况除外)。
- LIBRARY
这种类型的输出品很少用到。 - RUNTIME
这种类型的输出品包含下来文件(默认位于 bin 目录):- 各个系统所支持的可执行文件,如 Windows 上的 .exe。
- 动态库,如 .dll 和 .so 文件。
- OBJECTS
与对象库(使用add_library(<name> OBJECT ...)
方式定义)关联的对象文件。 - FRAMEWORK
在 macOS 上,标有 FRAMEWORK 属性的静态库和共享库都被视为 FRAMEWORK 类型。 - BUNDLE
在 macOS 上,标有 MACOSX_BUNDLE 属性的可执行文件被视为 BUNDLE 类型。 - PUBLIC_HEADER
- PRIVATE_HEADER
2.1 install
使用 install 指令可以定义在安装时需要执行的操作,install 可以定义很多类型的操作,包含但不限于拷贝输出品到指定位置、执行脚本等。
下面是 install 指令支持的调用形式:
1 | install(TARGETS <target>... [...]) |
通过执行 make install
命令或者编译 CMake 生成的 INSTALL 项目,就可以执行 install 指令预先定义的安装操作。
2.2 不同的安装命令
2.2.1 安装 Target
所谓安装 Target 就是将目标的输出品及其关联文件拷贝到指定位置。
1 | install(TARGETS <target>... [EXPORT <export-name>] |
<artifact-option>
用于指定与输出品相关的选项,可以是下面选项中的一个或多个(常用的选项主要是 DESTINATION 和 PERMISSIONS):
1 | [DESTINATION <dir>] |
第一组 <artifact-option>
所设置的选项应用于在本次调用中没有指定输出品类型时。
我们通常会为不同类型的输出品指定不同的选项,如下面示例为不同类型的输出品指定了不同的安装位置:
1 | set_target_properties(hello_cmake PROPERTIES PUBLIC_HEADER include/my_lib.h) |
在实际项目中,我们不会使用 PUBLIC_HEADER 来安装头文件,而是使用下面介绍的
install(DIRECTORY ... )
命令。
EXPORT选项
EXPORT
是一个非常有用的选项,当我们的项目需要作为库被第三方使用时,为了让第三方能够通过 find_package 所查找到所安装的库,就需要在安装时生成 xxxConfig.cmake
文件。
当然,仅仅通过在此指定 EXPORT 选项还不够,此处的 EXPORT 选项仅仅表示将 Target 所安装的输出品绑定到 <export-name>
上,后面我们还需要使用单独的 install(EXPORT ...)
语句来生成 xxxConfig.cmake
文件,详见下面的 “安装导出依赖项” 节。
2.2.2 拷贝目录
拷贝目录到指定位置,语法如下:
1 | install(DIRECTORY dirs... |
可以使用 TYPE 或 DESTINATION 来指定目标路径,其中 TYPE 的取值来自于 GNUInstallDirs 提供的若干变量,如 BIN 等同于 CMAKE_INSTALL_BINDIR 变量。
示例:
1 | install(DIRECTORY ./common TYPE INCLUDE) |
2.2.3 拷贝文件
拷贝文件到指定位置,语法如下:
1 | install(FILES <file>... |
2.2.4 执行脚本
在安装时执行脚本文件或者脚本代码,语法如下:
1 | install([[SCRIPT <file>] [CODE <code>]] |
如果脚本文件的路径是相对路径,则该路径相对于当前项目的根目录(即 CMAKE_SOURCE_DIR 变量存储的路径)。
示例:
1 | install(SCRIPT "./my_lib/helper.cmake") |
1 | install(CODE "message(\"Sample install message.\")") |
2.2.5 安装导出依赖项
如果在安装 Target(见上面的5.2.1节) 时已经指定了 EXPORT 选项,则可以通过 install(EXPORT ...)
的形式来安装需要导出的依赖性,即生成 xxxConfig.cmake
文件,该文件名不是固定的,可以通过 FILE 选项来指定其他名称,但必须是 .cmake
类型的文件。
1 | install(EXPORT <export-name> DESTINATION <dir> |
<export-name>
选项所指定的名称需要与在 install(TARGET ... EXPORT ...)
语句中指定名称一致。
通常为了防止和其他库命名冲突,我们会使用 NAMESPACE 添加命名空间。
在生成 xxxConfig.cmake
文件以后,就可以使用 find_package
来查找并引用依赖库了:
1 | find_package(MyLib REQUIRED) |
三、与 CMake 交互
CMake 与构建项目交互的方式有两种:
- 使用 configure_file 指令动态生成配置文件,通过配置文件的方式来将数据传递给项目,如在 C/C++ 项目中动态生成 .h 文件。
- 使用 file 指令创建配置文件,file 指令的功能非常强大,包含众多与文件相关的操作,如读写文件、下载上传文件、遍历目录等。
本节只介绍 configure_file 指令,该指令用于根据模板文件在指定位置生成新的文件。
1 | configure_file(<input> <output> |
我们首先需要通过 <input>
选项来指定一个模板文件,虽然模板文件可以是任意的后缀名,但我们通常使用 .in
后缀名,例如我们需要通过模板文件生成 version.h 文件,则模板文件名为 version.h.in
。
configure_file 指令会将 <input>
模板文件中的诸如 @VAR@
、${VAR}
、$CACHE{VAR}
、$ENV{VAR}
形式的变量都替换为对应变量的值,如果变量没有被定义,则替换为空字符串。
我们通常还会指定 @ONLY
选项,指定该选项后,就只有 @VAR@
形式的变量会被替换,而其他形式的变量会保留不变,这种方式虽然在生成 .h 文件时没有什么用途,但谁又说 configure_file 只能生成 .h 文件了?如果是生成 .cmake 文件,是不是就有作用了咧。
1 | // version.h.in |
1 | # CMakeLists.txt |
执行 CMake 脚本,动态生成的 version.h 内容如下:
1 |
上述变量替换的方式有一个弊端:虽然能动态替换模板语句中变量的值,但却不能控制语句是否存在。比如我们经常在 C/C++ 项目中根据宏是否被定义来做判断,而不是根据宏的值来做判断:
1 |
这个是时候就需要使用另外一个语法形式了:#cmakedefine VAR ...
,在这种形式中是直接使用变量名的,而不需要使用 @ @
进行包裹。
当定义了 VAR 变量时,将替换为(…就是模板文件中 VAR 后面的内容):
1 |
当没有定义 VAR 变量时,将替换为:
1 | /* #undef VAR */ |