第 10 步:查找依赖项

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

然而,对于打包正确的项目,无需使用这些高级工具。如今许多流行的库和工具项目都能生成正确的安装树(类似我们在 Step 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 变量报告其结果,该变量会被设置为 true 或 false,分别表示找到和未找到包。

目标

将外部安装的测试框架集成到 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 文件已更新,采用了类似 GoogleTest 或 Catch2 的 SimpleTest 框架。请按顺序执行 TODO 1TODO 5 以使用新的测试框架。

注意

毋庸置疑,SimpleTest 是一个非常糟糕的测试框架,它只是表面上看起来像一个功能性的测试库。虽然本教程中的许多 CMake 代码可以在其他项目中原样使用,但你不应该在教程之外使用 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() 调用时带有 QUIETREQUIREDfind_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 目标在该项目中不可用而导致配置失败。

最后,我们包含 CMakeFindDependencyMacro 并在 SimpleTest 包配置文件中调用 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 包含目录添加到项目中。

解决方案

首先,我们调用 find_path() 来发现 Unpackaged 包含目录。我们使用 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>