第十一章:模块

Posted by lili on

前面的章节主要关注了CMake的核心方面。变量、属性、流程控制、生成器表达式、函数等都是可以被视为CMake语言的一部分。相比之下,模块是构建在核心语言功能之上的预先构建的CMake代码块。它们提供了丰富的功能集,项目可以使用这些功能来实现各种各样的目标。作为普通的CMake代码编写和打包,因此易于阅读,模块也可以成为学习在CMake中完成任务的有用资源。

模块被集成在一个单独的目录中,作为CMake发布的一部分提供。项目可以以两种方式之一使用模块,直接使用或作为查找外部包的一部分。直接使用模块的更直接的方法是使用include()命令,将模块代码注入到当前作用域中。这与前面在第7.2节“include()”中讨论的行为完全相同,只是在include()命令中只需要提供模块的基本名称,而不是完整的路径或文件扩展名。所有include()的选项都与以前完全相同。

include(module
  [OPTIONAL]
  [RESULT_VARIABLE myVar]
  [NO_POLICY_SCOPE]
)

当给定一个模块名称时,include()命令将在一组预定义的位置查找一个文件,该文件的名称是模块的名称(区分大小写),并附加了.cmake。例如,include(FooBar)将导致CMake查找一个名为FooBar.cmake的文件,在Linux等区分大小写的系统上,文件名如foobar.cmake将不匹配。

在查找模块文件时,CMake首先查阅变量CMAKE_MODULE_PATH。假定这是一个目录列表,CMake将按顺序搜索其中的每个目录。将使用第一个匹配的文件,或者如果未找到匹配的文件,或者CMAKE_MODULE_PATH为空或未定义,则CMake将在其自己的内部模块目录中搜索。此搜索顺序允许项目通过将目录添加到CMAKE_MODULE_PATH无缝地添加自己的模块。一个有用的模式是将项目的模块文件集中放在一个单独的目录中,并在顶层CMakeLists.txt文件的开头的某处将其添加到CMAKE_MODULE_PATH。下面的目录结构展示了这样的安排:

然后,相应的CMakeLists.txt文件只需要将cmake目录添加到CMAKE_MODULE_PATH,然后在加载每个模块时,可以只使用基本文件名调用include()。

# CMakeLists.txt:
cmake_minimum_required(VERSION 3.0)
project(Example)
list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
# 注入来自项目提供的模块的代码
include(CoolThings)
include(MyModule)

CMake在查找模块时使用的搜索顺序有一个例外。如果调用include()的文件本身位于CMake自己的内部模块目录内,那么在查阅CMAKE_MODULE_PATH之前,将首先搜索内部模块目录。这可以防止项目代码意外(或故意)用自己的模块替换官方模块并更改已记录的行为。

另一种使用模块的方法是使用find_package()命令。这将在第25.5节“查找软件包”中详细讨论,但目前,该命令的简化形式演示了其基本用法,不包含任何可选关键字:

find_package(PackageName)

在这种用法中,行为与include()非常相似,只是CMake将搜索名为FindPackageName.cmake而不是PackageName.cmake的文件。这通常是将外部软件包的详细信息引入构建的方法,包括导入的目标、定义相关文件位置的变量、库或程序的信息、有关可选组件的信息、版本详细信息等等。与include()提供的选项和功能相比,与find_package()相关的选项和功能要丰富得多,第25章“查找事物”致力于详细介绍这个主题。

本章的其余部分介绍了作为CMake发布的一部分包含的一些有趣的模块。这绝不是一个全面的集合,但它们确实展示了可用功能的一些特点。其他模块在后续章节中介绍,其中它们的功能与讨论主题密切相关。CMake文档提供了所有可用模块的完整列表,每个模块都有自己的帮助部分,解释模块提供的内容以及如何使用它。但请注意,文档的质量在模块之间可能有所不同。

11.1. 检查存在性和支持

CMake的模块涵盖的领域之一是检查各种事物的存在性或支持性,这是比较全面的。这类模块基本上以相同的方式工作,编写少量测试代码,然后尝试编译、链接和可能运行生成的可执行文件,以确认在代码中测试的内容是否得到支持。所有这些模块的名称都以Check开头。

一些更基础的Check…模块是那些将短小的测试文件编译和链接到可执行文件中,并返回成功/失败结果的模块。对于CMake版本3.19或更高版本,CheckSourceCompiles模块提供了这个功能。它定义了check_source_compiles()命令:

include(CheckSourceCompiles)
check_source_compiles(lang code resultVar
  [FAIL_REGEX regexes...]
  [SRC_EXT extension]
)

