查找包

许多软件项目提供工具和库,旨在作为其他项目和应用程序的构建块。依赖于外部包的 CMake 项目使用 find_package 命令来定位其依赖项。一个典型的调用形式如下:

find_package(<Package> [version])

其中 <Package> 是要查找的包的名称,[version] 是一个可选的版本请求(格式为 major[.minor.[patch]])。该命令对包的概念与 CPack 的概念不同,CPack 用于创建源代码和二进制分发包以及安装程序。

该命令有两种模式:Module 模式和 Config 模式。在 Module 模式下,该命令搜索 find module:一个名为 Find<Package>.cmake 的文件。它首先在 CMAKE_MODULE_PATH 中查找,然后在 CMake 安装目录中查找。如果找到查找模块,则加载它以搜索包的各个组件。查找模块包含有关其期望找到的库和其他文件的包特定知识,并在内部使用 find_library 等命令来定位它们。CMake 为许多常用包提供了查找模块;请参阅 cmake-modules(7) 手册。

find_package 命令的 Config 模式通过与要查找的包合作提供了一种强大的替代方案。在无法找到查找模块或被调用方显式请求时,它将进入此模式。在 Config 模式下,该命令搜索 package configuration file:一个名为 <Package>Config.cmake<package>-config.cmake 的文件,该文件由要查找的包提供。给定包的名称,find_package 命令知道如何在安装前缀内部深处搜索以下位置:

<prefix>/lib/<package>/<package>-config.cmake

(有关位置的完整列表,请参阅 find_package 命令的文档)。CMake 创建一个名为 <Package>_DIR 的缓存条目来存储找到的位置或允许用户设置它。由于包配置文件随其包的安装一起提供,因此它确切地知道在哪里可以找到安装提供的所有内容。一旦 find_package 命令找到该文件,它将提供包组件的位置,无需任何其他搜索。

[version] 选项要求 find_package 定位特定版本的包。在 Module 模式下,该命令将请求传递给查找模块。在 Config 模式下,该命令在每个候选包配置文件旁边查找 package version file:一个名为 <Package>ConfigVersion.cmake<package>-config-<version>.cmake 的文件。加载版本文件以测试包版本是否与请求的版本匹配(有关版本文件 API 规范,请参阅 find_package 的文档)。如果版本文件声明兼容,则接受配置文件,否则忽略它。这种方法允许每个项目定义自己的版本兼容性规则。

内置查找模块

CMake 拥有许多预定义的模块,这些模块可以在 CMake 的 Modules 子目录中找到。这些模块可以查找许多常见的软件包。有关详细列表,请参阅 cmake-modules(7) 手册。

每个 Find<XX>.cmake 模块定义一组变量,一旦找到软件包,这些变量将允许项目使用该软件包。所有这些变量都以要查找的软件 <XX> 的名称开头。在 CMake 中,我们试图为命名这些变量建立约定,但您应该阅读模块顶部的注释以获得更确定的答案。以下变量在需要时按约定使用

<XX>_INCLUDE_DIRS

在哪里可以找到包的头文件,通常是 <XX>.h 等。

<XX>_LIBRARIES

要链接以使用 <XX> 的库。这些包括完整路径。

<XX>_DEFINITIONS

在编译使用 <XX> 的代码时要使用的预处理器定义。

<XX>_EXECUTABLE

在哪里可以找到作为包一部分的 <XX> 工具。

<XX>_<YY>_EXECUTABLE

在哪里可以找到随 <XX> 提供的 <YY> 工具。

<XX>_ROOT_DIR

在哪里可以找到 <XX> 安装的基本目录。这对于大型包很有用,在这些包中,您希望引用相对于公共基本(或根)目录的许多文件。

<XX>_VERSION_<YY>

如果为真,则找到包的版本 <YY>。查找模块的作者应确保最多只有一个为真。例如 TCL_VERSION_84。

<XX>_<YY>_FOUND

如果为假,则 <XX> 包的可选 <YY> 部分不可用。

<XX>_FOUND

如果我们没有找到或不想使用 <XX>,则设置为 false 或未定义。

并非所有变量都存在于每个 FindXX.cmake files 中。但是,<XX>_FOUND 应该在大多数情况下存在。如果 <XX> 是一个库,那么 <XX>_LIBRARIES 也应该被定义,并且 <XX>_INCLUDE_DIR 通常也应该被定义。

