这是每个 CMakeLists.txt
都必须包含的第一行:
1 | cmake_minimum_required(VERSION 3.1) # 指定该工程最低支持的版本号 |
从 CMake 3.12 开始,版本号可以声明为一个范围,例如 VERSION 3.1...3.15
;这意味着这个工程最低可以支持 3.1 版本,但是也最高在 3.15 版本上测试成功过。
当你开始一个新项目,起始推荐这么写:
1 | cmake_minimum_required(VERSION 3.7...3.21) |
如果 CMake 的版本低于 3.12,CMake 将会被设置为当前版本;否则,将会遵守 cmake_minimum_required
中的规定,程序将继续正常运行。
接下来,每一个顶层 CMakelists.txt
文件都应该加入下面这一行:
1 | project(MyProject |
现在我们看到了更多的语法。项目名称是这里的第一个参数。所有的关键字参数都可选的。语言可以是 C, CXX, …,默认是 C 和 CXX。
项目名称就没有什么特别要注意的。目前为止,我们还没有添加任何的目标 (target)。
1 | $ grep "VERSION" ./CMakeCache.txt |
1 | add_executable(one two.cpp three.h) |
这里有一些语法需要解释。one 既是生成的可执行文件的名称,也是创建的 CMake 目标 (target) 的名称。紧接着是源文件的列表,你想列多少个都可以。
CMake 很聪明,它根据拓展名只编译源文件。在大多数情况下,头文件将会被忽略;列出它们的唯一原因是为了让它们在 IDE 中被展示出来,目标文件在许多 IDE 中被显示为文件夹。
制作一个库是通过 add_library
命令完成的,并且非常简单:
1 | add_library(one STATIC two.cpp three.h) |
你可以选择库的类型,可以是 STATIC
, SHARED
, MODULE
。如果你不选择它,CMake 将会通过 BUILD_SHARED_LIBS
的值来选择构建 STATIC
还是 SHARED
类型的库。
你经常需要生成一个 虚构的目标,也就是说,一个不需要编译的目标。例如,只有一个头文件的库。这被叫做 INTERFACE
库,这是另一种选择,和上面唯一的区别是后面不能有文件名。
【虚拟目标示例 - 只有一个头文件的库:目录树】
1 | $ tree ./interface-lib/ --dirsfirst -a |
【虚拟目标示例 - 只有一个头文件的库:CMakeLists.txt】
1 | cmake_minimum_required(VERSION 3.10.2) |
【虚拟目标示例 - 只有一个头文件的库:源代码】
1 | // $ cat include/only_header_lib/utils.h |
【虚拟目标示例 - 一个虚拟的编译选项目标:CMakeLists.txt】
1 | cmake_minimum_required(VERSION 3.10.2) |
现在我们已经指定了一个目标,那我们如何添加关于它的信息呢?例如,它可能需要包含一个目录:
1 | target_include_directories(one PUBLIC "include") # 为目标添加了一个目录 |
PUBLIC
对于一个可执行文件目标没有什么含义;但对于库来说,它让 CMake 知道,任何链接到这个目标的目标也必须包含这个目录。其他选项还有 PRIVATE
(只影响当前目标,不影响依赖),以及 INTERFACE
(只影响依赖)。
接下来,我们可以将目标之间链接起来:
1 | add_library(another STATIC another.c another.h) |
target_link_libraries
可能是 CMake 中最有用也最令人迷惑的命令。这个命令需要指定一个目标 another,并且在给出该目标的名字(another)后为此目标添加一个依赖 one。
如果 CMake 项目中不存在名称为 one 的目标(没有定义该 target),那它会直接添加名字为 one 的库到依赖中(一般而言,会去 /usr、CMake 项目指定寻找库的路径等所有能找的路径找到叫 one 的库——译者注)(这也是命令叫 target_link_libraries
的原因)。或者你可以给定一个库的完整路径,或者是链接器标志。
链接的目标可以有包含的目录、链接库(或链接目标)、编译选项、编译定义、编译特性等等。
看看你是否能理解以下文件。它生成了一个简单的 C11 的库并且在程序中使用了它。没有依赖。代码中使用的是 CMake 3.8。
1 | cmake_minimum_required(VERSION 3.8) |
获取一个目录的父目录(上级目录):
1 | get_filename_component(PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY) |
目录存在性检查:
1 | if(EXISTS "${PARENT_DIR}"/testfile.txt) |
我们首先讨论变量。你可以这样声明一个本地 (local) 变量:
1 | set(MY_VARIABLE "value") |
变量名通常全部用大写,变量值跟在其后。你可以通过 ${}
来解析一个变量,例如 ${MY_VARIABLE}
。
CMake 有作用域的概念,在声明一个变量后,你只可以在它的作用域内访问这个变量。如果你将一个函数或一个文件放到一个子目录中,这个变量将不再被定义。你可以通过在变量声明末尾添加
PARENT_SCOPE
来将它的作用域指定为当前的上一级作用域。
列表就是简单地包含一系列变量:
1 | set(MY_LIST "one" "two" "three") # 也可以这样写: set(MY_LIST "one;two;three") |
有一些和列表进行协同的命令,separate_arguments
可以把一个以空格分隔的字符串分割成一个列表。当一个变量用 ${}
括起来的时候,空格的解析规则和上述相同。
1 | set(ARGS_STRING "hello world 'CMake example' 123") |
对于路径来说要特别小心,路径很有可能会包含空格,因此你应该总是将解析变量得到的值用引号括起来,也就是,应该这样
"${MY_PATH}"
。
CMake 提供了一个缓存变量,来允许你从命令行中设置变量。CMake 中已经有一些预置的变量,像 CMAKE_BUILD_TYPE
。如果一个变量还没有被定义,你可以这样声明并设置它。
1 | set(CACHE_VAR "VALUE" CACHE STRING "Description info for the cache variable") |
这么写不会覆盖 CMakeCache.txt
中已定义的值——这是为了让你只能在命令行中设置这些变量,而不会在 CMake 文件执行的时候被重新覆盖。这是什么意思呢?
1 | $ cmake -L . # 不设置该缓存值(执行时还不存在 CMakeCache.txt 文件) |
如果你想把这些变量作为一个临时的全局变量,你可以这样做:
1 | set(CACHE_VAR_FORCE "VALUE_FORCE" CACHE STRING "" FORCE) |
【示例 - 缓存变量:CMakeLists.txt】:
1 | set(CACHE_VAR "VALUE" CACHE STRING "Description info for the cache variable") |
【示例 - 缓存变量:cmake 输出】:
1 | # 每次都从命令行设置缓存变量的值,就不会使用 CMakeCache.txt 中已经缓存的旧值了 |
你也可以通过 set(ENV{variable_name} value)
和 $ENV{variable_name}
来设置和获取环境变量,不过一般来说,我们最好避免这么用。
缓存实际上就是个文本文件(CMakeCache.txt),当你运行 CMake 构建目录时会创建它。 CMake 可以通过它来记住你设置的所有东西,因此你可以在不重新运行 CMake 的情况下,再次列出所有的选项。
CMake 也可以通过属性来存储信息。这就像是一个变量,但它被附加到一些其他的物体 (item) 上,像是一个目录或者是一个目标。
一个全局的属性可以是一个有用的、非缓存的全局变量。许多目标属性都是被以 CMAKE_
为前缀的变量来初始化的。例如你设置 CMAKE_CXX_STANDARD
这个变量,这意味着你之后创建的所有目标的 CXX_STANDARD
都将被设为 CMAKE_CXX_STANDARD
变量的值。
你可以这样来设置属性:
1 | # 该方法可以一次性设置多个目标、文件 |
第一种方式更加通用 (general) ,它可以一次性设置多个目标、文件、或测试,并且有一些非常有用的选项。第二种方式是为一个目标设置多个属性的快捷方式。此外,你可以通过类似于下面的方式来获得属性:
1 | get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD) |
如果你的 CMake 版本不大于 3.1:
1 | if(variable) |
如果你的 CMake 版本大于 3.1 ,那么你也可以这么写:
1 | if("${variable}") |
这里还有一些关键字可以设置,例如:
NOT
, TARGET
, EXISTS
(文件), DEFINED
等。STREQUAL
, AND
, OR
, MATCHES
(正则表达式), VERSION_LESS
, VERSION_LESS_EQUAL
(CMake 3.7+) 等。CMake 函数和宏只有作用域上存在区别,宏没有作用域的限制。所以说,如果你想让函数中定义的变量对外部可见,你需要使用 PARENT_SCOPE
来改变其作用域。
如果是在嵌套函数中,这会变得异常繁琐,因为你必须在想要变量对外的可见的所有函数中添加 PARENT_SCOPE
标志。但是这样也有好处,函数不会像宏那样对外“泄漏”所有的变量。接下来用函数举一个例子:
1 | function(SIMPLE REQUIRED_ARG) |
输出如下:
1 | -- Simple arguments: This, followed by Foo;Bar |
如果你想要有一个指定的参数,你应该在列表中明确的列出,除此之外的所有参数都会被存储在 ARGN
这个变量中( ARGV
中存储了所有的变量,包括你明确列出的 )。
CMake 的函数没有返回值,你可以通过设定变量值的形式来达到同样地目的。在上面的例子中,你可以通过指定变量名来设置一个变量的值。
CMake 拥有一个变量命名系统。你可以通过 cmake_parse_arguments
函数来对变量进行命名与解析。如果你想在低于 3.5 版本的 CMake 系统中使用它,你应该包含 CMakeParseArguments
模块,此函数在 CMake 3.5 之前一直存在于上述模块中。这是使用它的一个例子:
1 | function(COMPLEX) |
在调用这个函数后,会生成以下变量:
1 | COMPLEX_PREFIX_SINGLE = TRUE |
你可以通过 set
来避免在 list 中使用分号。此外,其他剩余的参数(因此参数的指定是可选的)都会被保存在 COMPLEX_PREFIX_UNPARSED_ARGUMENTS
变量中。
CMake 允许你在代码中使用 configure_file
来访问 CMake 变量。该命令将一个文件( 一般以 .in
结尾 )的内容复制到另一个文件中,并替换其中它找到的所有 CMake 变量。如果你想要在你的输入文件中避免替换掉使用 ${}
包含的内容,你可以使用 @ONLY
关键字。
这个功能在 CMake 中使用的相当频繁,例如在下面的 Version.h.in
中:
Version.h.in:
1 |
|
CMake lines:
1 | cmake_minimum_required(VERSION 3.22.1) |
cmake 后在 include 目录下生成的 Version.h 头文件:
1 |
|
总结:
场景 | 用法 | 示例输入 | 示例输出 |
---|---|---|---|
普通变量替换 | @VAR@ |
"@PROJECT_NAME@" |
"MyApp" |
条件定义 | #cmakedefine VAR |
#cmakedefine FOO |
#define FOO 或 /* #undef FOO */ |
强制 0/1 定义 | #cmakedefine01 VAR |
#cmakedefine01 BAR |
#define BAR 1 或 #define BAR 0 |
避免 ${} 替换 |
@ONLY 选项 |
${SHELL_VAR} |
保留 ${SHELL_VAR} |
另外一个方向也是行得通的, 你也可以从源文件中读取一些东西(例如版本号)。例如,你有一个仅包含头文件的库,你想要其在无论有无 CMake 的情况下都可以使用,上述方式将是你处理版本的最优方案。可以像下面这么写:
1 | # Assuming the canonical version is listed in a single line |
如上所示,file(STRINGS file_name variable_name REGEX regex)
选择了与正则表达式相匹配的行,并且使用了相同的正则表达式来匹配出其中版本号的部分。
这是一个简单、完整并且合理的 CMakeLists.txt
的例子。对于这个程序,我们有一个带有头文件与源文件的库文件(MyLibExample),以及一个带有源文件的应用程序(MyExample)。
目录树结构:
1 | $ tree ./simple-project/ --dirsfirst -a |
CMakeLists.txt:
1 | # CMake simple example |
其它源文件的代码:
1 | // $ cat simple_lib.c |
创建构建目录并编译:
1 | $ mkdir build && cd build |
生成静态库的可执行文件执行方式:
1 | $ cd output && ./bin/MyExample |
生成动态库的可执行文件执行方式:
1 | $ export LD_LIBRARY_PATH=/home/simple-project/build/output/lib/:${LD_LIBRARY_PATH} |
下面的说法可能存在一些偏见,但我认为这是一种好的组织方式。我将会讲解如何组织项目的目录结构,这是基于以往的惯例来写的,这么做对你有以下好处:
首先,如果你创建一个名为 project 的项目,它有一个名为 lib 的库,有一个名为 app 的可执行文件,那么目录结构应该如下所示:
1 | $ tree ./project/ --dirsfirst -a |
其中,文件的名称不是绝对的,并且应用程序所在的文件夹可能为其他的名称(或者一个项目只有库文件)。你也许也会看到 cmake 文件夹用于存储如 Find<library>.cmake
这样的 .cmake 辅助文件。但是一些比较基础的东西都在上面包括了。
可以注意到一些很明显的问题, CMakeLists.txt
文件被分割到除了 include
目录外的所有源代码目录下。这是为了能够将 include
目录下的所有文件拷贝到 /usr/include
目录或其他类似的目录下,因此为了避免冲突等问题,其中不能有除了头文件外的其他文件。这也是为什么在 include
目录下有一个名为项目名的目录。顶层 CMakeLists.txt
中应使用 add_subdirectory
命令来添加一个包含 CMakeLists.txt
的子目录。
你经常会需要一个 cmake 文件夹,里面包含所有用到的辅助模块。你可以通过以下语句将 cmake 目录添加到你的 CMake Path 中:
1 | set(CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH}) |
你应该在 .gitignore
中添加形如 build*
的规则,这样用户就可以在源代码目录下创建 build
目录来构建项目,而不用担心将生成的目标文件添加到 .git
中。
如果你想要避免构建目录在有效的源代码目录中,你可以在顶层 CMakeLists.txt
文件头部添加如下语句:
1 | ### Require out-of-source builds |
略。
CMake 通常会设置一个 “既不是 Release 也不是 Debug” 的空构建类型来作为默认的构建类型,如果你想要自己设置默认的构建类型,可以参考如下方法:
1 | # Set a default build type if none was specified |
当然,如果你已经知道用什么构建类型了,你也可以在命令行中指定:如 cmake -DCMAKE_BUILD_TYPE=Debug ..
。
这是支持 C 标准的一种方式,(在目标及全局级别)设置三个特定属性的值。这是全局的属性:
1 | # 将 C 标准设为缓存变量,允许用户通过 -D 覆盖 |
上述前三条配置,可以在最终包(即可执行程序,如 main()
入口的 final package)中使用,但不推荐在库中使用。原因如下:库通常需要兼容不同调用方的 C 标准,硬编码 CMAKE_C_STANDARD
会限制库的灵活性。
target_compile_features
(见下文)。1 | # 库的 CMakeLists.txt(不硬编码标准) |
你也可以对目标来设置这些属性:
1 | set_target_properties(myTarget PROPERTIES |
这种方式相比于上面来说更好,但是仍然没法对 PRIVATE
和 INTERFACE
目标的属性有明确的控制,所以他们也仍然只对最终目标 (final targets) 有用。
如何换一种方式,在 C 代码中使用了 GNU 特性呢?
1 | # 全局启用 gnu11 |
用标志 -fPIC
来设置这个是最常见的。大部分情况下,你不需要去显式地声明它的值。CMake 将会在 SHARED
以及 MODULE
类型的库中自动的包含此标志。如果你需要显式地声明,可以这么写:
1 | set(CMAKE_POSITION_INDEPENDENT_CODE ON) |
这样会对全局的目标进行此设置,或者可以这么写:
1 | set_target_properties(lib1 PROPERTIES POSITION_INDEPENDENT_CODE ON) |
来对某个目标进行设置是否开启此标志。
首先,让我们来盘点一下调试 CMakeLists 和其他 CMake 文件的方法。
通常我们使用的打印语句如下:
1 | message(STATUS "MY_VARIABLE=${MY_VARIABLE}") |
然而,通过一个内置的模组 CMakePrintHelpoers
可以更方便的打印变量:
1 | include(CMakePrintHelpers) |
如果你只是想要打印一个变量,那么上述方法已经很好用了!如果你想要打印一些关于某些目标 (或者是其他拥有变量的项目,比如 SOURCES
、DIRECTORIES
、TESTS
, 或 CACHE_ENTRIES
- 全局变量好像因为某些原因缺失了) 的变量,与其一个一个打印它们,你可以简单的列举并打印它们:
1 | cmake_print_properties( |
你可能想知道构建项目的时候你的 CMake 文件究竟发生了什么,以及这些都是如何发生的?用 --trace-source="filename"
就很不错,它会打印出你指定的文件现在运行到哪一行,让你可以知道当前具体在发生什么。另外还有一些类似的选项,但这些命令通常给出一大堆输出,让你找不着头脑。
例子:
1 | cmake -S . -B build --trace-source=CMakeLists.txt |
如果你添加了 --trace-expand
选项,变量会直接展开成它们的值。
对于单一构建模式的生成器 (single-configuration generators),你可以使用参数 -DCMAKE_BUILD_TYPE=Debug
来构建项目,以获得调试标志 (debugging flags)。对于支持多个构建模式的生成器 (multi-configuration generators),像是多数 IDE,你可以在 IDE 里打开调试模式。这种模式有不同的标志(变量以 _DEBUG
结尾,而不是 _RELEASE
结尾),以及生成器表达式的值 CONFIG:Debug
或 CONFIG:Release
。
如果你使用了 debug 模式构建,你就可以在上面运行调试器了,比如 gdb 或 lldb。