1. 简介
在了解了CMake构件系统中的基本概念后,我们便能够更好地利用CMake以完成更加复杂的代码构建管理。在实际的项目开发中,项目经常依赖于其他的项目或工件。CMake提供了许多方法来将这些东西合并到构建中。用户可以灵活地选择最适合他们需要的方法。本文将详细介绍如何在CMake中使用这些依赖。文章是对Using Dependencies Guide的翻译,并增加了必要的解释。
将依赖项引入构建的主要方法是find_package()
命令和FetchContent
模块。有时也会使用FindPkgConfig
模块,尽管它缺少其他两个模块的一些集成,因此本指南中不会进一步讨论它。
依赖项也可以通过自定义依赖项提供程序(dependency provider)提供。这可能是第三方包管理器,也可能是开发人员实现的自定义代码。依赖提供程序可以与上面提到的常用方法进行配合以扩展它们的灵活性。
2. 通过find_package()来使用预构建的包
项目所需的软件包可能已经构建完成,并且已经安装到了用户系统的某个位置上。该包可能是由CMake构建的,或者使用完全不同的构建系统。它甚至可能只是一个根本不需要构建的文件集合。CMake为这些场景提供了find_package()
命令。它搜索已知的位置,以及用户提供的额外提示和路径。它还支持包组件和可选包。find_package()
提供的结果变量允许项目根据是否找到包或特定组件来定制自己的行为。
在大多数情况下,项目通常应该使用find_package()
的基本签名(Basic Signature)。大多数情况下,这将只包含包名、版本约束,如果依赖项必需的,则还包括REQUIRED
关键字。另外,我们还可以指定一组包组件。下面的例子给出了find_package()
基本签名的用法:
find_package(Catch2)
find_package(GTest REQUIRED)
find_package(Boost 1.79 COMPONENTS date_time)
在这个例子中,我们使用find_package()
来查找Catch2
、GTest
和Boost
。这里将Catch2
和GTest
视作单一的包并且不关心其版本,但对于Boost
,这里仅使用了其中的一个包组件date_time
,并且指定了其版本为1.79。
find_package()
命令支持两种主要的方法来执行搜索:
- 配置模式(Config mode):使用此方法,该命令查找通常由包本身提供的文件。这是这两种方法中更可靠的一种,因为包的详细信息应该始终与包保持同步。
- 模块模式(Module mode):并不是所有的包都支持cmake。许多包不提供支持配置模式所需的文件。对于这种情况,可以使用Find模块文件,该文件可以由项目或CMake单独提供。Find模块通常是一个启发式实现,它知道包通常提供什么以及如何将该包呈现给项目。由于Find模块通常不是和软件包一起分发的,因此它们不那么可靠。它们通常是单独维护的,并且它们可能遵循不同的发布时间表,因此它们很容易过时。
根据所使用的参数,find_package()
可以使用上述方法中的一种或者两种。通过将选项限制为基本签名,配置模式和模块模式都可以被使用以满足依赖项。其他选项的存在可能会限制CMake只能使用这两个方法中的一个,这可能会降低该命令查找依赖项的能力。更详细信息,请参阅find_package()
文档。
对于这两种搜索方法,用户还可以在cmake命令行或ccmake或cmake-gui UI工具中设置缓存变量,以影响和覆盖查找包的位置。有关如何设置缓存变量的更多信息,请参阅用户交互指南。
2.1. 提供配置文件的软件包
第三方为CMake提供可执行文件、库、头文件和其他文件的首选方式是提供配置文件。它们是包附带的文本文件,它们定义了CMake目标、变量、命令等等。配置文件是一个普通的CMake脚本,由find_package()
命令读取。
配置文件通常可以在与lib/cmake/<PackageName>
匹配的目录中找到,此外,它们还可能在其他位置:下表显示了搜索的目录。每个条目都标明了适用的平台,包括Windows (W)、UNIX (U)或Apple (A),其中<prefix>
指的是CMAKE_PREFIX_PATH
。
目录条目 | 平台 |
---|---|
<prefix>/ | W |
<prefix>/(cmake|CMake)/ | W |
<prefix>/<name>*/ | W |
<prefix>/<name>*/(cmake|CMake)/ | W |
<prefix>/<name>*/(cmake|CMake)/<name>*/ [1] | W |
<prefix>/(lib/<arch>|lib*|share)/cmake/<name>*/ | U |
<prefix>/(lib/<arch>|lib*|share)/<name>*/ | U |
<prefix>/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ | U |
<prefix>/<name>*/(lib/<arch>|lib*|share)/cmake/<name>*/ | W/U |
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/ | W/U |
<prefix>/<name>*/(lib/<arch>|lib*|share)/<name>*/(cmake|CMake)/ | W/U |
[1]是在CMake 3.25版本引入的。
<PackageName>
通常是find_package()
命令的第一个参数。也可以使用NAMES
选项指定其他名称,如下例子所示:
find_package(SomeThing
NAMES
SameThingOtherName # Another name for the package
SomeThing # Also still look for its canonical name
)
配置文件必须命名为<PackageName>Config.cmake
或<LowercasePackageName>-config.cmake
(前者用于本指南的其余部分,但两者都受支持)。这个文件是CMake包的入口点。一个可选文件<PackageName>ConfigVersion.cmake
或<LowercasePackageName>-config-version.cmake
也可以存在于同一目录下,CMake使用这个文件来确定包的版本是否满足find_package()
调用中包含的版本约束。在调用find_package()
并非必须指定包的版本,即使在<PackageName>ConfigVersion.cmake
文件存在的情况下也一样。
如果找到<PackageName>Config.cmake
文件并且满足版本约束,则find_package()
命令会认为已经找到了正确的包,并假设整个包与设计时一样完整。
可能有其他文件提供CMake命令或IMPORTED
目标供您使用。CMake不强制要求这些文件应该遵循任何命名约定。它们与主<PackageName>Config.cmake
文件相关。<PackageName>Config.cmake
文件通常会为您包含这些,因此除了调用find_package()
之外,它们通常不需要任何额外的步骤。
如果包的位置在CMake已知的目录中,则find_package()
调用应该成功。CMake已知的目录是特定于平台的。例如,使用标准系统包管理器安装在Linux上的包将自动在/usr前缀中找到。类似地,安装在Windows上的程序文件中的包也会自动找到。
如果软件包位于CMake不知道的位置,例如/opt/mylib
或$HOME/dev/prefix
,则在没有帮助的情况下不会自动找到它们。这是一种正常的情况,CMake为用户提供了几种方法来指定在哪里找到这样的库。
CMAKE_PREFIX_PATH
变量可以在调用CMake时设置。它被视为用于搜索配置文件的基本路径列表。安装在/opt/somepackage
中的包通常会将配置文件安装在类似于/opt/somepackage/lib/cmake/somepackage/SomePackageConfig.cmake
的路径中。在这种情况下,应该将/opt/somepackage
添加到CMAKE_PREFIX_PATH
中。
环境变量CMAKE_PREFIX_PATH
也可以用于搜索包。与PATH
环境变量一样,CMAKE_PREFIX_PATH
环境变量是一个列表,但它需要使用特定于平台的环境变量列表项分隔符(在Unix为:
,在Windows上为;
)。
CMAKE_PREFIX_PATH
的使用是非常方便的,尤其是在需要指定多个前缀,或者在同一前缀下有多个包的情况下。除此之外,包的路径也可以通过设置与<PackageName>_DIR
匹配的变量来指定,比如SomePackage_DIR
。请注意,这不是一个前缀,而应该是一个包含配置文件的目录的完整路径,例如上面示例中的/opt/somepackage/lib/cmake/somepackage
。有关其他可能影响搜索的CMake变量和环境变量,请参阅find_package()
文档。
2.2. Find模块文件
不提供配置文件的包仍然可以用find_package()
命令找到,前提是存在一个FindSomePackage.cmake
文件。这些Find模块文件不同于配置文件,表现在下面几个方面:
- 查找模块文件不应该由包本身提供。
Find<PackageName>.cmake
文件存在并不代表包的可用性,也不代表包的任何特定部分的可用性。- CMake不会在
CMAKE_PREFIX_PATH
变量中指定的位置搜索Find<PackageName>.cmake
文件。相反,CMake在CMAKE_MODULE_PATH
变量给出的位置中搜索这些文件。用户通常在运行CMake时设置CMAKE_MODULE_PATH
,并且CMake项目通常会被附加到CMAKE_MODULE_PATH
以允许使用本地查找模块文件。 - CMake会为一些第三方软件包发布
Find<PackageName>.cmake
文件。这些文件是CMake的维护负担,并且这些文件经常落后于它们所关联的包的最新版本。一般来说,新的Find模块不会再添加到CMake中。项目应该鼓励上游包在可能的情况下提供配置文件。如果上游包并不提供支持,项目应该为包提供自己的Find模块。
有关如何编写查找模块文件的详细讨论,请参阅Find模块。
2.3. 来自软件包的IMPORTED目标
配置文件和Find模块文件都可以定义IMPORTED
目标。它们通常具有SomePrefix::ThingName
形式的名称。若存在这些IMPORTED
目标,项目应该更倾向于使用它们,从而避免使用可能存在的其他CMake变量。因为这样的目标通常带有使用需求,并自动将诸如头搜索路径、编译器定义等应用于链接到它们的其他目标(例如使用target_link_libraries()
)。相比通过变量的方式手动应用这些构建规格,这种方式更健壮,也更方便。可以通过查看包或Find模块的文档,看看它定义了哪些IMPORTED
目标(如果有的话)。
IMPORTED
目标还应该封装特定于配置的路径。这包括二进制文件(库、可执行文件)的位置、编译器标志和任何其他依赖于配置的变量。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模块解析。它只使用基本签名支持的基本参数。例如,${CMAKE_CURRENT_SOURCE_DIR}/cmake
目录中的FindSomePackage.cmake
文件将允许使用模块模式成功地执行find_package()
命令。如果没有这样的模块文件存在,系统将搜索配置文件。
3. 使用FetchContent下载源代码并进行构建
在CMake中使用的依赖项不一定是预先构建好的。它们可以作为主项目的一部分从源代码构建。FetchContent模块提供了下载内容(通常是源代码,但可以是任何内容)的功能,如果依赖项也使用CMake,则将其添加到主项目中。依赖项的源代码将与项目的其余部分一起构建,就好像这些源代码是项目自己源代码的一部分一样。
一般的模式是,项目应该首先声明它想要使用的所有依赖项,然后要求它们成为可以使用的状态。下面演示了该原理(参见示例了解更多):
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 605a34765aa5d5ecbf476b4598a862ada971b0cc # v3.0.1
)
FetchContent_MakeAvailable(googletest Catch2)
在这个例子中,项目将使用GoogleTest
和Catch2
这两个依赖项。FetchContent_Declare()
命令分别声明了两个依赖项。FetchContent_MakeAvailable()
命令将依赖项添加到项目中,以便它们可以使用。
FetchContent支持各种下载方法,包括从URL下载和提取存档(支持一系列存档格式),以及许多版本管理的存储库格式,包括Git、Subversion和Mercurial。还可以使用自定义下载、更新和打补丁命令来支持任意用例。
当使用FetchContent将依赖项添加到项目中时,项目就会链接到依赖项的目标,就像项目中的任何其他目标一样。如果依赖项提供了SomePrefix::ThingName
形式的带有名称空间的目标,项目应该链接到这些目标,而不是链接到任何没有名称空间的目标。请参阅下一节,了解为什么要这样做。
并不是所有的依赖都可以通过这种方式引入到项目中。一些依赖项中定义的目标,其名称与项目或其他依赖项中的其他目标冲突。由add_executable()
和add_library()
创建的具体可执行文件和库目标是全局的,因此在整个构建中每个目标都必须是唯一的。如果依赖项将添加冲突的目标名称,则不能使用此方法将其直接引入构建。
4. 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
模块,所以如果它找到安装在某处的GTest包,它将使其可被使用,并且依赖将不会从源代码构建。如果没有找到GTest包,它将从源代码构建。在任何一种情况下,GTest::gtest_main
目标都应该被定义,因此我们将单元测试可执行文件链接到该目标。
可以通过FETCHCONTENT_TRY_FIND_PACKAGE_MODE
变量从更高层级来控制这种行为。可以将该变量设置为NEVER
来禁止调用find_package()
。也可以设置为ALWAYS
以总是调用find_package()
,即使没有指定FIND_PACKAGE_ARGS
(这种方式应该谨慎使用)。
项目还可能指定特定的依赖项,使其必须从源代码构建。例如这些使用场景:如果需要依赖项的补丁或未发布版本,或者满足某些必须要从源代码构建所有依赖项的策略。项目可以通过在FetchContent_Declare()
中添加OVERRIDE_FIND_PACKAGE
关键字来强制执行这一点。对该依赖的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)
对于更高级的用例,请参阅CMAKE_FIND_PACKAGE_REDIRECTS_DIR变量。
5. 依赖提供程序(Dependency Providers)
3.24新版功能。
前一节讨论了项目可以用来指定依赖项的技术。理想情况下,项目不应该关心依赖项来自哪里,只要它提供了它所期望的东西(通常只是一些导入的目标)。在没有任何其他细节的情况下,项目说明它需要什么,或许还需要指定从哪里获得它,这样它仍然可以开箱即用。
另一方面,开发人员可能对控制依赖项如何提供给项目更感兴趣。您可能希望使用自己构建的包的特定版本。您可能希望使用第三方包管理器。出于安全性或性能原因,您可能希望将某些请求重定向到您控制的系统上的不同URL。CMake通过依赖提供程序支持这类场景。
依赖提供程序可以设置为拦截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 ...
关于如何实现自己的自定义依赖提供程序的详细信息,请参见cmake_language(SET_DEPENDENCY_PROVIDER)命令。
6. Reference
- Using Dependencies Guide 3.27.0-rc5. 2023/07/15.