cmake-packages(7)

简介

包为基于 CMake 的构建系统提供依赖信息。包通过 find_package() 命令查找。使用 find_package() 的结果可以是一组 IMPORTED 目标,或者是一组与构建相关信息对应的变量。

使用包

CMake 直接支持两种形式的包:配置文件包Find-module 包。通过 FindPkgConfig 模块,也间接支持 pkg-config 包。在所有情况下,find_package() 调用的基本形式是相同的

find_package(Qt4 4.7.0 REQUIRED) # CMake provides a Qt4 find-module
find_package(Qt5Core 5.1.0 REQUIRED) # Qt provides a Qt5 package config file.
find_package(LibXml2 REQUIRED) # Use pkg-config via the LibXml2 find-module

在已知上游提供了包配置文件,并且只想使用它的情况下,可以将 CONFIG 关键字传递给 find_package()

find_package(Qt5Core 5.1.0 CONFIG REQUIRED)
find_package(Qt5Gui 5.1.0 CONFIG)

类似地,MODULE 关键字表示仅使用 find-module

find_package(Qt4 4.7.0 MODULE REQUIRED)

显式指定包的类型可以改进在未找到包时向用户显示的出错消息。

两种类型的包都支持指定包的组件,可以在 REQUIRED 关键字之后指定

find_package(Qt5 5.1.0 CONFIG REQUIRED Widgets Xml Sql)

或者作为单独的 COMPONENTS 列表

find_package(Qt5 5.1.0 COMPONENTS Widgets Xml Sql)

或者作为单独的 OPTIONAL_COMPONENTS 列表

find_package(Qt5 5.1.0 COMPONENTS Widgets
                       OPTIONAL_COMPONENTS Xml Sql
)

COMPONENTSOPTIONAL_COMPONENTS 的处理由包定义。

通过将 CMAKE_DISABLE_FIND_PACKAGE_<PackageName> 变量设置为 TRUE,将不会搜索 <PackageName> 包,并且始终为 NOTFOUND。同样,将 CMAKE_REQUIRE_FIND_PACKAGE_<PackageName> 设置为 TRUE 将使包成为 REQUIRED。

配置文件包

配置文件包是由上游提供给下游使用的一组文件。正如 find_package() 文档中所述,CMake 在多个位置搜索包配置文件。CMake 用户告诉 cmake(1) 在非标准前缀中搜索包的最简单方法是设置 CMAKE_PREFIX_PATH 缓存变量。

配置文件包由上游供应商作为开发包的一部分提供,也就是说,它们与头文件以及为帮助下游使用包而提供的任何其他文件一起存在。

当使用配置文件包时,还会自动设置一组提供包状态信息的变量。<PackageName>_FOUND 变量根据是否找到包设置为 true 或 false。<PackageName>_DIR 缓存变量设置为包配置文件的位置。

Find-module 包

Find-module 是一个文件,其中包含一组用于查找依赖项所需部分的规则,主要包括头文件和库。通常,当上游不是使用 CMake 构建的,或者 CMake 感知程度不足以提供包配置文件时,才需要 find-module。与包配置文件不同,find-module 不随上游一起发布,而是由下游使用,通过猜测文件位置和平台特定的提示来查找文件。

与上游提供的包配置文件的情况不同,没有单一的参考点来标识包是否已找到,因此 <PackageName>_FOUND 变量不会由 find_package() 命令自动设置。但是,仍然可以期望按照约定设置它,并且应该由 Find-module 的作者设置。类似地,没有 <PackageName>_DIR 变量,但是每个工件(例如库位置和头文件位置)都提供单独的缓存变量。

有关创建 Find-module 文件的更多信息,请参阅 cmake-developer(7) 手册。

包布局

配置文件包由一个 包配置文件 和可选的 包版本文件 组成,它们与项目分发一起提供。

包配置文件

考虑一个项目 Foo,它安装以下文件:

<prefix>/include/foo-1.2/foo.h
<prefix>/lib/foo-1.2/libfoo.a

它还可以提供一个 CMake 包配置文件

<prefix>/lib/cmake/foo-1.2/FooConfig.cmake

其内容定义了 IMPORTED 目标,或者定义了变量,例如:

# ...
# (compute PREFIX relative to file location)
# ...
set(Foo_INCLUDE_DIRS ${PREFIX}/include/foo-1.2)
set(Foo_LIBRARIES ${PREFIX}/lib/foo-1.2/libfoo.a)

