第11章:模块

前面的章节主要关注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添加目录来添加自己的模块。一种模式是将项目的模块文件收集到单个目录中,并将其添加到CMAKE_MODULE_PATH中顶级CMakeLists.txt文件开头附近的位置。下面的目录结构显示了这种方式:

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

CMakeLists.txt:

cmake_minimum_required(VERSION 3.0)
project(Example)

list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake")

# Inject code from project-provided modules
include(CoolThings)
include(MyModule)

CMake查找模块的搜索顺序有一个例外。如果调用include()的文件本身在CMake自己的内部模块目录中,在查询CMAKE_MODULE_PATH之前将首先搜索内部模块目录。这可以防止项目代码意外地(或故意地)用自己的模块替换正式模块的行为。

使用模块的另一种方法是使用find_package()命令。在23.5节中有详细的讨论,目前该命令的简化形式没有任何可选关键字:

find_package(PackageName)

以这种方式使用时,除其行为与include()相似,CMake将搜索名为FindPackageName的文件,而不是PackageName.cmake。通过这种方法,外部包的详细信息可以引入到构建中,包括导入的目标、定义相关文件、库或程序位置的变量、关于可选组件的信息、版本信息等等。与find_package()相关联的选项和特性集要比include()和第23章提供的内容丰富得多。

本章的剩余部分将介绍一些有趣的模块,这些模块是CMake发布版的一部分。这绝不是一个全面的集合,但确实提供了一些好用的功能。其他模块将在后面的章节中介绍,它们的功能与所讨论的主题密切相关。CMake文档提供了所有可用模块的完整列表,每个模块都有自己的帮助部分,解释了模块提供了什么,以及如何使用它。尽管文档的质量因模块的不同而有所不同,但请先了解一下。

11.1. 开发建议

CMakePrintHelpers模块提供了两个宏,这使得在开发过程中打印属性和变量的值更加方便。这两个宏并不打算永久使用,而是旨在帮助开发人员快速、轻松地临时记录信息,以帮助调试项目中的问题。

cmake_print_properties([TARGETS target1 [target2...]]
 [SOURCES source1 [source2...]]
 [DIRECTORIES dir1 [dir2...]]
 [TESTS test1 [test2...]]
 [CACHE_ENTRIES var1 [var2...]]
 PROPERTIES property1 [property2...]
)

这个宏实际上是将get_property()和message()组合到一个调用中。必须准确地指定其中一种属性类型,并且列出每个实例,打印每个已命名的属性。它在记录多个实例和/或属性的信息时特别方便。例如:

add_executable(myApp main.c)
add_executable(myAlias ALIAS myApp)
add_library(myLib STATIC src.cpp)

include(CMakePrintHelpers)
cmake_print_properties(TARGETS myApp myLib myAlias
 PROPERTIES TYPE ALIASED_TARGET)

输出如下所示:

Properties for TARGET myApp:
 myApp.TYPE = "EXECUTABLE"
 myApp.ALIASED_TARGET = <NOTFOUND>
Properties for TARGET myLib:
 myLib.TYPE = "STATIC_LIBRARY"
 myLib.ALIASED_TARGET = <NOTFOUND>
Properties for TARGET myAlias:
 myAlias.TYPE = "EXECUTABLE"
 myAlias.ALIASED_TARGET = "myApp"

该模块还提供了一个类似的函数,来记录一个或多个变量的值:

cmake_print_variables(var1 [var2...])

这对所有变量都有效,不管它们是否在项目中进行显式设置,是否被CMake自动设置,或者根本没有被设置。

set(foo "My variable")
unset(bar)

include(CMakePrintHelpers)
cmake_print_variables(foo bar CMAKE_VERSION)

上面的输出内容如下:

foo="My variable" ; bar="" ; CMAKE_VERSION="3.8.2"

11.2. 字节顺序

