前言:
学习笔记,随时更新。如有谬误,欢迎指正。
说明:
- 红色字体为较为重要部分。
- 绿色字体为个人理解部分。
10 查找包
许多软件项目提供工具和库作为其他项目和应用程序的构建模块。依赖外部包的 CMake 项目使用 find_package 命令来定位它们的依赖。典型的调用是这样的形式:
find_package(<Package> [version])
<package> 是要被查找的包的名称, [version] 是一个可选的版本要求(以 major[.minor.[patch]] 的形式)。一个包的命令的概念与 CPack 的命令的概念截然不同,它意味着创建源代码和二进制发行版以及安装器。
该命令有两种工作模式: Module 模式和 Config 模式。在 Module 模式中,该命令搜索一个查找模块:一个名为 Find<Package>.cmake 的文件。它首先在 CMAKE_MODULE_PATH 中查找,然后在 CMake 安装(目录)中查找。如果找到了查找模块,则加载它来搜索包的各个组件。查找模块包含特定于包的它们期望查找的库和其他文件的知识,并且在内部使用 find_library 之类的命令来定位它们。 CMake 为许多常用包提供查找模块。详见 cmake-modules 手册。
find_package 的 Config 模式通过与要查找的包的合作提供了一个强大的替代方案。在定位查找模块失败或调用者显式请求时,它将进入此模式。在 Config 模式下,该命令搜索一个包配置文件:一个名为 <Package>Config.camke 或 <package>-config.camke 的文件,该文件由要找的包提供的。给定一个包的名称, find_package 命令知道如何深入搜索安装前缀中的位置,比如:
<prefix>/lib/<package>/<package>-config.cmake
(有关位置的完整列表,请参阅 find_package 命令的文档)。 CMake 创建一个名为 <Package>_DIR 的缓存项来存储找到的位置或允许用户设置它。由于包配置文件附带其包的安装,它确切地知道在哪里可以找到安装所提供的所有内容。一旦 find_package 命令定位到文件,它将提供包组件的位置,而不需要进行任何额外的搜索。
[version] 选项要求 find_package 查找包的特定版本。在 Module 模式中,该命令将请求传递给查找模块。在 Config 模式下,该命令查看每个候选包配置文件旁边的包版本文件:一个名为 <Package>ConfigVersion.cmake 或 <package>-config-<version>.cmake 的文件。加载版本文件以测试包版本是否与请求的版本相匹配(有关版本文件 API 规范,请参阅 find_package 的文档)。如果版本文件声明兼容性,则接受配置文件,否则将忽略配置文件。这种方法允许每个项目为版本兼容性定义自己的规则。
10.1 内置查找模块
CMake 有许多预定义的模块,可以在 CMake 的 Modules 子目录中找到。这些模块可以找到许多常用的软件包。详细列表请参见 cmake-modules 手册 。
每个 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>
- 如果为 true ,表明查找到包的 <YY> 版本。查找模块的作者应该确保其中最多有一个是正确的。例如 TCL_VERSION_84 。
<XX>_<YY>_FOUND
- 如果为 false ,则包 <XX> 的可选部分 <YY> 不可用。
<XX>_FOUND
- 如果我们没有找到或不想使用 <XX> ,则设置为 false 或未定义。
并非所有变量都出现在每个 FindXX.cmake 文件。但是, <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 包将在后面章节介绍。
10.2 创建 CMake 包配置文件
项目必须提供包配置文件,以便外部应用程序能够找到它们。考虑一个简单的项目“ Gromit ”,它提供一个可执行文件来生成源代码和一个库,生成的代码必须链接到这个库。 CMakeLists.txt 文件可以这样开始:
cmake_minimum_required(VERSION 3.20)
project(Gromit C)
set(version 1.0)
# 创建库和可执行文件
add_library(gromit STATIC gromit.c gromit.h)
add_executable(gromit-gen gromit-gen.c)
为了安装 Gromit 并导出它的目标以供外部项目使用,添加以下代码:
# 安装并导出目标
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 找到它:
# 创建并安装包配置文件和版本文件
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 中有代码:
# 计算相对于此文件的安装前缀
get_filename_component(_dir "${CMAKE_CURRENT_LIST_FILE}" PATH)
get_filename_component(_prefix "${_dir}/../.." ABSOLUTE)
# 导入目标
include("${_prefix}/lib/gromit-@version@/gromit-targets.cmake")
# 输出其他信息
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) # 兼容旧版本
if("${PACKAGE_FIND_VERSION}" VERSION_EQUAL "@version@")
set(PACKAGE_VERSION_EXACT 1) # 正好匹配此版本
endif()
endif()
使用 Gromit 包的应用程序可能会创建一个类似这样的 CMake 文件:
cmake_minimum_required(VERSION 3.20)
project(MyProject C)
find_package(gromit 1.0 REQUIRED)
include_directories(${gromit_INCLUDE_DIRS})
# 运行导入的可执行文件
add_custom_command(OUTPUT generated.c
COMMAND gromit-gen generated.c)
add_executable(myexe generated.c)
target_link_libraries(myexe gromit) # 链接导入的库
对 find_package 的调用会定位 Gromit 的安装,如果没有找到则终止并显示错误消息(由于 REQUIRED )。命令执行成功后, Gromit 包配置文件 gromit-config.cmake 已经加载,因此已经导入 Gromit 目标,并定义了 gromit_INCLUDE_DIRS 等变量。
上面的示例创建一个包配置文件,并将其放在 install 树中。还可以在 build 树中创建包配置文件,以允许应用程序在不安装的情况下使用项目。为了做到这一点,我们扩展 Gromit 的 CMake 文件,代码如下:
# 使项目从构建树中可用
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 包含:
# 导入目标
include("@Gromit_BINARY_DIR@/gromit-targets.cmake")
# 输出其他信息
set(gromit_INCLUDE_DIRS "@Gromit_SOURCE_DIR@")
包配置文件 gromit-config.cmake 位于构建树中,向外部项目提供与安装树中相同的信息,但指向源文件和构建树中的文件。它共享一个相同的包版本文件 gromit-config-version.cmake ,它位于安装树中。
10.3 CMake 包注册表
CMake 提供了两个主要位置来注册已经在系统上构建或安装的包:一个用户包注册表和一个系统包注册表。 find_package 命令搜索两个包注册表,作为其文档中指定的两个搜索步骤。注册表对于帮助项目在非标准安装位置或直接在包构建树中查找包特别有用。项目可以填充用户或系统注册表(使用自己的方法)来引指向其位置。在任何一种情况下,包都应该在注册位置存储一个包配置文件和本章前面提到的可选的包版本文件。
用户包注册表存储在特定于平台的每个用户位置。在 Windows 上,它存储在 Windows 注册表中的 HKEY_CURRENT_USER 键下。 <package> 可能作为 REG_SZ 值出现在注册表项
HKEY_CURRENT_USER\Software\Kitware\CMake\Packages\<package>
下,该值可以具有任意名称(即其 key 可以是任意的),指定包含包配置文件的目录。在 UNIX 平台上,用户包注册表存储在 ~/.cmake/packages 下的用户主目录中。 <package> 可能出现在
~/.cmake/packages/<package>
目录下,是一个任意名称的文件,其内容指定包含包配置文件的目录。可以使用 export(PACKAGE) 命令在用户包注册表中注册一个项目构建树。 CMake 目前没有提供将安装树添加到用户包注册表的接口,如果需要,必须由安装程序自己来注册他们的软件包。
系统包注册表存储在特定于平台的、系统级别的位置。在 Windows 上,它存储在 Windows 注册表中的 HKEY_LOCAL_MACHINE 键下。 <package> 可能作为 REG_SZ 值出现在注册表项
HKEY_LOCAL_MACHINE\Software\Kitware\CMake\Packages\<package>
下,该值可以具有任意名称(即其 key 可以是任意的),指定包含包配置文件的目录。在非 Windows 平台上没有系统包注册表。 CMake 不提供添加到系统包注册表的接口。如果需要,必须由安装程序自己来注册他们的软件包。
包注册表项由它们所指向的项目安装单独拥有。包安装程序负责添加自己的项,相应的卸载程序负责删除它。但是,为了保持注册表干净,如果 find_package 命令有足够的权限,它会自动删除遇到的过时的包注册表项。如果项指向的目录不存在或不包含匹配的包配置文件,则该项被认为是过时的。这对于由 export(PACKAGE) 命令为构建树创建的用户包注册表项特别有用,这些构建树没有卸载事件,开发人员可以简单地删除它们。
包注册表项可以是任意名称。命名它们的一个简单约定是使用内容哈希,因为它们是确定性的,不太可能发生冲突。 export(PACKAGE) 命令使用这种方法。指向特定目录的项的名称只是目录路径本身的内容散列。例如,项目可以创建包注册表项,例如在 Window 上
> 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
或者在 Unix 上
$ cat ~/.cmake/packages/MyPackage/7d1fb77e07ce59a81bed093bbee945bd
/home/me/work/lib/cmake/MyPackage
$ cat ~/.cmake/packages/MyPackage/f92c1db873a1937f3100706657c63e07
/home/me/work/MyPackage-build
find_package(MyPackage) 命令将搜索包配置文件的注册位置。单个包的包注册表项之间的搜索顺序未指定。注册位置可能包含包版本文件,以告诉 find_package 特定位置(的文件)是否适合请求的版本。