编写 CMakeLists 文件

本章将介绍为软件编写有效的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-variables 手册中讨论了 CMake 自动定义了许多有用的变量。这些变量以 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

可以在 CMake 中直接访问系统环境变量和 Windows 注册表值。要访问系统环境变量,请使用语法 $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 从函数返回一个值,即对 set 命令使用 PARENT_SCOPE 选项。我们可以修改先前的示例,以便 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 时将对其进行定义。相反,只有在处理 dir2 时,才会对 bar 进行定义。同样,在处理 file1.cmake 和 file2.cmake 时,将对 foo 进行定义,而只有在处理 file2.cmake 时,才会对 bar 进行定义。

命令

命令由命令名称、开始括号、用空格分隔的参数以及结束括号组成。每个命令按照其出现在 CMakeLists 文件中的顺序进行评估。有关 CMake 命令的完整列表,请参见 cmake-commands 手册。

CMake 不再对命令名称区分大小写,因此在看到 command 时,你可以使用 COMMANDCommand 作为替代。使用小写命令被认为是最佳实践。除了分隔参数外,将忽略所有空格(空格、换行符、制表符)。因此,只要命令名称和开始括号在同一行上,命令就可以跨越多行。

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

基本命令

如我们之前所见,setunset 命令显式设置或取消设置变量。 stringlistseparate_arguments 命令提供字符串和列表的基本操作。

定义要构建的可执行文件和库以及包含这些文件的文件源是 add_executableadd_library 命令的主要命令。对于 Visual Studio 项目,源文件会像往常一样显示在 IDE 中,但项目使用的任何头文件都不会显示。若要显示头文件,只需将它们添加到可执行文件或库的源文件列表即可;可以针对所有生成器完成此操作。任何不直接使用头文件的生成器(如基于 Makefile 的生成器)都会简单忽略这些头文件。

流控制

CMake 语言提供三种流控制结构,帮助组织 CMakeLists 文件,并保持其可维护性。

条件语句

我们首先考虑 if 命令。在许多方面,CMake 中的 if 命令就像任何其他语言中的 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 命令记录了它可以测试的许多条件。

循环结构

foreachwhile 命令允许您处理按顺序出现的重复性任务。 break 命令在 foreachwhile 循环正常结束之前使其退出。

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 循环的主体只有一个 CMake 命令 add_test。在 foreach 的主体中,循环变量(在本示例中为 tfile)每次被引用时,都将被替换为列表中的当前值。在第一次迭代中,${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()

过程定义

macrofunction 命令支持可能分散于 CMakeLists 文件中的重复性任务。一旦定义了宏或函数,即可使用其定义之后处理的任何 CMakeLists 文件。

CMake 中的函数非常类似于 C 或 C++ 中的函数。您可以向其中传递参数,它们会在函数中变成变量。同样,某些标准变量(如 ARGCARGVARGNARGV0ARGV1 等)是已定义的。函数调用具有动态范围。在函数中,您处于一个新的变量范围内;这类似于使用 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 的值调用 set 命令,该值为 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,那么将显示一条消息,指示错误条件。

命令还支持定义使用可变参数列表的宏。如果你想定义具有可选参数或多个签名的宏,这会很有用。可以使用 ARGCARGV0ARGV1 等来引用可变参数,而不是形式参数。 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")

在这个示例中,两个必需参数是 TESTCOMMENT。这些必需参数可以通过名称引用,就像在此示例中一样,或者通过引用 ARGV0ARGV1 来引用。如果你想将参数作为列表处理,请使用 ARGVARGN 变量。 ARGV(相对于 ARGV0ARGV1 等)是宏的所有参数的列表,而 ARGN 是形式参数后所有参数的列表。在你的宏内部,可以使用 命令来迭代 ARGVARGN

命令从函数、目录或文件返回。请注意,与函数不同,宏会直接展开,因此无法处理

正则表达式

一些 CMake 命令在使用常规表达式,或者可以将常规表达式作为参数,例如ifstring。其最简单的形式是,常规表达式是一串用于查找确切字符匹配项的字符。但是很多时候要找到的确切序列是未知的,或者只想匹配字符串的开头或结尾。由于有几种不同的惯例用于指定常规表达式,因此 CMake 的标准在 string 命令文档中描述。该描述基于 Texas Instruments 的开源常规表达式类,CMake 用它对常规表达式进行解析。

高级命令

有几个命令非常有用,但通常不用于编写 CMakeLists 文件。本节将讨论这些命令中的几个以及使用它们的时间。

首先,考虑一下add_dependencies命令,它在两个目标之间创建一个依赖关系。CMake 可以在确定依赖关系时自动在目标之间创建依赖关系。例如,CMake 将自动为依赖库目标的可执行目标创建依赖关系。add_dependencies命令通常用于指定至少一个目标是自定义目标(请参阅添加自定义命令部分)的目标之间的目标间依赖关系。

include_regular_expression命令也与依赖关系有关。此命令控制用于跟踪源代码依赖关系的常规表达式。默认情况下,CMake 将跟踪源文件的所有依赖关系,包括 stdio.h 等系统文件。如果使用include_regular_expression 命令指定了常规表达式,则将使用该常规表达式来限制处理哪些 include 文件。例如;如果您的软件项目的 include 文件全部以前缀 foo 开始(例如 fooMain.c fooStruct.h 等),您可以指定 ^foo.*$ 的常规表达式,以将依赖关系检查限制为项目的特定文件。