使用各种架构的嵌入式平台或项目时,最好让了解一下目标系统的字节顺序。TestBigEndian模块提供了test_big_endian()宏,该宏编译一个小测试程序来确定目标的字节顺序。然后缓存此结果,以便后续CMake不用重做该测试。这个宏只有一个参数,它是一个变量的名字,用来存储布尔结果(true意味着系统是大端字节序):

include(TestBigEndian)
test_big_endian(isBigEndian)
message("Is target system big endian: ${isBigEndian}")

11.3. 检查存在性和支持程度

CMake的模块涵盖的更全面的领域,是检查是否存在或支持什么。这类模块的工作方式基本相同,即编写少量测试代码,然后尝试编译、链接并运行结果的可执行文件,以确认代码中测试的内容是否得到支持。所有这些模块的名称都以Check开头。

一些更基本的Check…模块可以编译并链接一个简短的测试文件到一个可执行文件中,然后返回一个成功/失败的结果。这些模块的名称都为Check<LANG>SourceCompiles的形式,每个模块都提供一个相关的宏来执行测试:

include(CheckCSourceCompiles)
check_c_source_compiles(code resultVar [FAIL_REGEX regex])

include(CheckCXXSourceCompiles)
check_cxx_source_compiles(code resultVar [FAIL_REGEX regex])

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

对于每个宏,代码参数应该是一个字符串,该字符串包含为所选语言生成可执行文件的源代码。尝试编译和链接代码的结果,作为缓存变量存储在resultVar中,true表示成功,false可以是空字符串、错误消息等。测试执行一次之后,后续的CMake运行将使用缓存的结果,而不再次执行测试。即使测试的代码被更改,也会出现这种情况,因此要强制重新求值,必须手动从缓存中删除变量。如果指定了FAIL_REGEX选项,则可以使用附加条件。如果测试编译和链接的输出与regex正则表达式匹配,即使代码成功编译和链接,检查也将视为失败。

