第四章 CMakeList编写(1)
Table of Contents
本章将您介绍如何为软件编写有效的CMakeLists文件的基础。 它涵盖了处理大多数项目所需的所有基本命令和问题。 它还将讨论如何将现有的UNIX或Windows项目转换为CMakeLists文件。 尽管CMake可以处理极其复杂的项目,但是对于大多数项目,您会发现本章的内容将告诉您所有您需要了解的内容。 CMake由为软件项目编写的CMakeLists.txt文件驱动。 CMakeLists文件决定了所有,包含放入缓存的选项,乃至需要编译的源文件。 除了讨论如何编写CMakeLists文件之外,本章还将介绍如何使它们健壮和可维护。 CMakeLists.txt文件的基本语法和CMake的关键概念已在第2章和第3章中进行了讨论。本章将对这些概念进行扩展,并介绍一些新概念。
4.1 CMake语法
CMakeLists文件遵循一个简单语法,它由注释组、命令和空格组成。使用#字符表示注释,注释从该字符开始直到行尾。命令由命令名称,左括号,空格分隔的参数和右括号组成。除分隔参数外,所有空格(空格,换行符,制表符)都将被忽略。像大多数语言一样,将双引号内的所有内容都视为一个参数。反斜杠可用于转义字符,从而防止无法正常解释它们。本章中的后续示例将帮助您澄清一些语法问题。您可能想知道为什么CMake决定使用自己的语言而不使用现有的语言(例如Python,Java或Tcl)。主要原因是我们不想让CMake依赖其他工具而工作。使用这些其他语言之一,将要求CMake的所有用户安装该语言,并可能安装该语言的特定版本。为了围绕CMake展开工作除了需要掌握扩展的语言之外,同时也出于对性能和功能原因的考虑。
4.2 基础命令
尽管前面的章节已经介绍了CMakeLists文件的许多基本命令,但本章将对其进行回顾和扩展。顶层CMakeLists文件应具有的第一个命令是PROJECT命令。该命令既可以命名项目,也可以选择指定将使用的语言。其语法如下:
Project(project name [CXX][C][Java][NONE])
如果未指定语言,则CMake默认支持C和C ++。如果传递了NONE语言,则CMake不包含特定语言的支持。只要指定了C ++语言支持,就会加载C语言支持。
对于项目中出现的每个项目命令,CMake将创建一个顶级IDE项目文件。该项目将包含CMakeLists.txt文件中的所有目标,以及由add_subdirectory命令指定的任何子目录。如果在add_subdirectory命令中使用了EXCLUDE_FROM_ALL选项,则生成的项目将不会出现在顶层Makefile或IDE项目文件中。这对于生成在主构建过程中没有意义的子项目很有用。考虑到具有多个示例的项目可以使用此功能为一次运行CMake的为每个示例生成构建文件,但没有将示例作为正常构建过程的一部分进行构建。
set命令可能是最常用的命令之一,因为它用于定义和修改变量和列表。remove和split_arguments命令可以作为set命令的补充。 remove命令可用于从变量列表中删除值,而split_arguments命令可用于获取单个变量值(与列表相反)并将其分成基于空格分开的列表。
add-executable和add-library命令是用于定义要构建的库和可执行文件以及构成它们的源文件的主要命令。对于Visual Studio项目,源文件将照常显示在IDE中,但项目使用的任何头文件都将不存在。要同时显示头文件,您只需将它们添加到可执行文件或库的源文件列表中。所有的生成器均可以执行此操作。任何不直接使用头文件的生成器(例如基于Makefile的生成器)都将忽略它们。
4.3 流程控制
在许多方面,编写CMakeLists文件就像使用简单语言编写程序一样。 像大多数语言一样,CMake提供了流控制结构来帮助您。 CMake提供三种流量控制结构;
条件语句(例如主if}
循环结构(例如foreach和while}
过程定义(例如宏macro和功能funciton)
If
首先,我们将考虑 if 命令。 在许多方面,CMake中的“ if”命令与其他任何语言中的主if命令一样。 它评估其表达式,并以此为基础执行其主体中的代码或选择性地执行else子句中的代码。 例如:
if (FOO)
#do something
else (FOO)
#do something
endif (FOO)
您可能会注意到的一个区别是,if语句的条件在else和endif子句中重复。 这是可选的,在本书中,您将看到两种样式的示例。 您也可以选择编写:
if (FOO)
#do something
else ( )
#do something
endif ( )
当您在else和endif子句中包含条件时,它们用于提供附加的错误检查。 因此,它们必须与if语句的原始条件完全匹配。 以下代码不起作用:
set(FOO 1)
if ($(FOO))
#do something
endif(1)
#ERROR, it doesn't match the original if conditional
幸运的是,如果if语句与endif不正确匹配,CMake会提供详细的错误消息。 这应该可以帮助您找到条件匹配的问题。 假如在else和endif命令上提供条件语句还具有帮助记录CMakeLists文件的附加好处。 使用长的if语句,很容易追踪到该if语句的结束标志endif。 if语句可以嵌套到任何深度,并且任何命令都可以在if或else子句中使用。
与许多其他语言一样,CMake支持elseif,以便您可以顺序测试多个条件。 例如:
if (MSVC80)
#do something
if (MSVC90)
#do something
if (APPLE)
#do something
endif()
if命令有一组约束的操作供我们使用。它不支持通用的C样式表达式,例如$ {FOO} && $ {BAR} || $ {FUBAR},相反,它提供了一组有限的表达式子集能够满足工作中的绝大部分需要。 if支持表达式:
if(variable)
如果variable的值不为空,0,FALSE,OFF或者NOTFOUND则返回真.
if(NOT variable)
如果variable的值为空,0,FALSE,OFF或者NOTFOUND则返回真.
if(variable1 AND variable2)
如果variable1, variable2均为真则返回真.
if(variable1 OR variable2)
variable1, variable2至少一个为真则返回真.
if(COMMAND command-name)
如果给定的命令行被调用了则返回真.
if(DEFINDED variable)
如果variable被赋值则返回真,不管所赋值的内容.
if(EXISTS file-name)
if(EXISTS directory-name)
如果文件或者路径存在则返回真.
if(IS_DIRECTORY name)
if(IS_ABSOLUTE name)
如果name分别为路径或者绝对路径则返回真.
if(name1 IS_NEWER_THAN name2)
如果name1所指文件的修改时间比name2更近则返回真.
if(variable MATCHTES regex)
if(string MATCHTES regex)
如果string或真variable的值和正则表达式匹配则返回真.
EQUAL,LESS和GREATER等选项可用于数值比较。 可以使用STRELEES,STREQUAL和TRGREATER进行词典比较。 VERSION_LESS,VERSION_EQUAL,VERSION_GREATER可用于比较major [.minor [.patch [.tweak]]]形式的版本信息。 与C和C ++相似,这些表达式可以相互组合构成功能更强大的条件语句。 例如,考虑以下条件:
在复合if语句中,存在优先级顺序,该优先级顺序指定了运算符计算的顺序。 例如,在下面的语句中,将首先对NOT进行求值,然后对AND求值,反之则不然。 因此,该语句将为假,并且消息将永远不会打印。 如果首先对AND进行了求值,则该语句输出将是真。
CMake定义了运算的优先级顺序,首先对括号内的内容进行评估,然后评估EXISTS,COMMAND,DEFINED和类似前缀的运算符,然后评估任何EQUAL,LESS,GEATER,STREQUAL,STRLESS,STRGEATER和MATCHS运算符。 接下来计算NOT运算符,最后将对AND和OR表达式进行求值。 对于具有相同优先级的操作(例如AND和OR),将从左到右对其进行计算。 一旦所有表达式都被求值,将测试最终结果判断其为真还是假。 CMake认为以下任何值均为真:ON,1,YES,TRUE,Y。 以下值都被认为是假:OFF,0,FALSE,N,NOTFOUND,*-NOTFOUND,IGNORE。 该检测不区分大小写,因此将true,True和TRUE都视为相同。
Foreach & while
现在让我们考虑其他流控制命令。 foreach,while,macro和function命令是减小CMakeLists文件大小并提高可维护性的最佳方法。 使用Foreach命令,您可以在列表的成员上重复执行一组CMake命令。 考虑以下改编自VTK的示例:
foreach(tfile
TestAnisotropicDiffusion2D
TestButterworthLowPass
TestButterworthHighPass
TestCityBlockDisatnce
TestConvolve
)
add_test($(tfile)-image $(VTK_EXECUTABLE)
${VTK_SOURCE_DIR}/Ttsets/trImageTest.tcl
${VTK_SOURCE_DIR}/Ttsets/${tfile}.tcl
-D ${VTK_DATA_ROOT}
-V Baseline/Imaging/${tfile}.png
-A ${VTK_SOURCE_DIR}/Wrapping/Tcl
)
endforeach(tfile)
foreacn命令的第一个参数是变量的名称,它将在循环的每次迭代中采用不同的值。其余参数是要循环的值的列表。在此示例中,foreacn循环的主体只是一个CMake命令add_tes。在foreacn循环的主体中,每当引用循环变量(此示例中的tfile)时,它将被列表中的当前值替换。在第一次迭代中,出现的${tfile}将替换为TestAnisotropicDiffusion2D。在下一次迭代中,$ {tfile}将替换为TestButterworthLowPass。foreach循环将继续循环下去直到处理完所有参数为止。
值得一提的是,可以嵌套foreach循环,在任何其他变量扩展之前该循环变量可以被替换。这意味着在foreach循环的主体中,您可以使用循环变量构造变量名称。在下面的代码中,循环变量tfile被展开,然后与_TEST_ RESULT串联,然后对该新变量名称进行展开并测试其值看其是否为FAILED。
while命令提供基于测试条件的循环。 while命令中测试表达式的格式与前面所述的if命令的格式相同。 考虑下面的示例,该示例由CTest使用。 请注意,CTest在内部更新CTEST_ ELAPSED_TIME的值。
通过foreacn和while命令,您可以处理顺序流程中的重复性任务,而macro和function命令则支持那些可能分散在整个CMakeLists文件中的重复性任务。 一旦定义了宏或函数,任何在它定义之后处理的CMakeLists文件都可以使用它。
Function
CMake中的函数非常类似于C或C ++中的函数。 您可以将参数传递给它,传入的参数将成为函数内的局部变量。 同样也规定了一些标准变量,例如ARGC,ARGV,ARGN和ARGV0,ARGV1等。 在函数中,您将处于新的变量作用域内,就像使用add-subdirectory命令加入子目录后你所处的新的变量作用域内。 在调用函数时定义的所有变量始终被定义在那里,但是对变量或新变量的任何更改仅存在于该函数内部。 当函数返回时,这些变量将随之消失。 简而言之,当您调用一个函数时,一个新的变量作用域将会压入栈内,并在返回时将该变量作用域弹出栈。
第一个参数是要定义的函数的名称。 所有其他参数都是函数的形式参数。
值得注意的是,在此示例中,_time用于传递返回变量的名称。 调用set命令设置_time的值,在本示例中该值实则为current_time的值。 最后,set命令使用PARENT_ SCOPE选项在父级作用域中设置该变量而不是本地作用域。
Macro
宏的定义和调用方式与函数类似。 主要区别在于宏不会压入和弹出新的变量作用域,并且宏的参数不会被视为变量,而会在执行之前视为字符串。 这非常类似于C或C ++中宏与函数之间的差异。 第一个参数是要创建的宏的名称。 所有其他参数都是macro 的形式参数。
上面的简单示例中创建了一个名为assert的宏。定义该宏接受两个参数。第一个参数是要测试的值,第二个参数是要在测试失败时打印出的注释。宏的主体是一个简单的if命令,其中带有message命令。找到endmacro命令后,宏主体结束。可以像使用命令一样通过使用其名称来简单地调用该宏。在上面的示例中,如果未找到FOO_LIB,则会显示一条消息,指示错误情况。
macro命令还支持定义可变参数列表的宏。如果要定义具有可选参数或多个签名的宏,这将很有用。可以使用ARGC和ARGV0,ARGV1等变量的引用参数,而不需要使用形式参数。 ARGV0代表宏的第一个参数,ARGV1代表宏的接下来的参数,依此类推。您甚至可以混合使用形参和变参,如下面的示例所示。
在此示例中,两个必需的参数是TEST和COMMENT。 这些必需的参数可以按名称引用,可以如本示例中一样,或者可以使用ARGV0和ARGV1进行引用。 如果要将参数作为列表处理,则可以使用ARGV和ARGN变量。 ARGV(与ARGV0,ARGV1等相对)是宏的所有参数的列表,而ARGN是形式参数之后的所有参数的列表。 在您的宏内,您可以根据需要使用foreach命令遍历ARGV或ARGN。
CMake有两个用于中断处理流程的命令。 break命令将在正常结束之前从foreach或while循环中中断。 函数或列表文件结束之前,return命令将从其中返回。