cmake-developer(7)

简介

本手册旨在供使用 cmake-language(7) 代码的开发人员参考,无论是编写自己的模块、编写自己的构建系统,还是开发 CMake 本身。

请参阅 https://cmake.com.cn/get-involved/ 了解如何参与 CMake 上游开发。其中包含贡献说明的链接,贡献说明又链接到 CMake 本身的开发指南。

访问 Windows 注册表

CMake 提供了一些在 Windows 平台上访问注册表的工具。

查询 Windows 注册表

在 3.24 版本中添加。

cmake_host_system_information() 命令提供了查询本地计算机注册表的可能性。有关更多信息,请参阅 cmake_host_system(QUERY_WINDOWS_REGISTRY)

使用 Windows 注册表进行查找

3.24 版本中的变化。

find_file()find_library()find_path()find_program()find_package() 命令的 HINTSPATHS 选项在 Windows 平台上提供了查询注册表的可能性。

注册表查询的正式语法,使用 BNF 符号和常规扩展指定如下:

registry_query  ::=  '[' sep_definition? root_key
                         ((key_separator sub_key)? (value_separator value_name_)?)? ']'
sep_definition  ::=  '{' value_separator '}'
root_key        ::=  'HKLM' | 'HKEY_LOCAL_MACHINE' | 'HKCU' | 'HKEY_CURRENT_USER' |
                     'HKCR' | 'HKEY_CLASSES_ROOT' | 'HKCC' | 'HKEY_CURRENT_CONFIG' |
                     'HKU' | 'HKEY_USERS'
sub_key         ::=  element (key_separator element)*
key_separator   ::=  '/' | '\\'
value_separator ::=  element | ';'
value_name      ::=  element | '(default)'
element         ::=  character\+
character       ::=  <any character except key_separator and value_separator>

sep_definition 可选项提供了指定用于分隔 sub_keyvalue_name 项的字符串的可能性。如果未指定,则使用字符 ;。可以指定多个 registry_query 项作为路径的一部分。

# example using default separator
find_file(... PATHS "/root/[HKLM/Stuff;InstallDir]/lib[HKLM\\\\Stuff;Architecture]")

# example using different specified separators
find_library(... HINTS "/root/[{|}HKCU/Stuff|InstallDir]/lib[{@@}HKCU\\\\Stuff@@Architecture]")

如果未指定 value_name 项或其特殊名称为 (default),则返回默认值的内容(如果有)。value_name 支持的类型有:

  • REG_SZ.

  • REG_EXPAND_SZ。返回的数据已扩展。

  • REG_DWORD.

  • REG_QWORD.

当注册表查询失败时(通常是因为键不存在或数据类型不支持),字符串 /REGISTRY-NOTFOUND 将替换 [] 查询表达式。

查找模块

“查找模块”是一个 Find<PackageName>.cmake 文件,当为 <PackageName> 调用 find_package() 命令时加载。

查找模块的主要任务是确定包是否可用,设置 <PackageName>_FOUND 变量以反映这一点,并提供使用包所需的任何变量、宏和导入目标。在上游库不提供 配置文件包 的情况下,查找模块很有用。

传统方法是使用变量来表示所有内容,包括库和可执行文件:请参阅下面的 标准变量名 部分。这是 CMake 提供的大多数现有查找模块所做的。

更现代的方法是尽可能地像 配置文件包 文件一样,通过提供 导入目标。这具有将 使用要求 传播给消费者的优点。

无论哪种情况(甚至同时提供变量和导入目标),查找模块都应提供与具有相同名称的旧版本向后兼容性。

FindFoo.cmake 模块通常会由命令加载:

find_package(Foo [major[.minor[.patch[.tweak]]]]
             [EXACT] [QUIET] [REQUIRED]
             [[COMPONENTS] [components...]]
             [OPTIONAL_COMPONENTS components...]
             [NO_POLICY_SCOPE])

有关为查找模块设置哪些变量的详细信息,请参阅 find_package() 文档。其中大部分通过使用 FindPackageHandleStandardArgs 来处理。