lang应该是CMake支持的语言之一,如C、CXX、CUDA等。在较早的CMake版本中,单独的按语言分的模块提供相同的功能,但支持的语言集合要小得多。这些模块的名称形式为CheckSourceCompiles,每个模块都提供一个相关的命令来执行测试:

include(CheckCSourceCompiles)
check_c_source_compiles(code resultVar
  [FAIL_REGEX regexes...]
)

include(CheckCXXSourceCompiles)
check_cxx_source_compiles(code resultVar
  [FAIL_REGEX regexes...]
)

include(CheckFortranSourceCompiles)
check_fortran_source_compiles(code resultVar
  [FAIL_REGEX regexes...]
  [SRC_EXT extension]
)

对于所有这些命令,code参数应该是一个包含应该为相应语言生成可执行文件的源代码的字符串。尝试编译和链接代码的结果被存储在resultVar中,作为一个缓存变量,其中true表示成功。False值可能是一个空字符串、错误消息等,具体取决于情况。一旦测试已经执行过一次,后续的CMake运行将使用缓存的结果而不是再次执行测试。即使更改了被测试的代码,也是如此。要强制重新评估,必须手动从缓存中删除变量。

如果指定了FAIL_REGEX选项,那么会应用附加的条件。如果测试编译和链接的输出与指定的正则表达式(正则表达式列表)匹配,即使代码成功编译和链接,检查也将被认为失败。

include(CheckCSourceCompiles)
check_c_source_compiles("int main() { int myVar; }"
  unusedNotDetected
  FAIL_REGEX "[Ww]arn"
)

if(unusedNotDetected)
  message("Unused variables do not generate warnings")
endif()

对于Fortran,文件扩展名可能会影响编译器处理源文件的方式,因此可以使用SRC_EXT选项显式指定文件扩展名以获得预期的行为。在使用旧的CheckSourceCompiles模块时,C或C++的情况没有等效的选项,但新的CheckSourceCompiles模块支持所有语言的这个选项。

在调用任何编译测试命令之前,可以设置一些形式为CMAKE_REQUIRED_…的变量,以影响它们如何编译代码:

CMAKE_REQUIRED_FLAGS: 在相关CMAKE_FLAGS和CMAKEFLAGS变量的内容之后,传递给编译器命令行的额外标志。这必须是一个单独的字符串,其中多个标志用空格分隔,与下面的所有其他变量不同,它们都是CMake列表。

CMAKE_REQUIRED_DEFINITIONS: 编译器定义的CMake列表,每个以-DFOO或-DFOO=bar的形式指定。

CMAKE_REQUIRED_INCLUDES: 指定搜索头文件的目录。必须将多个路径指定为CMake列表,其中空格被视为路径的一部分。

CMAKE_REQUIRED_LIBRARIES: 要添加到链接阶段的库的CMake列表。不要为库名称添加任何-l选项或类似的选项,只提供库名称或CMake导入目标的名称(在第18章“目标类型”中讨论)。

CMAKE_REQUIRED_LINK_OPTIONS: 要在构建可执行文件时传递给链接器,或在构建静态库时传递给归档程序的CMake列表。仅在CMake 3.14或更高版本中支持此变量。

CMAKE_REQUIRED_QUIET: 如果存在此选项,则命令将不会打印任何状态消息。

这些变量用于构建传递给try_compile()调用的参数,try_compile()在内部执行以执行检查。CMake对try_compile()的文档讨论了可能对检查产生影响的其他变量,而与try_compile()行为相关的其他方面,如工具链选择和构建目标类型,已在第23.5节“编译器检查”中进行了讨论。

除了检查代码是否能够构建之外,CMake还提供了模块来测试构建的可执行文件是否可以成功运行。成功的衡量标准是根据提供的源代码创建的可执行文件的退出代码,其中0被视为成功,所有其他值表示失败。在CMake 3.19或更高版本中,一个单一的模块提供了适用于所有语言的命令:

include(CheckSourceRuns)
check_source_runs(lang code resultVar
  [SRC_EXT extension]
)

同样,在较早的CMake版本中,单独的按语言分的模块提供相同的功能,但支持的语言较少:

# 所有CMake版本都支持
include(CheckCSourceRuns)
check_c_source_runs(code resultVar)

include(CheckCXXSourceRuns)
check_cxx_source_runs(code resultVar)

# 需要CMake 3.14或更高版本
include(CheckFortranSourceRuns)
check_fortran_source_runs(code resultVar
  [SRC_EXT extension]
)