可以使用 include 命令或 find_package 命令将模块包含在项目中。

find_package(OpenGL)

等效于

include(${CMAKE_ROOT}/Modules/FindOpenGL.cmake)

include(FindOpenGL)

如果项目将其构建系统转换为 CMake,则如果包提供 <XX>Config.cmake 文件,则 find_package 仍然有效。如何在后面章节中介绍如何创建 CMake 包。

创建 CMake 包配置文

项目必须提供包配置文,以便外部应用程序能够找到它们。考虑一个简单的项目“Gromit”,它提供一个生成源代码的可执行文件和一个生成的代码必须链接到的库。 CMakeLists.txt 文件可能以以下内容开头:

cmake_minimum_required(VERSION 3.20)
project(Gromit C)
set(version 1.0)

# Create library and executable.
add_library(gromit STATIC gromit.c gromit.h)
add_executable(gromit-gen gromit-gen.c)

为了安装 Gromit 并导出其目标供外部项目使用,请添加以下代码:

# Install and export the targets.
install(FILES gromit.h DESTINATION include/gromit-${version})
install(TARGETS gromit gromit-gen
        DESTINATION lib/gromit-${version}
        EXPORT gromit-targets)
install(EXPORT gromit-targets
        DESTINATION lib/gromit-${version})

最后,Gromit 必须在其安装树中提供一个包配置文,以便外部项目可以使用 find_package 定位它。

# Create and install package configuration and version files.
configure_file(
   ${Gromit_SOURCE_DIR}/pkg/gromit-config.cmake.in
   ${Gromit_BINARY_DIR}/pkg/gromit-config.cmake @ONLY)

configure_file(
   ${Gromit_SOURCE_DIR}/gromit-config-version.cmake.in
   ${Gromit_BINARY_DIR}/gromit-config-version.cmake @ONLY)

install(FILES ${Gromit_BINARY_DIR}/pkg/gromit-config.cmake
         ${Gromit_BINARY_DIR}/gromit-config-version.cmake
         DESTINATION lib/gromit-${version})

此代码配置并安装包配置文和相应的包版本文件。包配置输入文件 gromit-config.cmake.in 包含以下代码:

# Compute installation prefix relative to this file.
get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_prefix "${_dir}/../.." ABSOLUTE)

# Import the targets.
include("${_prefix}/lib/gromit-@version@/gromit-targets.cmake")

# Report other information.
set(gromit_INCLUDE_DIRS "${_prefix}/include/gromit-@version@")

安装后,配置后的包配置文 gromit-config.cmake 知道其他已安装文件相对于自身的位置。相应的包版本文件根据其输入文件 gromit-config-version.cmake.in 配置,该文件包含以下代码:

set(PACKAGE_VERSION "@version@")
if(NOT "${PACKAGE_FIND_VERSION}" VERSION_GREATER "@version@")
  set(PACKAGE_VERSION_COMPATIBLE 1) # compatible with older
  if("${PACKAGE_FIND_VERSION}" VERSION_EQUAL "@version@")
    set(PACKAGE_VERSION_EXACT 1) # exact match for this version
  endif()
endif()

使用 Gromit 包的应用程序可能会创建一个如下所示的 CMake 文件:

cmake_minimum_required(VERSION 3.20)
project(MyProject C)

find_package(gromit 1.0 REQUIRED)
include_directories(${gromit_INCLUDE_DIRS})
# run imported executable
add_custom_command(OUTPUT generated.c
                   COMMAND gromit-gen generated.c)
add_executable(myexe generated.c)
target_link_libraries(myexe gromit) # link to imported library

调用find_package 会查找 Gromit 的安装路径,如果找不到(因为使用了 REQUIRED),则会终止并显示错误消息。该命令执行成功后,会加载 Gromit 包的配置文件 gromit-config.cmake,从而导入 Gromit 目标并定义变量,例如 gromit_INCLUDE_DIRS

上面的示例创建了一个包配置文件,并将其放置在 install 目录树中。也可以在 build 目录树中创建包配置文件,以便应用程序在不进行安装的情况下使用该项目。为此,需要使用以下代码扩展 Gromit 的 CMake 文件:

