依赖使用指南¶
简介¶
项目通常会依赖其他项目、资源和工件。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。许多包不提供支持配置模式所需的文件。对于这种情况,可以单独提供一个查找模块文件,由项目或 CMake 提供。查找模块通常是一种启发式实现,它知道包通常提供什么以及如何将该包呈现给项目。由于查找模块通常与包分开分发,因此它们不太可靠。它们通常是单独维护的,并且很可能遵循不同的发布计划,因此它们很容易过时。
根据使用的参数, 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。有关可能影响搜索的其他 CMake 变量和环境变量,请参阅 find_package() 文档。
查找模块文件¶
不提供配置文件但仍可使用 find_package() 命令找到的包,前提是存在 FindSomePackage.cmake 文件。这些查找模块文件与配置文件不同,因为:
查找模块文件不应由包本身提供。
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 带来了维护负担,这些文件落后于它们所关联的包的最新版本并不罕见。一般来说,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() 创建的具体可执行文件和库目标是全局的,因此每个目标在整个构建中都必须是唯一的。如果依赖项会添加冲突的目标名称,则无法通过此方法将其直接引入构建。
FetchContent 与 find_package() 集成¶
在 3.24 版本中添加。
某些依赖项支持通过 find_package() 或 FetchContent 添加。这些依赖项必须确保在已安装和从源代码构建的场景中都定义相同的命名空间目标。然后,使用者项目会链接到这些命名空间目标,并且可以透明地处理这两种场景,只要项目不使用这两种方法都未提供的其他任何内容。
项目可以使用 FetchContent_Declare() 的 FIND_PACKAGE_ARGS 选项来指示它乐意接受任一方法提供的依赖项。这允许 FetchContent_MakeAvailable() 首先尝试使用 FIND_PACKAGE_ARGS 关键字之后的参数调用 find_package() 来满足依赖项。如果那样找不到依赖项,则会像前面描述的那样从源代码构建。
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)
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) 命令。