文章目录

  • 介绍
  • 使用Pre-built包和find_package()
  • 包配置文件
  • Find模块文件(Find Module Files)
  • 导入目标(Imported Targets)
  • 用FetchContent从源码下载并构建
  • FetchContent和find_package()一体化
  • 依赖提供者(Dependency Providers)


介绍

项目往往依赖于其他的项目、成品、工件,CMake 提供了多种方法将这些东西合并到构建中,项目和用户可以灵活地选择最适合他们需要的方法。

将依赖项引入构建的主要方法是 find_package()命令和FetchContent模块。 有时也会用到FindPkgConifg模块,尽管它缺少其他两个的一些集成并且在本指南中没有进一步讨论。

使用Pre-built包和find_package()

项目所需的包可能已经安装在系统的某个位置并可用。该包也可能由 CMake 构建,或者它可能完全使用不同的构建系统。它甚至可能只是根本不需要构建的文件集合。

对上面的场景,CMake 提供了find_package()命令:

  • 它搜索众所周知的位置,以及项目或用户提供的其他提示和路径。
  • 它还支持包组件,而且包是可选的。
  • 结果变量允许根据是否找到包(和特定的组件)来自定义项目的行为。

大多数情况下,项目应该使用基本标志(Basic Signature)。通常,这只涉及包名称、一个版本约束和REQUIRED关键字(如果是强制,而非可选的话)。也可以指定包的组件集合。
基本标志例子:

find_package(Cach2)
find_package(GTest REQUIRED)
find_package(Boost 1.79 COMPONENTS date_time)

find_package()主要以两种模式来搜索:

  • Config模式
  • 这个模式下,命令会查找通常由包本身提供的文件。
  • 这个模式更可靠,因为包详细信息应始终与包同步。
  • Module模式
    并非所有包都支持 CMake,有许多包不提供配置模式所需要的配置文件。
    对于这种情况,可以由项目或 CMake 单独提供 Find 模块文件:
  • Find模块通常是探索式的实现,它知道包通常提供什么以及如何将该包呈现给项目。
  • 由于Find模块通常与包分开分发,因此它们不那么可靠(通常是分开维护的,可能尊徐不同的发布时间表,容易变得过时)。

根据使用的参数,find_package()可以使用上述一种或两种方法。
通过将选项限制为基本签名,可以使用配置模式和模块模式来满足依赖性。
其他选项的存在可能会将调用限制为仅使用两种方法中的一种,从而可能降低命令查找依赖项的能力。

关于这个复杂主题的详细信息,请阅读find_package()文档。

对于这两种搜索模式,用户可以设定缓存变量(在cmake命令行或ccmake、cmake-gui)来影响或设定到哪里查找包。有关如何设置缓存变量的更多信息,请参阅User Interaction Guide。

包配置文件

第三方提供可执行文件、库、标头和其他与 CMake 一起使用的文件的首选方式是提供config files(配置文件)。配置文件是包中附带的文本文件,定义了 CMake 目标、变量、命令等。配置文件是一个普通的CMake脚本,供find_package()命令读取。

配置文件通常可以在名称与模式匹配的目录中找到lib/cmake/<PackageName>,尽管它们可能位于其他位置(请参阅Config Mode Search Procedure)

通常<PackageName>find_package()命令的第一个参数,甚至是唯一一个参数。也可以用NAMES选项来指定替代名称,例如:

find_package(SomeThing
  NAMES
    SameThingOtherName   # 包的另一个名字
    SomeThing            # 仍然寻找它的规范名称
)

配置文件必须命名为<PackageName>Config.cmake<LowercasePackageName>-config.cmake(本指南后续用第一种,但两种都受支持)。此文件是Cmake包的入口点。

单独的名为<PackageName>ConfigVersion.cmake<LowercasePackageName>-config-version.cmake的可选文件也可能位于同一目录。

CMake 使用此文件来确定 包的版本是否满足find_package()调用中包含的版本约束

find_package()调用中指定版本是可选的,即使<PackageName>ConfigVersion.cmake文件已经存在。

如果配置文件被找到,并且所有版本约束都满足,则find_package()命令就认为这个包被找到了,并且假定整个包按设计完成。

可能还有其他文件提供 CMake 命令或Imported Targets(导入的目标)供您使用,CMake不对这些文件执行任何命名约束。它们通过CMake include()命令来与主<PackageName>Config.cmake文件相关。<PackageName>Config.cmake文件通常会为你包含(include)这些文件,因此除了find_package()调用外无需其他步骤。

如果包的位置在 CMake已知的目录(directory known to CMake)中,则 find_package()调用应该成功。CMake 已知的目录是特定于平台的。例如,使用标准系统包管理器安装在 Linux 上的包将自动在前缀/usr中找到。安装在Windows 上的包在Program Files同样会被找到。

如果包位于 CMake 不知道的位置,例如/opt/mylib$HOME/dev/prefix,这时不指定位置的话,那么包就不会被找到。CMake 提供了几种方式让用户指定到哪里去找这些库。