简而言之,模块应仅定位与所请求版本兼容的包版本,如 Foo_FIND_VERSION 变量族所描述。如果 Foo_FIND_QUIETLY 设置为 true,则应避免打印消息,包括任何抱怨包未找到的消息。如果 Foo_FIND_REQUIRED 设置为 true,则如果找不到包,模块应发出 FATAL_ERROR。如果两者均未设置为 true,则如果找不到包,它应打印非致命消息。

查找多个半独立部分(如库捆绑包)的包应搜索 Foo_FIND_COMPONENTS 中列出的组件(如果已设置),并且仅当对于每个未找到的搜索组件 <c>Foo_FIND_REQUIRED_<c> 未设置为 true 时,才将 Foo_FOUND 设置为 true。find_package_handle_standard_args()HANDLE_COMPONENTS 参数可用于实现此目的。

如果 Foo_FIND_COMPONENTS 未设置,则搜索和必需的模块由查找模块决定,但应进行文档说明。

对于内部实现,通常接受的约定是以下划线开头的变量仅用于临时用途。

标准变量名

对于采取设置变量方法(无论是替代还是补充创建导入目标)的 FindXxx.cmake 模块,应使用以下变量名以保持查找模块之间的一致性。请注意,所有变量都以 Xxx_ 开头,这(除非另有说明)必须与 FindXxx.cmake 文件的名称完全匹配,包括大小写。变量名上的此前缀确保它们不会与其他查找模块的变量冲突。任何由查找模块定义的宏、函数和导入目标也应遵循相同的模式。

Xxx_INCLUDE_DIRS

最终的包含目录集,列在一个变量中供客户端代码使用。这不应是缓存条目(请注意,这也意味着此变量不应作为 find_path() 命令的结果变量 - 有关此内容的详细信息,请参阅下面的 Xxx_INCLUDE_DIR)。

Xxx_LIBRARIES

要与模块一起使用的库。这些可以是 CMake 目标、库二进制文件的完整绝对路径或链接器必须在其搜索路径中找到的库名称。这不应是缓存条目(请注意,这也意味着此变量不应作为 find_library() 命令的结果变量 - 有关此内容的详细信息,请参阅下面的 Xxx_LIBRARY)。

Xxx_DEFINITIONS

编译使用模块的代码时要使用的编译定义。这不应包含诸如 -DHAS_JPEG 等选项,客户端源代码文件使用这些选项来决定是否 #include <jpeg.h>

Xxx_EXECUTABLE

可执行文件的完整绝对路径。在这种情况下,Xxx 可能不是模块的名称,它可能是工具的名称(通常转换为全大写),假设该工具具有如此众所周知的名称,以至于不可能存在另一个同名工具。将此作为 find_program() 命令的结果变量是合适的。

Xxx_YYY_EXECUTABLE

类似于 Xxx_EXECUTABLE,但此处 Xxx 始终是模块名称,而 YYY 是工具名称(同样,通常是全大写)。如果工具名称不是广为人知或有可能与其他工具冲突,则首选此形式。为了提高一致性,如果模块提供多个可执行文件,也首选此形式。

Xxx_LIBRARY_DIRS

可选地,最终的库目录集,列在一个变量中供客户端代码使用。这不应是缓存条目。

Xxx_ROOT_DIR

如果找到 Xxx,查找模块可以可选地设置 Xxx 安装的基本目录。这对于大型包很有用,因为许多文件需要相对于公共基(或根)目录引用。不要与外部为查找模块设置的 Xxx_ROOT 提示变量混淆,查找模块需要知道在哪里查找 Xxx

Xxx_VERSION_VV

此形式的变量指定所提供的 Xxx 模块是否是模块的 VV 版本。对于给定模块,不应将多个此形式的变量设置为 true。例如,模块 Barry 可能已经发展了多年,并经历了许多不同的主版本。模块 Barry 的版本 3 可能会将变量 Barry_VERSION_3 设置为 true,而模块的旧版本可能会将 Barry_VERSION_2 设置为 true。将 Barry_VERSION_3Barry_VERSION_2 都设置为 true 是错误的。

