快速掌握CMake「二、编写CMakeLists文件」
「CMake」是一个跨平台的C++项目管理解决方案,它可以帮助用户方便地管理大型C++项目,支持生成不同平台的Makefile。本系列文章可以帮助你快速掌握CMake,所有内容来源于Mastering Cmake。
本篇文章为该系列的第二篇,我们将讨论如何为你的项目编写一个基本的CMakeLists文件,包括基本的命令和你在项目中可能会遇到的问题。本文还将讨论CMakeLists文件的健壮性和可维护性。
2.1 编辑CMakeLists文件
你可以使用大多数的文本编辑器来编辑CMakeLists文件,比如Notepad++,VS Code等等。它们自带了CMake语法高亮显示和缩进支持。而其他的一些编辑器比如Emacs或者Vim,CMake包含缩进和语法高亮模块,你可以在源码的Auxiliary
目录中找到它们,或者从CMake的下载页面下载。
在任何支持的生成器(Makefiles, Visual Studio等)中,如果你编辑了一个CMakeLists文件并使用CMake重新构建,生成器会根据需要自动调用CMake来更新生成的文件(例如Makefiles或项目文件)。这有助于确保生成的文件总是与CMakeLists文件同步。
2.2 CMake的语言
CMake语言由注释,命令和变量组成。
2.3 注释
注释由#
开头,直到一行的末尾。可以查看手册中的cmake-language
了解更多细节。
2.2 变量
想很多编程语言那样,CMakeLists文件有变量的概念。变量名是大小写敏感的,并且仅包含字母,数字和下划线。
CMake已经默认定义了很多有用的变量,你可以在cmake-variables
手册中查看它们。这些变量都以CMAKE_
开头。请避免在你的项目中使用这种形式命名的变量,以免产生冲突。
所有的CMake变量都在内部都作为字符串存储(虽然一些CMake变量有时可以解释为其他的类型)。
你可以使用set
命令来设置变量的值。set
命令的一种最简单的形式是,第一个参数是变量名,剩余部分是变量值。多个值的参数会被连接成到一个以分号分隔的列表,并作为字符串存储在变量中。
set(Foo "") # 1 quoted arg -> value is "" |
变量可以以${VAR}
的语法格式进行引用并作为命令的参数,其中VAR
是变量名。如果该变量未被定义,那么这个引用就会被空字符串所取代;否则就是该变量的值。变量替换在未加引号的参数展开之前执行,因此包含分号的变量值被分割成零个或多个参数,以代替原始的未加引号的参数。
set(Foo a b c) # 3 unquoted args -> value is "a;b;c" |
系统的环境变量以及Windows的注册表值可以直接在CMake中使用。要使用系统环境变量,可以通过$ENV{VAR}
语法。CMake也可以通过[HKEY_CURRENT_USER\\Software\\path1\\path2;key]
形式的语法来引用注册表项,其中的path是注册表的路径。
2.2.1 变量作用域
Cmake的变量作用域与大多数编程语言有一点小小的差异,当你设置一个变量时,它不仅在当前的CMakeLists文件或者函数中可见,同时在所有子目录的CMakeLists文件,任何被调用的函数和宏,以及任何被include
命令导入的文件中都是可见的。
当CMake处理一个新的子目录(或一个函数被调用)时,一个新的变量作用域就被创建且用调用作用域中所有变量的当前值初始化。子作用域中创建的新变量,或者对已经存在的变量的修改,都不会影响到父作用域。考虑以下例子:
function(foo) |
在某些情况下,你可能希望函数或子目录在其父作用域内设置变量。CMake有一种从函数返回值的方法,可以通过使用PARENT SCOPE
选项和set
命令来实现。我们可以修改前面的例子,使函数foo
在其父作用域中更改test
的值,如下所示:
function(foo) |
CMake中的变量都是按照set
命令执行的顺序定义的。考虑以下的例子:
# FOO is undefined |
为了理解变量的作用域,考虑以下的例子:
set(foo 1) |
在这个例子中,由于变量foo
一开始就被定义了,所以它会在处理dir1
和dir2
时都被定义。与之相对的,只有在dir2
的作用域中,才会定义bar
。类似的,在处理file1.cmake
和file2.cmake
时,foo
都会被定义,而bar
只会在file2.cmake
中被定义。
2.3 命令
一条命令包含命令名,左括号,空格分隔的参数和右括号。每个命令都是按照它在CMakeLists文件中出现的顺序生成的。你可以查看cmake-commands
手册来了解所有的CMake命令。
CMake现在对于命令不再是大小写敏感的了,所以你可以使用COMMAND
或者Command
来代替command
。最佳实践是采用全小写的方式来拼写命令。除了分隔符之外,所有的空白字符(空格符,换行符,制表符)都将被忽略。因此,只要命令名和左括号在同一行,那么一条命令就可以拆分成多行。
CMake命令参数是以空格隔开且大小写敏感的。命令参数可以使用引号包裹起来。用引号括起来的参数以双引号开始和结束,并且总是只表示一个参数。参数值中包含的任何双引号都必须使用反斜杠进行转义。考虑对需要转义的参数使用括号参数可以查看cmake-language
手册。未被引号包裹的参数不能以双引号开头,并且如果你使用了分隔符,CMake会自动将其拆分为多个参数。例如:
command("") # 1 quoted argument |
2.3.1 基本命令
正如前文所提到的,set
命令和unset
命令用于设置和取消一个变量。string
,list
和separate_arguments
命令提供了对字符串和列表的基本操作。
add_executable
和add_library
命令用于定义要构建的可执行文件和库的主要命令,同时还指明了这些可执行文件或库由哪些源文件生成。对于Visual Studio项目,源文件将像往常一样显示在IDE中,但项目使用的任何头文件将不会显示在IDE中。要显示头文件,只需将它们添加到可执行文件或库的源文件列表中;所有生成器都可以这样做。任何不直接使用头文件的生成器(比如基于Makefile的生成器)都将忽略头文件。
2.4 控制流
CMake语言提供三种控制流来帮助你组织CMakeLists文件,并且使得它们更易于维护。
2.4.1 条件语句
首先我们考虑if
语句。在很多情况下,CMake中的if
语句和其他语言类似。它根据表达式的值决定是否执行块中的语句。例如:
if(FOO) |
CMake同样支持多分支的elseif
,例如:
if(MSVC80) |
2.4.2 循环结构
foreach
和while
命令允许你处理一些重复性的任务。break
命令可以在foreach
和while
循环正常退出前跳出循环。
foreach
命令可以让你循环处理一个列表中的元素。下列例子来自VTK:
foreach(tfile |
foreach
命令的第一个参数是迭代循环中的变量名;剩下的参数是要进行迭代的列表值。在这个例子中,foreach
的循环体值使用了一个命令add_test
。在循环体中,每一次循环tfile
的引用都会被替换成列表中的下一个元素。第一轮迭代时,${tfile}
会被替换成TestAnisotropicDiffusion2D
。下一轮迭代时,${tfile}
会被替换成TestButterworthLowPass
。foreach
会一直执行直到所有元素都遍历一遍。
值得一提的是,foreach
循环支持嵌套,并且循环变量将在任何其他变量展开之前先被替换。这意味着在foreach
的循环体中,你可以使用循环变量构建变量名。在下面的代码中,循环变量tfile
首先被展开,然后与_TEST_RESULT
连接。最终展开的变量名将用于检查该变量的值是否与FAILED
相匹配。
if(${${tfile}}_TEST_RESULT} MATCHES FAILED) |
while
命令提供了基于条件的循环。它的条件表达式的格式与if
命令中的一样。考虑下面的例子(来自CTest,注意CTest在内部更新了CTEST_ELAPSED_TIME
的值):
##################################################### |
2.4.1 过程定义
macro
和function
命令支持重复的任务,这些任务可能分散在CMakeLists文件中。一旦定义了宏或函数,任何在其定义之后处理的CMakeLists文件都可以使用它。
CMake中的函数和C、C++中的非常像。你可以给它们传入参数,这些参数就将作为函数中的遍历。类似的,CMake默认已经定义了一些标准变量,比如ARGC
,ARGV
,ARGN
和ARGV0
,ARGV1
等等。函数调用拥有与一个动态作用域。一个函数内部就是一个新的作用域,这一点和你使用add_subdirectory
命令非常像。调用函数时定义的所有变量仍然是已定义的,但对变量的任何更改或新变量只存在于函数内部。当函数返回时,这些变量就会消失。更简单地说:当你调用一个函数时,一个新的变量范围会产生:当它返回时,变量作用域消失。
function
命令定义了一个新函数。第一个参数是要定义的函数名;所有附加参数都是函数的形参。
function(DetermineTime _time) |
注意,在本例中,_time
用于传递返回变量的名称。set
命令使用了_time
的值,该值为current_time
。最后,set
命令使用PARENT SCOPE
选项在调用者的作用域(而不是当前作用域)中设置变量。
宏的定义和调用方式与函数相同。主要的区别是,宏不会产生和销毁一个新的变量作用域,并且宏的参数不被视为变量,而是作为字符串在执行之前被替换。这非常类似于C或C++中的宏和函数之间的区别。第一个参数是要创建的宏的名称;所有其他参数都是宏的形参。
# define a simple macro |
上面的简单示例创建了一个名为assert
的宏。该宏定义了两个参数:第一个是要测试的值,第二个是要在测试失败时打印的注释。宏的主体是一个简单的if命令,其中包含一个message
命令。当遇到endmacro
命令时,宏的主体结束。可以像调用命令一样简单地使用宏的名称来调用宏。在上面的例子中,如果没有找到FOO_LIB
,则会打印一条消息,提示错误条件。
宏命令还支持定义接受变量参数列表的宏。如果你想定义一个具有可选参数或多个签名(signature)的宏,这个特性非常有用。变量参数可以使用ARGC
和ARGV0
、ARGV1
等来引用,而不是使用形参。ARGV0
表示宏的第一个参数,ARGV1
表示下一个,依此类推。你还可以使用形参和变量参数的混合形式,如下面的示例所示。
# define a macro that takes at least two arguments |
在本例中,两个必需的参数是TEST
和COMMENT
。这些必需的参数可以按名称引用,就像在本例中一样,也可以通过引用ARGV0
和ARGV1
来引用。如果希望将参数作为一个列表处理,请使用ARGV
和ARGN
变量。ARGV
(与ARGV0
、ARGV1
等相反)是宏的所有参数的列表,而ARGN
是在正式参数之后的所有参数的列表。在宏内部,可以根据需要使用foreach
命令遍历ARGV
或ARGN
。
return
命令从函数、目录或文件中返回。请注意,与函数不同,宏是就地展开的,因此不能处理返回。
2.5 正则表达式
一些CMake命令,如if
和string
,使用正则表达式或可以接受正则表达式作为参数。在最简单的形式中,正则表达式是用于搜索精确字符匹配的字符序列。然而,很多时候要找到的确切序列是未知的,或者只需要在字符串的开头或结尾匹配。由于有几个不同的约定来指定正则表达式,CMake标准在string
命令文档中有描述。
2.6 高级命令
有一些命令可能非常有用,但通常不用于编写CMakeLists文件。本节将讨论其中一些命令,以及它们的作用。
首先,考虑add_depentencies
命令,该命令在两个目标之间创建依赖关系。当目标确定目标时,CMAKE会在目标之间自动创建依赖关系。例如,CMAKE将自动为取决于库目标的可执行目标创建一个依赖关系。add_depentencies
命令通常用于指定目标之间的目标间依赖关系,其中至少一个目标是自定义目标(请参阅添加自定义命令(Add Custom Command)部分)。
include_regular_expression
命令也与依赖关系有关。该命令控制用于追踪源代码依赖性的正则表达式。默认情况下,CMAKE将跟踪源文件的所有依赖项,包括stdio.h等系统文件。如果你使用include_regular_expression
命令指定正则表达式,则该正则表达式将用于限制头文件的处理。例如,如果你的软件项目的头文件全部以前缀foo开始(例如foomain.c,foostruct.h等),则可以指定一个类似^foo.*$
的正则表达式来限制依赖检查的范围。