第 10 步:查找依赖项

在 C/C++ 软件开发中,管理构建依赖项始终是现代开发者面临的最高优先级挑战之一。CMake 提供了一个广泛的工具集,用于发现和验证各种类型的依赖项。

但是,对于正确打包的项目,无需使用这些高级工具。如今,许多流行的库和实用程序项目都会生成正确的安装树,就像我们在第 9 步中设置的那样,这些安装树易于集成到 CMake 中。

在这种最佳场景下,我们只需要find_package() 将依赖项导入到我们的项目中。

背景

CMake 中用于发现依赖项的命令共有五个,前四个是:

find_file()

查找并报告指定文件的完整路径,这是find命令中最灵活的。

find_library()

查找并报告静态归档或共享对象的完整路径,适用于与target_link_libraries()一起使用。

find_path()

查找并报告*包含*文件的目录的完整路径。这通常与target_include_directories()结合用于查找头文件。

find_program()

查找并报告程序的可调用名称或路径。通常与execute_process()add_custom_command()结合使用。

这些命令应被视为“备用”选项,当主要的查找命令不适用时使用。主要的查找命令是find_package()。它使用全面的内置启发式方法和上游提供的打包文件,为请求的依赖项提供最佳接口。

练习 1 - 使用find_package()

find_package() 使用的搜索路径和行为在其文档中有详细描述,但此处过于冗长无法复制。可以肯定地说,它搜索了众所周知、鲜为人知、晦涩难懂以及用户提供的位置,试图找到满足其要求的包。

find_package(ForeignLibrary)

使用find_package() 的最佳方法是确保所有依赖项在构建之前都已安装到单个安装树中,然后通过CMAKE_PREFIX_PATH变量将该安装树的位置告知find_package()

注意

构建和安装依赖项本身可能是一项繁重的工作。尽管本教程会为说明目的这样做,但**强烈**建议使用包管理器进行项目本地依赖项管理。

find_package() 除了要查找的包之外,还接受几个参数。最值得注意的是:

  • 一个位置参数<version>,用于描述要与包的配置版本文件进行比较的版本。这应该谨慎使用,最好通过包管理器控制正在安装的依赖项的版本,而不是可能因无伤大碍的版本更新而中断构建。

    如果已知该包依赖于旧版本的依赖项,则可以使用版本要求。

  • REQUIRED 用于非可选依赖项,如果找不到,则应中止构建。

  • QUIET 用于可选依赖项,如果找不到,则不应向用户报告任何内容。

find_package() 通过<PackageName>_FOUND变量报告其结果,对于找到和未找到的包,该变量将分别设置为真或假值。

目标

将外部安装的测试框架集成到 Tutorial 项目中。

有用资源

要编辑的文件

  • TutorialProject/CMakePresets.json

  • TutorialProject/Tests/CMakeLists.txt

  • TutorialProject/Tests/TestMathFunctions.cxx

开始

与前面的步骤不同,Step10文件夹的组织方式也不同。我们需要编辑的教程项目位于Step10/TutorialProject下。现在存在另一个项目SimpleTest,以及一个部分填充的安装树,我们将在后续练习中使用它。您不需要为这个练习编辑这些其他目录中的任何内容,所有TODO和解决方案步骤都适用于TutorialProject

SimpleTest包提供了两个有用的构造:要链接到测试二进制文件的SimpleTest::SimpleTest目标,以及用于自动将测试添加到 CTest 的simpletest_discover_tests函数。

与其他测试框架类似,simpletest_discover_tests只需要传入包含测试的可执行目标的名称。

simpletest_discover_tests(MyTestExe)

TestMathFunctions.cxx文件已更新,以采用SimpleTest框架,类似于 GoogleTest 或 Catch2。请执行TODO 1TODO 5以使用新的测试框架。

注意

不言而喻,SimpleTest是一个非常糟糕的测试框架,它只是表面上像一个功能性测试库。尽管本教程中的大部分 CMake 代码可以在其他项目中 unaltered 使用,但在本教程之外,您不应使用SimpleTest,也不应尝试从其提供的 CMake 代码中学习。

构建并运行

首先,我们必须安装SimpleTest框架。导航到Help/guide/Step10/SimpleTest目录并运行以下命令:

cmake --preset tutorial
cmake --install build

注意

SimpleTest预设配置了安装SimpleTest所需的一切,以供教程使用。由于原因超出了本教程的范围,因此无需为SimpleTest构建或提供任何其他配置。

我们可以看到Step10/install目录现在已由SimpleTest头文件和包文件填充。

现在,我们可以像往常一样配置和构建 Tutorial 项目,导航到Help/guide/Step10/TutorialProject并运行:

cmake --preset tutorial
cmake --build build

通过运行 CTest 测试来验证SimpleTest框架是否已正确使用。

解决方案

首先,我们调用find_package()来发现SimpleTest包。我们使用REQUIRED来实现这一点,因为没有SimpleTest,测试就无法构建。

TODO 1 点击显示/隐藏答案
TODO 1: TutorialProject/Tests/CMakeLists.txt
find_package(SimpleTest REQUIRED)

接下来,我们将SimpleTest::SimpleTest目标添加到TestMathFunctions中。

TODO 2 点击显示/隐藏答案

现在,我们可以用对simpletest_discover_tests的调用替换我们的测试描述代码。

TODO 3 点击显示/隐藏答案
TODO 3: TutorialProject/Tests/CMakeLists.txt
simpletest_discover_tests(TestMathFunctions)

我们通过将安装树添加到CMAKE_PREFIX_PATH来确保find_package()可以发现SimpleTest

