CMake 入门

警告

免责声明:本文档由 LLVM 项目贡献者编写,并非任何隶属于 CMake 项目的人员。本文档可能包含不准确的术语、措辞或技术细节。它以最大的善意提供。

简介

LLVM 项目和许多基于 LLVM 构建的核心项目都使用 CMake 构建。本文档旨在为修改 LLVM 项目或在 LLVM 之上构建自己的项目的开发人员提供 CMake 的简要概述。

官方 CMake 语言参考可在 cmake-language 手册页和cmake-language 在线文档中找到。

一览

CMake 是一个工具,它读取自身语言的脚本文件,这些文件描述了软件项目如何构建。当 CMake 评估脚本时,它会构建软件项目的内部表示。一旦脚本被完全处理,如果没有错误,CMake 将生成构建文件以实际构建项目。CMake 支持为各种命令行构建工具以及流行的 IDE 生成构建文件。

当用户运行 CMake 时,它会执行各种检查,类似于 autoconf 历史上的工作方式。在检查和构建描述脚本的评估期间,CMake 将值缓存到 CMakeCache 中。这很有用,因为它允许构建系统在增量开发期间跳过长时间运行的检查。CMake 缓存也有一些缺点,但这将在后面讨论。

脚本概述

CMake 的脚本语言具有非常简单的语法。每个语言构造都是一个命令,它匹配 _name_(_args_) 模式。命令主要有三种类型:语言定义的(在 CMake 的 C++ 中实现的命令)、定义函数和定义宏。CMake 发行版还包含一套 CMake 模块,其中包含有用功能的定义。

下面的示例是构建 C++ “Hello World” 程序的完整 CMake 构建。该示例仅使用 CMake 语言定义的函数。

cmake_minimum_required(VERSION 3.20.0)
project(HelloWorld)
add_executable(HelloWorld HelloWorld.cpp)

CMake 语言以 foreach 循环和 if 代码块的形式提供控制流构造。为了使上面的示例更复杂,您可以添加一个 if 代码块,以便在以 Apple 平台为目标时定义 “APPLE”

cmake_minimum_required(VERSION 3.20.0)
project(HelloWorld)
add_executable(HelloWorld HelloWorld.cpp)
if(APPLE)
  target_compile_definitions(HelloWorld PUBLIC APPLE)
endif()

变量、类型和作用域

解引用

在 CMake 中,变量是“字符串”类型的。所有变量在整个评估过程中都表示为字符串。将变量包装在 ${} 中会对其进行解引用,并导致名称被值字面替换。CMake 在其文档中将此称为“变量求值”。解引用在被调用的命令接收参数之前执行。这意味着解引用列表会导致将多个单独的参数传递给命令。

变量解引用可以嵌套,并可用于建模复杂数据。例如

set(var_name var1)
set(${var_name} foo) # same as "set(var1 foo)"
set(${${var_name}}_var bar) # same as "set(foo_var bar)"

解引用未设置的变量会导致空扩展。在 CMake 中,条件性地设置变量是一种常见的模式,因为知道它将被用于变量未设置的代码路径中。在 LLVM CMake 构建系统中,有很多这样的例子。

变量空扩展的一个例子是

if(APPLE)
  set(extra_sources Apple.cpp)
endif()
add_executable(HelloWorld HelloWorld.cpp ${extra_sources})

在此示例中,extra_sources 变量仅在您以 Apple 平台为目标时定义。对于所有其他目标,extra_sources 将在将参数提供给 add_executable 之前被评估为空。

列表

在 CMake 中,列表是以分号分隔的字符串,强烈建议您避免在列表中使用分号;它不会顺利进行。定义列表的一些示例

# Creates a list with members a, b, c, and d
set(my_list a b c d)
set(my_list "a;b;c;d")

# Creates a string "a b c d"
set(my_string "a b c d")

列表的列表

CMake 中更复杂的模式之一是列表的列表。由于列表不能包含带有分号的元素来构造列表的列表,因此您创建一个变量名称列表,这些名称引用其他列表。例如

set(list_of_lists a b c)
set(a 1 2 3)
set(b 4 5 6)
set(c 7 8 9)