如果另一个项目希望使用 Foo,它只需要找到 FooConfig.cmake 文件并加载它,即可获得关于包内容位置的所有必要信息。由于包配置文件由包安装提供,因此它已经知道所有文件位置。

find_package() 命令可用于搜索包配置文件。此命令构建一组安装前缀,并在每个前缀下的多个位置进行搜索。给定名称 Foo,它会查找名为 FooConfig.cmakefoo-config.cmake 的文件。完整的位置集合在 find_package() 命令文档中指定。其中一个搜索位置是:

<prefix>/lib/cmake/Foo*/

其中 Foo* 是不区分大小写的文件名 globbing 表达式。在我们的示例中,globbing 表达式将匹配 <prefix>/lib/cmake/foo-1.2,并且将找到包配置文件。

一旦找到,包配置文件会立即加载。它与包版本文件一起,包含项目使用包所需的所有信息。

包版本文件

find_package() 命令找到候选包配置文件时,它会在其旁边查找版本文件。加载版本文件是为了测试包版本是否与请求的版本可接受地匹配。如果版本文件声明兼容,则接受配置文件。否则,它将被忽略。

包版本文件的名称必须与包配置文件的名称匹配,但在 .cmake 扩展名之前,名称后附加 -versionVersion。例如,以下文件:

<prefix>/lib/cmake/foo-1.3/foo-config.cmake
<prefix>/lib/cmake/foo-1.3/foo-config-version.cmake

<prefix>/lib/cmake/bar-4.2/BarConfig.cmake
<prefix>/lib/cmake/bar-4.2/BarConfigVersion.cmake

分别是包配置文件和相应的包版本文件对。

find_package() 命令加载版本文件时,它首先设置以下变量:

PACKAGE_FIND_NAME

<PackageName>

PACKAGE_FIND_VERSION

完整的请求版本字符串

PACKAGE_FIND_VERSION_MAJOR

如果请求,则为主要版本;否则为 0

PACKAGE_FIND_VERSION_MINOR

如果请求,则为次要版本;否则为 0

PACKAGE_FIND_VERSION_PATCH

如果请求,则为补丁版本;否则为 0

PACKAGE_FIND_VERSION_TWEAK

如果请求,则为微调版本;否则为 0

PACKAGE_FIND_VERSION_COUNT

版本组件的数量,0 到 4

版本文件必须使用这些变量来检查它是否与请求的版本兼容或完全匹配,并使用结果设置以下变量:

PACKAGE_VERSION

完整的提供版本字符串

PACKAGE_VERSION_EXACT

如果版本完全匹配,则为 True

PACKAGE_VERSION_COMPATIBLE

如果版本兼容,则为 True

PACKAGE_VERSION_UNSUITABLE

如果不适合任何版本,则为 True

版本文件在嵌套作用域中加载,因此它们可以自由设置它们希望作为其计算一部分的任何变量。当版本文件完成并且它已检查输出变量时,find_package 命令会清除作用域。当版本文件声明与请求的版本可接受地匹配时,find_package 命令会设置以下变量供项目使用

<PackageName>_VERSION

完整的提供版本字符串

<PackageName>_VERSION_MAJOR

如果提供,则为主要版本;否则为 0

<PackageName>_VERSION_MINOR

如果提供,则为次要版本;否则为 0

<PackageName>_VERSION_PATCH

如果提供,则为补丁版本;否则为 0

<PackageName>_VERSION_TWEAK

如果提供,则为微调版本;否则为 0

<PackageName>_VERSION_COUNT

版本组件的数量,0 到 4

这些变量报告实际找到的包的版本。它们名称的 <PackageName> 部分与传递给 find_package() 命令的参数匹配。

创建包

通常,上游依赖于 CMake 本身,并且可以使用一些 CMake 功能来创建包文件。考虑一个上游,它提供一个单独的共享库:

project(UpstreamLib)

set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_INCLUDE_CURRENT_DIR_IN_INTERFACE ON)

set(Upstream_VERSION 3.4.1)

include(GenerateExportHeader)

add_library(ClimbingStats SHARED climbingstats.cpp)
generate_export_header(ClimbingStats)
set_property(TARGET ClimbingStats PROPERTY VERSION ${Upstream_VERSION})
set_property(TARGET ClimbingStats PROPERTY SOVERSION 3)
set_property(TARGET ClimbingStats PROPERTY
  INTERFACE_ClimbingStats_MAJOR_VERSION 3)
set_property(TARGET ClimbingStats APPEND PROPERTY
  COMPATIBLE_INTERFACE_STRING ClimbingStats_MAJOR_VERSION
)

