使用依赖项指南

引言

项目经常依赖于其他项目、资产和制品。CMake 提供了许多方法来将此类内容纳入构建。项目和用户可以选择最适合其需要的构建方法。

将依赖项引入构建的主要方法是 find_package() 命令和 FetchContent 模块。有时也会使用 FindPkgConfig 模块,尽管它缺少其他两个集成的一些功能,且在此指南中不再讨论它。

依赖项也可以通过自定义 依赖项提供程序 获得。这可能是一个第三方包管理器,也可能是开发人员实施的自定义代码。依赖项提供程序与上述主要方法合作以扩展其灵活性。

使用预构建包与 find_package()

项目所需的一个包可能已经构建并可以在用户系统上的某个位置获得。该包也可能已由 CMake 构建,或者完全可能已使用不同的构建系统。它甚至可能只是一个根本不需要构建的文件集合。对于这些场景,CMake 提供了 find_package() 命令。它搜索众所周知的位置,以及项目或用户提供的附加提示和路径。它还支持包组件和可选包。提供结果变量,以便项目可以根据找到包或特定组件来自定义其自身的行为。

在大多数情况下,项目通常应使用 基本签名。大多数时候,这仅仅涉及包名称、版本约束以及非可选依赖项的 REQUIRED 关键字。还可以指定一组包组件。

find_package() 基本签名的示例
find_package(Catch2)
find_package(GTest REQUIRED)
find_package(Boost 1.79 COMPONENTS date_time)

find_package() 命令支持使用两种主要方法来执行搜索

配置模式

使用这种方法,该命令会查找通常由包自身提供的文件。这是两种方法中更为可靠的方法,因为包详细信息应始终与包同步。

模块模式

并非所有包都能感知 CMake。许多包不提供支持配置模式所需的必要文件。对于此类情况,Find 模块文件可以由项目或 CMake 分开发放。Find 模块通常是启发式实现,它了解包通常提供什么以及如何向项目呈现该包。由于 Find 模块通常独立于包分发,因此它们不太可靠。它们通常是独立维护的,并且可能会遵循不同的发布时间表,因此很容易过时。

根据使用的参数,find_package() 可能使用以上一种或两种方法。仅仅通过将选项限制为基本签名,既可以使用配置模式,也可以使用模块模式来满足依赖项。其他选项的存在可能会将调用限制为仅使用这两种方法中的一种,从而可能降低命令查找依赖项的能力。有关此复杂主题的完整详细信息,请参阅 find_package() 文档。

对于这两种搜索方法,用户还可以在 cmake(1) 命令行或 ccmake(1)cmake-gui(1) UI 工具中设置缓存变量,以影响和覆盖在何处查找包。有关如何设置缓存变量的更多信息,请参阅 用户交互指南

配置文件包

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

配置文件通常可以在目录中找到,该目录的名称与模式 lib/cmake/<PackageName> 匹配,但它们也可能位于其他位置(请参见 配置模式搜索程序)。<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() 的调用所包含的版本约束。即使存在 <PackageName>ConfigVersion.cmake 文件,调用 find_package() 时指定版本也是可选的。

如果找到了 <PackageName>Config.cmake 文件并且满足了任何版本约束,那么 find_package() 命令将认为软件包已找到,并且整个软件包被假设为设计完成。

可能有其他文件提供 CMake 命令或 导入目标 供您使用。CMake 不会强制这些文件遵循任何命名约定。它们通过使用 CMake include() 命令与主 <PackageName>Config.cmake 文件相关。通常,<PackageName>Config.cmake 文件会为您包含这些文件,因此通常不需要任何其他步骤以外的 find_package() 的调用。

如果软件包的位置位于 CMake 已知的 目录 中,则 find_package() 调用应该会成功。CMake 已知的目录取决于平台。例如,使用标准系统软件包管理器安装在 Linux 上的软件包将自动在 /usr 前缀中找到。类似地,安装在 Windows 上的 Program Files 中的软件包也将自动找到。