对于这些命令,没有FAIL_REGEX选项,因为成功或失败纯粹由测试进程的退出代码确定。如果代码无法构建,这也被视为失败。所有影响check_source_compiles()或check__source_compiles()中代码构建方式的相同变量对于这些模块的命令同样有效。

对于进行交叉编译到不同目标平台的构建,check_source_runs()和check__source_runs()命令的行为会有很大不同。如果已经提供了必要的详细信息,它们可能会在模拟器下运行代码,这可能会显著减慢CMake阶段。如果未提供模拟器详细信息,命令将期望通过一组变量提供预定的结果,而不尝试运行任何内容。这是一个相当高级的主题,CMake的try_run()命令的文档涵盖了这一点,该命令是模块命令在内部执行检查时使用的。

某些类别的检查非常常见,CMake为它们提供了专用的模块。这些模块消除了定义测试代码的大量样板代码,并允许项目为检查指定最小的信息。这些通常只是对上述提到的模块之一提供的命令的包装器,因此仍然适用于用于自定义测试代码构建方式的相同一组变量。这些更专业化的模块检查编译器标志、预处理器符号、函数、变量、头文件等。

与上述模块一样,CMake 3.19或更高版本为所有支持的语言提供了一个单一的模块和命令。对于较早的CMake版本,必须使用一组按语言分的模块。

# 需要CMake 3.19或更高版本
include(CheckCompilerFlag)
check_compiler_flag(lang flag resultVar)

# 所有CMake版本都支持
include(CheckCCompilerFlag)
check_c_compiler_flag(flag resultVar)

include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(flag resultVar)

# 需要CMake 3.3或更高版本
include(CheckFortranCompilerFlag)
check_fortran_compiler_flag(flag resultVar)

这些检查标志的命令在内部更新CMAKE_REQUIRED_DEFINITIONS变量,以在对具有简单测试文件的check_source_compiles()的调用中包含标志。一个内部的失败正则表达式集合也作为FAIL_REGEX选项传递,测试标志是否导致发出诊断消息。如果没有发出匹配的诊断消息,调用的结果将是一个true值。请注意,这意味着任何导致编译器警告但编译成功的标志仍将被视为未通过检查。还要注意,这些命令假定任何已经存在于相关的CMAKE__FLAGS变量中(见第15.5节“编译器和链接器变量”)的标志本身不会生成任何编译器警告。如果存在警告,那么每个这些标志测试命令的逻辑将失效,所有这样的检查的结果将是失败。

CMake 3.18还引入了CheckLinkerFlag模块。它提供了类似的命令check_linker_flag(),它在很大程度上只是check_source_compiles()的一个便利包装器。因此,它支持之前讨论的所有相同的变量,除了它接管了CMAKE_REQUIRED_LINK_OPTIONS变量的处理。

include(CheckLinkerFlag)
check_linker_flag(language flag resultVar)

指定的标志不会直接传递给链接器。链接器通过编译器调用,编译器会在内部添加额外的特定于语言的标志、库等,以成功链接指定语言的目标。原始的链接器标志通常不起作用,通常需要一些前缀,比如-Wl,…或-Xlinker,告诉编译器将其传递给链接器。这个前缀是特定于编译器的,但可以使用特殊前缀LINKER:,CMake将自动替换为正确的编译器特定前缀。请参见第15.1.2节“链接器标志”和第15.2节“目标属性命令”的相关讨论。

include(CheckLinkerFlag)
check_linker_flag(CXX LINKER:-stats LINKER_STATS_SUPPORTED)