install(TARGETS ClimbingStats EXPORT ClimbingStatsTargets
  LIBRARY DESTINATION lib
  ARCHIVE DESTINATION lib
  RUNTIME DESTINATION bin
  INCLUDES DESTINATION include
)
install(
  FILES
    climbingstats.h
    "${CMAKE_CURRENT_BINARY_DIR}/climbingstats_export.h"
  DESTINATION
    include
  COMPONENT
    Devel
)

include(CMakePackageConfigHelpers)
write_basic_package_version_file(
  "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsConfigVersion.cmake"
  VERSION ${Upstream_VERSION}
  COMPATIBILITY AnyNewerVersion
)

export(EXPORT ClimbingStatsTargets
  FILE "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsTargets.cmake"
  NAMESPACE Upstream::
)
configure_file(cmake/ClimbingStatsConfig.cmake
  "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsConfig.cmake"
  COPYONLY
)

set(ConfigPackageLocation lib/cmake/ClimbingStats)
install(EXPORT ClimbingStatsTargets
  FILE
    ClimbingStatsTargets.cmake
  NAMESPACE
    Upstream::
  DESTINATION
    ${ConfigPackageLocation}
)
install(
  FILES
    cmake/ClimbingStatsConfig.cmake
    "${CMAKE_CURRENT_BINARY_DIR}/ClimbingStats/ClimbingStatsConfigVersion.cmake"
  DESTINATION
    ${ConfigPackageLocation}
  COMPONENT
    Devel
)

CMakePackageConfigHelpers 模块提供了一个用于创建简单 ConfigVersion.cmake 文件的宏。此文件设置包的版本。当调用 find_package() 以确定与请求版本的兼容性并设置一些特定于版本的变量 <PackageName>_VERSION<PackageName>_VERSION_MAJOR<PackageName>_VERSION_MINOR 等时,CMake 会读取此文件。install(EXPORT) 命令用于导出 ClimbingStatsTargets 导出集中(先前由 install(TARGETS) 命令定义)的目标。此命令生成 ClimbingStatsTargets.cmake 文件,以包含 IMPORTED 目标,这些目标适合下游使用,并安排将其安装到 lib/cmake/ClimbingStats。生成的 ClimbingStatsConfigVersion.cmakecmake/ClimbingStatsConfig.cmake 安装在同一位置,从而完成包的创建。

生成的 IMPORTED 目标设置了适当的属性,以定义它们的 usage requirements(例如 INTERFACE_INCLUDE_DIRECTORIESINTERFACE_COMPILE_DEFINITIONS 和其他相关的内置 INTERFACE_ 属性)。在 COMPATIBLE_INTERFACE_STRING 和其他 Compatible Interface Properties 中列出的用户定义属性的 INTERFACE 变体也会传播到生成的 IMPORTED 目标。在上述情况下,ClimbingStats_MAJOR_VERSION 被定义为一个字符串,它必须在任何依赖项的依赖者之间兼容。通过在此版本和 ClimbingStats 的下一个版本中设置此自定义的用户定义属性,如果尝试将版本 3 与版本 4 一起使用,cmake(1) 将发出诊断信息。如果包的不同主要版本设计为不兼容,则包可以选择采用这种模式。

在导出目标以进行安装时,指定了带有双冒号的 NAMESPACE。双冒号的约定给 CMake 一个提示,即当下游使用 target_link_libraries() 命令时,该名称是一个 IMPORTED 目标。这样,如果尚未找到提供它的包,CMake 可以发出诊断信息。

在这种情况下,当使用 install(TARGETS) 时,指定了 INCLUDES DESTINATION。这会导致 IMPORTED 目标的 INTERFACE_INCLUDE_DIRECTORIES 填充 CMAKE_INSTALL_PREFIX 中的 include 目录。当下游使用 IMPORTED 目标时,它会自动使用该属性中的条目。

创建包配置文件

在这种情况下,ClimbingStatsConfig.cmake 文件可以像下面这样简单:

include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsTargets.cmake")

因为这允许下游使用 IMPORTED 目标。如果 ClimbingStats 包应提供任何宏,则它们应位于单独的文件中,该文件与 ClimbingStatsConfig.cmake 文件安装在同一位置,并从那里包含。

这也可以扩展到涵盖依赖项

# ...
add_library(ClimbingStats SHARED climbingstats.cpp)
generate_export_header(ClimbingStats)

find_package(Stats 2.6.4 REQUIRED)
target_link_libraries(ClimbingStats PUBLIC Stats::Types)