使用此布局,您可以遍历列表的列表,使用以下代码打印每个值

foreach(list_name IN LISTS list_of_lists)
  foreach(value IN LISTS ${list_name})
    message(${value})
  endforeach()
endforeach()

您会注意到内部 foreach 循环的列表被双重解引用。这是因为第一次解引用将 list_name 转换为子列表的名称(在示例中为 a、b 或 c),然后第二次解引用是为了获取列表的值。

此模式在 CMake 中广泛使用,最常见的示例是编译器标志选项,CMake 使用以下变量扩展来引用它们:CMAKE_${LANGUAGE}_FLAGS 和 CMAKE_${LANGUAGE}_FLAGS_${CMAKE_BUILD_TYPE}。

其他类型

在命令行上缓存或指定的变量可以具有与之关联的类型。变量的类型由 CMake 的 UI 工具用于显示正确的输入字段。变量的类型通常不影响评估,但是 CMake 确实对某些变量(例如 PATH)有特殊的处理。您可以在 CMake 的 set 文档中阅读有关特殊处理的更多信息。

作用域

CMake 本质上具有基于目录的作用域。在 CMakeLists 文件中设置变量,将为该文件和所有子目录设置变量。在 CMakeLists 文件中包含的 CMake 模块中设置的变量将在它们包含的作用域及其所有子目录中设置。

当在子目录中再次设置已设置的变量时,它会覆盖该作用域和任何更深层子目录中的值。

CMake set 命令提供了两个与作用域相关的选项。PARENT_SCOPE 将变量设置到父作用域,而不是当前作用域。CACHE 选项将变量设置在 CMakeCache 中,这导致它在所有作用域中设置。除非指定 FORCE 选项,否则 CACHE 选项不会设置 CMakeCache 中已存在的变量。

除了基于目录的作用域之外,CMake 函数也有自己的作用域。这意味着在函数内部设置的变量不会泄漏到父作用域中。宏并非如此,因此 LLVM 在合理的情况下更喜欢函数而不是宏。

注意

与基于 C 的语言不同,CMake 的循环和控制流代码块没有自己的作用域。

控制流

CMake 具有您在任何脚本语言中期望看到的相同基本控制流构造,但有一些怪癖,因为与 CMake 中的所有内容一样,控制流构造都是命令。

If、ElseIf、Else

注意

有关 CMake if 命令的完整文档,请访问此处。该资源更加完整。

通常,CMake if 代码块的工作方式与您期望的方式相同

if(<condition>)
  message("do stuff")
elseif(<condition>)
  message("do other stuff")
else()
  message("do other other stuff")
endif()

从 C 背景来看,关于 CMake 的 if 代码块,最重要的一点是它们没有自己的作用域。在条件代码块内设置的变量在 endif() 之后仍然存在。

循环

CMake foreach 代码块的最常见形式是

foreach(var ...)
  message("do stuff")
endforeach()

foreach 代码块的 variable 参数部分可以包含解引用的列表、要迭代的值或两者的混合

foreach(var foo bar baz)
  message(${var})
endforeach()
# prints:
#  foo
#  bar
#  baz

set(my_list 1 2 3)
foreach(var ${my_list})
  message(${var})
endforeach()
# prints:
#  1
#  2
#  3

foreach(var ${my_list} out_of_bounds)
  message(${var})
endforeach()
# prints:
#  1
#  2
#  3
#  out_of_bounds

还有一种更现代的 CMake foreach 语法。以下代码等效于上面的代码

foreach(var IN ITEMS foo bar baz)
  message(${var})
endforeach()
# prints:
#  foo
#  bar
#  baz

set(my_list 1 2 3)
foreach(var IN LISTS my_list)
  message(${var})
endforeach()
# prints:
#  1
#  2
#  3

foreach(var IN LISTS my_list ITEMS out_of_bounds)
  message(${var})
endforeach()
# prints:
#  1
#  2
#  3
#  out_of_bounds

与条件语句类似,这些语句通常按您期望的方式运行,并且它们没有自己的作用域。

CMake 还支持 while 循环,尽管它们在 LLVM 中没有广泛使用。