另外两个值得注意的模块是CheckSymbolExists和CheckCXXSymbolExists。前者提供了一个构建测试C可执行文件的命令,而后者以同样的方式构建测试C++可执行文件。两者都检查特定符号是否存在,可以是预处理器符号(即可以通过#ifdef语句测试的内容)、函数或变量。

include(CheckSymbolExists)
check_symbol_exists(symbol headers resultVar)

include(CheckCXXSymbolExists)
check_cxx_symbol_exists(symbol headers resultVar)

对于在headers中指定的每个项目(如果需要给出多个头文件,则是CMake列表),将向测试源代码添加相应的#include。在大多数情况下,要检查的符号将由这些头文件之一定义。测试的结果以通常的方式存储在resultVar缓存变量中。

对于函数和变量,符号需要解析为测试可执行文件的一部分。如果函数或变量由库提供,那么必须将该库链接为测试的一部分,可以使用CMAKE_REQUIRED_LIBRARIES变量来完成。

include(CheckSymbolExists)
check_symbol_exists(sprintf stdio.h HAVE_SPRINTF)

include(CheckCXXSymbolExists)
set(CMAKE_REQUIRED_LIBRARIES SomeCxxSDK)
check_cxx_symbol_exists(SomeCxxInitFunc somecxxsdk.h HAVE_SOMECXXSDK)

这些命令对于可以由这些命令检查的函数和变量的类型有一些限制。只有符合预处理器符号命名要求的符号才能使用。对于check_cxx_symbol_exists()而言,其含义更为严格,因为它意味着只有全局命名空间中的非模板函数或变量才能被检查,因为任何作用域(::)或模板标记(<>)对于预处理器符号都是无效的。此外,不可能区分相同函数的不同重载,因此也不能检查这些函数。还有其他的模块旨在提供与CheckSymbolExists所涵盖的功能相似或子集的功能。这些其他模块要么来自CMake的较早版本,要么是用于C或C++之外的语言。CheckFunctionExists模块已经被文档化为已弃用,而CheckVariableExists模块没有提供任何CheckSymbolExists尚未提供的功能。CheckFortranFunctionExists模块对于与Fortran一起工作的项目可能会有用,但请注意,没有CheckFortranVariableExists模块。Fortran项目可能希望出于一致性考虑使用CheckFortranSourceCompiles。

其他模块提供了更详细的检查功能。例如,可以使用CheckStructHasMember检查结构成员,可以使用CheckPrototypeDefinition检查特定的C或C++函数原型,而非用户类型的大小可以使用CheckTypeSize进行测试。还有其他一些更高级别的检查,如CheckLanguage、CheckLibraryExists和各种CheckIncludeFile…模块所提供的。随着CMake的发展,将继续添加更多的检查模块,因此请参阅CMake模块文档以查看可用功能的完整集合。

在进行多个检查或需要将检查的效果与当前范围的其余部分或彼此隔离的情况下,手动保存和恢复状态可能会很繁琐。特别是各种CMAKE_REQUIRED_…变量通常需要保存和恢复。为了帮助解决这个问题,CMake提供了CMakePushCheckState模块,该模块定义了以下三个命令:

include(CMakePushCheckState)
cmake_push_check_state([RESET])
cmake_pop_check_state()
cmake_reset_check_state()

这些命令允许将各种CMAKE_REQUIRED_…变量视为一个集合,并将它们的状态推送到/从虚拟堆栈中。每次调用cmake_push_check_state()时,它实际上开始一个仅用于CMAKE_REQUIRED_…变量的新虚拟变量范围(还有仅由CheckTypeSize模块使用的CMAKE_EXTRA_INCLUDE_FILES变量)。cmake_pop_check_state()则相反,它丢弃当前CMAKE_REQUIRED_…变量的值,并将它们还原为前一个堆栈级别的值。cmake_reset_check_state()命令是一种清除所有CMAKE_REQUIRED_…变量的便捷方法,而cmake_push_check_state()的RESET选项也只是在推送的同时清除变量的一种便捷方式。但是,请注意,在CMake 3.10之前存在一个bug,导致忽略RESET选项,因此对于需要与3.10之前的版本一起使用的项目,最好使用单独的调用cmake_reset_check_state()。

# 从一个已知的状态开始,我们可以修改和撤消
include(CMakePushCheckState)
cmake_push_check_state()
cmake_reset_check_state()
set(CMAKE_REQUIRED_FLAGS -Wall)
include(CheckSymbolExists)
check_symbol_exists(FOO_VERSION foo/version.h HAVE_FOO)
if(HAVE_FOO)
  # 保留 -Wall 并添加更多的内容进行额外的检查
  cmake_push_check_state()
  set(CMAKE_REQUIRED_INCLUDES foo/inc.h foo/more.h)
  set(CMAKE_REQUIRED_DEFINES -DFOOBXX=1)
  check_symbol_exists(FOOBAR "" HAVE_FOOBAR)
  check_symbol_exists(FOOBAZ "" HAVE_FOOBAZ)
  check_symbol_exists(FOOBOO "" HAVE_FOOBOO)
  cmake_pop_check_state()
  # 现在回到只有 -Wall
endif()
# 清除此最后一个检查的CMAKE_REQUIRED_...变量
cmake_reset_check_state()
check_symbol_exists(__TIME__ "" HAVE_PPTIME)
# 将所有CMAKE_REQUIRED_...变量还原到这个示例顶部的原始值
cmake_pop_check_state()

11.2. 其他模块

CMake在某些语言,特别是C和C++方面,具有出色的内置支持。它还包括一些模块,以更可扩展和可配置的方式为其他语言提供支持。这些模块允许通过定义相关的命令、变量和属性,使某些语言或与语言相关的软件包的方面可用于项目。许多这些模块作为对find_package()调用的支持的一部分提供(参见第25.5节,“查找软件包”),而其他模块则更直接地通过include()用于将内容引入当前范围。以下模块列表应该让您对可用的语言支持有所了解:

  • CSharpUtilities
  • FindCUDA(但请注意,近期CMake版本中已将对CUDA的支持作为顶层语言的支持)
  • FindJava、FindJNI、UseJava
  • FindLua
  • FindMatlab
  • FindPerl、FindPerlLibs
  • FindPython
  • FindPHP4
  • FindRuby
  • FindSWIG、UseSWIG
  • FindTCL
  • FortranCInterface

此外,还提供了用于与外部数据和项目交互的模块(参见第29章,“ExternalProject”和第30章,“FetchContent”)。还提供了许多模块来促进测试和打包的各个方面。这些模块与CMake套件的一部分分发的CTest和CPack工具密切相关,分别在第26章,“测试”和第28章,“打包”中深入讨论。CMakePrintHelpers模块(参见第13.3节,“Print Helpers”)还提供了调试辅助功能。

11.3. 最佳实践

CMake的模块集合在核心CMake语言的基础上提供了丰富的功能。项目可以通过将自定义模块添加到特定目录并将该路径添加到CMAKE_MODULE_PATH变量中,轻松扩展可用功能的集合。使用CMAKE_MODULE_PATH应优于在include()调用中跨复杂目录结构硬编码绝对或相对路径,因为这将鼓励将通用CMake逻辑与适用该逻辑的地方解耦。这反过来使得在项目演变过程中更容易将CMake模块迁移到不同的目录,或在不同项目之间重复使用逻辑。实际上,组织建立自己的模块集合并将其存储在其自己的单独存储库中并不罕见。通过在每个项目中适当设置CMAKE_MODULE_PATH,这些可重用的CMake构建块就可以广泛地用于需要的任何地方。

随着时间的推移,开发人员通常会接触到越来越多的有趣场景,对于这些场景,CMake模块可能提供了有用的快捷方式或现成的解决方案。有时,快速查看可用模块可以发现意外的隐藏宝石,或者新的模块可能提供了一个更好的、维护得更好的实现,这可能是项目在那一点上一直在以较低质量的方式实现的东西。CMake的模块受益于一个潜在庞大的开发者和项目群体,他们在各种平台和情况下使用这些模块,因此在许多情况下,它们可能提供了一个更有吸引力的替代方案,而不是在项目中手动执行逻辑。然而,这些模块的质量从一个模块到另一个模块可能会有所不同。一些模块在CMake存在的早期阶段就开始了它们的生命周期,如果不随CMake或与这些模块相关的领域的变化保持同步,它们有时可能会变得不太有用。对于Find…模块来说,这可能尤其成立,因为它们可能不像人们希望的那样跟踪它们正在查找的软件包的新版本。另一方面,模块只是普通的CMake代码,因此任何人都可以检查它们、从中学习、改进或更新它们,而无需学习超出项目中基本CMake使用所需的知识。事实上,它们是希望与希望参与CMake开发的开发人员互动的绝佳起点。

由CMake提供的各种Check…模块可能是一把双刃剑。开发人员可能会被诱导过于热衷于检查各种事物,这可能导致在配置阶段减慢速度,而这种减慢在某些情况下可能效果有限。通常会看到与这些检查相关的命令在CMake运行的性能分析结果(参见第13.6节,“对CMake调用进行性能分析”)中占主导地位。请考虑在实施和维护这些检查的时间、项目的复杂性方面,收益是否超过成本。有时,少数明智的检查足以覆盖最有用的情况,或者可以捕捉可能在后期导致难以跟踪的问题的微妙问题。此外,如果使用任何Check…模块,请尽量将检查逻辑与可能调用它的范围隔离开来。强烈推荐使用CMakePushCheckState模块,但如果支持CMake版本在3.10之前的版本很重要,则避免使用RESET选项来调用cmake_push_check_state()。

当最低CMake版本可以设置为3.20或更高时,请避免使用相对受欢迎但已被弃用的TestBigEndian模块。该模块在CMake 3.20中已被弃用,以支持一个新的CMAKE__BYTE_ORDER变量,该变量也在相同的CMake版本中引入。正在使用TestBigEndian的项目应该尽可能过渡到新变量。