目录
第二部分:实用 CMake (Practical CMake – Getting Your Hands Dirty with CMake)
3.0 集成第三方库和依赖管理 (Integrating Third-Party Libraries and Dependency Management)
3.1 本章介绍(Intro)
3.2 使用 CMake 查找文件、程序和路径(Finding files, programs, and paths with CMake)
3.2.1 查找文件和路径(Finding files and paths)
3.2.2 交叉编译时搜索文件(Searching for files when cross-compiling)
3.2.3 寻找程序(Finding programs)
3.2.4 查找库(Finding libraries)
3.3 在 CMake 项目中使用第三方库(Using third-party libraries in your CMake project)
3.4 编写自己的查找模块 (Writing your own find module)
3.5 在 CMake 中使用包管理器(Using package managers with CMake)
3.5.1 从柯南获取依赖(Getting dependencies from Conan)
3.5.2 使用 vcpkg 进行依赖管理(Using vcpkg for dependency management)
3.6 将依赖项作为源代码获取(Getting the dependencies as source code)
3.6.1 使用纯 CMake 下载依赖项作为源(Downloading dependencies as the source using pure CMake)
3.7 总结&问题 (Summary&Questions)
1.本章内容介绍
到目前为止,在前文中,我们已经介绍了如何使用 CMake 构建和安装我们自己的代码。在本章中,我们将了解如何使用不属于 CMake 项目的文件、库和程序。
本章的第一部分将介绍如何找到这些东西,而后一部分将重点介绍如何管理依赖项以构建您的 CMake 项目。使用 CMake 的最大优势之一是它具有内置的依赖管理功能,可用于发现许多第三方库。
在本章中,我们将了解如何集成安装在系统上的库和本地下载的依赖项。此外,您将了解如何下载第三方库并将其用作二进制文件,以及如何直接从 CMake 项目的源代码构建它们。
我们将研究如何为 CMake 编写指令以可靠地找到您系统上的几乎所有库。最后,我们将看看如何在 CMake 中使用包管理器,例如 Conan 和 vcpkg。本章介绍的依赖管理实践将帮助您使用 CMake 创建稳定且可移植的构建。无论您是使用预编译的二进制文件还是从头开始编译它们,设置 CMake 以结构化和一致的方式处理依赖项都将减少将来修复损坏的构建所花费的时间。
以下是我们将在本章中介绍的
• 使用 CMake 查找文件、程序和路径
• 在 CMake 项目中使用第三方库
• 在 CMake 中使用包管理器
• 将依赖项作为源代码获取
2. 环境要求
与前几章一样,所有示例都使用 CMake 3.21 进行测试,并在以下任一编译器上运行: • GCC 9 或更高版本
• Clang 12 或更高版本
• MSVC 19 或更高版本
此外,一些示例需要安装 OpenSSL 1.1 才能使用编译。一些示例从各种在线位置提取依赖项,因此还需要 Internet 连接。所有示例和源代码都可以从本文的 GitHub 存储库中获得,可以在 https://github.com/PacktPublishing/CMake-Best-Practices 找到。
外部包管理器的示例需要安装在系统上的Conan(1.40 或更高版本)和 vcpkg 才能运行。您可以在此处获取软件:
• Conan:https://conan.io/
• Vcpkg:https://github.com/microsoft/vcpkg
大多数项目迅速增长到依赖于文件、库甚至可能在项目外部管理的程序的规模和复杂性。 CMake 提供了内置命令来查找这些内容。
乍一看,搜索和查找事物的过程似乎很简单。然而,仔细分析,有很多事情需要考虑。
首先,我们必须处理在哪里查找文件的搜索顺序。
然后,我们可能想要添加文件可能存在的其他位置;
最后,我们必须考虑不同操作系统之间的差异。
在高于单个文件的抽象级别上,CMake 能够找到定义目标、包含路径和包特定变量的整个包。有关更多详细信息,请参阅 CMake 项目部分中的库。
有五个 find_... 命令具有非常相似的选项和行为:
• find_file:查找单个文件。
• find_path:查找包含特定文件的目录。
• find_library:查找库文件。
• find_program:查找可执行程序。
• find_package:查找完整的包集。
所有这些命令都以类似的方式工作,但在查找内容方面存在一些微小但重要的差异。特别是,find_package 不仅仅是定位文件;它不仅查找包,而且使文件内容可用于在 CMake 项目中轻松使用。在本章中,首先,我们将了解更简单的查找函数,然后再介绍如何查找复杂的包。
1.基本语法
要查找的最底层和最基本的东西是文件和路径。 find_file 和 find_path 函数共享相同的签名。
它们之间的唯一区别是 find_path 存储在结果中找到文件的目录,而 find_file 将存储完整路径,包括文件名。 find_file 命令的签名如下所示:
find_file ( name | NAMES name1 [name2 ...] [HINTS [path | ENV var]... ] [PATHS [path | ENV var]... ] [PATH_SUFFIXES suffix1 [suffix2 ...]] [DOC "cache documentation string"] [NO_CACHE] [REQUIRED] [NO_DEFAULT_PATH] [NO_PACKAGE_ROOT_PATH] [NO_CMAKE_PATH] [NO_CMAKE_ENVIRONMENT_PATH] [NO_SYSTEM_ENVIRONMENT_PATH] [NO_CMAKE_SYSTEM_PATH] [CMAKE_FIND_ROOT_PATH_BOTH | ONLY_CMAKE_FIND_ROOT_PATH | NO_CMAKE_FIND_ROOT_PATH] )
2.参数解释
VAR:file的路径存储在这个变量中,如果找不到文件,变量将包含
-NOTFOUND name:单个文件名
NAMES name1 [name2]:如果指定,则Config会根据这些指定的name进行搜索,而不是默认的PackageName(如果正在搜索的文件的名称有变化,例如不同的大写或可能包含或不包含版本号的命名约定等,则传递名称列表很有用。传递名称列表时,应以首选方式对名称进行排序,因为一旦找到第一个文件,搜索就会停止。搜索包含版本号的文件:建议您先搜索没有版本号的文件名,然后再搜索包含某种形式的版本号的文件名。这样一来,本地构建的文件优先于操作系统安装的文件。(比如文件名为libpea3.0.2,先搜索libpea,则默认路径先去找默认路径下的,再找别的地方的;然后再加大范围限定,找到对应版本号的文件)
HINTS:查找路径,添加路径。
PATHS:搜索标准的系统环境变量PATH
DOC:注释字符串
3.代码解释
我们来测试一下这些参数:
我们建立这样一个文件夹,include里面有abc.txt。所有文件均是空。
Ⅰ、cmakelists.txt
cmake_minimum_required(VERSION 3.21) project(testfind) find_file(VAR a.txt ) message(STATUS "testfind_SOURCE_DIR : ${testfind_SOURCE_DIR}")
//Path to a file. VAR:FILEPATH=VAR-NOTFOUND
我们发现没有找到这个文件,这是为什么呢?我们不妨先看4.搜索路径解释再返回看这个,我们默认从MODULE模式下开始查找, 先从CMAKE_MODULE_PATH下开始查找,然后到cmake的安装目录下查找,也没有,于是跳到CONFIG模式下查找,.....,最终没找到。
Ⅱ、我们增加HINTS关键字,给系统一个提示:
cmake_minimum_required(VERSION 3.21) project(testfind) find_file(VAR a.txt HINTS "/home/liuhongwei/桌面/testfind" )
//Path to a file. VAR:FILEPATH=/home/liuhongwei/桌面/testfind/a.txt
我们发现成功找到了这个文件,我们在cmakelists.txt中打印这个地址:
message(STATUS "filepath is : ${VAR}")
Ⅲ、我们尝试一下NAMES参数:
cmake_minimum_required(VERSION 3.21) project(testfind) find_file(VAR NAMES ab.txt a.txt HINTS "/home/liuhongwei/桌面/testfind" ) message(STATUS "filepath is : ${VAR}")
我们发现它查找到第一个文件就停止了,若第一个文件是非法文件呢?我们接着试一试?
Ⅳ、测试一下是否能搜索子目录和非法文件:
cmake_minimum_required(VERSION 3.21) project(testfind) find_file(VAR NAMES abcd.txt abc.txt a.txt HINTS "/home/liuhongwei/桌面/testfind" ) message(STATUS "filepath is : ${VAR}")
我们发现它无法找到abc.txt,即它不能搜索子目录,但可以一个一个name往下查找,于是我们要定位若干版本文件可以先打出版本名称再打出版本号。
4.搜索路径解释
MODULE模式搜索路径
在MODULE模式下,cmake通过查找名为Find
.cmake的文件来查找包。 1.首先在变量CMAKE_MODULE_PATH对应的路径中去查找
2.如果该变量为空或者在该路径下没有找到,则进入cmake的安装目录下查找(比如我这里是/usr/share/cmake_3.16/Module)。
如果以上两个路径下没有找到指定包,则根据命令的MODULE参数设置与否决定是否进入CONFIG模式下继续查找。
CONFIG模式搜索路径
在CONFIG模式下,查找路径比MODULE多得多,而且查找的目标配置文件以
Config.cmake 或者 -config.cmake命名
查找顺序为:Ⅰ.在cmake变量或者环境变量_ROOT指定的路径下查找,如果命令中设置了NO_CMAKE_FIND_ROOT_PATH或者CMAKE_FIND_USE_PACKAGE_ROOT_PATH变量设置为false则会跳过此路径;
Ⅱ.在特定的cmake变量指定的位置查找:
CMAKE_PREFIX_PATH、CMAKE_FRAMEWORK_PATH、CMAKE_APPBUNDLE_PATH
(如果设置了NO_CMAKE_PATH参数或者将变量CMAKE_FIND_USE_CMAKE_PATH设置为False,那么会跳过这一步)
Ⅲ.cmake特定的环境变量_DIR 、CMAKE_PREFIX_PATH、CMAKE_FRAMEWORK_PATH、CMAKE_APPBUNDLE_PATH
可以通过NO_CMAKE_ENVIRONMENT_PATH来跳过。Ⅳ.HINT字段指定的路径
Ⅴ.搜索标准的系统环境变量PATH。
其中如果是以/bin或者/sbin结尾的,会自动转化为其父目录。通过指定NO_SYSTEM_ENVIRONMENT_PATH来跳过。Ⅵ.存储在cmake的"User Package Registry"(用户包注册表)中的路径。通过设定NO_CMAKE_PACKAGE_REGISTRY,或者:设定CMAKE_FIND_PACKAGE_NO_PACKAGE_REGISTRY为true,
来避开。Ⅶ.设定为当前系统定义的cmake变量:
CMAKE_SYSTEM_PREFIX_PATH
CMAKE_SYSTEM_FRAMEWORK_PATH
CMAKE_SYSTEM_APPBUNDLE_PATH
通过设定NO_CMAKE_SYSTEM_PATH来跳过。Ⅷ.在cmake的"System Package Registry"(系统包注册表)中查找。
通过设定NO_CMAKE_SYSTEM_PACKAGE_REGISTRY跳过。
或者通过设定CMAKE_FIND_PACKAGE_NO_SYSTEM_PACKAGE_REGISTRY为true。
Ⅸ.从PATHS字段指定的路径中查找。
5.几个路径做一下解释
·CMAKE_PREFIX_PATH:前缀路径是任何搜索的基点,常用文件结构(如 bin、lib、include 等)位于该路径下。 CMAKE_PREFIX_PATH 是路径列表,对于每个条目,如果已设置相应的变量,find_file 将搜索
/include. /include/${CMAKE_LIBRARY_ARCHITECTURE}。通常,CMake 会自动设置变量,开发人员不应更改它们。特定于体系结构的路径优先于通用路径。 ·CMAKE_INCLUDE_PATH 和 CMAKE_FRAMEWORK_PATH 缓存变量只应在标准目录结构不适用时使用。它们不会向路径添加额外的包含后缀。
• 假设 PATH 选项包含 C:\myfolder\bin;C:\yourfolder,
并且 CMAKE_LIBRARY_ARCHITECTURE 设置为 x86_64,则搜索顺序如下:
I. C:\myfolder\include\x86_64
II. C:\myfolder\include\
III. C:\myfolder\bin
IV. C:\yourfolder\include\x86_64
V. C:\yourfolder\include\
VI. C:\yourfolder\
交叉编译时,查找文件的过程往往是不同的,因为交叉编译工具链是收集在自己的自包含目录结构下,不与系统工具链混用。通常,首先,您需要在工具链的目录中查找文件。
通过设置 CMAKE_FIND_ROOT 变量,可以将所有搜索的原点更改为新位置。此外,CMAKE_SYSROOT、CMAKE_SYSROOT_COMPILE 和 CMAKE_SYSROOT_LINK 变量会影响搜索位置,但它们只能在工具链文件中设置,而不是由项目本身设置。如果任何常规搜索位置已经在 sysroot 下或由 CMAKE_FIND_ROOT 指定的位置,它们将不会被更改。任何以波浪号 (~) 开头并传递给 find_ 命令的路径都不会更改,以避免跳过用户主目录下的目录。默认情况下,CMake 首先搜索上一段中任何变量提供的位置,然后继续搜索主机系统。通过将 CMAKE_FIND_ROOT_PATH_MODE_INCLUDE 变量设置为 BOTH、NEVER 或 ONLY,可以全局更改此行为。或者,您可以将 CMAKE_FIND_ROOT_PATH_BOTH 选项、ONLY_CMAKE_FIND_ROOT_PATH 选项或 NO_CMAKE_FIND_ROOT_PATH 选项设置为 find_file。
下表显示了在不同搜索模式下设置选项或变量时的搜索顺序:
CMAKE_STAGING_PREFIX 变量用于提供交叉编译的安装路径。 CMAKE_SYSROOT 变量不应通过在其上安装东西来更改。设置交叉编译工具链将在第 11 章中详细介绍,使用 CMake 进行自动化模糊测试,我们将在其中讨论交叉编译。
查找可执行文件与查找文件和路径非常相似,并且 find_program 命令与 find_file 具有几乎相同的签名。
此外, find_program 有 NAMES_PER_DIR 选项,它告诉命令一次搜索一个目录并搜索每个目录中所有提供的文件名,而不是在每个目录中搜索每个文件。在 Windows 上,.exe 和 .com 文件扩展名会自动添加到提供的文件名中,但不会添加 .bat 或 .cmd。
• find_program 自动将bin和sbin 添加到CMAKE_PREFIX_PATH 提供的搜索位置。
• CMAKE_LIBRARY_ARCHITECTURE 中的值被忽略且无效。
• 使用CMAKE_PROGRAM_PATH 代替CMAKE_INCLUDE_PATH。
• 使用CMAKE_APPBUNDLE_PATH 代替CMAKE_FRAMEWORK_PATH。
• CMAKE_FIND_ROOT_PATH_MODE_PROGRAM 用于更改搜索程序的模式。与其他查找命令一样,如果 CMake 无法找到程序, find_program 将设置
-NOTFOUND 变量。这通常有助于确定是否应启用依赖于某个外部程序的自定义构建步骤。
查找库是查找文件的一种特殊情况,因此 find_library 命令支持与 find_file 相同的选项集。
与 find_program 命令类似,它有一个额外的 NAMES_PER_DIR 选项,在移动到下一个目录之前首先检查所有文件名。查找常规文件和查找库之间的区别在于 find_library 自动将特定于平台的命名约定应用于文件名。在 Unix 平台上,名称将以 lib 为前缀,而在 Windows 上,将添加 .dll 或 .lib 扩展名。
同样,缓存变量与 find_file 和 find_program 中使用的变量略有不同:
• find_library 通过CMAKE_PREFIX_PATH 将lib 添加到搜索位置,它使用CMAKE_LIBRARY_PATH 而不是CMAKE_INCLUDE_PATH 来查找库。 CMAKE_FRAMEWORK_PATH 变量的使用与 find_file 类似。 CMAKE_LIBRARY_ARCHITECTURE 变量的工作方式与 find_file 中的相同。
• 这是通过将相应的文件夹附加到搜索路径来完成的。 find_library 以与 find_file 相同的方式搜索 PATH 环境变量中的位置,但它将 lib 附加到每个前缀。此外,如果已设置 LIB 环境变量而不是 INCLUDE 变量,则它使用该变量。
• CMAKE_FIND_ROOT_PATH_MODE_LIBRARY 用于更改搜索库的模式。CMake 通常了解有关 32 位和 64 位搜索位置的约定,例如使用 lib32 和 lib64 文件夹的平台用于同名的不同库。行为由 FIND_LIBRARY_USE_LIB[32|64|X32]_PATHS 变量控制,这些变量控制应该首先搜索的内容。此外,项目可以使用 CMAKE_FIND_LIBRARY_CUSTOM_LIB_SUFFIX 变量定义自己的后缀,该变量会覆盖其他变量的行为。但是,这样做的需求非常少,而且篡改 CMakeLists.txt 文件中的搜索顺序会很快使项目难以维护,并严重影响系统之间的可移植性。
Ⅰ. 查找静态或共享库
在大多数情况下,只需将库的基本名称传递给 CMake 就足够了,但有时,必须重写该行为。原因之一是,在某些平台上,库的静态版本应该优先于共享库,反之亦然。最好的方法是将 find_library 调用拆分为两个调用,而不是尝试在单个调用中实现此目的。如果静态库与动态库位于不同的目录中,则更加健壮:
find_library(MYSTUFF_LIBRARY libmystuff.a) find_library(MYSTUFF_LIBRARY mystuff)
在 Windows 上,不能使用这种方法,因为 DLL 的静态库和导入库确实具有相同的 .lib 后缀,因此无法通过名称区分它们。 find_file、find_path、find_program 和 find_library 命令在查找特定内容时通常很方便。另一方面,发现依赖关系发生在更高的层次上。这就是 CMake 通过提供 find_package 方法真正擅长的地方。使用 find_package,我们不需要首先搜索所有包含文件,然后搜索所有库文件,然后手动将它们添加到每个目标,最后,考虑所有特定于平台的行为。接下来让我们深入了解如何查找依赖项的过程。
如果你认真地编写软件,迟早你会遇到项目依赖项目外部库的地步。 CMake 社区和我们的作者推荐的方法不是查找单个库文件或头文件,而是使用 find_package 命令执行此操作。在 CMake 中查找依赖项的首选方法是使用包。
包提供了一组关于 CMake 和生成的构建系统的依赖关系的信息。它们可以两种形式集成到项目中。
这要么是由上游项目提供的配置详细信息(也称为配置文件包.cmake文件),要么是所谓的查找模块包,通常在与包无关的地方定义,由 CMake 本身或由项目使用包。
这两种类型都可以使用 find_package 找到,结果是一组导入的目标和/或一组包含与构建系统相关的信息的变量。
findPkgConfig 模块使用 find-pkg 来查找依赖项的相关元信息,也为包提供间接支持。通常,查找模块用于定位依赖项。
例如,当上游没有提供包配置的必要信息时。不要将它们与与 include() 一起使用的 CMake 实用程序模块混淆。只要有可能,应使用上游提供的包而不是查找模块。如果可能,修复上游项目以提供必要的信息比编写查找模块更可取。
请注意,find_package 命令有两个签名:基本或短签名和完整或长签名。在几乎所有场景中,使用短签名就足以找到我们正在寻找的包,并且应该首选它,因为它更易于维护。短格式同时支持模块和配置包,但长格式只支持配置模式。
短模式的签名如下:
find_package(
[version] [EXACT] [QUIET] [MODULE] [REQUIRED] [[COMPONENTS] [components...]] [OPTIONAL_COMPONENTS components...] [NO_POLICY_SCOPE]) 假设我们要编写一个程序,通过使用 OpenSSL 库的适当函数将字符串转换为 sha256 哈希。
要编译和链接此示例,我们必须通知 CMake 该项目需要 OpenSSL 库,然后将其附加到目标。现在,让我们假设必要的库已经安装在系统的默认位置;例如,通过使用常规包管理器,例如 Linux 的 apt、RPM 或类似工具,Windows 的 Chocolatey 或 macOS 的 brew。
示例 CMakeLists.txt 文件可能如下所示:
cmake_minimum_required(VERSION 3.21) project( ch5_find_package_example VERSION 1.0 DESCRIPTION "A C++ project to demonstrate searching third-party dependencies" LANGUAGES CXX ) #搜索软件包 OpenSSL 及其组件 SSL,如果没有找到则配置失败,因为关键字 REQUIRED find_package(OpenSSL REQUIRED COMPONENTS SSL) #创建目标以构建可执行文件 add_executable(find_package_example) target_compile_features( ch5_find_package_example PRIVATE cxx_std_11 ) #将源文件添加到“hello_world”目标 target_sources( ch5_find_package_example PRIVATE src/main.cpp ) #将 openssl 库链接到可执行文件 target_link_libraries(find_package_example PRIVATE OpenSSL::SSL)
//Install path prefix, prepended onto install directories. CMAKE_INSTALL_PREFIX:PATH=/usr/local //Path to a program. CMAKE_LINKER:FILEPATH=/usr/bin/ld //Path to a program. CMAKE_MAKE_PROGRAM:FILEPATH=/usr/bin/make //Path to a library. OPENSSL_CRYPTO_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libcrypto.so //Path to a file. OPENSSL_INCLUDE_DIR:PATH=/usr/include //Path to a library. OPENSSL_SSL_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libssl.so //Arguments to supply to pkg-config PKG_CONFIG_ARGN:STRING= //pkg-config executable PKG_CONFIG_EXECUTABLE:FILEPATH=/usr/bin/pkg-config //Path to a library. OPENSSL_CRYPTO_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libcrypto.so //Path to a file. OPENSSL_INCLUDE_DIR:PATH=/usr/include //Path to a library. OPENSSL_SSL_LIBRARY:FILEPATH=/usr/lib/x86_64-linux-gnu/libssl.so //Arguments to supply to pkg-config PKG_CONFIG_ARGN:STRING= //pkg-config executable PKG_CONFIG_EXECUTABLE:FILEPATH=/usr/bin/pkg-config //Value Computed by CMake ch5_find_package_example_BINARY_DIR:STATIC=/home/liuhongwei/桌面/CMake-Best-Practices-main/chapter05/find_package_example/build //Value Computed by CMake ch5_find_package_example_IS_TOP_LEVEL:STATIC=ON //Value Computed by CMake ch5_find_package_example_SOURCE_DIR:STATIC=/home/liuhongwei/桌面/CMake-Best-Practices-main/chapter05/find_package_example //Path to a library. pkgcfg_lib__OPENSSL_crypto:FILEPATH=/usr/lib/x86_64-linux-gnu/libcrypto.so //Path to a library. pkgcfg_lib__OPENSSL_ssl:FILEPATH=/usr/lib/x86_64-linux-gnu/libssl.so
源文件main.cpp如下所示:
#include
#include #include #include #include int main(int, char**) { unsigned char hash[SHA256_DIGEST_LENGTH]; const std::string message{"CMake is awesome!"}; SHA256_CTX sha256; SHA256_Init(&sha256); SHA256_Update(&sha256, message.c_str(), message.size()); SHA256_Final(hash, &sha256); std::stringstream ss; for(const auto& c : hash) { ss << std::hex << std::setw(2) << std::setfill('0') << (int)c; } std::cout << "The sha256 hash of the message '" << message << "' is:\n"; std::cout << "\t" << ss.str() << "\n"; } 前面的示例做了以下事情:
1.有一个 find_package(OpenSSL REQUIRED COMPONENTS SSL) 调用。这告诉 CMake 我们正在为 OpenSSL 寻找一组库和头文件。具体来说,我们正在寻找 SSL 组件并忽略加密组件(PRIVATE)。 REQUIRED 关键字告诉 CMake 构建这个项目是必需的。这意味着如果找不到库,CMake 将停止配置过程并出现错误。
2. 找到包后,我们告诉 CMake 使用 target_link_libary 将库链接到目标。具体来说,我们告诉 CMake 链接 OpenSSL 包提供的 OpenSSL::SSL 目标。如果依赖项必须是某个版本,则可以将其指定为 major[.minor[.patch[.tweak]]] 格式的单个版本,也可以指定为带有 versionMin..[<]versionMax 的版本范围格式。
对于版本范围,versionMin 和 versionMax 应该具有相同的格式,并且通过指定 <,将排除较高版本。
从 3.21 版开始,CMake 无法在模块中查询可用组件。因此,我们必须依靠模块或库提供者的文档来找出可用的组件。可以使用以下命令查询可用的模块:
cmake --help-module-list #< lists all available modules cmake --help-module
#< prints the documentation for module cmake --help-modules #< lists all modules and their documentation CMake 附带的模块列表可以在 https://cmake.org/cmake/help/latest/manual/cmake-modules.7.html 找到。
查找单个库和文件:可以查找单个库和文件,但首选方法是使用包。查找单个文件并使它们可用于 CMake 将在编写您自己的查找模块部分中介绍。
在模块模式下运行时,find_package 命令搜索名为 Find
.cmake 的文件;这首先发生在 CMAKE_MODULE_PATH 指定的路径中,然后发生在 CMake 安装提供的查找模块中。如果您想了解如何创建 CMake 包,请转到第 4 章,打包、部署和安装 CMake 项目。 在CONFIG模式下运行时,find_package 搜索在以下任一模式之后调用的文件:
•
-config.cmake •
Config.cmake •
-config-version.cmake(如果指定了版本详细信息) •
ConfigVersion.cmake(如果指定了版本详细信息) 所有搜索都将按照明确定义的顺序在一组位置上进行;如果需要,可以通过将相应的选项传递给 CMake 来跳过某些位置。 find_ package包含比其他 find_ 命令更多的选项。下表显示了较高级别的搜索顺序:
让我们更仔细地看一下搜索顺序和搜索位置:
• Package root variables:每个 find_package 调用的包根存储在一个名为
_ROOT 的变量中。它们是搜索属于包的文件的第一优先级。包根变量的工作方式与 CMAKE_PREFIX_PATH 相同,不仅适用于对 find_package 的调用,而且适用于可能在属于该包的 find 模块内发生的所有其他 find_ 调用。 • CMake-specific cache variables:这些是从 CMAKE_PREFIX_PATH 派生的位置。对于 macOS,CMAKE_FRAMEWORK_PATH 变量也被视为搜索位置。
• 通过将 CMAKE_FIND_USE_CMAKE_PATH 变量设置为 false,将跳过来自 CMake 特定缓存变量的位置。
• CMake-specific environment variables:除了将 CMAKE_PREFIX_PATH 和 CMAKE_FRAMEWORK_PATH 指定为缓存变量外,如果将它们设置为环境变量,CMake 也会考虑它们。
• 将CMAKE_FIND_USE_ENVIRONMENT_PATH 变量设置为false 将禁用此行为。
• find_package 的提示:这些是传递给 find_package 的可选路径。
•System-specific environment variables:PATH 环境变量用于查找包和文件,并且删除了尾随的 bin 和 sbin 目录。此时通常会搜索每个系统的默认位置,例如 /usr、/lib 和类似位置。
• User package registry:通常,包位于标准位置或使用CMAKE_PREFIX_PATH 选项传递给CMake 的位置。包注册表是告诉 CMake 在哪里查找依赖项的另一种方法。包注册表是包集合所在的特殊位置。用户注册表对当前用户帐户有效,而系统包注册表在系统范围内有效。在 Windows 上,用户包注册表的位置存储在 Windows 注册表中的以下位置:
• HKEY_CURRENT_USER\Software\Kitware\CMake\ Packages\
\ • 在 Unix 平台上,它存储在用户的主目录中,如下所示:如下: ~/.cmake/packages/ • Platform-specific cache variables::对于 find_package,特定于平台的缓存变量 CMAKE_SYSTEM_PREFIX_PATH、CMAKE_SYSTEM_FRAMEWORK_PATH 和 CMAKE_SYSTEM_APPBUNDLE_PATH 的工作方式与其他 find 调用类似。它们由 CMake 自己设置,不应由项目更改。
•System package registry:与用户包注册表类似,这是 CMake 查找包的位置。在 Windows 上,它存储在 HKEY_LOCAL_MACHINE\Software\Kitware\CMake\Packages\
\ 下。 • Unix 系统不提供系统包注册表。
• find_package的PATHS:这些是传递给find_package 的可选路径。通常,HINTS 选项是根据其他值计算的或取决于变量,而 PATHS 选项是固定路径。
具体来说,在 config 模式下查找包时,CMake 会在各种前缀下查找以下文件结构:
/ /(cmake|CMake)/ / */ / */(cmake|CMake)/ /(lib/ |lib*|share)/cmake/ */ /(lib/ |lib*|share)/ */ /(lib/ |lib*|share)/ */(cmake|CMake)/ / */(lib/ |lib*|share)/cmake/ */ / */(lib/ |lib*|share)/ */ / */(lib/ |lib*|share)/ */ (cmake|CMake)/ 您可以在 https://cmake.org/cmake/help/latest/manual/cmake-packages.7.html 的官方 CMake 文档中找到有关包的更多信息。在模块方面,到目前为止,我们只介绍了如何查找现有模块。但是,如果我们要查找既没有集成到 CMake 中,也没有集成到标准位置,或者它们不提供 CMake 配置说明的依赖项会发生什么?好吧,让我们在下一节中了解这一点。
虽然 CMake 几乎是一个行业标准,但仍有许多库不使用 CMake 管理,或者使用 CMake 管理但不导出 CMake 包。
如果它们可以安装在系统的默认位置,找到这些库通常不是问题,但这并不总是可能或不想要的。一个常见的情况是使用专有的第三方库,该库仅用于某个项目,或者使用不同版本的库来构建系统包管理器安装的库。
如果您正在并排开发多个项目,您可能希望在本地处理每个项目的依赖关系。无论哪种方式,最好的做法是设置您的项目,以便在本地管理依赖项并且不要过多地依赖系统上安装的内容。第 11 章“使用 CMake 进行自动化模糊测试”中描述了创建完全可重现的构建;但是,现在,让我们专注于查找依赖项。
如果依赖项不存在模块且不存在配置文件,通常情况下,编写所谓的查找模块就是解决方案。目标是提供足够的信息,以便以后我们可以通过 find_package 使用任何包。
查找模块是 CMake 关于如何为库查找必要的头文件和二进制文件以及创建导入目标以供 CMake 使用的说明。如本章前面所述,在模块模式下调用 find_package 时,CMake 会在 CMAKE_MODULE_PATH 中搜索名为 Find
.cmake 的文件。 假设我们正在构建一个项目,其中已经下载或构建了依赖项,并且在我们使用它们之前已将其放置在名为 dep 的文件夹中。因此,项目结构可能如下所示:
├── dep <-- The folder where we locally keep dependencies ├── cmake │ └── FindLibImagePipeline.cmake <-- 这是我们需要写的 ├── CMakeLists.txt <-- Main CmakeLists.txt ├── src │ ├── *.cpp files
我们要做的第一件事是将cmake文件夹添加到CMAKE_MODULE_PATH,这是一个列表。因此,首先,我们将以下行添加到 CMakeLists.txt 文件中:
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/ cmake")
这告诉 CMake 它应该在 cmake 文件夹中查找查找模块。通常,查找模块按以下顺序执行操作:
1. 它查找属于包的文件。
2. 它设置包含包的包含目录和库目录的变量。
3. 它为导入的包设置目标。
4. 它为目标设置属性。一个名为 obscure 的库的简单 FindModules.cmake 可能如下所示:
cmake_minimum_required(VERSION 3.21) find_library( OBSCURE_LIBRARY NAMES obscure HINTS ${PROJECT_SOURCE_DIR}/dep/ PATH_SUFFIXES lib bin build/Release build/Debug ) find_path( OBSCURE_INCLUDE_DIR NAMES obscure/obscure.hpp HINTS ${PROJECT_SOURCE_DIR}/dep/include/ ) include(FindPackageHandleStandardArgs) find_package_handle_standard_args( Obscure DEFAULT_MSG OBSCURE_LIBRARY OBSCURE_INCLUDE_DIR ) mark_as_advanced(OBSCURE_LIBRARY OBSCURE_INCLUDE_DIR) if(NOT TARGET Obscure::Obscure) add_library(Obscure::Obscure UNKNOWN IMPORTED ) set_target_properties(Obscure::Obscure PROPERTIES IMPORTED_LOCATION "${OBSCURE_LIBRARY}" INTERFACE_INCLUDE_DIRECTORIES "${OBSCURE_INCLUDE_DIR}" IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" ) endif()
在查看示例时,我们可以观察到发生了以下情况:
1. 首先,使用 find_library 命令搜索属于依赖项的实际库文件。如果找到,它的路径(包括实际文件名)将存储在 OBSCURE_LIBRARY 变量中。命名
_LIBRARY 变量是一种常见的做法。 NAMES 参数是库的可能名称列表。名称会自动使用通用前缀和扩展名进行扩展。因此,尽管在前面的示例中,我们查找名称“obscure”,但仍会找到名为 libobscure.so 或 obscure.dll 的文件。本节稍后将介绍有关搜索顺序、提示和路径的更多详细信息。2. 接下来,Find 模块尝试定位包含路径。这是通过查找库的已知路径模式来完成的,通常是公共头文件之一。结果存储在 OBSCURE_INCLUDE_DIR 变量中。同样,通常的做法是将此变量命名为
_INCLUDE_DIR。 3. 由于处理查找模块的所有要求可能很乏味并且通常非常重复,因此 CMake 提供了 FindPackageHandleStandardArgs 模块,该模块提供了一个方便的函数来处理所有常见情况。它提供了 find_package_handle_standard_args 函数,该函数处理 REQUIRED、QUIET 和 find_package 的版本相关参数。 find_package_handle_standard_args 有一个短签名和一个长签名。在示例中,使用了短签名:
find_package_handle_standard_args(
(DEFAULT_MSG| ) ... ) 4. 对于大多数情况,find_package_handle_standard_args 的缩写形式就足够了。在简写形式中,find_package_handle_standard_args 函数将包名称作为第一个参数和包所需的变量列表。 DEFAULT_MSG 参数告诉它在成功或失败的情况下打印默认消息,这取决于 find_package 是使用 REQUIRED 还是 QUIET 调用的。可以自定义消息,但我们建议您尽可能坚持使用默认消息。这样,所有 find_package 命令的消息都是一致的。在前面的示例中,find_package_handle_standard_args 会检查已传递的 OBSCURE_LIBRARY 和 OBSCURE_INCLUDE_DIR 变量是否有效。如果是这种情况,则设置
_FOUND 变量。5. 如果一切顺利,find 模块定义目标。在我们这样做之前,检查我们尝试创建的目标是否不存在是有帮助的(以避免在我们多次调用 find_package 以获取相同的依赖项的情况下覆盖它)。使用 add_library 创建目标。由于我们无法确定它是静态库还是动态库,因此类型为 UNKNOWN 并且设置了 IMPORTED 标志。
6. 最后,设置库的属性。我们推荐的最低设置是 MPORTED_LOCATION 属性和 INTERFACE_INCLUDE_DIR 中包含文件的位置。
如果一切都按预期工作,则可以像这样使用该库:
find_package(Obscure PRIVATE REQUIRED) ... target_link_libraries(find_module_example Obscure::Obscure)
所以,现在我们了解了如果其他库已经可供使用,它们是如何添加到您的项目中的。但是我们首先如何将库放入我们的系统中呢?让我们在下一节中找出答案。
将依赖项添加到项目中的最简单方法是使用 apt-get、brew 或 Chocolatey 定期安装它们。安装所有东西的缺点是您可能会使用许多不同版本的库污染您的系统,并且您正在寻找的版本可能根本不可用。如果您同时处理多个对依赖项有不同要求的项目,则尤其如此。通常,开发人员会为每个项目在本地下载依赖项,以便每个项目都可以独立工作。处理依赖关系的一个很好的方法是使用包管理器,例如 Conan 或 vcpkg。
在依赖管理方面,使用专用的包管理器有很多优势。用于处理 C++ 依赖项的两个比较流行的方法是 Conan 和 vcpkg。他们都可以处理复杂的构建系统,并且掌握它们将需要他们自己的整本书,所以我们在这里只介绍开始使用它们的基本必需品。在本书中,我们将专注于使用 CMake 项目中已经可用的包,而不是创建自己的包。
在过去的几年里,Conan 包管理器获得了很大的欢迎,主要是因为它与 CMake 的集成非常好。 Conan 是一个分散的包管理器,建立在客户端/服务器架构上。这意味着本地客户端获取或上传包到一个或多个远程服务器。 Conan 最强大的功能之一是它可以为多个平台、配置和版本创建和管理二进制包。创建包时,会使用 conanfile.py 文件对它们进行描述,该文件列出了所有依赖项、源代码和构建说明。这些包是用柯南客户端构建并上传到远程服务器的。这有一个额外的好处,如果找不到适合您本地配置的二进制包,则可以从其源代码在本地构建该包。在 CMake 中使用柯南的一种非常方便的方法是使用 CMake 本身的柯南。但是,如果您不想这样做,则在外部调用柯南也可以。虽然并非绝对必要,但我们建议您在使用柯南之前使用 find_program 检查柯南程序。
为了直接从 CMake 中调用柯南,柯南提供了一个 CMake 包装器供下载。以下示例下载 conan-cmake 包装器,然后从 ConanCenter 拉取 fmt 格式化库以用作项目中的常规库:
if(NOT EXISTS "${CMAKE_CURRENT_BINARY_DIR}/conan.cmake") message(STATUS "Downloading conan.cmake from https://github.com/conan-io/cmake-conan") file( DOWNLOAD "https://raw.githubusercontent.com/conan-io/cmake- conan/0.17.0/conan.cmake" "${CMAKE_CURRENT_BINARY_DIR}/conan.cmake" EXPECTED_HASH SHA256=3bef79da16c2e031dc429e1dac87a08b9226418b300ce00 4cc125a82687baeef STATUS download_status ) if(NOT download_status MATCHES "^0;") message(FATAL_ERROR "Downloading conan.cmake failed with ${download_status}") endif() endif() include(${CMAKE_CURRENT_BINARY_DIR}/conan.cmake) conan_cmake_autodetect(CONAN_SETTINGS) conan_cmake_configure(REQUIRES fmt/6.1.2 GENERATORS cmake_find_ package_multi) conan_cmake_install(PATH_OR_REFERENCE . BUILD missing SETTINGS ${CONAN_SETTINGS} ) list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_BINARY_DIR}) find_package(fmt 6.1 REQUIRED) add_executable(conan_example) target_link_libraries(conan_example PRIVATE fmt::fmt)
前面的 CMake 代码做了以下事情:
1. 如果尚未下载,则将 conan.cmake 文件下载到当前二进制目录。
2.接下来,为了使柯南函数可用,它被包含进来。
3. 一旦包含,Conan 被告知从当前 CMake 配置中检测设置,例如编译器、平台等,并将它们存储在 CONAN_SETTINGS 变量中。
4. conan_cmake_configure 函数定义了 fmt 的需求,并为 Conan 设置了生成器,以便我们可以使用 find_package 来包含依赖项。这将生成一个 conanfile.txt 文件,其中包含当前构建目录中柯南的必要说明。
5. 最后,conan_cmake_install 安装依赖项。
6. PATH_OR_REFERENCE 告诉我们依赖的定义在哪里。此命令与 conan_cmake_configure 在同一构建目录中运行,因此传递单个点将搜索同一目录。 BUILD missing 告诉柯南在本地构建软件包,如果它们不能作为远程服务器的二进制文件可用。
7. SETTINGS 将检索到的设置传递给柯南。
8. 由于生成的查找模块将位于当前二进制目录中,因此必须将它们添加到 CMAKE_MODULE_PATH。
9. 下载后,可以使用 find_package 包含依赖项,然后照常添加到现有目标中。除了直接在 CMake 中声明依赖关系之外,还可以将信息作为 conanfile.txt 文件提供。它可能看起来像这样:
[requires] fmt/6.1.2 [generators] cmake_find_package
这里,不是运行 conan_cmake_configure 和 conan_cmake_install,而是通过 conan_cmake_run 调用柯南。修改前面的示例以使用 conanfile.txt 文件将类似于以下内容(下载 conan.cmake 文件的部分保持不变):
include(${CMAKE_CURRENT_BINARY_DIR}/conan.cmake) conan_cmake_autodetect(CONAN_SETTINGS) conan_cmake_run(CONANFILE ${CMAKE_CURRENT_LIST_DIR}/conanfile. txt BASIC_SETUP BUILD missing SETTINGS ${CONAN_SETTINGS}) list(APPEND CMAKE_PREFIX_PATH ${CMAKE_CURRENT_BINARY_DIR}) find_package(fmt 6.1 REQUIRED) add_executable(conan_conanfile_example) target_link_libraries(conan_conanfile_example PRIVATE fmt::fmt)
此设置类似于前面的示例,不同之处在于柯南指向 conanfile.txt 文件,而不是从 CMake 指令生成它。 BASIC_SETUP 将告诉柯南自动创建必要的 CMake 变量。 conan_cmake_run 命令也可用于运行几乎所有的柯南命令。当然,也可以在 CMake 外部手动调用柯南。虽然有些人发现这是一种更简洁的方法,但由于 Conan 和 CMake 之间的关注点分离,维护起来可能很乏味。实际上,必须在两个地方跟踪有关依赖项的信息,并且不仅要为 CMake 配置构建配置,还必须为 Conan 配置编译配置,例如编译器、libc 版本、平台等。完整的柯南文档可以在 https://docs.conan.io/en/latest/ 找到。
另一个流行的开源包管理器是来自 Microsoft 的 vcpkg。它的工作方式类似于柯南,它被设置为客户端/服务器架构。它最初是为与 Visual Studio 编译器环境一起工作而构建的,后来添加了 CMake。包可以手动安装,在所谓的经典模式下调用 vcpkg,或者在所谓的清单模式下直接从 CMake 中安装。在经典模式下使用 vcpkg 安装软件包的命令如下:
vcpkg install [packages]
在清单模式下运行时,项目的依赖项在项目根目录下的 vcpkg.json 文件中定义。 Manifest 模式有一个很大的优势,它可以更好地与 CMake 集成,因此,尽可能使用 manifest 模式。 vcpkg 清单可能如下所示:
{ "name" : "vcpkg-example", "version-semver" : "0.0.1", "dependencies" : [ "someLibrary", "anotherLibrary", ] }
为了让 CMake 找到包,必须将 vcpkg 工具链文件传递给 CMake,因此对 CMake 的调用如下:
cmake -S
-D -DCMAKE_TOOLCHAIN_ FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake 如果它们以清单模式运行,则 vcpkg.json 文件中指定的包将自动下载并安装到本地。如果它们以经典模式运行,则必须在运行 CMake 之前手动安装这些包。传递 vcpkg 工具链文件时,可以像往常一样通过使用 find_package 和 target_link_libraries 来使用已安装的包。 Microsoft 建议您将 vcpkg 作为子模块安装在与 CMake 根项目相同级别的存储库中,但它几乎可以安装在任何地方。设置工具链文件可能会导致交叉编译时出现问题,因为 CMAKE_TOOLCHAIN_FILE 可能已经指向不同的文件。
在这种情况下,可以使用 VCPKG_CHAINLOAD_TOOLCHAIN_FILE 变量传递第二个工具链文件。在这种情况下,对 CMake 的调用将如下所示:
cmake -S
-D -DCMAKE_TOOLCHAIN_ FILE=[vcpkg root]/scripts/buildsystems/vcpkg.cmake -DVCPKG _CHAINLOAD_TOOLCHAIN_FILE=/path/to/other/toolchain.cmake Conan 和 vcpkg 只是流行的 C++ 和 CMake 的两个包管理器。当然,还有更多,但需要一本单独的书来描述它们。特别是当项目变得更加复杂时,我们强烈建议您使用包管理器。您选择哪个包管理器将取决于正在开发项目的环境和您的个人偏好。
柯南比 vcpkg 有一点优势,因为它在 Python 运行的任何地方都可以运行,因此它在更多平台上得到支持。就交叉编译的特性和能力而言,两者或多或少是相等的。
总体而言,柯南提供了更高级的配置选项和对软件包的控制,但代价是更复杂的处理。使用本地依赖项的另一种方法是使用容器、sysroot 等创建完全隔离的构建环境。
这将在第 12 章,跨平台编译和自定义工具链中介绍。目前,假设我们在标准系统安装中运行 CMake。在处理特定于项目的依赖项时,推荐使用包管理器进行依赖项管理。但是,有时,包管理器不是一种选择。这可能是由于神秘的公司政策或其他原因。在这些情况下,CMake 还支持下载依赖项作为源,并将它们作为外部目标集成到项目中。
有几种方法可以将依赖项作为源获取到您的项目中。一种相对简单但危险的方法是手动将它们下载或克隆到项目内的子文件夹中,然后使用 add_subdirectory 添加此文件夹。虽然这很有效并且速度非常快,但它很快就会变得乏味且难以维护。因此,这应该尽快实现自动化。
注意:将第三方软件的副本直接下载并集成到产品中的做法称为供应商。虽然它的优势在于它通常使构建软件变得容易,但它会产生打包库的问题。通过使用包管理器或在系统上的某个位置安装第三方软件来避免供应商。
获取外部内容的基础是 CMake ExternalProject 模块和基于 ExternalProject 构建的更复杂的 FetchContent 模块。虽然 ExternalProject 提供了更大的灵活性,但 FetchContent 通常使用起来更方便,特别是如果下载的项目也是使用 CMake 构建的。它们都将项目作为源文件下载,并可用于构建它们。
1. 使用 FetchContent
对于使用 CMake 构建的外部项目,使用 FetchContent 模块是添加源依赖项的选择方式。对于二进制依赖,使用 find_package 和 find modules 仍然是首选方式。 ExternalProject 和 FetchContent 之间的主要区别之一是 FetchContent 在配置期间下载和配置外部项目,而 ExternalProject 在构建步骤中完成所有事情。这样做的缺点是源及其配置在配置期间不可用。
在 FetchContent 之前,您将使用 Git 子模块手动下载依赖项,然后使用 add_subdirectory 添加它们。这在某些情况下有效,但维护起来可能相当不方便和麻烦。 FetchContent 提供了拉入源依赖的函数列表,主要是 FetchContent_Declare,它定义了下载和构建 FetchContent_MakeAvailable 的参数,它填充依赖的目标并使它们可用于构建。在以下示例中,用于按合同设计的 bertrand 库是使用 GitHub 从 Git 中提取并可供使用的:
include(FetchContent) FetchContent_Declare( bertrand GIT_REPOSITORY https://github.com/bernedom/bertrand.git GIT_TAG 0.0.17) FetchContent_MakeAvailable(bertrand) add_executable(fetch_content_example) target_link_libraries( fetch_content_example PRIVATE bertrand::bertrand )
FetchContent_MakeAvailable 从 3.14 版本开始可用,建议您使用 FetchContent_Populate 手动过度填充项目。应该尽可能使用它,因为它的简单性使得代码库非常易于维护。作为 ExternalProject,FetchContent 可以从 HTTP(S)、Git、SVN、Mercurial 和 CVS 下载,并且适用相同的良好做法,例如为下载的内容指定 MD5 哈希或使用 Git 哈希。
FetchContent_MakeAvailable 是使基于 CMake 的外部项目可用的推荐方法,但如果您想对外部项目有更多控制权,也可以手动填充项目。以下示例与前面的示例相同,但方式更详细:
FetchContent_Declare( bertrand GIT_REPOSITORY https://github.com/bernedom/bertrand.git GIT_TAG 0.0.17) if(NOT bertrand_POPULATED) FetchContent_Populate(bertrand) add_subdirectory(${bertrand_SOURCE_DIR} ${bertrand_BINARY_DIR}) endif()
FetchContent_Populate 有额外的选项被指定来更紧密地控制构建。签名如下:
FetchContent_Populate(
[QUIET] [SUBBUILD_DIR ] [SOURCE_DIR ] [BINARY_DIR ] ... ) 让我们看一下 FetchContent_Populate 的选项:
• QUIET:可以指定此项以在填充成功时抑制输出。如果命令失败,即使指定了允许调试的选项,也会显示输出。
• SUBBUILD_DIR:指定外部项目的位置。默认值为 ${CMAKE_CURRENT_BINARY_DIR}/
-subbuild。通常,此选项应保持原样。 • SOURCE_DIR 和BINARY_DIR:它们改变了外部项目的源目录和构建目录的位置。 SOURCE_DIR 的默认设置是 ${CMAKE_ CURRENT_BINARY_DIR}/
-src 和 BINARY_DIR 的默认设置是 ${CMAKE_ CURRENT_BINARY_DIR}/ -build • 添加的任何附加参数都将传递到基础 ExternalProject_Add。但是,FetchContent 禁止您编辑不同步骤的命令,因此尝试篡改 CONFIGURE_COMMAND、BUILD_COMMAND、INSTALL_COMMAND 和 TEST_COMMAND 将导致 FetchContent_Populate 失败并出现错误。
注意 如果您发现自己需要将选项传递给底层 ExternalProject_Add,请考虑直接使用 ExternalProject 而不是首先通过 FetchContent。
可以通过读取
_SOURCE_DIR、 _BINARY_DIR 和 _POPULATED 变量或调用 FetchContent_GetProperties 来检索有关源目录和构建目录的信息,以及是否已填充项目。请注意, 将始终以全部大写和全部小写形式提供。这样 CMake 就可以识别不同大小写的包。 FetchContent 的另一大优势是它可以处理外部项目共享公共依赖项的情况,并防止它们被多次下载和构建。第一次在 FetchContent 上定义依赖项时。,详细信息会被缓存,并且任何进一步的定义都将被忽略。这样做的好处是父项目可以推翻子项目的依赖关系。
假设我们有一个名为 MyProject 的顶级项目,它获取两个外部项目 Project_A 和 Project_B,每个项目都依赖于名为 AwesomeLib 的第三个外部项目,但在不同的次要版本上。在大多数情况下,我们不想下载和使用两个版本的 AwesomeLib,而只使用一个版本来避免冲突。下图显示了依赖关系图的样子:
为了解决这个问题,我们可以通过在顶级 CMakeLists.txt 文件中对 AwesomeLib 进行 FetchContent_Declare 调用来指定要提取的 AwesomeLib 版本。 CMakeLists.txt 文件中的声明顺序与此处无关,仅与声明的级别有关。由于 Project_A 和 Project_B 都包含填充 AwesomeLib 的代码,因此顶级项目不需要使用 FetchContent_MakeAvailable 或 FetchContent_Populate。生成的顶级 CMakeLists。 txt 文件可能如下所示:
include(FetchContent) FetchContent_Declare(Project_A GIT_REPOSITORY ... GIT_TAG ...) FetchContent_Declare(Project_B GIT_REPOSITORY ... GIT_TAG ...) # Force AwesomeLib dependency to a certain version FetchContent_Declare(AwesomeLib GIT_REPOSITORY … GIT_TAG 1.2 ) FetchContent_MakeAvailable(Project_A) FetchContent_MakeAvailable(Project_B)
这将强制将 AwesomeLib 固定到所有项目的 1.2 版。当然,这只有在 Project_A 和 Project_B 所需的版本之间的接口兼容的情况下才有效,从而产生依赖关系图,如下图所示:
将依赖项添加为源有一些优点,但它也有主要缺点,因为它会显着增加配置和构建时间。在第 9 章,创建可重现的构建环境中,我们将使用分布式存储库处理超级构建,并提供有关如何处理源依赖关系的更多信息。
在本章的开头,我们看了find_package,它可以用来包含二进制依赖,但是我们并没有讨论如何使用CMake方便地下载本地二进制依赖。虽然 FetchContent 和 ExternalProject 可用于此目的,但这不是它们的目的。相反,像柯南和 vcpkg 这样的专用包管理器将更适合。接下来让我们进一步了解它们。
2. 使用外部项目
ExternalProject 模块用于下载和构建未完全集成到主项目中的外部项目。在构建外部项目时,构建是完全隔离的,这意味着它不会自动接管有关架构或平台的任何设置。这种隔离可以派上用场,以避免在命名目标或组件时发生冲突。外部项目创建一个主要目标和几个包含以下独立构建步骤的子目标:
1. 下载:ExternalProject 可以通过多种方式下载内容,例如通过纯 HTTPS 下载或通过访问版本控制系统(如 Git、Subversion、Mercurial、和 CVS。如果内容已存档,下载步骤也将解压缩它们。
2. 更新和打补丁:如果从服务器配置监视器 (SCM) 中提取内容,下载的源代码可以打补丁或更新到最新版本。
3. 配置:如果下载的源使用CMake,则在其上执行配置步骤。对于非 CMake 项目,可以提供执行配置的自定义命令。
4. 构建:默认情况下,使用与主项目相同的构建工具来构建依赖项,但如果不需要,可以提供自定义命令。如果提供了自定义构建命令,则由用户负责确保传递必要的编译器标志,以便结果与 ABI 兼容。
5.安装:隔离的构建可以安装在本地,通常在主项目的构建树的某个地方。
6. 测试:如果外部内容带有一组测试,主项目可能会选择运行它们。默认情况下,不运行测试。
所有步骤,包括下载,都在构建时运行。因此,根据外部项目,这会显着增加构建时间。 CMake 缓存下载和构建,因此除非外部项目已更改,否则开销主要用于第一次运行。确实存在向外部构建添加更多步骤的可能性,但对于大多数项目,默认步骤就足够了。
这些步骤可以自定义或省略,我们稍后会发现。在以下示例中,使用按合同设计的 bertrand 库通过 HTTPS 下载并本地安装在当前构建目录中:
include(ExternalProject) ExternalProject_Add( bertrand URL https://github.com/bernedom/bertrand/archive /refs/tags/0.0.17.tar.gz URL_HASH MD5=354141c50b8707f2574b69f30cef0238 INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/bertrand_install CMAKE_CACHE_ARGS -DBERTRAND_BUILD_TESTING:BOOL=OFF -DCMAKE_INSTALL_PREFIX:PATH=
) 请注意,默认情况下 ExternalProject 模块不可用,必须使用 include(ExternalProject) 将其包含在第一行中。由于外部库安装在本地构建目录中,因此指定了 INSTALL_DIR 选项。由于 bertrand 本身是一个 CMake 项目,安装目录作为
通过使用 CMAKE_INSTALL_PREFIX 变量来构建项目。 是一个指向 INSTALL_DIR 选项的占位符。 ExternalProject 知道各种目录的占位符,例如 、 和 。如需完整列表,请参阅 https://cmake.org/cmake/help/latest/module/ExternalProject.html 上的模块文档。 验证您的下载:强烈建议您将下载哈希添加到任何 URL,因为如果工件的内容发生更改,这会向您发送通知。
为此,任何依赖于 bertrand 的目标都必须在外部依赖之后构建。由于 bertrand 是一个仅包含头文件的库,我们希望将包含路径添加到目标。在 CMake 中将外部项目用于另一个目标可能类似于以下内容:
ExternalProject_Get_Property(bertrand INSTALL_DIR) set(BERTRAND_DOWNLOADED_INSTALL_DIR "${INSTALL_DIR}") # Create a target to build an executable add_executable(external_project_example) # make the executable to be built depend on the external project # to force downloading first add_dependencies(external_project_example bertrand) # make the header file for bertrand available target_include_directories(external_project_example PRIVATE ${BERTRAND_DOWNLOADED_INSTALL_DIR}/include)
在第一行中,使用 ExternalProject_Get_Property 检索安装目录并将其存储在 INSTALL_DIR 变量中。不幸的是,变量名称始终与属性相同,因此建议您在检索后立即将其存储在具有唯一名称的变量中,以便更好地表达其用途。
接下来,我们要构建的目标被创建并依赖于 ExternalProject_Add 创建的目标。这是强制执行正确的构建顺序所必需的。最后,使用 target_include_directories 将本地安装的路径添加到目标中。此外,我们可以导入外部库提供的 CMake 目标,但这样做的目的是说明如果外部项目不是由 CMake 构建的,这将如何工作。从源代码管理系统下载使用相应的选项。对于 Git,这通常如下所示:
ExternalProject_Add(MyProject GIT_REPOSITORY https://github.com/PacktPublishing/SomeRandomProject.git GIT_TAG 56cc1aaf50918f208e2ff2ef5e8ec0111097fb8d )
请注意,GIT_TAG 可以是 Git 的任何有效修订号,包括标签名称和长短散列。如果省略 GIT_TAG,则下载默认分支的最新版本(通常称为 main 或 master)。我们强烈建议您始终指定要下载的版本。最可靠的方法是定义提交哈希,因为标签可以移动,尽管它们很少在实践中使用。从 SVN 下载类似于从 Git 下载。有关更多详细信息,请参阅 ExternalProject 的官方文档。
3. 使用非 CMake 项目和交叉编译
ExternalProject 的一个常见用例是构建不由 CMake 处理而是由 autotools 或 automake 处理的依赖项。在这种情况下,您需要指定配置和构建命令,如下所示:
find_program(MAKE_EXECUTABLE NAMES nmake gmake make) ExternalProject_Add(MyAutotoolsProject URL someUrl INSTALL_DIR ${CMAKE_CURRENT_BINARY_DIR}/myProject_install CONFIGURE_COMMAND
/configure --prefix= BUILD_COMMAND ${MAKE_EXECUTABLE} ) 请注意,第一个 find_program 命令用于查找 make 的版本并将其存储在 MAKE_EXECUTABLE 变量中。外部项目的一个常见问题是您必须密切控制依赖项的安装位置。大多数项目都希望安装到默认系统位置,这通常需要 root 权限并且可能会意外污染系统。因此,通常需要将必要的选项传递给配置或构建步骤。另一种处理方法是通过将 INSTALL_COMMAND 替换为空字符串来完全避免安装过程,如下所示:
ExternalProject_Add(MyAutotoolsProject URL someUrl CONFIGURE_COMMAND
/configure BUILD_COMMAND ${MAKE_EXECUTABLE} INSTALL_COMMAND "" ) 使用诸如此类的非 CMake 项目的一个问题是它们没有定义直接使用依赖项的必要目标。因此,为了在另一个目标中使用外部构建的库,您通常必须将完整的库名称添加到 target_link_ 库调用中。这样做的主要缺点是您必须为各种平台手动维护文件的不同名称和位置。 find_library 或 find_file 调用几乎没有用,因为它们发生在配置时,而 ExternalProject 只在构建时创建必要的文件。
另一个常见用例是使用 ExternalProject 为不同的目标平台构建现有源目录的内容。在这种情况下,处理下载的参数被简单地省略了。如果外部项目使用 CMake 构建,则可以将工具链文件作为 CMake 选项传递给外部项目。有关工具链文件的更多信息,请参阅第 11 章,使用 CMake 进行自动化模糊测试。这里一个非常常见的陷阱是 ExternalProject 不会识别对外部项目源的任何更改,因此 CMake 可能不会重建它们。出于这个原因,应该传递 BUILD_ALWAYS 选项,它的缺点是通常会使构建时间大大延长:
总
4. 管理 ExternalProject 中的步骤
如上一节所述,ExternalProject 的步骤可以进一步配置并以更精细的方式使用。通过传递 STEP_TARGETS 选项或调用 ExternalProject_Add_StepsTargets,可以告诉 ExternalProject 为每个步骤创建常规目标。以下调用将外部项目的配置步骤和构建步骤公开为目标:
ExternalProject_Add(MyProject # various options STEP_TARGETS configure build ) ExternalProject_Add_StepTargets(MyProject configure build)
目标以
-step 命名。在前面的示例中,将创建两个附加目标 MyProject-configure 和 MyProject-build。创建步骤目标有两个主要用途:您可以创建按下载、配置、构建、安装或测试顺序排序的自定义步骤,或者您可以使这些步骤依赖于其他目标。这些可以是常规目标,由 add_executable、add_library 或 add_custom_target 创建,也可以是来自其他添加可执行文件的目标。一个常见的情况是外部项目相互依赖,因此一个配置步骤必须依赖另一个。在下一个示例中,项目 B 的配置步骤将取决于项目 A 的完成: ExternalProject_Add(ProjectA ... # various options STEP_TARGETS install ) ExternalProject_Add(ProjectB ... # various options ) ExternalProject_Add_StepDependencies(ProjectB configure ProjectA)
最后,我们还可以创建自定义步骤以插入到外部项目中。添加步骤的过程是使用 ExternalProject_Add_Step 命令完成的。自定义步骤不能与任何预定义步骤(例如 mkdir、下载、更新、修补、配置、构建、安装或测试)命名相同。以下示例将创建一个步骤,将外部项目的许可证信息添加到构建后的特定 tar 文件中:
ExternalProject_Add_Step(bertrand_downloaded copy_license COMMAND ${CMAKE_COMMAND} -E tar "cvzf" ${CMAKE_CURRENT_ BINARY_DIR}/licenses.tar.gz
/LICENSE DEPENDEES build ) 总而言之,ExternalProject 是一个非常强大的工具;但是,管理起来可能会变得非常复杂。通常,正是这种灵活性也使 ExternalProject 难以使用。虽然它可以帮助隔离构建,但它通常会迫使项目维护者手动将来自外部项目内部工作的任何信息公开给 CMake,具有讽刺意味的是,这正是 CMake 应该首先提供帮助的。
1.总结
在本章中,我们介绍了查找文件、库和程序的一般方法,以及更复杂的 CMake 包搜索。您学习了如何通过提供自己的查找模块来创建导入的包定义,如果它无法自动找到。我们使用 ExternalProject 和 FetchContent 查看了基于源代码的依赖关系,以及如何使用 CMake 构建非 CMake 项目。此外,如果您想在依赖管理方面变得更加复杂,我们简要介绍了 Conan 和 vcpkg 作为与 CMake 完美集成的两个包处理程序。依赖管理是一个很难涵盖的话题,有时可能很乏味。尽管如此,花时间使用本章中描述的技术正确设置它是值得的。 CMake 的多功能性及其查找依赖项的各种方式是其最大的优势,但也是其最大的弱点。通过使用各种 find_ 命令、FetchContent、ExternalProject 或将任何可用的包管理器与 CMake 集成,几乎所有依赖项都可以集成到项目中。然而,有这么多方法可供选择,找到最好的方法可能很困难。不过,我们建议尽可能使用 find_package。 CMake 越流行,其他项目可以无缝集成的机会就越大。
在下一章中,您将学习如何为您的代码自动生成和打包文档。
2.问题
回答以下问题以测试您对本章的了解:
1. CMake 中存在哪些 find_ 程序?
2. find 模块导入的目标应该设置哪些属性?
3. 找东西时,哪个选项优先,HINTS 还是 PATHS?
4. ExternalProject 在什么阶段下载外部内容?
5. FetchContent 在什么阶段下载外部内容?