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

在已知包配置文件由上游提供,并且只应使用该文件的情况下,可以在 find_package() 中传递 CONFIG 关键字。

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
)

组件 (COMPONENTS) 和可选组件 (OPTIONAL_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

<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 文件。此文件设置包的版本。CMake 在调用 find_package() 来确定与请求版本的兼容性,以及设置一些版本特定的变量 <PackageName>_VERSION<PackageName>_VERSION_MAJOR<PackageName>_VERSION_MINOR 等时会读取它。 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。此双冒号约定会提示 CMake,当下游使用 target_link_libraries() 命令时,该名称是一个导入(IMPORTED)目标。这样,如果提供该包的包尚未找到,CMake 就可以发出诊断。

在这种情况下,当使用 install(TARGETS) 时,指定了 INCLUDES DESTINATION。这会导致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 包以确保这一点。来自 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)目标使用,这些目标具有自己的 IMPORTED_LOCATION 和使用要求属性,例如INTERFACE_INCLUDE_DIRECTORIES 已正确填充。然后,这些导入目标可以使用 target_link_libraries() 命令与 ClimbingStats 结合使用:

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

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

不幸的是,许多 CMake 附带的模块还没有提供导入(imported)目标,因为它们的开发早于这种方法。随着时间的推移,这可能会逐步改进。使用这些模块创建可重定位包的解决方法包括:

  • 构建包时,将每个 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) 后删除引用现有构建树的条目的接口。但是,如果项目将其包配置文件从构建树中删除,那么引用该位置的条目将被视为过时。