cmake-packages(7)

引言

包向基于 CMake 的构建系统提供依赖项信息。包通过 find_package() 命令找到。使用 find_package() 的结果要么是一组 IMPORTED 目标,要么是一组与构建相关的信息变量。

使用包

CMake 为两种形式的包提供直接支持:配置文件包查找模块包。通过 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_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。

配置文件包

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

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

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

查找模块包

查找模块是一个文件,其中包含查找依赖项所需部分(主要是头文件和库)的规则。通常,当上游不是用 CMake 构建时,或者不够了解 CMake 以至于无法提供包配置文件时,就需要查找模块。与包配置文件不同,它不随上游分发,而是由下游使用,通过猜测特定于平台的文件的位置来查找文件。

与上游提供的包配置文件不同,没有一个单一的参考点标识包已被找到,因此 find_package() 命令不会自动设置 <PackageName>_FOUND 变量。然而,按照惯例仍然可以预期它会被设置,并且应该由查找模块的作者设置。同样,没有 <PackageName>_DIR 变量,但每个伪像(如库位置和头文件位置)都提供一个单独的缓存变量。

有关创建查找模块文件的更多信息,请参阅 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* 是一个不区分大小写的通配符表达式。在我们的示例中,通配符表达式将匹配 <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

The <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) 用于导出在之前由 install(TARGETS) 命令定义的 ClimbingStatsTargets 导出集中的目标。此命令生成 ClimbingStatsTargets.cmake 文件,其中包含适合下游使用的 IMPORTED 目标,并安排将其安装到 lib/cmake/ClimbingStats。生成的 ClimbingStatsConfigVersion.cmakecmake/ClimbingStatsConfig.cmake 被安装到同一位置,从而完成包。

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

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

在本例中,在使用 install(TARGETS) 时指定了 INCLUDES DESTINATION。这会导致 IMPORTED 目标使用 CMAKE_INSTALL_PREFIX 中的 include 目录填充其 INTERFACE_INCLUDE_DIRECTORIES。当下游使用 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 包。来自 CMakeFindDependencyMacrofind_dependency 宏通过传播包是 REQUIRED 还是 QUIET 等来提供帮助。包的所有 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,并附带一个诊断信息,表明 ClimbingStats 包在没有 Stats 包的情况下无法使用。

当下游使用 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 定义目标接口时,请记住,应将包含目录指定为相对于 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> 生成器表达式 作为安装前缀的占位符,而不会导致不可重定位的包。如果使用复杂的生成器表达式,这是必需的。

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}>"
  )

引用的变量可能包含依赖项的库和包含目录的绝对路径,在包构建的机器上。这将创建一个带有硬编码依赖项路径的包,不适合重定位。

理想情况下,此类依赖项应通过其自己的 导入目标 来使用,这些目标具有各自的 IMPORTED_LOCATION 和使用要求属性,例如 INTERFACE_INCLUDE_DIRECTORIES 已正确填充。然后,这些导入的目标可以与 target_link_libraries() 命令一起用于 ClimbingStats

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

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

不幸的是,许多 CMake 附带的 模块 尚未提供 导入目标,因为它们的开发早于此方法。随着时间的推移,这可能会逐渐改善。使用此类模块创建可重定位包的解决方法包括:

  • 构建包时,将每个 Foo_LIBRARY 缓存条目指定为仅库名,例如 -DFoo_LIBRARY=foo。这会告知相应的查找模块将 Foo_LIBRARIES 仅填充为 foo,从而要求链接器搜索该库而不是硬编码路径。

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

包注册表

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

注册表特别有助于项目查找非标准安装位置的包或直接查找其构建树中的包。项目可以通过(使用其自己的方法,见下文)填充用户或系统注册表来引用其位置。无论哪种情况,包都应在注册位置存储一个 包配置文件<PackageName>Config.cmake)和可选的 包版本文件<PackageName>ConfigVersion.cmake)。

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

用户包注册表

用户包注册表存储在每个用户的位置。命令 export(PACKAGE) 可用于在用户包注册表中注册项目构建树。CMake 目前不提供将安装树添加到用户包注册表的接口。如果需要,必须手动告知安装程序注册其包。

在 Windows 上,用户包注册表存储在 Windows 注册表中,位于 HKEY_CURRENT_USER 下的某个键下。

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

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

作为 REG_SZ 值,具有任意名称,该值指定包含包配置文件的目录。

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

~/.cmake/packages/<PackageName>

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

系统包注册表

系统包注册表存储在系统范围的位置。CMake 目前不提供添加到系统包注册表的接口。如果需要,必须手动告知安装程序注册其包。

在 Windows 上,系统包注册表存储在 Windows 注册表中,位于 HKEY_LOCAL_MACHINE 下的某个键下。在注册表键下可能会出现 <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() 命令会在具有足够权限的情况下自动删除它遇到的过时条目。CMake 不提供在调用 export(PACKAGE) 后删除引用现有构建树的条目的接口。但是,如果项目从构建树中删除了其包配置文件,那么引用该位置的条目将被视为过时。