Xxx_WRAP_YY

当此形式的变量设置为 false 时,表示不应使用相关的包装命令。包装命令取决于模块,它可能由模块名称暗示,也可能由变量的 YY 部分指定。

Xxx_Yy_FOUND

对于此形式的变量,Yy 是模块的组件名称。它应与传递给模块的 find_package() 命令的有效组件名称之一完全匹配。如果此形式的变量设置为 false,则表示模块 XxxYy 组件未找到或不可用。此形式的变量通常用于可选组件,以便调用方可以检查可选组件是否可用。

Xxx_FOUND

find_package() 命令返回给调用方时,如果模块被认为成功找到,则此变量将设置为 true。

Xxx_NOT_FOUND_MESSAGE

如果配置文件已将 Xxx_FOUND 设置为 FALSE,则应由配置文件设置此变量。其中包含的消息将由 find_package() 命令和 find_package_handle_standard_args() 打印,以告知用户问题。请使用此方法,而不是直接调用 message() 来报告未找到模块或包的原因。

Xxx_RUNTIME_LIBRARY_DIRS

可选地,运行时库搜索路径,用于运行链接到共享库的可执行文件时。该列表应由用户代码用于在 Windows 上创建 PATH 或在 UNIX 上创建 LD_LIBRARY_PATH。这不应是缓存条目。

Xxx_VERSION

找到的包的完整版本字符串(如果有)。请注意,一些现有模块可能还会提供 Xxx_VERSION_STRING,因为在当前命名约定之前传统上使用它。

Xxx_VERSION_MAJOR

找到的包的主版本(如果有)。

Xxx_VERSION_MINOR

找到的包的次版本(如果有)。

Xxx_VERSION_PATCH

找到的包的补丁版本(如果有)。

以下名称通常不应在 CMakeLists.txt 文件中使用。它们旨在供查找模块使用,以指定和缓存特定文件或目录的位置。用户通常可以设置和编辑这些变量来控制查找模块的行为(例如手动输入库路径)

Xxx_LIBRARY

库的路径。仅当模块提供单个库时才使用此形式。将其用作 find_library() 命令的结果变量是合适的。

Xxx_Yy_LIBRARY

模块 Xxx 提供的库 Yy 的路径。当模块提供多个库或其他模块也可能提供同名库时,请使用此形式。将其用作 find_library() 命令的结果变量也是合适的。

Xxx_INCLUDE_DIR

当模块仅提供单个库时,此变量可用于指定在哪里查找使用该库的头文件(或更准确地说,库的消费者应添加到其头文件搜索路径的路径)。将其用作 find_path() 命令的结果变量是合适的。

Xxx_Yy_INCLUDE_DIR

如果模块提供多个库或如果其他模块也可能提供同名库,则建议使用此形式来指定在哪里查找模块提供的库 Yy 的头文件。同样,将其用作 find_path() 命令的结果变量是合适的。

为了防止用户被过多的配置设置所淹没,请尝试将尽可能多的选项保留在缓存之外,至少留下一个选项可用于禁用模块的使用或定位未找到的库(例如 Xxx_ROOT_DIR)。出于同样的原因,将大多数缓存选项标记为高级。对于提供调试和发布二进制文件的包,通常会创建带有 _LIBRARY_<CONFIG> 后缀的缓存变量,例如 Foo_LIBRARY_RELEASEFoo_LIBRARY_DEBUGSelectLibraryConfigurations 模块在此类情况下可能会有所帮助。

虽然这些是标准变量名称,但对于替换旧版本的查找模块中以前使用的任何名称,都应提供向后兼容性。旧变量名称应标记为已弃用,以阻止进一步使用。

一个示例查找模块

我们将描述如何为库 Foo 创建一个简单的查找模块。

模块的顶部应以许可证声明开头,后跟一个空行,然后是一个 方括号注释。注释应以 .rst: 开头,表示其其余内容是 reStructuredText 格式的文档。例如:

# Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
# file LICENSE.rst or https://cmake.com.cn/licensing for details.

#[=======================================================================[.rst:
FindFoo
-------

Finds the Foo library.

Imported Targets
^^^^^^^^^^^^^^^^

This module provides the following imported targets, if found:

``Foo::Foo``
  The Foo library

Result Variables
^^^^^^^^^^^^^^^^

This will define the following variables:

``Foo_FOUND``
  True if the system has the Foo library.
``Foo_VERSION``
  The version of the Foo library which was found.
``Foo_INCLUDE_DIRS``
  Include directories needed to use Foo.
``Foo_LIBRARIES``
  Libraries needed to link to Foo.

Cache Variables
^^^^^^^^^^^^^^^

The following cache variables may also be set:

``Foo_INCLUDE_DIR``
  The directory containing ``foo.h``.
``Foo_LIBRARY``
  The path to the Foo library.

#]=======================================================================]

模块文档包括:

  • 一个带下划线的标题,指定模块名称。

  • 模块查找内容的简单描述。对于某些包,可能需要更多描述。如果模块用户应注意的注意事项或其他详细信息,请在此处指定。

  • 一个列出模块提供的导入目标的章节(如果有)。

  • 一个列出模块提供的结果变量的章节。

  • 可选地,一个列出模块使用的缓存变量的章节(如果有)。

如果包提供任何宏或函数,它们应列在附加章节中,但可以通过紧邻这些宏或函数定义上方的附加 .rst: 注释块进行文档说明。

查找模块的实现可以在文档块下方开始。现在必须找到实际的库等。这里的代码显然会因模块而异(毕竟,处理这些是查找模块的重点),但库通常存在一个通用模式。

首先,我们尝试使用 pkg-config 来查找库。请注意,我们不能依赖此方法,因为它可能不可用,但它提供了一个很好的起点。

find_package(PkgConfig)
if(PKG_CONFIG_FOUND)
  pkg_check_modules(PC_Foo QUIET Foo)
endif()

这应该定义一些以 PC_Foo_ 开头并包含来自 Foo.pc 文件信息的变量。

现在我们需要找到库和包含文件;我们使用来自 pkg-config 的信息向 CMake 提供查找位置的提示,然后再检查其他默认路径。

find_path(Foo_INCLUDE_DIR
  NAMES foo.h
  HINTS ${PC_Foo_INCLUDE_DIRS}
  PATH_SUFFIXES Foo
)
find_library(Foo_LIBRARY
  NAMES foo
  HINTS ${PC_Foo_LIBRARY_DIRS}
)

或者,如果库有多种配置,可以使用 SelectLibraryConfigurations 模块自动设置 Foo_LIBRARY 变量,而不是:

find_library(Foo_LIBRARY_RELEASE
  NAMES foo
  HINTS ${PC_Foo_LIBRARY_DIRS}/Release
)
find_library(Foo_LIBRARY_DEBUG
  NAMES foo
  HINTS ${PC_Foo_LIBRARY_DIRS}/Debug
)

include(SelectLibraryConfigurations)
select_library_configurations(Foo)

如果有一种好的方式获取版本(例如,从头文件),该信息可以用来设置 Foo_VERSION。否则,尝试使用来自 pkg-config 的信息。

set(Foo_VERSION ${PC_Foo_VERSION})

现在我们可以使用 FindPackageHandleStandardArgs 模块为我们处理其余大部分步骤:

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Foo
  REQUIRED_VARS
    Foo_LIBRARY
    Foo_INCLUDE_DIR
  VERSION_VAR Foo_VERSION
)

这将检查 REQUIRED_VARS 是否包含值(不以 -NOTFOUND 结尾),并适当设置 Foo_FOUND。它还会缓存这些值。如果 Foo_VERSION 已设置,并且将所需版本传递给 find_package(),它将检查所请求的版本与 Foo_VERSION 中的版本。它还将适当地打印消息;请注意,如果找到了包,它将打印第一个所需变量的内容,以指示在哪里找到它。