模块、函数和宏

模块

模块是 CMake 实现代码重用的工具。CMake 模块只是 CMake 脚本文件。它们可以包含在包含时执行的代码以及命令的定义。

在 CMake 中,宏和函数通常被称为命令,它们是定义可以多次调用的代码的主要方法。

在 LLVM 中,我们有几个 CMake 模块,作为我们发行版的一部分包含在内,供不从源代码构建我们项目的开发人员使用。这些模块是使用 CMake 构建基于 LLVM 的项目所需的基本组件。我们还依靠模块作为组织构建系统功能的方式,以实现 LLVM 项目中的可维护性和重用。

参数处理

在定义 CMake 命令时,处理参数非常有用。本节中的示例都将使用 CMake function 代码块,但这都适用于 macro 代码块。

CMake 命令可以具有在每个调用站点都需要的命名参数。此外,所有命令都将隐式接受可变数量的额外参数(用 C 语言来说,所有命令都是 varargs 函数)。当使用额外的参数(超出命名参数)调用命令时,CMake 会将所有参数(命名和未命名)的完整列表存储在名为 ARGV 的列表中,并将未命名参数的子列表存储在 ARGN 中。下面是为 CMake 的内置函数 add_dependencies 提供包装器函数的简单示例。

function(add_deps target)
  add_dependencies(${target} ${ARGN})
endfunction()

此示例定义了一个名为 add_deps 的新宏,它接受必需的第一个参数,并且只调用另一个函数,传递第一个参数和所有尾随参数。

CMake 提供了一个模块 CMakeParseArguments,它提供了高级参数解析的实现。我们在 LLVM 中广泛使用它,并且建议任何具有基于复杂参数的行为或可选参数的函数使用它。CMake 关于该模块的官方文档在 cmake-modules 手册页中,也可在 cmake-modules 在线文档中找到。

注意

从 CMake 3.5 开始,cmake_parse_arguments 命令已成为本机命令,CMakeParseArguments 模块为空,仅为兼容性而保留。

函数 vs 宏

函数和宏在使用方式上看起来非常相似,但两者之间存在一个根本区别。函数有自己的作用域,而宏没有。这意味着在宏中设置的变量将泄漏到调用作用域中。这使得宏仅适用于定义非常小的功能片段。

CMake 函数和宏之间的另一个区别是如何传递参数。宏的参数未设置为变量,而是在执行宏之前解析对参数的解引用。如果使用未引用的变量,这可能会导致一些意外行为。例如

macro(print_list my_list)
  foreach(var IN LISTS my_list)
    message("${var}")
  endforeach()
endmacro()

set(my_list a b c d)
set(my_list_of_numbers 1 2 3 4)
print_list(my_list_of_numbers)
# prints:
# a
# b
# c
# d

一般来说,这个问题并不常见,因为它需要使用名称在父作用域中重叠的未解引用变量,但重要的是要注意到这一点,因为它可能导致细微的错误。

LLVM 项目包装器

LLVM 项目为关键的 CMake 内置命令提供了许多包装器。我们使用这些包装器来提供跨 LLVM 组件的一致行为并减少代码重复。

我们通常(但并非总是)遵循以 llvm_ 开头的命令仅用作其他命令的构建块的约定。旨在直接使用的包装器命令通常以项目名称在命令名称中间的方式命名(即,add_llvm_executableadd_executable 的包装器)。LLVM add_* 包装器函数都在 AddLLVM.cmake 中定义,该文件作为 LLVM 发行版的一部分安装。任何需要 LLVM 的 LLVM 子项目都可以包含和使用它。

注意

并非所有 LLVM 项目在所有用例中都需要 LLVM。例如,compiler-rt 可以在没有 LLVM 的情况下构建,并且 compiler-rt sanitizer 库与 GCC 一起使用。

有用的内置命令

CMake 有很多有用的内置命令。本文档不会详细介绍它们,因为 CMake 项目有优秀的文档。要突出显示一些有用的函数,请参阅

CMake 命令的完整文档在 cmake-commands 手册页中,也可在 CMake 网站上找到