include(CheckCSourceCompiles)
check_c_source_compiles("
   int main(int argc, char* argv[])
   {
   int myVar;
   return 0;
   }" noWarnUnused FAIL_REGEX "[Ww]arn")
if(noWarnUnused)
 message("Unused variables do not generate warnings by default")
endif()

对于Fortran,文件扩展名会影响编译器处理源文件的方式,因此可以使用SRC_EXT显式地指定文件扩展名,获得预期的行为。对于C或C++情况,没有等效的选项。

调用任何编译测试宏来影响它们编译代码的方式之前,可以设置CMAKEREQUIRED

CMAKE_REQUIRED_FLAGS

相关的CMAKE<LANG>FLAGSCMAKE<LANG>FLAGS_<CONFIG>变量需要传递给编译器命令行,作为附加标志(参见14.3节“编译器和链接器变量”),这一定是一个字符串中对多个标志进行了分离。

CMAKE_REQUIRED_DEFINITIONS

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

CMAKE_REQUIRED_INCLUDES

指定搜索目录。多个路径必须指定为CMake列表,空格被视为路径的一部分。

CMAKE_REQUIRED_LIBRARIES

CMake的库列表添加到链接阶段。不要在库名前加上任何-l选项或类似的前缀,只提供库名或CMake导入的目标的名称即可(在第16章,目标类型中讨论)。

CMAKE_REQUIRED_QUIET

如果存在此选项,宏将不会打印任何状态消息。

这些变量用于构造内部执行检查的try_compile()参数。try_compile()的CMake文档讨论了可能会对检查产生影响的其他变量,而try_compile()与工具链选择相关的其他方面的行为会在第21.5节“编译器检查”中介绍。

除了检查代码是否可以构建外,CMake还提供了测试C或C++代码是否可以成功执行的模块。成功与否是通过从提供的源创建的可执行文件执行后的退出码来衡量,0被视为成功,其他值表示失败。

include(CheckCSourceRuns)
check_c_source_runs(code resultVar)

include(CheckCXXSourceRuns)
check_cxx_source_runs(code resultVar)

这些宏没有FAIL_REGEX选项,因为成功或失败完全由测试过程的退出代码决定。如果不能构建代码,也会视为失败。所以check_c_source_compiles()和check_cxx_source_compiles()的代码构建变量,同样也会对这两个模块的宏产生同样的效果。

对于交叉编译到不同目标平台的构建,check_c_source_runs()和check_cxx_source_runs()宏的行为完全不同。如果提供细节信息,他们可以在模拟器下运行代码,这可能会大大降低CMake的速度。如果没有提供模拟器细节信息,宏将期望通过一组变量提供预先确定的结果,而不会尝试运行任何东西。这个相当高级的主题在CMake的try_run()命令文档中介绍,这是宏在内部执行检查所使用的命令。

某些类型的检查非常常见,CMake为此提供了专门的模块。这些方法删除了定义测试代码的许多模板文件,并允许项目为检查指定最小的信息集。这些通常只是对Check<LANG>SourceCompiles模块所提供的宏进行包装,因此用于定制如何构建测试代码的同一组变量仍然可用。这些更专业的模块会检查编译器标记、预处理符号、函数、变量、头文件等等信息。

可以使用Check<LANG>CompilerFlag模块来检查对特定编译器标志的支持,每个模块都提供一个宏,其名称遵循同一个模式:

include(CheckCCompilerFlag)
check_c_compiler_flag(flag resultVar)

include(CheckCXXCompilerFlag)
check_cxx_compiler_flag(flag resultVar)

include(CheckFortranCompilerFlag)
check_fortran_compiler_flag(flag resultVar)

标志检查宏在内部更新CMAKE_REQUIRED_DEFINITIONS变量,以便在调用check<LANG>source_compiles()宏时使用一个简单的测试文件。失败正则表达式的内部集合也作为FAIL_REGEX选项传递,确定测试该标志是否会触发特定的诊断消息。如果没有触发匹配的诊断消息,调用的结果将是true。这意味着,任何导致编译器警告但编译成功的标志仍会视为失败。还有,这些宏假设在相关CMAKE<LANG>FLAGS变量中已经存在的任何标志(参见14.3节“编译器和链接器变量”)本身不会让编译器产生任何编译警告。如果会产生编译警告,那么每个标记测试宏的逻辑都将失败,所有检查的结果都将失败。

另外两个值得注意的模块是CheckSymbolExists和CheckCXXSymbolExists。前者提供了一个宏来构建一个测试C可执行文件,后者与C++可执行文件做同样的事情。两者都检查特定符号是否作为预处理符号(即可以通过#ifdef语句进行测试的符号)、函数或变量。

include(CheckSymbolExists)
check_symbol_exists(symbol headers resultVar)

include(CheckCXXSymbolExists)
check_cxx_symbol_exists(symbol headers resultVar)

对于header中指定的每一项(如果需要给出多个header,则使用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模块文档以查看当前可用功能的完整集。

进行多次检查或执行检查时,需要在相互隔离或与当前范围于其他部分隔离的情况下进行,检查之前和之后手动保存和恢复状态可能会很麻烦。特别是各种CMAKEREQUIRED…变量经常需要保存和恢复。为了帮助实现这一点,CMake提供了CMakePushCheckState模块,该模块定义了以下三个宏:

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

这些宏允许将各种CMAKE_REQUIRED_…变量作为集合处理,并将它们的状态推入或弹出堆栈。每次调用cmakepush_check_state(),它都会将`CMAKE_REQUIRED变量(以及仅由CheckTypeSize模块使用的CMAKE_EXTRA_INCLUDE_FILES变量)设置为新的虚拟变量。cmake_pop_check_state()则相反,丢弃CMAKEREQUIRED变量的当前值,并恢复为前一个堆栈级别的值。cmake_reset_check_state()宏可以方便地清除所有CMAKEREQUIRED…`变量,而cmake_push_check_state()的重置选项也可以方便地清除一部分的变量。但CMake 3.10之前存在一个重置选项的bug,因此对于需要使用3.10之前版本的项目,最好使用单独的cmake_reset_check_state()。

include(CheckSymbolExists)
include(CMakePushCheckState)

# Start with a known state we can modify and undo later
cmake_push_check_state() # Could use RESET option, but needs CMake >= 3.10
cmake_reset_check_state() # Separate call, safe for all CMake versions
set(CMAKE_REQUIRED_FLAGS -Wall)
check_symbol_exists(FOO_VERSION foo/version.h HAVE_FOO)

if(HAVE_FOO)
 # Preserve -Wall and add more things for extra checks
 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()
 # Now back to just -Wall
endif()

# Clear all the CMAKE_REQUIRED_... variables for this last check
cmake_reset_check_state()
check_symbol_exists(__TIME__ "" HAVE_PPTIME)

# Restore all CMAKE_REQUIRED_... variables to their original values
# from the top of this example
cmake_pop_check_state()

11.4. 其他模块

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

  • CSharpUtilities

  • FindCUDA (但请注意,在最近的CMake版本中,已经被CUDA的支持所取代,CUDA本身就是一种一流的语言)

  • FindJava, FindJNI, UseJava

  • FindLua

  • FindMatlab

  • FindPerl, FindPerlLibs

  • FindPython, FindPythonInterp

  • FindPHP4

  • FindRuby

  • FindSWIG, UseSWIG

  • FindTCL

  • FortranCInterface

此外,还提供了与外部数据和项目交互的模块,这个题将在第27章“外部内容”中深入讨论。还提供了许多模块来方便测试和打包的各个方面。这些工具与作为CMake套件的一部分CTest和CPack工具有着密切的关系,将在第24章“测试”和第26章“打包”中进行介绍。

11.5. 推荐

CMake的模块集合提供了丰富的功能。通过在特定目录下添加定制模块,并将该路径附加到CMAKE_MODULE_PATH变量中,项目可以轻松地扩展可用的功能集。在include()调用中,应该优先使用CMAKE_MODULE_PATH,而不是硬编码为绝对或相对路径,鼓励通用CMake逻辑从可能的地方进行解耦。这反过来使得更容易重新定位CMake模块到不同的目录作为一个项目拓展,或重用与不同的项目。实际上,组织构建自己的模块集合并不罕见,甚至可能将它们存储在自己单独的目录中。通过在每个项目中适当地设置CMAKE_MODULE_PATH,这些可重用的CMake构建块就可以根据需要进行使用了。

随着时间的推移,开发人员通常会接触到越来越多的有趣场景,CMake模块可以为这些场景提供有用的快捷方式或现成的解决方案。有时,对可用模块的快速浏览可能会发现意想不到的隐藏奖励,或者新模块可能提供了一个更好维护的实现,而项目在此之前一直在以一种较差的方式实现。CMake的模块有潜在的开发人员池和项目跨不同的平台,这样的情况使得使用模块有很多好处,所以在许多情况下,模块的方式可能提供一个更引人注目的替代方案。然而,不同模块的质量是不同的,有些模块在CMake早期就体现了他们的价值,如果不跟上CMake或相关模块领域的变化,这些模块有时就会不那么有用。对于Find…模块来说尤其如此,它们不会像人们期望的那样紧密地跟踪软件包的最新版本。另一方面,模块终只是普通的CMake代码,所以任何人都可以检查、学习、改进或更新它们,而不需要学习在项目中使用除了基本CMake以外的东西。事实上,对于希望参与CMake本身工作的开发人员来说,模块是一个非常很好的起点。

CMake提供的大量不同的Check…模块是一件喜忧参半的事情。开发人员可能会过于热衷于检查所有类型的东西,这可能会导致配置阶段的速度变慢,有时会导致一些问题。考虑在实现和维护检查的时间以及项目的复杂性方面,收益是否大于成本。有时,一些明智的检查就足以覆盖大部分情况,或者捕获可能导致以后难以跟踪问题的细微问题。此外,如果使用任何Check…模块,目标会将检查逻辑与调用的范围隔离开来。强烈建议使用CMakePushCheckState模块,但如果需要支持3.10之前的CMake版本,则尽可能避免使用cmake_push_check_state()的RESET选项。

Last updated

Was this helpful?