由于 Stats::Types 目标是 ClimbingStatsPUBLIC 依赖项,因此下游还必须找到 Stats 包并链接到 Stats::Types 库。应在 ClimbingStatsConfig.cmake 文件中找到 Stats 包,以确保这一点。CMakeFindDependencyMacro 中的 find_dependency 宏通过传播包是否为 REQUIREDQUIET 等来帮助实现这一点。包的所有 REQUIRED 依赖项都应在 Config.cmake 文件中找到

include(CMakeFindDependencyMacro)
find_dependency(Stats 2.6.4)

include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsTargets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsMacros.cmake")

如果未找到依赖项,find_dependency 宏还会将 ClimbingStats_FOUND 设置为 False,并诊断出没有 Stats 包就无法使用 ClimbingStats 包。

如果下游使用 find_package() 时指定了 COMPONENTS,则它们会列在 <PackageName>_FIND_COMPONENTS 变量中。如果特定组件是非可选的,则 <PackageName>_FIND_REQUIRED_<comp> 将为 true。这可以使用包配置文件中的逻辑进行测试

include(CMakeFindDependencyMacro)
find_dependency(Stats 2.6.4)

include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsTargets.cmake")
include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStatsMacros.cmake")

set(_ClimbingStats_supported_components Plot Table)

foreach(_comp ${ClimbingStats_FIND_COMPONENTS})
  if (NOT ";${_ClimbingStats_supported_components};" MATCHES ";${_comp};")
    set(ClimbingStats_FOUND False)
    set(ClimbingStats_NOT_FOUND_MESSAGE "Unsupported component: ${_comp}")
  endif()
  include("${CMAKE_CURRENT_LIST_DIR}/ClimbingStats${_comp}Targets.cmake")
endforeach()

在这里,ClimbingStats_NOT_FOUND_MESSAGE 设置为诊断信息,指示由于指定了无效的组件而无法找到包。对于 _FOUND 变量设置为 False 的任何情况,都可以设置此消息变量,并将显示给用户。

为构建树创建包配置文件

export(EXPORT) 命令创建一个特定于构建树且不可重定位的 IMPORTED 目标定义文件。类似地,它可以与合适的包配置文件和包版本文件一起使用,以定义构建树的包,该包可以在不安装的情况下使用。构建树的使用者只需确保 CMAKE_PREFIX_PATH 包含构建目录,或者在缓存中将 ClimbingStats_DIR 设置为 <build_dir>/ClimbingStats

创建可重定位包

可重定位包不得引用包构建所在计算机上文件的绝对路径,这些路径在包可能安装的计算机上不存在。

install(EXPORT) 创建的包设计为可重定位的,使用相对于包自身位置的路径。在为 EXPORT 定义目标的接口时,请记住,include 目录应指定为相对于 CMAKE_INSTALL_PREFIX 的相对路径

target_include_directories(tgt INTERFACE
  # Wrong, not relocatable:
  $<INSTALL_INTERFACE:${CMAKE_INSTALL_PREFIX}/include/TgtName>
)

target_include_directories(tgt INTERFACE
  # Ok, relocatable:
  $<INSTALL_INTERFACE:include/TgtName>
)

$<INSTALL_PREFIX> generator expression 可以用作安装前缀的占位符,而不会导致不可重定位的包。如果使用复杂的 generator expression,则这是必要的

target_include_directories(tgt INTERFACE
  # Ok, relocatable:
  $<INSTALL_INTERFACE:$<$<CONFIG:Debug>:$<INSTALL_PREFIX>/include/TgtName>>
)

这也适用于引用外部依赖项的路径。不建议使用与依赖项相关的路径填充任何可能包含路径的属性,例如 INTERFACE_INCLUDE_DIRECTORIESINTERFACE_LINK_LIBRARIES

target_link_libraries(ClimbingStats INTERFACE
  ${Foo_LIBRARIES} ${Bar_LIBRARIES}
  )
target_include_directories(ClimbingStats INTERFACE
  "$<INSTALL_INTERFACE:${Foo_INCLUDE_DIRS};${Bar_INCLUDE_DIRS}>"
  )

引用的变量可能包含库和 include 目录的绝对路径(在制作包的计算机上找到的)。这将创建一个包含依赖项硬编码路径的包,并且不适合重定位。

理想情况下,此类依赖项应通过它们自己的 IMPORTED targets 使用,这些目标具有自己的 IMPORTED_LOCATION 和 usage requirement 属性(例如 INTERFACE_INCLUDE_DIRECTORIES)并已适当填充。然后,这些导入的目标可以与 target_link_libraries() 命令一起用于 ClimbingStats