在调用CMake时可以设置环境变量CMAKE_PREFIX_PATH(set when invoking CMake),它是配置文件的基路径列表,例如/opt/somepackage通常会将配置文件安装为/opt/somepackage/lib/cmake/somePackage/SomePackageConfig.cmake,这时,/opt/somepackage应该被添加到CMAKE_PREFIX_PATH

环境变量CMAKE_PREFIX_PATH也可以用前缀填充以搜索包,它是一个列表,但它需要使用平台特定的环境变量列表项分隔符(Unix为冒号: ,Windows 上为分号;)。

在需要指定多个前缀的情况下,或者需要同一前缀下多个包时,CMAKR_PREFIX_PATH提供了便利。包的路径也可以通过设置<PackageName>_DIR来指定,例如SomePackage_DIR。注意,这不是一个前缀,而是一个完整的路径,其中包含包的配置文件,例如/opt/somepackage/lib/cmake/SomePackage
关于可能影响搜索的其他 CMake 变量和环境变量,参见find_package()文档。

Find模块文件(Find Module Files)

如果有FindSomePackage.cmake文件,那么find_package()仍然可以找到不提供配置文件的包。

这些 Find模块文件与配置文件的不同之处在于:

  1. 包本身不提供 Find模块文件
  2. 能找到Find<PackageName>.cmake并不代表该包可用(也就是说它不可靠)。
  3. CMake不会为Find<PackageName>.cmake文件搜索CMAKR_PREFIX_PATH中的位置。相反,CMake会搜索CMAKR_MODULE_PATH中的文件。通常,用户在运行CMake时设置CMAKR_MODULE_PATH,CMake项目通常会在CMAKE_MODULE_PATH附加,来使用本地的Find模块文件。
  4. CMake也为一些第三方包提供了Find<PackageName>.cmake文件,这些文件由CMake维护,并且这些文件落后于与其关联的包的最新版本并不罕见。一般来说,新的 Find 模块不再添加到 CMake 中。项目应该鼓励上游包尽可能提供配置文件。如果不成功,项目应该为包提供自己的查找模块。


导入目标(Imported Targets)

配置文件和查找模块文件都可以定义Imported Targets,它们通常具有形如SomePrefix::ThingName的名字。当这些目标可用时,项目应该倾向于使用这些目标,而非CMake变量。这些目标通常带有使用要求,并将诸如头文件搜索路径、编译器定义等内容自动应用于链接到它们的其他目标(例如使用target_link_libraries())。这笔使用功能变量手动应用相同东西更健壮也更方便。检查包或查找模块的文档以查看它定义的导入目标(如果有的话)。

导入的目标还应该封装任何特定于配置的路径。这包括二进制文件(库、可执行文件)的位置、编译器标志和任何其他与配置相关的数量。Find模块在提供这些详细信息方面可能不如配置文件可靠。

查找第三方包并使用其中的库的完整示例可能如下所示:

cmake_minimum_required(VERSION 3.10)
project(MyExeProject VERSION 1.0.0)

# Make project-provided Find modules available
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")

find_package(SomePackage REQUIRED)
add_executable(MyExe main.cpp)
target_link_libraries(MyExe PRIVATE SomePrefix::LibName)

请注意,上面的find_package()调用可以通过配置文件或Find模块来解决。它仅使用基本标志支持的基本参数。例如FindSomePackage.cmake目录中的 文件${CMAKE_CURRENT_SOURCE_DIR}/cmake将允许 find_package()成功使用Module模式。如果不存在这样的模块文件,系统将搜索配置文件。

用FetchContent从源码下载并构建

依赖项不一定要预先构建才能与 CMake 一起使用。它们可以作为主项目的一部分从源代码构建。

FetchContent模块提供下载内容通常是源代码,但可以是任何内容)并将其添加到主项目(如果依赖项也使用 CMake)的功能.依赖项的源代码将与项目的其余部分一起构建,就好像这些源代码是项目自己的源代码的一部分一样。

一般模式( general pattern)下,项目应该首先声明它想要使用的所有依赖项,然后让它们变得可用。下面演示了这个原理(更多信息请参见示例):

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        703bd9caab50b139428cea1aaff9974ebee5742e # release-1.10.0
)
FetchContent_Declare(
  Catch2
  GIT_REPOSITORY https://github.com/catchorg/Catch2.git
  GIT_TAG        de6fe184a9ac1a06895cdd1c9b437f0a0bdf14ad # v2.13.4
)
FetchContent_MakeAvailable(googletest Catch2)

支持各种下载方法,包括从 URL 下载和提取存档(支持一系列存档格式),以及多种存储库格式,包括 Git、Subversion 和 Mercurial。自定义下载、更新和补丁命令也可用于支持任意用例。

通过FetchContent将依赖项添加到项目时,项目链接到依赖项的目标,就像项目中的任何其他目标一样。如果依赖项提供形如SomePrefix::ThingName的命名空间目标,则项目应该链接到那些命名空间目标,而非 那些非命名空间的目标。请参阅下一节了解为什么建议这样做。

