使用依赖项指南

简介

项目经常会依赖其他项目、资源和制品。CMake 提供了多种将这些内容纳入构建的方法。项目和用户可以灵活选择最适合其需求的方法。

将依赖项引入构建的主要方法是 find_package() 命令和 FetchContent 模块。FindPkgConfig 模块有时也会被使用,但它缺乏上述两个模块的部分集成能力,因此本指南不再赘述。

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

配合 find_package() 使用预构建包

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

在大多数情况下,项目通常应该使用 基本签名 (Basic Signature)。大多数时候,这只需要包名,可能还会包含版本约束,如果依赖项不是可选的,则使用 REQUIRED 关键字。也可以指定一组包组件。

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

find_package() 命令支持两种主要的搜索方法:

配置模式 (Config mode)

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

模块模式 (Module mode)

并非所有包都支持 CMake。许多包不提供支持配置模式所需的文件。对于这种情况,可以由项目或 CMake 单独提供“查找模块”文件 (Find module file)。查找模块通常是一种启发式实现,它知道包通常提供什么,以及如何向项目展示该包。由于查找模块通常与包分开分发,因此可靠性较低。它们通常被单独维护,且很可能遵循不同的发布周期,因此很容易过时。

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

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

配置文件包

第三方提供可执行文件、库、头文件和其他文件以供 CMake 使用的首选方式是提供 配置文件 (config files)。这些是随包一起分发的文本文件,定义了 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 命令或 导入目标 (Imported Targets) 的文件供您使用。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。有关可能影响搜索的其他 CMake 变量和环境变量,请参阅 find_package() 文档。

查找模块文件

如果提供了 FindSomePackage.cmake 文件,则即使不提供配置文件的包,也可以使用 find_package() 命令找到。这些查找模块文件与配置文件不同,原因如下:

  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 中。项目应鼓励上游包尽可能提供配置文件。如果尝试失败,项目应为该包提供自己的查找模块。

有关如何编写查找模块文件的详细讨论,请参阅 查找模块 (Find Modules)

导入目标

配置文件和查找模块文件都可以定义 导入目标 (Imported Targets)。这些目标通常具有 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_ARGS,也会尝试 find_package()(应谨慎使用)。

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

有关如何实现您自己的自定义依赖项提供程序的详细信息,请参阅 cmake_language(SET_DEPENDENCY_PROVIDER) 命令。