本章将介绍为您的软件编写有效的 CMakeLists 文件的基础知识。它将涵盖处理大多数项目所需的基本命令和问题。虽然 CMake 可以处理极其复杂的项目,但对于大多数项目,你会发现本章的内容会告诉你所有你需要知道的。CMake 由为软件项目编写的 CMakeLists.txt 文件驱动。CMakeLists 文件确定从向用户呈现哪些选项到要编译哪些源文件的所有内容。除了讨论如何编写 CMakeLists 文件之外,本章还将介绍如何使它们变得健壮和可维护。
编写CMakeLists文件
CMakeLists 文件几乎可以在任何文本编辑器中进行编辑。一些编辑器,例如 Notepad++,内置了 CMake 语法高亮和缩进支持。对于 Emacs 或 Vim 等编辑器,CMake 包括缩进和语法高亮模式。这些可以在Auxiliary源代码分发的目录中找到,或者从 CMake下载页面下载。
在任何受支持的生成器(Makefile、Visual Studio 等)中,如果您编辑 CMakeLists 文件并重新构建,则有一些规则会根据需要自动调用 CMake 来更新生成的文件(例如 Makefile 或项目文件)。这有助于确保您生成的文件始终与您的 CMakeLists 文件同步。
CMake语言
CMake 语言由注释、命令和变量组成。
注释
注释从#开始并一直运行到行尾。见 cmake-language手册了解更多详情。
变量
CMakeLists 文件使用变量很像任何编程语言。CMake 变量名称区分大小写,并且只能包含字母数字字符和下划线。
许多有用的变量由 CMake 自动定义,并在 cmake-variables手动的。这些变量以CMAKE_. 对于特定于您的项目的变量,请避免使用这种命名约定(理想情况下,建立您自己的命名约定)
尽管有时可能将它们解释为其他类型,但所有 CMake 变量都在内部存储为字符串。
使用set命令来设置变量值。以最简单的形式,第一个参数set是变量的名称,其余参数是值。多个值参数被打包到分号分隔的列表中,并作为字符串存储在变量中。例如:
set(Foo "") # 1 quoted arg -> value is ""
set(Foo a) # 1 unquoted arg -> value is "a"
set(Foo "a b c") # 1 quoted arg -> value is "a b c"
set(Foo a b c) # 3 unquoted args -> value is "a;b;c"
可以使用语法 ${VAR}在命令中引用变量,其中VAR是变量名。如果未定义命名变量,则将引用替换为空字符串;否则它被变量的值替换。替换是在扩展未引用的参数之前执行的,因此包含分号的变量值被拆分为零个或多个参数来代替原始的未引用参数。例如:
set(Foo a b c) # 3 unquoted args -> value is "a;b;c"
command(${Foo}) # unquoted arg replaced by a;b;c
# and expands to three arguments
command("${Foo}") # quoted arg value is "a;b;c"
set(Foo "") # 1 quoted arg -> value is empty string
command(${Foo}) # unquoted arg replaced by empty string
# and expands to zero arguments
command("${Foo}") # quoted arg value is empty string
系统环境变量和 Windows 注册表值可以直接在 CMake 中访问。要访问系统环境变量,请使用语法$ENV{VAR}. CMake 还可以使用表单语法在许多命令中引用注册表项 [HKEY_CURRENT_USER\Software\path1\path2;key],其中路径是从注册表树和键构建的。
变量范围
CMake 中的变量的作用域与大多数语言略有不同。设置变量时,当前 CMakeLists 文件或函数以及任何子目录的 CMakeLists 文件、调用的任何函数或宏以及使用include命令。当处理一个新的子目录(或调用一个函数)时,会创建一个新的变量范围,并使用调用范围内所有变量的当前值进行初始化。在子作用域中创建的任何新变量或对现有变量所做的更改都不会影响父作用域。考虑以下示例
function(foo)
message(${test}) # test is 1 here
set(test 2)
message(${test}) # test is 2 here, but only in this scope
endfunction()
set(test 1)
foo()
message(${test}) # test will still be 1 here
在某些情况下,您可能希望函数或子目录在其父范围内设置变量。CMake 有一种方法可以从函数返回值,可以通过使用 PARENT_SCOPE带有set命令。我们可以修改前面的示例,以便该函数foo在其父范围内更改 test 的值,如下所示:
function(foo)
message(${test}) # test is 1 here
set(test 2 PARENT_SCOPE)
message(${test}) # test still 1 in this scope
endfunction()
set(test 1)
foo()
message(${test}) # test will now be 2 here
CMake中的变量是按照执行顺序定义的 set命令。
考虑以下示例:
# FOO is undefined
set(FOO 1)
# FOO is now set to 1
set(FOO 0)
# FOO is now set to 0
要了解变量的范围,请考虑以下示例:
set(foo 1)
# process the dir1 subdirectory
add_subdirectory(dir1)
# include and process the commands in file1.cmake
include(file1.cmake)
set(bar 2)
# process the dir2 subdirectory
add_subdirectory(dir2)
# include and process the commands in file2.cmake
include(file2.cmake)
在这个例子中,因为变量foo是在开头定义的,所以在处理 dir1 和 dir2 时都会定义它。相反,bar只会在处理dir2时定义。同样, foo将在处理 file1.cmake 和 file2.cmake 时定义,而bar仅在处理 file2.cmake 时定义。这个例子有点神奇呀,可以细细的考虑一下
命令
命令由命令名称、左括号、空格分隔的参数和右括号组成。每个命令都按照它在 CMakeLists 文件中出现的顺序进行评估。见 cmake-commands手册以获取 CMake 命令的完整列表。
CMake 不再对命令名称区分大小写,因此在您看到command的地方,您可以使用COMMAND或是Command代替。使用小写命令被认为是最佳实践。除了分隔参数外,所有空格(空格、换行符、制表符)都将被忽略。因此,只要命令名称和左括号在同一行,命令就可以跨越多行。
CMake 命令参数以空格分隔且区分大小写。命令参数可以被引用或不被引用。带引号的参数以双引号 (") 开始和结束,并且始终只表示一个参数。值中包含的任何双引号都必须使用反斜杠进行转义。考虑对需要转义的参数使用方括号参数,请参阅 cmake-language手动的。不带引号的参数以双引号以外的任何字符开头(后来的双引号是文字),并通过在值中用分号分隔来自动扩展为零个或多个参数。例如:
command("") # 1 quoted argument
command("a b c") # 1 quoted argument
command("a;b;c") # 1 quoted argument
command("a" "b" "c") # 3 quoted arguments
command(a b c) # 3 unquoted arguments
command(a;b;c) # 1 unquoted argument expands to 3
基本命令
正如我们在前面看到的,set和unset命令显式设置或取消设置变量。string,list和 separate_arguments命令提供对字符串和列表的基本操作。
add_executable和add_library命令是用于定义要构建的可执行文件和库的主要命令,以及包含它们的源文件。对于 Visual Studio 项目,源文件将照常显示在 IDE 中,但项目使用的任何头文件都不会显示。要显示头文件,只需将它们添加到可执行文件或库的源文件列表中即可;这可以为所有发电机完成。任何不直接使用头文件的生成器(例如基于 Makefile 的生成器)将简单地忽略它们。
流的控制
CMake 语言提供了三种流控制结构来帮助组织您的 CMakeLists 文件并保持它们的可维护性。
- 条件语句(例如if)
- 循环结构(例如foreach和while)
- 程序定义(例如macro和function)
条件语句
首先,我们将考虑if命令。在许多方面, if在CMake 中的命令就像if任何其他语言的命令。它评估其表达式并使用它来执行其主体中的代码或可选地执行else条款。例如:
if(FOO)
# do something here
else()
# do something else
endif()
CMake 还支持elseif帮助顺序测试多个条件。例如
if(MSVC80)
# do something here
elseif(MSVC90)
# do something else
elseif(APPLE)
# do something else
endif()
if命令记录了它可以测试的许多条件,在documentation中找到。
循环语句
foreach和while命令允许您处理按顺序发生的重复性任务。break命令从一个foreach或者while在它正常结束之前循环。
foreach命令使您能够对列表的成员重复执行一组 CMake 命令。考虑以下改编自 VTK 的示例
foreach(tfile
TestAnisotropicDiffusion2D
TestButterworthLowPass
TestButterworthHighPass
TestCityBlockDistance
TestConvolve
)
add_test(${tfile}-image ${VTK_EXECUTABLE}
${VTK_SOURCE_DIR}/Tests/rtImageTest.tcl
${VTK_SOURCE_DIR}/Tests/${tfile}.tcl
-D ${VTK_DATA_ROOT}
-V Baseline/Imaging/${tfile}.png
-A ${VTK_SOURCE_DIR}/Wrapping/Tcl
)
endforeach()
foreach命令的第一个参数是循环变量的名称,它将在循环的每次迭代中采用不同的值;其余参数是循环的值列表。在本个示例当中,foreach命令的循环主体为一个add_test的命令。在foreach主体中,每次tfile引用循环变量(在此示例中)时,都将替换为列表中的当前值。在第一次迭代中,出现的
t
f
i
l
e
将替换为
T
e
s
t
A
n
i
s
o
t
r
o
p
i
c
D
i
f
f
u
s
i
o
n
2
D
。在下一次迭代中,
{tfile}将替换为 TestAnisotropicDiffusion2D。在下一次迭代中,
tfile将替换为TestAnisotropicDiffusion2D。在下一次迭代中,{tfile} 将替换为TestButterworthLowPass.这foreach循环将继续循环,直到处理完所有参数。
值得一提的是foreach循环可以嵌套,并且循环变量在任何其他变量扩展之前被替换。这意味着在foreach循环一个主体中,您可以使用循环变量构造变量名称。在下面的代码中,循环变量tfile被展开,然后与 _TEST_RESULT串联. 然后扩展新变量名并测试它是否匹配FAILED
if(${${tfile}}_TEST_RESULT} MATCHES FAILED)
message("Test ${tfile} failed.")
endif()
while命令提供基于测试条件的循环。测试表达式的格式while命令是一样的if命令,如前所述。考虑以下 CTest 使用的示例。请注意,CTest 会在 CTEST_ELAPSED_TIME内部更新值
。
#####################################################
# run paraview and ctest test dashboards for 6 hours
#
while(${CTEST_ELAPSED_TIME} LESS 36000)
set(START_TIME ${CTEST_ELAPSED_TIME})
ctest_run_script("dash1_ParaView_vs71continuous.cmake")
ctest_run_script("dash1_cmake_vs71continuous.cmake")
endwhile()
程序(函数)定义
这macro和function命令支持可能分散在 CMakeLists 文件中的重复性任务。一旦定义了宏或函数,它就可以被定义后处理的任何 CMakeLists 文件使用。
CMake 中的函数非常类似于 C 或 C++ 中的函数。您可以将参数传递给它,它们将成为函数中的变量。同样,定义了一些标准变量,如ARGC、 ARGV、ARGN和ARGV0、ARGV1等。函数调用具有动态范围。在一个函数中,您处于一个新的变量范围内;这就像你如何使用add_subdirectory命令并且在一个新的变量范围内。调用函数时定义的所有变量都保持定义,但对变量或新变量的任何更改仅存在于函数内。当函数返回时,这些变量将消失。更简单地说:当您调用一个函数时,会推送一个新的变量范围;当它返回时,该变量范围被弹出。
function命令 定义了一个新函数。第一个参数是要定义的函数的名称;所有附加参数都是函数的形式参数。
function(DetermineTime _time)
# pass the result up to whatever invoked this
set(${_time} "1:23:45" PARENT_SCOPE)
endfunction()
# now use the function we just defined
DetermineTime(current_time)
if(DEFINED current_time)
message(STATUS "The time is now: ${current_time}")
endif()
注意在本例中,_time用于传递返回变量的名称。这set使用 的值调用命令 _time,这将是current_time。最后,set 命令使用该PARENT_SCOPE选项在调用者的范围内而不是本地范围内设置变量。
宏的定义和调用方式与函数相同。主要区别在于宏不会推送和弹出新的变量范围,并且宏的参数不被视为变量,而是在执行之前被替换为字符串。这非常类似于 C 或 C++ 中宏和函数之间的区别。第一个参数是要创建的宏的名称;所有附加参数都是宏的形式参数。
# define a simple macro
macro(assert TEST COMMENT)
if(NOT ${TEST})
message("Assertion failed: ${COMMENT}")
endif()
endmacro()
# use the macro
find_library(FOO_LIB foo /usr/local/lib)
assert(${FOO_LIB} "Unable to find library foo")
上面的简单示例创建了一个名为assert. 宏定义为两个参数;第一个是要测试的值,第二个是测试失败时要打印的注释。宏的主体很简单if命令与message里面的命令。宏主体结束时endmacro找到命令。可以简单地通过使用它的名称来调用宏,就好像它是一个命令一样。在上面的示例中,如果FOO_LIB未找到,则会显示一条消息,指示错误情况。
这macro命令还支持定义采用可变参数列表的宏。如果您要定义具有可选参数或多个签名的宏,这可能很有用。ARGC可以使用and ARGV0,等来引用变量参数ARGV1,而不是形式参数。ARGV0表示宏的第一个参数;ARGV1代表下一个,依此类推。您还可以混合使用形式参数和可变参数,如下例所示。
# define a macro that takes at least two arguments
# (the formal arguments) plus an optional third argument
macro(assert TEST COMMENT)
if(NOT ${TEST})
message("Assertion failed: ${COMMENT}")
# if called with three arguments then also write the
# message to a file specified as the third argument
if(${ARGC} MATCHES 3)
file(APPEND ${ARGV2} "Assertion failed: ${COMMENT}")
endif()
endif()
endmacro()
# use the macro
find_library(FOO_LIB foo /usr/local/lib)
assert(${FOO_LIB} "Unable to find library foo")
在此示例中,两个必需的参数是TEST和 COMMENT。这些必需的参数可以通过名称来引用,就像在本例中一样,也可以通过引用ARGV0and 来引用ARGV1。如果要将参数作为列表处理,请使用 ARGV和ARGN变量。ARGV(与 , 等相反ARGV0) ARGV1是宏 ARGN的所有参数的列表,而是形式参数之后的所有参数的列表。在您的宏中,您可以使用foreach命令迭代ARGV或ARGN根据需要。
return命令从函数、目录或文件返回。请注意,与函数不同,宏是在原地展开的,因此无法处理return.
正则的表达
一些 CMake 命令,例如if和string, 使用正则表达式或可以将正则表达式作为参数。在最简单的形式中,正则表达式是用于搜索精确字符匹配的字符序列。但是,很多时候要找到的确切序列是未知的,或者只需要在字符串的开头或结尾进行匹配。由于指定正则表达式有几种不同的约定,因此 CMake 的标准在string命令文档。该描述基于 Texas Instruments 的开源正则表达式类,CMake 使用该类来解析正则表达式。
高级命令
有一些命令可能非常有用,但通常不用于编写 CMakeLists 文件。本节将讨论其中一些命令以及它们何时有用。
首先,考虑add_dependencies在两个目标之间创建依赖关系的命令。当 CMake 可以确定目标时,它会自动创建目标之间的依赖关系。例如,CMake 将自动为依赖于库目标的可执行目标创建依赖关系。add_dependencies命令通常用于指定目标之间的目标间依赖关系,其中至少一个目标是自定义目标(请参阅添加自定义命令部分)
include_regular_expression命令也与依赖关系有关。此命令控制用于跟踪源代码依赖关系的正则表达式。默认情况下,CMake 将跟踪源文件的所有依赖项,包括系统文件,例如stdio.h. 如果您使用 include_regular_expression命令,该正则表达式将用于限制处理哪些包含文件。例如; 如果您的软件项目的包含文件都以前缀 foo 开头(例如,等),您可以指定正则表达式 of以将依赖项检查限制为仅对项目的文件进行检查。