如果软件包位于 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 环境变量类似,它是一个列表,但它需要使用特定于平台的环境变量列表项分隔符(Unix 上的 : 和 Windows 上的 ;)。

变量 CMAKE_PREFIX_PATH 在需要指定多个前缀或者同一前缀下有可用的多个包时提供了便利。包路径也可以通过设置与 <PackageName>_DIR 一致的变量来指定,例如 SomePackage_DIR。注意,这不是前缀,而是应该指向包含类似 /opt/somepackage/lib/cmake/SomePackage(如上述示例)这种配置风格的包文件的目录的完整路径。请参阅 find_package() 文档了解其他可能会影响搜索的 CMake 变量和环境变量。

查找模块文件

如果没有提供配置文件,也仍然可以通过 find_package() 命令查找包,只要有 FindSomePackage.cmake 文件可供使用。查找模块文件与配置文件的区别如下:

  1. 查找模块文件不应由包本身提供。

  2. Find<PackageName>.cmake 文件的可用性并不能说明包的可用性,或包的任何特定部分的可用性。

  3. CMake 不会在 CMAKE_PREFIX_PATH 变量中指定的位置搜索 Find<PackageName>.cmake 文件。而是,CMake 在 CMAKE_MODULE_PATH 变量给出的位置搜索此类文件。用户在运行 CMake 时通常会设置 CMAKE_MODULE_PATH,而 CMake 项目通常会追加到 CMAKE_MODULE_PATH 以允许使用本地的查找模块文件。

  4. 对于某些第三方 软件包,CMake 附带 Find<PackageName>.cmake 文件。对于 CMake 来说,这些文件会带来维护负担,而且这些文件很容易落后于所关联软件包的最新版本发布。总体而言,CMake 不再添加新的查找模块。项目应当鼓励上游软件包尽可能提供配置文件。如果此操作不成功,项目应为该软件包提供自己的查找模块。

请参阅查找模块,了解有关如何编写查找模块文件的详细说明。

导入目标

配置文件和查找模块文件都可以定义导入的目标。这些通常具有 SomePrefix::ThingName 格式的名称。如果有,项目应优先使用这些名称,而不是任何可能同时提供的 CMake 变量。此类目标通常承担使用要求,并自动对链接到它们的其它目标(例如使用target_link_libraries())应用诸如头搜索路径、编译器定义等内容。这比尝试使用变量手动应用相同内容既更可靠又更便捷。检查软件包或查找模块的文档以查看(如果存在)定义了哪些导入的目标。

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

以下示例查找第三方软件包并使用其中的库:

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() 的上述调用,可以使用配置文件或查找模块进行解决。它仅使用 基本签名 支持的基本参数。例如,${CMAKE_CURRENT_SOURCE_DIR}/cmake 目录中的 FindSomePackage.cmake 文件可允许 find_package() 命令利用模块模式成功执行。如果不存在此类模块文件,则会在系统中搜索配置文件。

使用 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)

支持各种下载方法,包括从 URL 下载并解压归档(支持多种归档格式),以及 Git、Subversion 和 Mercurial 等多种存储库格式。还可以使用自定义下载、更新和修补命令来支持任意用例。

当使用 FetchContent 向项目添加依赖项时,项目将链接到依赖项的目标,就像链接到项目中的任何其他目标一样。如果依赖项提供类似于 SomePrefix::ThingName 格式的命名空间目标,则项目应链接到它们,而不是链接到任何非命名空间目标。有关原因的建议,请参见下一节。

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

FetchContentfind_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(应慎用)。

该项目也可能决定必须从源代码构建特定的依赖项。如果需要具有依赖项的修补或未发布版本,则可能需要这样做,或者为了满足要求所有依赖项都从源代码构建的某些策略。项目可以通过将 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 变量。

依赖项提供商

在 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) 命令。