并不是所有的依赖都可以通过这种方式引入到项目中。一些依赖项定义的目标名称与项目或其他依赖项中的其他目标冲突。由add_executable()add_library()创建的具体的可执行文件和库目标是全局的 ,所以每个可执行文件/库目标在整个构建中都必须是唯一的。如果依赖项会添加冲突的目标名称,则不能使用此方法将其直接引入构建中。

FetchContent和find_package()一体化

3.24 版中的新功能。

一些依赖项支持通过find_package()或者 FetchContent添加。这些依赖项必须确保在安装和从源代码构建的场景中定义了相同的命名空间目标。然后,使用了它们的项目需要链接到那些命名空间目标,并且显式地处理这两种场景,只要该项目不使用任何这两种方法未提供的其他内容。

项目可以指明它乐意接受一个依赖项,通过FetchContent_Declare()FIND_PACKAGE_ARGS选项。这允许 FetchContent_MakeAvailable()尝试调用find_package()来满足一个依赖,通过使用FIND_PACKAGE_ARGS关键字后的参数(如果有的话)。如果没有找到依赖项,它将按照前面所述从源代码构建。

include(FetchContent)
FetchContent_Declare(
  googletest
  GIT_REPOSITORY https://github.com/google/googletest.git
  GIT_TAG        703bd9caab50b139428cea1aaff9974ebee5742e # release-1.10.0
  FIND_PACKAGE_ARGS NAMES GTest
)
FetchContent_MakeAvailable(googletest)

add_executable(ThingUnitTest thing_ut.cpp)
target_link_libraries(ThingUnitTest GTest::gtest_main)

上面的例子先调用了find_package(googletest NAMES GTest)。CMake 提供了一个FindGTest模块,所以如果CMake找到安装在某处的 GTest 包,CMake将使其可用,并且不会从源代码构建依赖项。如果没有找到GTest包,它将从源代码构建。
在任何一种情况下,GTest::gtest_main目标都应该被定义,因此我们将单元测试可执行文件链接到该目标。

也可以通过FETCHCONTENT_TRY_FIND_PACKAGE_MODE变量实现高层控制。将它设置为NERVER来禁止对find_package()的重定向;设置为ALWAYS来尝试find_package(),即使没有指定FIND_PACKAGE_ARGS(应谨慎使用)。

项目可能要求必须从源代码构建特定的依赖项。例如,如果需要依赖项的补丁版本或未发布版本,或者满足某些要求所有依赖项都从源代码构建的策略,则可能需要这样做。
可以通过将OVERRIDE_FIND_PACKAGE关键字添加到FetchContent_Declare()来强制从源代码构建。之后对该依赖项的find_package()调用将被重定向到FetchContent_MakeAvailable()

include(FetchContent)
FetchContent_Declare(
  Catch2
  URL https://intranet.mycomp.com/vendored/Catch2_2.13.4_patched.tgz
  URL_HASH MD5=abc123...
  OVERRIDE_FIND_PACKAGE
)

# The following is automatically redirected to FetchContent_MakeAvailable(Catch2)
find_package(Catch2)

有关更高级的用例,请参阅CMAKE_FIND_PACKAGE_REDIRECTS_DIR变量。

依赖提供者(Dependency Providers)

3.24 版中的新功能。

上一节讨论了项目指定其依赖项的技术。项目不应该真正关心依赖项来自何处,只要它提供了它期望的东西(通常只是一些导入目标(命名空间目标))。在没有任何其他细节的情况下,项目说明了它需要什么并且可以指定从哪里获得它,以便它仍然可以开箱即用。

另一方面,开发人员可能对控制 如何向项目提供依赖项更感兴趣。您可能想要使用您自己构建的包的特定版本。您可能想要使用第三方包管理器。出于安全或性能原因,您可能希望将某些请求重定向到您控制的系统上的不同 URL。CMake 通过Dependency Providers(依赖提供者)来支持这些场景 。

可以设置一个依赖的提供者来拦截find_package()FetchContent_MakeAvailable()调用。 如果提供者不满足请求,则提供者有机会在回退到内置实现之前满足此类请求。

只能设置一个提供者,并且只能在 CMake 运行前期的特定时间点设置。 CMAKE_PROJECT_TOP_LEVEL_INCLUDES变量列出了在处理第一个project() 调用(也只有这一个调用)时要读取的CMake文件。这是唯一可以设置依赖项提供程序的时间。在整个项目中最多只能使用一个提供者。

对于某些场景,用户不需要知道依赖提供者的设置细节。第三方可能会提供可以添加到CMAKE_PROJECT_TOP_LEVEL_INCLUDES的文件,这个变量将代表用户设置依赖项提供者。对于包管理器,这是推荐的方法。开发人员可以像这样使用这些文件:

cmake -DCMAKE_PROJECT_TOP_LEVEL_INCLUDES=/path/to/package_manager/setup.cmake ...