[cmake]使用依赖项指南

引言

项目经常依赖于其他项目、资产和工件。 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 模块通常与包分开分发,因此它们不太可靠。 它们通常是单独维护的,并且可能遵循不同的发布时间表,因此它们很容易过时。
根据所使用的参数,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
)

配置文件必须命名为 Config.cmake 或 -config.cmake(前者用于本指南的其余部分,但两者均受支持)。 该文件是 CMake 包的入口点。 同一目录中还可能存在名为 ConfigVersion.cmake 或 -config-version.cmake 的单独可选文件。 CMake 使用此文件来确定包的版本是否满足对 find_package() 的调用中包含的任何版本约束。 即使存在 ConfigVersion.cmake 文件,调用 find_package() 时也可以选择指定版本。
如果找到 Config.cmake 文件并且满足任何版本约束,则 find_package() 命令将认为已找到该包,并且假定整个包按设计完整。
可能还有其他文件提供 CMake 命令或导入目标供您使用。 CMake 不强制执行这些文件的任何命名约定。 它们通过使用 CMake include() 命令与主 Config.cmake 文件相关。 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 变量提供了便利。 包的路径也可以通过设置与 _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 的维护负担,并且这些文件落后于与其关联的包的最新版本的情况并不罕见。 一般来说,新的 Find 模块不再添加到 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() 来满足依赖关系,并使用 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() 的重定向。 即使未指定 FIND_PACKAGE_ARGS,也可以将其设置为 ALWAYS 来尝试 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 通过依赖项提供程序支持此类场景。
可以设置依赖项提供程序来拦截 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) 命令。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值