# Make project usable from build tree.
export(TARGETS gromit gromit-gen FILE gromit-targets.cmake)
configure_file(${Gromit_SOURCE_DIR}/gromit-config.cmake.in
               ${Gromit_BINARY_DIR}/gromit-config.cmake @ONLY)

configure_file 调用使用不同的输入文件 gromit-config.cmake.in,其中包含:

# Import the targets.
include("@Gromit_BINARY_DIR@/gromit-targets.cmake")

# Report other information.
set(gromit_INCLUDE_DIRS "@Gromit_SOURCE_DIR@")

放置在构建目录树中的包配置文件 gromit-config.cmake 向外部项目提供与安装目录树中相同的信息,但会引用源代码和构建目录树中的文件。它共享一个相同的包版本文件 gromit-config-version.cmake,该文件位于安装目录树中。

CMake 包注册表

CMake 提供了两个中心位置来注册已在系统任何位置构建或安装的包:用户包注册表系统包注册表find_package 命令在其文档中指定的搜索步骤中将这两个包注册表作为两个搜索步骤。注册表对于帮助项目在非标准安装位置或直接在包构建目录树中查找包特别有用。项目可以使用自己的方法填充用户或系统注册表以引用其位置。在这两种情况下,包都应将包配置文件存储在注册位置,并可以选择性地存储本章前面提到的包版本文件。

用户包注册表存储在特定于平台的每个用户位置。在 Windows 上,它存储在 Windows 注册表中,位于 HKEY_CURRENT_USER 下的一个键中。一个 <package> 可能会出现在注册表键下

HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\<package>

作为具有任意名称的 REG_SZ 值,该值指定包含包配置文件的目录。在 UNIX 平台上,用户包注册表存储在用户主目录下的 ~/.cmake/packages 中。一个 <package> 可能会出现在目录下

~/.cmake/packages/<package>

作为一个具有任意名称的文件,其内容指定包含包配置文件的目录。export(PACKAGE) 命令可用于在用户包注册表中注册项目构建目录树。CMake 目前没有提供将安装目录树添加到用户包注册表的接口;如果需要,必须手动教导安装程序注册其包。

系统包注册表存储在特定于平台的系统范围位置。在 Windows 上,它存储在 Windows 注册表中,位于 HKEY_LOCAL_MACHINE 下的一个键中。一个 <package> 可能会出现在注册表键下

HKEY_LOCAL_MACHINE\Software\Kitware\CMake\Packages\<package>

作为具有任意名称的 REG_SZ 值,该值指定包含包配置文件的目录。在非 Windows 平台上没有系统包注册表。CMake 没有提供添加到系统包注册表的接口;如果需要,必须手动教导安装程序注册其包。

包注册表项由其引用的项目安装分别拥有。包安装程序负责添加自己的条目,相应的卸载程序负责删除它。但是,为了保持注册表的整洁,find_package 命令会在遇到过时的包注册表项时自动将其删除(如果它具有足够的权限)。如果某个条目引用的目录不存在或不包含匹配的包配置文件,则该条目被视为过时。这对于由 export(PACKAGE) 命令为构建目录树创建的用户包注册表条目特别有用,这些构建目录树没有卸载事件,并且只是由开发人员删除。

包注册表项可以具有任意名称。为其命名的一种简单约定是使用内容哈希,因为它们是确定性的并且不太可能发生冲突。export(PACKAGE) 命令使用这种方法。引用特定目录的条目的名称只是该目录路径本身的内容哈希。例如,项目可以创建以下包注册表项:

> reg query HKCU\Software\Kitware\CMake\Packages\MyPackage
HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\MyPackage
 45e7d55f13b87179bb12f907c8de6fc4
                          REG_SZ    c:/Users/Me/Work/lib/cmake/MyPackage
 7b4a9844f681c80ce93190d4e3185db9
                          REG_SZ    c:/Users/Me/Work/MyPackage-build

在 Windows 上,或

$ cat ~/.cmake/packages/MyPackage/7d1fb77e07ce59a81bed093bbee945bd
/home/me/work/lib/cmake/MyPackage
$ cat ~/.cmake/packages/MyPackage/f92c1db873a1937f3100706657c63e07
/home/me/work/MyPackage-build

在 UNIX 上。find_package(MyPackage) 命令将在注册位置搜索包配置文件。单个包的包注册表项之间的搜索顺序未指定。注册位置可能包含包版本文件,以告知 find_package 特定位置是否适合请求的版本。