CMake 是一个开源、跨平台的构建系统生成器(Build-system Generator)。
本文是 Modern CMake 简明教程系列的下篇,上篇请移步至 《Modern CMake 简明教程(上)》,中篇请移步至 《Modern CMake 简明教程(中)》。
本教程默认 CMake 最低版本为 3.16,即 cmake_minimum_required(VERSION 3.16)
。
一、集成第三方库
在项目中集成第三方库是一种非常常见的需求,CMake 提供了两种方式来集成第三方库。
第一种:直接集成第三方库的源码。
说到集成第三方库的源码,我们第一时间想到的可能就是将其源码直接拷贝到项目目录中,然后提交到 git 仓库,更加高级一点可能会使用 git submodule 的方式。但我以为这两种方式都不够优雅,无法很好的管理、更新依赖库,特别是在项目的依赖库的比较多时。
CMake 提供了两个模块(两种方式)来集成第三方库的源码:
- FetchContent
该模块支持在 CMake 生成项目时就下载第三方库,还会自动将第三方库添加到项目中,不需要手动调用 add_subdirectory。 - ExternalProject
该模板支持在构建(编译)项目时下载第三方库。显然 ExternalProject 的下载时机要晚于 FetchContent 。
第二种:使用预编译好的第三方库。
这种方式需要先单独编译安装第三方库,然后使用 find_package 查找该库,最后设置目标的相关属性,如包含目录、依赖库等。
1.1 FetchContent
FetchContent 的使用大致分为两个步骤。
步骤一: 使用 FetchContent_Declare 命令记录要获取的内容,可以多次调用 FetchContent_Declare 命令来记录获取多个内容。
FetchContent_Declare 的定义如下:
1 | FetchContent_Declare( |
其中,<name>
可以是任何不带空格的字符串,并且该名称不区分大小写,但通常我们仅使用字母、数字和下划线。
下面示例分别演示了如何从 Git 仓库、https链接、SVN仓库获取内容:
1 | FetchContent_Declare( |
步骤二: 调用 FetchContent_MakeAvailable 命令开始获取上面声明的内容。
如:
1 | FetchContent_MakeAvailable(googletest myCompanyIcons myCompanyCertificates) |
1.1.1 结果
如果 FetchContent 执行成功,下面几个变量会设置:
<lowercaseName>_POPULATED
始终被设置为 TRUE<lowercaseName>_SOURCE_DIR
目标内容的源码目录<lowercaseName>_BINARY_DIR
目标内容的构建目录
1.1.2 使用代理
限于国内的网络环境,在获取内容时,很可能出现下载失败的情况,此时可以尝试使用 http 和 https 代理来解决该问题。
通过设置相应的环境变量即可设置 http(s) 代理,如:
1 | set(ENV{http_proxy} "http://127.0.0.1:7890") |
1.2 find_package
find_package 用于查找已经安装到本机的包,将查找结果存储在 <PackageName>_FOUND
变量中(查找到包,值为 1,否则 为 0),包的安装路径存储在 <PackageName>_DIR
变量中,通常还会定义一些变量来指明包的版本、头文件目录的路径、.lib 或 .a 文件的路径等,这些变量名称的格式会根据查找方式的不同、包的不同而不同。
find_package 使用起来比较简单,通常我们只需要使用它的基础定义:
1 | find_package(<PackageName> [<version>] [REQUIRED] [COMPONENTS <components>...]) |
<PackageName>
指定包的名称,是唯一的必选参数。<version>
指定需要查找包的版本,major[.minor[.patch[.tweak]]]
,支持多种形式的版本,如 3、3.1、3.1.2 等,可以指定匹配大版本还是小版本匹配等,也可以通过指定 EXACT 选项来要求版本完全一致,当然也可以完全省略版本约束。- REQUIRED
参数用于指定该包是必须找到,如果没有找到则停止执行该 CMake 脚本。
find_package 的完整定义可以见:full-signature
下面示例用于查找大版本为 3 的 OpenCV 包:
1 | find_package(OpenCV 3 REQUIRED) |
不同的查找模式
find_package 有两种查找包的方式,优先使用 Module 模式,如果 Module 模式没有查找到,再使用 Config 模式查找。
Module 模式
find_package 使用 Module 模式查找包就是查找 Find<PackageName>.cmake
文件的过程,会尝试在下面位置中查找该文件,优先级从高到低依次为:
- CMAKE_PREFIX_PATH
该变量是以分号分隔的列表,默认为空,由用户设置;也可以定义环境变量 CMAKE_PREFIX_PATH,环境变量 $ENV{CMAKE_PREFIX_PATH} 定义的列表会附加到 ${CMAKE_PREFIX_PATH} 变量的后面。 - CMAKE_MODULE_PATH
该变量也是以分号分隔的列表,默认为空,由用户设置。
Find<PackageName>.cmake 文件从何而来?
如果你是库的开发者,你是不需要提供 Find<PackageName>.cmake
文件的,你只需要按照文章 Modern CMake 简明教程(中) “安装导出依赖项”章节介绍的那样,在安装时生成 <PackageName>Config.cmake
(或 <lowercasePackageName>-config.cmake
文件)即可,该文件可以用于 find_package 的 Config 模式查找。
对于那些没有按照规范提供上述 Config 文件的库,才需要使用者来编写 Find<PackageName>.cmake
,辅助查找包的安装路径。
Config 模式
find_package 指令在大多情况下都是通过 Config 模式来查找到包的具体位置的。
find_package 使用 Config 模式查找包就是查找 <PackageName>Config.cmake
或 <lowercasePackageName>-config.cmake
文件的过程。
在 Config 模式下,<PackageName>
可以通过 NAMES 参数来指定多个需要匹配查找的包名(PackageName),例如下面查找 Qt5 或 Qt6 的方式:
1 | find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) |
而且在该模式下,find_package 查找 config 文件的步骤也非常复杂,尤其是可以根据不同开关来查找不同的路径,下面介绍几个常用的查找 config 文件的位置(完整的见官方文档),按优先级从高到低依次为:
- <PackageName>_DIR 变量或环境变量所指定的目录,默认为空。
- CMAKE_PREFIX_PATH、CMAKE_FRAMEWORK_PATH、CMAKE_APPBUNDLE_PATH 变量或环境变量所指定的目录,该变量是以分号分隔的列表,默认为空。
- PATH 环境变量所指定的目录,该变量是以分号分隔的列表,默认为系统 PATH 环境变量的值。如果该变量中的路径以 bin 或 sbin 结尾,则自动回退到上一级目录进行查找。
与 Module 模式不同的是,在上述几个位置中,除了第 1 个指定的目录是“根目录”,CMake 只会在该目录的根目录下查找 <PackageName>Config.cmake
或 <lowercasePackageName>-config.cmake
文件,不会进入其子目录中查找,如设置 OpenCV_DIR 为 /home/jack
,CMake 只会查验如下文件:
1 | /home/jack/OpenCVConfig.cmake |
而第 2、3 所指定的目录都是“路径前缀”,CMake 不仅会在根目录下查找 <PackageName>Config.cmake
或 <lowercasePackageName>-config.cmake
文件,还会进入其子目录内查找。当然 CMake 不会无脑的遍历所有子目录,而且只在特定的子目录内进行查找,不同系统环境下所查找的子目录也不同,具体如下表所示。
查找路径 | 系统环境 |
---|---|
<prefix>/(cmake|CMake)/ |
Windows |
<prefix>/<name>*/ |
Windows |
<prefix>/<name>*/(cmake|CMake)/ |
Windows |
<prefix>/<name>*/(cmake|CMake)/<name>*/ |
Windows |
<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/ |
Uninx |
<prefix>/(lib/<arch>|lib*|share)/<name>*/ |
Uninx |
<prefix>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ |
Uninx |
<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/ |
Windows、Uninx |
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/ |
Windows、Uninx |
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ |
Windows、Uninx |
在上表中,<prefix>
就是第 2、3 项所指定的路径前缀;<name>
为包名,不区分大小写,<name>*
的意思是包名后面还可以接一些字符,如 OpenCV-3.0。
结果获取
无论是哪种查找模式,我们都需要获取查找的结果。目前我能确定的是,这两种查找模式都会定义 <PackageName>_FOUND
和 <PackageName>_DIR
变量,对于不同的库还会定义不同的变量,如何知道他们到底定义了哪些变量呢?
我有一个粗暴但好用的方法:在调试时,遍历当前 CMake 中的所有变量。
下面 dump_all_variables 函数会输出当前 CMake 的所有变量:
1 | function(dump_all_variables) |
使用:
1 | dump_all_variables() |
1.3 集成 Qt
在项目中集成 Qt 库需要先使用 find_package 查找 Qt 的安装位置。对于 Qt4, CMake 使用 Module 模式进行查找(FindQt4.cmake 由 CMake 提供),而 对于 Qt5、Qt6,则是使用 Config 模式进行查找,相应的 config 文件位于类似下面的目录中 D:\Qt\5.15.2\msvc2019\lib\cmake
。
具体从哪些位置查找 Qt,参见上面的“find_package”章节。
示例:
1 | find_package(Qt6 COMPONENTS Widgets DBus REQUIRED) |
众所周知,编译 Qt 代码需要依赖 Qt 提供的一些工具来生成相关的 C++ 代码,如:
- moc
元对象编译器,将 Qt 扩展的 C++ 语法(如 Q_OBJECT)转换成标准 C++ 语法。 - rcc
把 .qrc 资源文件编译成标准 C++ 代码。 - uic
把 .ui 文件编译成标准 C++ 代码。
在 CMake 中要使用这些工具并不复杂,只需要提前开启相关特性,CMake 就会自动调用相关工具。
1 | set(CMAKE_AUTOMOC ON) |
下面是一个简单的 CMake Qt 项目示例,该示例仅使用了 Qt 的 QWidget 模块。
源文件结构如下:
1 | CMakeLists.txt |
CMakeLists.txt 内容如下:
1 | cmake_minimum_required(VERSION 3.16) |
在上面示例中,设置了可执行字符集为 utf-8,这种方式可以防止 Qt 在 MSVC 环境下出现中文乱码,详见之前的文章 拨开字符编码的迷雾(2)--编译器处理文件编码。
设置 WIN32_EXECUTABLE 属性是为了让链接器使用 /SUBSYSTEM:WINDOWS
子系统,如下图所示:
而设置 VS_DEBUGGER_ENVIRONMENT 属性是为了设置 Visual Studio 的调试环境(如下图所示),确保在调试时能找到 Qt 的相关 dll 文件。
在之前的 Modern CMake 简明教程(上) 中已经介绍了目标属性的设置。
二、MSVC
2.1 设置 MSVC 运行库
MSVC 的运行库有 MD / MDd 和 MT / MTd 之分,下图是 Visual Studio 中设置运行库的界面。
CMake 针对 MSVC 环境默认使用的是 MD / MDd 运行库,通过下面的方式可以将运行库设置为 MT / MTd:
1 | # 需要 CMake 3.15 及以上版本 |
2.2 预编译头文件
CMake 在 3.16 版本中提供了 target_precompile_headers 指令来添加预编译头文件,语法如下:
1 | target_precompile_headers(<target> |
在 3.16 版本之前,需要支持预编译头,可以参考网络上的解决方案:
定义 USE_MSVC_PCH 宏:
1 | macro(USE_MSVC_PCH PCH_TARGET PCH_HEADER_FILE PCH_SOURCE_FILE) |
然后使用该宏添加预编译头文件:
1 | USE_MSVC_PCH(test_app stdafx.h stdafx.cpp) |