此时,我们必须为查找模块的用户提供一种链接到已找到的库的方法。正如上面 查找模块 部分所讨论的,有两种方法。传统的变量方法如下:

if(Foo_FOUND)
  set(Foo_LIBRARIES ${Foo_LIBRARY})
  set(Foo_INCLUDE_DIRS ${Foo_INCLUDE_DIR})
  set(Foo_DEFINITIONS ${PC_Foo_CFLAGS_OTHER})
endif()

如果找到多个库,所有库都应包含在这些变量中(有关更多信息,请参阅 标准变量名 部分)。

在提供导入目标时,这些目标应具有命名空间(因此带有 Foo:: 前缀);CMake 将识别传递给 target_link_libraries() 的值中包含 :: 的值应为导入目标(而不是仅仅是库名称),并且如果该目标不存在,将生成适当的诊断消息(请参阅策略 CMP0028)。

if(Foo_FOUND AND NOT TARGET Foo::Foo)
  add_library(Foo::Foo UNKNOWN IMPORTED)
  set_target_properties(Foo::Foo PROPERTIES
    IMPORTED_LOCATION "${Foo_LIBRARY}"
    INTERFACE_COMPILE_OPTIONS "${PC_Foo_CFLAGS_OTHER}"
    INTERFACE_INCLUDE_DIRECTORIES "${Foo_INCLUDE_DIR}"
  )
endif()

关于这一点需要注意的是,INTERFACE_INCLUDE_DIRECTORIES 和类似属性应该只包含关于目标本身的信息,而不包含任何其依赖项的信息。相反,这些依赖项也应该作为目标,并且应该告诉 CMake 它们是该目标的依赖项。然后 CMake 将自动组合所有必要的信息。

add_library() 命令中创建的 IMPORTED 目标的类型可以始终指定为 UNKNOWN 类型。这简化了在可能找到静态或共享变体的情况下的代码,并且 CMake 将通过检查文件来确定类型。

如果库有多种配置可用,则还应填充 IMPORTED_CONFIGURATIONS 目标属性

if(Foo_FOUND)
  if (NOT TARGET Foo::Foo)
    add_library(Foo::Foo UNKNOWN IMPORTED)
  endif()
  if (Foo_LIBRARY_RELEASE)
    set_property(TARGET Foo::Foo APPEND PROPERTY
      IMPORTED_CONFIGURATIONS RELEASE
    )
    set_target_properties(Foo::Foo PROPERTIES
      IMPORTED_LOCATION_RELEASE "${Foo_LIBRARY_RELEASE}"
    )
  endif()
  if (Foo_LIBRARY_DEBUG)
    set_property(TARGET Foo::Foo APPEND PROPERTY
      IMPORTED_CONFIGURATIONS DEBUG
    )
    set_target_properties(Foo::Foo PROPERTIES
      IMPORTED_LOCATION_DEBUG "${Foo_LIBRARY_DEBUG}"
    )
  endif()
  set_target_properties(Foo::Foo PROPERTIES
    INTERFACE_COMPILE_OPTIONS "${PC_Foo_CFLAGS_OTHER}"
    INTERFACE_INCLUDE_DIRECTORIES "${Foo_INCLUDE_DIR}"
  )
endif()

RELEASE 变体应首先列在属性中,以便如果用户使用的配置与任何列出的 IMPORTED_CONFIGURATIONS 不完全匹配,则选择该变体。

大多数缓存变量应标记为高级,以在 GUI 界面(例如 cmake-gui(1)ccmake(1))中保持隐藏,除非用户明确选择显示和修改它们

mark_as_advanced(
  Foo_INCLUDE_DIR
  Foo_LIBRARY
)

如果此查找模块取代了在当前标准变量命名约定之前提供变量的旧模块版本,则还应提供向后兼容性变量,以尽量减少中断。例如:

# Backward compatibility variables
set(Foo_VERSION_STRING ${Foo_VERSION})