CMake 入门

警告

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

简介

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

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

10,000 英尺视角

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})

在此示例中,仅当您针对 Apple 平台时才会定义 extra_sources 变量。对于所有其他目标,在 add_executable 获取其参数之前,extra_sources 将被评估为空。

列表

在 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 块的变量参数部分可以包含间接引用的列表、要迭代的值或两者的混合

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 模块为空,仅保留以保持兼容性。

函数与宏

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

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 编译器运行时库也与 GCC 一起使用。

有用的内置命令

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

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