TODO 4 点击显示/隐藏答案
TODO 4: TutorialProject/CMakePresets.json
"cacheVariables": {
  "CMAKE_PREFIX_PATH": "${sourceParentDir}/install",
  "TUTORIAL_USE_STD_SQRT": "OFF",
  "TUTORIAL_ENABLE_IPO": "OFF"
}

最后,我们通过删除占位符并包含相应的头文件来更新测试,使其使用SimpleTest提供的宏。

TODO 5 点击显示/隐藏答案
TODO 5: TutorialProject/Tests/TestMathFunctions.cxx
#include <MathFunctions.h>
#include <SimpleTest.h>

TEST("add")
{

练习 2 - 传递性依赖

库经常相互构建。多媒体应用程序可能依赖于提供各种容器格式支持的库,而该库又可能依赖于一个或多个其他库来进行压缩算法。

我们需要在安装树中的包配置文件中表达这些传递性需求。我们通过CMakeFindDependencyMacro模块来实现这一点,该模块提供了一种安全机制,供已安装的包递归地发现彼此。

include(CMakeFindDependencyMacro)
find_dependency(zlib)

find_dependency()还会转发来自顶级find_package()调用的参数。如果find_package()使用QUIETREQUIRED调用,find_dependency()也将使用QUIET和/或REQUIRED

目标

SimpleTest添加一个依赖项,并确保依赖于SimpleTest的包也能发现此传递性依赖。

有用资源

要编辑的文件

  • SimpleTest/CMakeLists.txt

  • SimpleTest/cmake/SimpleTestConfig.cmake

开始

在此步骤中,我们仅编辑SimpleTest项目。传递性依赖项TransitiveDep是一个虚拟依赖项,不提供任何行为。但是 CMake 不知道这一点,如果 CMake 找不到所有必需的依赖项,TutorialProject测试将无法配置和构建。

TransitiveDep包已安装到Step10/install树中。我们无需像SimpleTest那样安装它。

完成TODO 6TODO 8

构建并运行

我们需要重新安装 SimpleTest 框架。导航到Help/guide/Step10/SimpleTest目录并运行与之前相同的命令。

cmake --preset tutorial
cmake --install build

现在,我们可以重新配置和重建TutorialProject,导航到Help/guide/Step10/TutorialProject并执行常规步骤。

cmake --preset tutorial
cmake --build build

如果构建成功,我们很可能已成功传播了传递性依赖项。通过在TutorialProjectCMakeCache.txt中搜索名为TransitiveDep_DIR的条目来验证这一点。这表明TutorialProject搜索并找到了TransitiveDep,即使它没有直接要求它。

解决方案

首先,我们调用find_package()来发现TransitiveDep包。我们使用REQUIRED来验证我们已找到TransitiveDep

TODO 6 点击显示/隐藏答案
TODO 6: SimpleTest/CMakeLists.txt
find_package(TransitiveDep REQUIRED)

接下来,我们将TransitiveDep::TransitiveDep目标添加到SimpleTest中。

TODO 7 点击显示/隐藏答案

注意

如果此时我们构建了TutorialProject,我们预计配置将失败,因为TransitiveDep::TransitiveDep目标在TutorialProject中不可用。

最后,我们在SimpleTest包配置文件中包含CMakeFindDependencyMacro并调用find_dependency(),以传播传递性依赖项。

TODO 8 点击显示/隐藏答案
TODO 8: SimpleTest/cmake/SimpleTestConfig.cmake
include(CMakeFindDependencyMacro)
find_dependency(TransitiveDep)

练习 3 - 查找其他类型的文件

在一个完美的世界里,我们关心的每一个依赖项都会被正确打包,或者至少有其他开发者写了一个模块来发现它们。我们生活在一个不完美的世界,有时我们不得不亲自动手,手动发现构建需求。

为此,我们有前面步骤中列出的其他查找命令,例如find_path()

find_path(PackageIncludeFolder Package.h REQUIRED
  PATH_SUFFIXES
    Package
)
target_include_directories(MyApp
  PRIVATE
    ${PackageIncludeFolder}
)

目标

将一个未打包的头文件添加到TutorialProjectTutorial可执行文件中。

有用资源

要编辑的文件

  • TutorialProject/Tutorial/CMakeLists.txt

  • TutorialProject/Tutorial/Tutorial.cxx

入门

在此步骤中,我们仅编辑TutorialProject项目。未打包的头文件Unpackaged/Unpackaged.h已安装到Step10/install树中。

完成TODO 9TODO 11

构建和运行

此练习没有特殊的构建步骤,导航到Help/guide/Step10/TutorialProject并执行常规的构建步骤。

cmake --build build

如果构建成功,我们已成功将Unpackaged include 目录添加到项目中。

解决方案

首先,我们调用find_path()来发现Unpackaged include 目录。我们使用REQUIRED,因为如果找不到Unpackaged.h头文件,构建Tutorial将会失败。

TODO 9 点击显示/隐藏答案
TODO 9: TutorialProject/Tutorial/CMakeLists.txt
find_path(UnpackagedIncludeFolder Unpackaged.h REQUIRED
  PATH_SUFFIXES
    Unpackaged
)

接下来,我们使用target_include_directories()将发现的路径添加到Tutorial中。

TODO 10 点击显示/隐藏答案
TODO 10: TutorialProject/Tutorial/CMakeLists.txt
target_include_directories(Tutorial
  PRIVATE
    ${UnpackagedIncludeFolder}
)

最后,我们编辑Tutorial.cxx以包含发现的头文件。

TODO 11 点击显示/隐藏答案
TODO 11: TutorialProject/Tutorial/Tutorial.cxx
#include <MathFunctions.h>
#include <Unpackaged.h>