cmake 开发人员(7)

介绍

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

请参阅 https://cmake.org/get-involved/ 参与 CMake 上游的开发。它包括贡献说明的链接,这些说明又链接到 CMake 本身的开发人员指南。

访问 Windows 注册表

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

查询 Windows 注册表

在 3.24 版本加入.

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

使用 Windows 注册表查找

在 3.24 版本发生变更.

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

使用带有常规扩展名的`BNF <https://en.wikipedia.org/wiki/Backus%E2%80%93Naur_form>`_ 符号指定的正式语法,用于注册表查询如下:

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”变量来反映这一点,并提供使用包所需的任何变量、宏和导入目标。在上游库不提供 config 文件包 的情况下,查找模块很有用。

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

更现代的方法是通过提供 imported target 文件。这具有向消费者传播 传递使用要求 的优势。

在任何一种情况下(或者甚至在同时提供变量和导入目标时),查找模块都应该提供与具有相同名称的旧版本的向后兼容性。

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

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

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

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

找到多个半独立部分(如库包)的包应该搜索“Foo_FIND_COMPONENTS”中列出的组件(如果已设置),并且只有在每个搜索到的组件“”时才将“Foo_FOUND”设置为 true <c>`` 未找到,Foo_FIND_REQUIRED_<c> 未设置为 true。 find_package_handle_standard_args() 的``HANDLE_COMPONENTS`` 参数可以用来实现这个。

如果未设置“Foo_FIND_COMPONENTS”,则搜索和需要哪些模块取决于查找模块,但应记录在案。

对于内部实现,以下划线开头的变量仅供临时使用,这是一个普遍接受的约定。

标准变量名

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

Xxx_INCLUDE_DIRS

一个变量中列出的最后一组包含目录供客户端代码使用。这不应该是缓存条目(请注意,这也意味着该变量不应该用作 find_path() 命令的结果变量 - 请参阅下面的 Xxx_INCLUDE_DIR)。

Xxx_LIBRARIES

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

Xxx_DEFINITIONS

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

Xxx_EXECUTABLE

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

Xxx_YYY_EXECUTABLE

类似于 Xxx_EXECUTABLE 除了这里的 Xxx 总是模块名称和 YYY 是工具名称(同样,通常完全大写)。如果工具名称不是很广为人知或有可能与其他工具冲突,则首选此形式。为了更好的一致性,如果模块提供多个可执行文件,也更喜欢这种形式。

Xxx_LIBRARY_DIRS

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

Xxx_ROOT_DIR

在哪里可以找到模块的基本目录。

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

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

Xxx_Yy_FOUND

对于这种形式的变量,“Yy”是模块组件的名称。它应该与可能传递给模块的 find_package 命令的有效组件名称之一完全匹配。如果这种形式的变量设置为 false,则意味着模块“Xxx”的“Yy”组件未找到或不可用。这种形式的变量通常用于可选组件,以便调用者可以检查可选组件是否可用。

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 文件中使用。它们旨在供 Find 模块使用,以指定和缓存特定文件或目录的位置。用户通常能够设置和编辑这些变量来控制 Find 模块的行为(比如手动输入库的路径):

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 Copyright.txt or https://cmake.org/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)
pkg_check_modules(PC_Foo QUIET Foo)

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

现在我们需要找到库和包含文件;我们使用来自 pkg-config 的信息来向 CMake 提供有关查找位置的提示。

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

或者,如果库具有多种配置,您可以使用:module:SelectLibraryConfigurations 来自动设置 Foo_LIBRARY 变量:

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

include(SelectLibraryConfigurations)
select_library_configurations(Foo)

如果您有获取版本的好方法(例如,从头文件),您可以使用该信息来设置``Foo_VERSION``(尽管请注意,查找模块传统上使用``Foo_VERSION_STRING``,所以您可能想同时设置)。否则,尝试使用来自 pkg-config 的信息

set(Foo_VERSION ${PC_Foo_VERSION})

现在我们可以使用 FindPackageHandleStandardArgs 为我们完成大部分剩余工作

include(FindPackageHandleStandardArgs)
find_package_handle_standard_args(Foo
  FOUND_VAR Foo_FOUND
  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() 的值在其名称中包含 :: 应该是导入的目标(而不仅仅是库名称),如果该目标不存在,它将产生适当的诊断消息(参见政策:policy: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 不完全匹配的配置时选择该变体。

大多数缓存变量应该隐藏在 ccmake 接口中,除非用户明确要求编辑它们。

mark_as_advanced(
  Foo_INCLUDE_DIR
  Foo_LIBRARY
)

如果此模块替换了旧版本,您应该设置兼容性变量以尽可能减少中断。

# compatibility variables
set(Foo_VERSION_STRING ${Foo_VERSION})