target_link_libraries(ClimbingStats INTERFACE Foo::Foo Bar::Bar)

通过这种方法,包仅通过 IMPORTED targets 的名称引用其外部依赖项。当使用者使用已安装的包时,使用者将运行适当的 find_package() 命令(通过上面描述的 find_dependency 宏)来查找依赖项,并在他们自己的计算机上使用适当的路径填充导入的目标。

遗憾的是,CMake 附带的许多 modules 尚未提供 IMPORTED targets,因为它们的开发早于这种方法。这种情况可能会随着时间的推移逐渐改善。使用此类模块创建可重定位包的解决方法包括:

  • 构建包时,将每个 Foo_LIBRARY 缓存条目指定为仅库名称,例如 -DFoo_LIBRARY=foo。这会告诉相应的 find module 使用 foo 填充 Foo_LIBRARIES,以请求链接器搜索库,而不是硬编码路径。

  • 或者,在安装包内容之后,但在创建用于重新分发的包安装二进制文件之前,手动将绝对路径替换为占位符,以便在安装包时由安装工具进行替换。

包注册表

CMake 提供了两个中心位置来注册在系统上任何位置构建或安装的软件包

注册表对于帮助项目在非标准安装位置或直接在其自身的构建树中查找软件包尤其有用。项目可以填充用户或系统注册表(使用其自身的方法,见下文)以引用其位置。在任何情况下,软件包都应在注册位置存储一个软件包配置文件 (<PackageName>Config.cmake) 和可选地存储一个软件包版本文件 (<PackageName>ConfigVersion.cmake)。

find_package() 命令在其文档中指定的搜索步骤中,会搜索这两个软件包注册表。如果它具有足够的权限,它还会删除引用不存在或不包含匹配的软件包配置文件的目录的陈旧软件包注册表条目。

用户软件包注册表

用户软件包注册表存储在每个用户的位置。export(PACKAGE) 命令可以用于在用户软件包注册表中注册项目构建树。CMake 目前没有提供向用户软件包注册表添加安装树的接口。如果需要,必须手动教导安装程序注册其软件包。

在 Windows 上,用户软件包注册表存储在 HKEY_CURRENT_USER 中的 Windows 注册表中的一个键下。

一个 <PackageName> 可能出现在注册表键下

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

作为 REG_SZ 值,名称任意,用于指定包含软件包配置文件的目录。

在 UNIX 平台上,用户软件包注册表存储在用户主目录下的 ~/.cmake/packages 中。 <PackageName> 可能出现在目录

~/.cmake/packages/<PackageName>

中,作为一个文件,名称任意,其内容指定包含软件包配置文件的目录。

系统软件包注册表

系统软件包注册表存储在系统级位置。CMake 目前没有提供向系统软件包注册表添加内容的接口。如果需要,必须手动教导安装程序注册其软件包。

在 Windows 上,系统软件包注册表存储在 HKEY_LOCAL_MACHINE 中的 Windows 注册表中的一个键下。 一个 <PackageName> 可能出现在注册表键下

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

作为 REG_SZ 值,名称任意,用于指定包含软件包配置文件的目录。

在非 Windows 平台上,没有系统软件包注册表。

禁用软件包注册表

在某些情况下,使用软件包注册表可能不合需要。CMake 允许使用以下变量禁用它们

软件包注册表示例

命名软件包注册表条目的一个简单约定是使用内容哈希。它们是确定性的,并且不太可能冲突(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

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

那么 CMakeLists.txt 代码

find_package(MyPackage)

将在注册位置搜索软件包配置文件 (MyPackageConfig.cmake)。单个软件包的软件包注册表条目之间的搜索顺序是未指定的,并且条目名称(在本示例中为哈希)没有意义。注册位置可能包含软件包版本文件 (MyPackageConfigVersion.cmake),以告知 find_package() 特定位置是否适合请求的版本。

软件包注册表所有权

软件包注册表条目由它们引用的项目安装单独拥有。软件包安装程序负责添加其自己的条目,而相应的卸载程序负责删除它。

export(PACKAGE) 命令使用项目构建树的位置填充用户软件包注册表。构建树往往会被开发人员删除,并且没有可以触发删除其条目的“卸载”事件。为了保持注册表的清洁,find_package() 命令会在遇到陈旧条目时自动删除它们(如果它具有足够的权限)。一旦调用 export(PACKAGE),CMake 就不提供删除引用现有构建树的条目的接口。但是,如果项目从构建树中删除其软件包配置文件,则引用该位置的条目将被视为陈旧。