基础知识简介

最基础知识

最低版本要求

这是每个 CMakeLists.txt 都必须包含的第一行:

1
cmake_minimum_required(VERSION 3.1) # 指定该工程最低支持的版本号

从 CMake 3.12 开始,版本号可以声明为一个范围,例如 VERSION 3.1...3.15;这意味着这个工程最低可以支持 3.1 版本,但是也最高在 3.15 版本上测试成功过。

当你开始一个新项目,起始推荐这么写:

1
2
3
4
5
cmake_minimum_required(VERSION 3.7...3.21)

if(${CMAKE_VERSION} VERSION_LESS 3.12) # CMake 3.12 开始支持版本范围
cmake_policy(VERSION ${CMAKE_MAJOR_VERSION}.${CMAKE_MINOR_VERSION})
endif()

如果 CMake 的版本低于 3.12,CMake 将会被设置为当前版本;否则,将会遵守 cmake_minimum_required 中的规定,程序将继续正常运行。

设置一个项目

接下来,每一个顶层 CMakelists.txt 文件都应该加入下面这一行:

1
2
3
4
project(MyProject
VERSION 1.6.2
DESCRIPTION "Very nice project"
LANGUAGES C)

现在我们看到了更多的语法。项目名称是这里的第一个参数。所有的关键字参数都可选的。语言可以是 C, CXX, …,默认是 C 和 CXX。

项目名称就没有什么特别要注意的。目前为止,我们还没有添加任何的目标 (target)。

1
2
3
4
5
6
7
8
$ grep "VERSION" ./CMakeCache.txt
CMAKE_PROJECT_VERSION:STATIC=1.6.2 # 完整版本号
CMAKE_PROJECT_VERSION_MAJOR:STATIC=1 # 主版本号,重大变更或可能不兼容 API 修改
CMAKE_PROJECT_VERSION_NINOR:STATIC=6 # 次版本号,向后兼容的功能新增或改进
CMAKE_PROJECT_VERSION_PATCH:STATIC=2 # 修订号,向后兼容的问题修复
CMAKE_PROJECT_VERSION_TWEAK:STATIC= # 微调整号,可选,极小调整
$ grep "MyProject" ./CMakeCache.txt
CMAKE_PROJECT_NAME:STATIC=MyProject

生成一个可执行文件

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
2
3
4
5
6
7
8
$ tree ./interface-lib/ --dirsfirst -a
./interface-lib/
├── app
│   └── main.c
├── include
│   └── only_header_lib # 仅包含头文件的库
│   └── utils.h
└── CMakeLists.txt

【虚拟目标示例 - 只有一个头文件的库:CMakeLists.txt】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
cmake_minimum_required(VERSION 3.10.2)
project(learn-cmake-interface-lib
VERSION 1.0.0
DESCRIPTION "good good study, day day up."
LANGUAGES C)

# 创建一个 INTERFACE 库,它不会编译生成任何 .so 或 .a 文件,仅用于向其它依赖传递属性
add_library(only_header_lib INTERFACE)
# 添加头文件目录到 INTERFACE 属性中(供其它目标使用),任何链接此库的目标会自动获得该头文件搜索路径
target_include_directories(only_header_lib
INTERFACE
"${CMAKE_CURRENT_SOURCE_DIR}/include/only_header_lib") # 将该目录暴露给依赖项

# 创建一个可执行文件并链接到 INTERFACE 库,以继承该库的头文件路径以及编译选项(如有)
add_executable(app "app/main.c")
target_link_libraries(app PRIVATE only_header_lib)

【虚拟目标示例 - 只有一个头文件的库:源代码】

1
2
3
4
5
6
7
8
9
10
11
// $ cat include/only_header_lib/utils.h
#define TEST_DEFINE (10)

// $ cat app/main.c
#include <stdio.h>
#include "utils.h" // 已经获得了该头文件搜索路径

int main(int argc, char **argv) {
printf("TEST_DEFINE=%d\n", TEST_DEFINE);
return 0;
}

【虚拟目标示例 - 一个虚拟的编译选项目标:CMakeLists.txt】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cmake_minimum_required(VERSION 3.10.2)
project(learn-cmake-interface-lib
VERSION 1.0.0
DESCRIPTION "good good study, day day up."
LANGUAGES C)

add_library(compiler_flags INTERFACE)
target_compile_options(compiler_flags INTERFACE
"$<$<CONFIG:DEBUG>:-O0;-g;-std=gnu11;-Wall>"
"$<$<CONFIG:RELEASE>:-O2;-std=gnu11;-Wall>"
)

if (NOT CMAKE_SYSTEM_NAME MATCHES "Windows")
target_compile_options(compiler_flags INTERFACE "-Werror;-Wno-unused;-Wno-pointer-sign")
endif()

add_executable(app "app/main.c")
target_link_libraries(app PRIVATE compiler_flags)

目标时常伴随着你

现在我们已经指定了一个目标,那我们如何添加关于它的信息呢?例如,它可能需要包含一个目录:

1
target_include_directories(one PUBLIC "include") # 为目标添加了一个目录

PUBLIC 对于一个可执行文件目标没有什么含义;但对于库来说,它让 CMake 知道,任何链接到这个目标的目标也必须包含这个目录。其他选项还有 PRIVATE(只影响当前目标,不影响依赖),以及 INTERFACE(只影响依赖)

接下来,我们可以将目标之间链接起来:

1
2
3
add_library(another STATIC another.c another.h)
# 这里使用 PUBLIC,也就是目标 anthor 也会包含 include 这个目录
target_link_libraries(another PUBLIC one) # 为目标链接另一个目标

target_link_libraries 可能是 CMake 中最有用也最令人迷惑的命令。这个命令需要指定一个目标 another,并且在给出该目标的名字(another)后为此目标添加一个依赖 one。

如果 CMake 项目中不存在名称为 one 的目标(没有定义该 target),那它会直接添加名字为 one 的库到依赖中(一般而言,会去 /usr、CMake 项目指定寻找库的路径等所有能找的路径找到叫 one 的库——译者注)(这也是命令叫 target_link_libraries 的原因)。或者你可以给定一个库的完整路径,或者是链接器标志。

链接的目标可以有包含的目录、链接库(或链接目标)、编译选项、编译定义、编译特性等等。

更进一步

看看你是否能理解以下文件。它生成了一个简单的 C11 的库并且在程序中使用了它。没有依赖。代码中使用的是 CMake 3.8。

1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.8)
project(Calculator LANGUAGES C)

add_library(calclib STATIC "src/calclib.c" "include/calc/lib.h")
target_include_directories(calclib PUBLIC "include")
target_compile_features(calclib PUBLIC c_std_11)

add_executable(calc "apps/calc.c")
target_link_libraries(calc PUBLIC calclib)

实用函数

获取一个目录的父目录(上级目录):

1
get_filename_component(PARENT_DIR "${CMAKE_CURRENT_SOURCE_DIR}" DIRECTORY)

目录存在性检查:

1
2
3
if(EXISTS "${PARENT_DIR}"/testfile.txt)
# do something
endif()

变量与缓存

本地变量

我们首先讨论变量。你可以这样声明一个本地 (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
2
3
4
5
6
set(ARGS_STRING "hello world 'CMake example' 123")
separate_arguments(SPLIT_ARGS UNIX_COMMAND "${ARGS_STRING}")

foreach(arg IN LISTS SPLIT_ARGS)
message("- ${arg}")
endforeach()

对于路径来说要特别小心,路径很有可能会包含空格,因此你应该总是将解析变量得到的值用引号括起来,也就是,应该这样 "${MY_PATH}"

缓存变量(从命令行中设置变量)

CMake 提供了一个缓存变量,来允许你从命令行中设置变量。CMake 中已经有一些预置的变量,像 CMAKE_BUILD_TYPE。如果一个变量还没有被定义,你可以这样声明并设置它。

1
2
3
4
set(CACHE_VAR "VALUE" CACHE STRING "Description info for the cache variable")
set(CACHE_ENABLE_DEBUG ON CACHE BOOL "enable debug")
# 还可以用 option 来设置 BOOL 类型的变量
option(MY_OPTION "This is settable from the command line" OFF)

这么写不会覆盖 CMakeCache.txt 中已定义的值——这是为了让你只能在命令行中设置这些变量,而不会在 CMake 文件执行的时候被重新覆盖。这是什么意思呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$ cmake -L .                            # 不设置该缓存值(执行时还不存在 CMakeCache.txt 文件)
CACHE_VAR:STRING=VALUE
$ grep "CACHE_VAR" ./CMakeCache.txt
./CMakeCache.txt:CACHE_VAR:STRING=VALUE # 缓存中读出了 CMakeLists.txt 中的默认值
$
$ cmake -L -DCACHE_VAR=ABCD . # 设置了新值
CACHE_VAR:STRING=ABCD
$ grep "CACHE_VAR" ./CMakeCache.txt
./CMakeCache.txt:CACHE_VAR:STRING=ABCD # 缓存中读出了新值
$
$ cmake -L -DCACHE_VAR=xyz . # 又设置了新值,缓存中更新了其值
CACHE_VAR:STRING=xyz
$
$ cmake -L . # 不设置该缓存值
CACHE_VAR:STRING=xyz
$ grep "CACHE_VAR" ./CMakeCache.txt
./CMakeCache.txt:CACHE_VAR:STRING=xyz # 缓存中读出的是上一次从命令行中设置的值,不会是 CMakeLists.txt 中的默认值

如果你想把这些变量作为一个临时的全局变量,你可以这样做:

1
2
3
4
set(CACHE_VAR_FORCE "VALUE_FORCE" CACHE STRING "" FORCE)
mark_as_advanced(CACHE_VAR)
# 你也可以通过 `INTERNAL` 这个类型来达到同样的目的
set(CACHE_VAR_FORCE "VALUE_FORCE" CACHE INTERNAL "")

【示例 - 缓存变量:CMakeLists.txt】:

1
2
3
4
5
6
7
8
set(CACHE_VAR "VALUE" CACHE STRING "Description info for the cache variable")
set(CACHE_ENABLE_DEBUG ON CACHE BOOL "enable debug")
option(MY_OPTION "This is settable from the command line" OFF)

set(CACHE_VAR_FORCE1 "VALUE_FORCE1" CACHE STRING "" FORCE)
mark_as_advanced(CACHE_VAR_FORCE)

set(CACHE_VAR_FORCE2 "VALUE_FORCE1" CACHE INTERNAL "")

【示例 - 缓存变量:cmake 输出】:

1
2
3
4
5
6
7
8
# 每次都从命令行设置缓存变量的值,就不会使用 CMakeCache.txt 中已经缓存的旧值了
$ cmake -DCACHE_VAR="VALUE_FROM_CMD" -DMY_OPTION=ON -L .
-- Cache values # 没有 CACHE_VAR_FORCE1/2 哦
CMAKE_BUILD_TYPE:STRING=
CMAKE_INSTALL_PREFIX:PATH=/usr/local
CACHE_ENABLE_DEBUG:BOOL=ON
CACHE_VAR:STRING=VALUE_FROM_CMD
MY_OPTION:BOOL=ON

环境变量

你也可以通过 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
2
3
4
5
6
# 该方法可以一次性设置多个目标、文件
set_property(TARGET TargetName
PROPERTY CXX_STANDARD 11)
# 该方法可以为一个目标设置多个属性
set_target_properties(TargetName PROPERTIES
CXX_STANDARD 11)

第一种方式更加通用 (general) ,它可以一次性设置多个目标、文件、或测试,并且有一些非常有用的选项。第二种方式是为一个目标设置多个属性的快捷方式。此外,你可以通过类似于下面的方式来获得属性:

1
get_property(ResultVariable TARGET TargetName PROPERTY CXX_STANDARD)

用 CMake 进行编程

控制流程

如果你的 CMake 版本不大于 3.1:

1
2
3
4
5
6
7
8
if(variable)
# If variable is `ON`, `YES`, `TRUE`, `Y`, or non zero number
elseif(variable2)
# allow multiple elseif
else()
# If variable is `0`, `OFF`, `NO`, `FALSE`, `N`, `IGNORE`, `NOTFOUND`, `""`, or ends in `-NOTFOUND`
endif()
# If variable does not expand to one of the above, CMake will expand it then try again

如果你的 CMake 版本大于 3.1 ,那么你也可以这么写:

1
2
3
4
5
if("${variable}")
# True if variable is not false-like
else()
# Note that undefined variables would be `""` thus false
endif()

这里还有一些关键字可以设置,例如:

  • 一元的: NOT, TARGET, EXISTS (文件), DEFINED 等。
  • 二元的: STREQUAL, AND, OR, MATCHES (正则表达式), VERSION_LESS, VERSION_LESS_EQUAL (CMake 3.7+) 等。
  • 括号可以用来分组

宏定义与函数

CMake 函数和宏只有作用域上存在区别,宏没有作用域的限制。所以说,如果你想让函数中定义的变量对外部可见,你需要使用 PARENT_SCOPE 来改变其作用域。

如果是在嵌套函数中,这会变得异常繁琐,因为你必须在想要变量对外的可见的所有函数中添加 PARENT_SCOPE 标志。但是这样也有好处,函数不会像宏那样对外“泄漏”所有的变量。接下来用函数举一个例子:

1
2
3
4
5
6
7
function(SIMPLE REQUIRED_ARG)
message(STATUS "Simple arguments: ${REQUIRED_ARG}, followed by ${ARGN}")
set(${REQUIRED_ARG} "From SIMPLE" PARENT_SCOPE)
endfunction()

simple(This Foo Bar)
message("Output: ${This}")

输出如下:

1
2
-- Simple arguments: This, followed by Foo;Bar
Output: From SIMPLE

如果你想要有一个指定的参数,你应该在列表中明确的列出,除此之外的所有参数都会被存储在 ARGN 这个变量中( ARGV 中存储了所有的变量,包括你明确列出的 )。

CMake 的函数没有返回值,你可以通过设定变量值的形式来达到同样地目的。在上面的例子中,你可以通过指定变量名来设置一个变量的值。

参数的控制

CMake 拥有一个变量命名系统。你可以通过 cmake_parse_arguments 函数来对变量进行命名与解析。如果你想在低于 3.5 版本的 CMake 系统中使用它,你应该包含 CMakeParseArguments 模块,此函数在 CMake 3.5 之前一直存在于上述模块中。这是使用它的一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function(COMPLEX)
cmake_parse_arguments(
COMPLEX_PREFIX # 变量前缀
"SINGLE;ANOTHER" # 布尔类型变量,默认全为 FALSE
"ONE_VALUE;ALSO_ONE_VALUE" # 多个单一变量
"MULTI_VALUES" # 一个可变参数变量
${ARGN} # 可变参数列表
)

message(STATUS "COMPLEX_PREFIX_SINGLE = ${COMPLEX_PREFIX_SINGLE}")
message(STATUS "COMPLEX_PREFIX_ANOTHER = ${COMPLEX_PREFIX_ANOTHER}")

message(STATUS "COMPLEX_PREFIX_ONE_VALUE = ${COMPLEX_PREFIX_ONE_VALUE}")
message(STATUS "COMPLEX_PREFIX_ALSO_ONE_VALUE = ${COMPLEX_PREFIX_ALSO_ONE_VALUE}")

message(STATUS "COMPLEX_PREFIX_MULTI_VALUES = ${COMPLEX_PREFIX_MULTI_VALUES}")
endfunction()

# 如果你在调用函数的同级下打印这些变量,看到的全是未定义,因为它们的作用域在函数内
complex(SINGLE ONE_VALUE value MULTI_VALUES some other values)

在调用这个函数后,会生成以下变量:

1
2
3
4
5
COMPLEX_PREFIX_SINGLE = TRUE
COMPLEX_PREFIX_ANOTHER = FALSE
COMPLEX_PREFIX_ONE_VALUE = "value"
COMPLEX_PREFIX_ALSO_ONE_VALUE = <UNDEFINED>
COMPLEX_PREFIX_MULTI_VALUES = "some;other;values"

你可以通过 set 来避免在 list 中使用分号。此外,其他剩余的参数(因此参数的指定是可选的)都会被保存在 COMPLEX_PREFIX_UNPARSED_ARGUMENTS 变量中。

与你的代码交互

通过 CMake 配置文件

CMake 允许你在代码中使用 configure_file 来访问 CMake 变量。该命令将一个文件( 一般以 .in 结尾 )的内容复制到另一个文件中,并替换其中它找到的所有 CMake 变量。如果你想要在你的输入文件中避免替换掉使用 ${} 包含的内容,你可以使用 @ONLY 关键字。

这个功能在 CMake 中使用的相当频繁,例如在下面的 Version.h.in 中:

Version.h.in:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef MY_VERSION_H
#define MY_VERSION_H

#define APP_NAME "@PROJECT_NAME@"
#define MY_VERSION_MAJOR @PROJECT_VERSION_MAJOR@
#define MY_VERSION_MINOR @PROJECT_VERSION_MINOR@
#define MY_VERSION_PATCH @PROJECT_VERSION_PATCH@
#define MY_VERSION "@PROJECT_VERSION@"

#define SHELL_VAR "${SHEEL_VAR}"

// 特性开关
#cmakedefine ENABLE_LOGGING
#cmakedefine01 USE_GPU_ACCELERATION
#endif // MY_VERSION_H

CMake lines:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.22.1)
project(MyApp VERSION 1.2.3)

option(ENABLE_LOGGING "Enable logging" ON)
option(USE_GPU_ACCELERATION "Use GPU" OFF)

# 生成 Version.h
configure_file (
"${PROJECT_SOURCE_DIR}/Version.h.in" # input file
"${PROJECT_BINARY_DIR}/include/Version.h" # output file
@ONLY # 避免变量 ${} 被替换
)

# 在构建你的项目时,你也应该包括二进制头文件路径(将生成的头文件目录加入包含路径)
include_directories("${PROJECT_BINARY_DIR}/include")

cmake 后在 include 目录下生成的 Version.h 头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#ifndef MY_VERSION_H
#define MY_VERSION_H

#define APP_NAME "MyApp"
#define MY_VERSION_MAJOR 1
#define MY_VERSION_MINOR 2
#define MY_VERSION_PATCH 3
#define MY_VERSION "1.2.3"

#define SHELL_VAR "${SHEEL_VAR}"

// 特性开关
#define ENABLE_LOGGING
#define USE_GPU_ACCELERATION 0
#endif // MY_VERSION_H

总结:

场景 用法 示例输入 示例输出
普通变量替换 @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
2
3
4
5
6
7
8
9
10
11
12
13
# Assuming the canonical version is listed in a single line
# This would be in several parts if picking up from MAJOR, MINOR, etc.
set(VERSION_REGEX "#define MY_VERSION[\t]+\"(.+)\"")

# Read in the line containing the version
file(STRINGS "${CMAKE_CURRENT_SOURCE_DIR}/include/My/Version.h"
VERSION_STRING REGEX ${VERSION_REGEX})

# Pick out just the version
string(REGEX REPLACE ${VERSION_REGEX} "\\1" VERSION_STRING "${VERSION_STRING}")

# Automatically getting PROJECT_VERSION_MAJOR, My_VERSION_MAJOR, etc.
project(My LANGUAGES CXX VERSION ${VERSION_STRING})

如上所示,file(STRINGS file_name variable_name REGEX regex) 选择了与正则表达式相匹配的行,并且使用了相同的正则表达式来匹配出其中版本号的部分。

一个简单的例子

这是一个简单、完整并且合理的 CMakeLists.txt 的例子。对于这个程序,我们有一个带有头文件与源文件的库文件(MyLibExample),以及一个带有源文件的应用程序(MyExample)。

目录树结构:

1
2
3
4
5
6
$ tree ./simple-project/ --dirsfirst -a
./simple-project/
├── CMakeLists.txt
├── simple_example.c
├── simple_lib.c
└── simple_lib.h

CMakeLists.txt:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# CMake simple example

## [main]

# Almost all CMake files should start with this
# You should always specify a range with the newest
# and oldest tested versions of CMake. This will ensure
# you pick up the best policies.
cmake_minimum_required(VERSION 3.1...3.21)

# This is your project statement. You should always list languages;
# Listing the version is nice here since it sets lots of useful variables
project(
ModernCMakeExample
VERSION 1.0
DESCRIPTION "a nice example"
LANGUAGES C)

# If you set any CMAKE_ variables, that can go here.
# (But usually don't do this, except maybe for C++ standard)

# Find packages go here.

# You should usually split this into folders, but this is a simple example

# This is a "default" library, and will match the *** variable setting.
# Other common choices are STATIC, SHARED, and MODULE
# Including header files here helps IDEs but is not required.
# Output libname matches target name, with the usual extensions on your system
add_library(MyLibExample STATIC simple_lib.c simple_lib.h) # 生成静态库
#add_library(MyLibExample SHARED simple_lib.c simple_lib.h) # 生成动态库

# Link each target with other targets or add options, etc.

# Adding something we can run - Output name matches target name
add_executable(MyExample simple_example.c)

# Make sure you link your targets with this command. It can also link libraries and
# even flags, so linking a target that does not exist will not give a configure-time error.
target_link_libraries(MyExample PRIVATE MyLibExample)

# 安装规则 - 这会在 `make install` 时将文件复制到指定目录(默认可能是系统目录,可以通过 CMAKE_* 变量重新指定)
install(TARGETS MyLibExample
ARCHIVE DESTINATION lib # 静态库 (.a)
LIBRARY DESTINATION lib # 动态库 (.so)
RUNTIME DESTINATION bin # Windows DLL
)

install(TARGETS MyExample
RUNTIME DESTINATION bin # 可执行文件
)

# 可选:安装头文件
install(FILES simple_lib.h DESTINATION include)

## [main]

# This part is so the Modern CMake book can verify this example builds. For your code,
# you'll probably want tests too
enable_testing()
add_test(NAME MyExample COMMAND MyExample)

其它源文件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// $ cat simple_lib.c
#include <stdio.h>

char *simple_lib_function() {
return "Compiled in library.\n";
}

// $ cat simple_lib.h
#ifndef SIMPLE_LIB_H
#define SIMPLE_LIB_H

char *simple_lib_function();

#endif // SIMPLE_LIB_H

// $ cat simple_example.c
// This is an example file as part of Modern-CMake
#include <stdio.h>
#include "simple_lib.h"

int main(int argc, char **argv) {
printf("Simple example C compiled correctly and ran.\n");
printf("Output of calling lib function: %s", simple_lib_function());
return 0;
}

创建构建目录并编译:

1
2
3
$ mkdir build && cd build
$ cmake -DCMAKE_INSTALL_PREFIX=./output ..
$ make && make install

生成静态库的可执行文件执行方式:

1
2
3
$ cd output && ./bin/MyExample
Simple example C compiled correctly and ran.
Output of calling lib function: Compiled in library.

生成动态库的可执行文件执行方式:

1
2
$ export LD_LIBRARY_PATH=/home/simple-project/build/output/lib/:${LD_LIBRARY_PATH}
$ cd output && ./bin/MyExample -L./lib -I./include

如何组织你的项目

下面的说法可能存在一些偏见,但我认为这是一种好的组织方式。我将会讲解如何组织项目的目录结构,这是基于以往的惯例来写的,这么做对你有以下好处:

  • 可以很容易阅读以相同模式组织的项目
  • 避免可能造成冲突的组织形式
  • 避免使目录结构变得混乱和复杂

首先,如果你创建一个名为 project 的项目,它有一个名为 lib 的库,有一个名为 app 的可执行文件,那么目录结构应该如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ tree ./project/ --dirsfirst -a
./project/
├── apps
│   ├── app.c
│   └── CMakeLists.txt
├── cmake
│   ├── FindSomeLib.cmake
│   └── something_else.cmake
├── docs
│   └── CMakeLists.txt
├── include
│   └── project
│   └── lib.h
├── scripts
│   └── helper.py
├── src
│   ├── CMakeLists.txt
│   └── lib.c
├── CMakeLists.txt
├── .gitignore
└── README.md

其中,文件的名称不是绝对的,并且应用程序所在的文件夹可能为其他的名称(或者一个项目只有库文件)。你也许也会看到 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
2
3
4
5
6
### Require out-of-source builds
file(TO_CMAKE_PATH "${PROJECT_BINARY_DIR}/CMakeLists.txt" LOC_PATH)
if(EXISTS "${LOC_PATH}")
message(FATAL_ERROR "You cannot build in a source directory (or any directory with a CMakeLists.txt file).
Please make a build subdirectory. Feel free to remove CMakeCache.txt and CMakeFiles.")
endif()

一个完整的例子

略。

为 CMake 项目添加特性

默认的构建类型

CMake 通常会设置一个 “既不是 Release 也不是 Debug” 的空构建类型来作为默认的构建类型,如果你想要自己设置默认的构建类型,可以参考如下方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Set a default build type if none was specified
set(default_build_type "Release")
if(EXISTS "${CMAKE_SOURCE_DIR}/.git") # 当然,这里可以有其它处理逻辑
set(default_build_type "Debug")
endif()

if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) # 单构建模式 AND 多构建模式
message(STATUS "Setting build type to '${default_build_type}' as none was specified.")
set(CMAKE_BUILD_TYPE "${default_build_type}" CACHE
STRING "Choose the type of build." FORCE)
# Set the possible values of build type for cmake-gui
set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS
"Debug" "Release" "MinSizeRel" "RelWithDebInfo")
endif()

当然,如果你已经知道用什么构建类型了,你也可以在命令行中指定:如 cmake -DCMAKE_BUILD_TYPE=Debug ..

CMake 3.1+: 全局设置以及属性设置

这是支持 C 标准的一种方式,(在目标及全局级别)设置三个特定属性的值。这是全局的属性:

1
2
3
4
5
6
7
# 将 C 标准设为缓存变量,允许用户通过 -D 覆盖
set(CMAKE_C_STANDARD 11 CACHE STRING "The C standard to use (e.g., 90, 99, 11, 17, 23)")
set(CMAKE_C_STANDARD_REQUIRED ON) # 告诉 CMake 使用上述设置
set(CMAKE_C_EXTENSIONS OFF) # 是否启用拓展,来明确使用 -std=c11 还是 -std=gnu11

# 创建可执行文件(最终包)
add_executable(MyApp main.c)

上述前三条配置,可以在最终包(即可执行程序,如 main() 入口的 final package)中使用,但不推荐在库中使用。原因如下:库通常需要兼容不同调用方的 C 标准,硬编码 CMAKE_C_STANDARD 会限制库的灵活性。

  • 如果库强制设为 C11,但用户项目用 C17,可能导致兼容性问题。
  • 更好的方式是用 target_compile_features (见下文)。
1
2
3
# 库的 CMakeLists.txt(不硬编码标准)
add_library(MyLib lib.cpp)
target_compile_features(MyLib PUBLIC c_std_11) # 仅表示最低要求

你也可以对目标来设置这些属性:

1
2
3
4
5
set_target_properties(myTarget PROPERTIES
C_STANDARD 11
C_STANDARD_REQUIRED ON
C_EXTENSIONS NO
)

这种方式相比于上面来说更好,但是仍然没法对 PRIVATEINTERFACE 目标的属性有明确的控制,所以他们也仍然只对最终目标 (final targets) 有用。

如何换一种方式,在 C 代码中使用了 GNU 特性呢?

1
2
3
4
5
6
# 全局启用 gnu11
set(CMAKE_C_STANDARD 11) # 先设置 C11
add_compile_options(-std=gnu11) # 再覆盖为 GNU11

# 特定目标启用 gnu11
target_compile_options(YourTarget PUBLIC -std=gnu11)

一些小而常见的需求

地址无关代码 (Position independent code)

用标志 -fPIC 来设置这个是最常见的。大部分情况下,你不需要去显式地声明它的值。CMake 将会在 SHARED 以及 MODULE 类型的库中自动的包含此标志。如果你需要显式地声明,可以这么写:

1
set(CMAKE_POSITION_INDEPENDENT_CODE ON)

这样会对全局的目标进行此设置,或者可以这么写:

1
set_target_properties(lib1 PROPERTIES POSITION_INDEPENDENT_CODE ON)

来对某个目标进行设置是否开启此标志。

调试 CMake

首先,让我们来盘点一下调试 CMakeLists 和其他 CMake 文件的方法。

打印变量

通常我们使用的打印语句如下:

1
message(STATUS "MY_VARIABLE=${MY_VARIABLE}")

然而,通过一个内置的模组 CMakePrintHelpoers 可以更方便的打印变量:

1
2
include(CMakePrintHelpers)
cmake_print_variables(MY_VARIABLE)

如果你只是想要打印一个变量,那么上述方法已经很好用了!如果你想要打印一些关于某些目标 (或者是其他拥有变量的项目,比如 SOURCESDIRECTORIESTESTS , 或 CACHE_ENTRIES - 全局变量好像因为某些原因缺失了) 的变量,与其一个一个打印它们,你可以简单的列举并打印它们:

1
2
3
4
cmake_print_properties(
TARGETS my_target
PROPERTIES POSITION_INDEPENDENT_CODE
)

跟踪运行

你可能想知道构建项目的时候你的 CMake 文件究竟发生了什么,以及这些都是如何发生的?用 --trace-source="filename" 就很不错,它会打印出你指定的文件现在运行到哪一行,让你可以知道当前具体在发生什么。另外还有一些类似的选项,但这些命令通常给出一大堆输出,让你找不着头脑。

例子:

1
cmake -S . -B build --trace-source=CMakeLists.txt

如果你添加了 --trace-expand 选项,变量会直接展开成它们的值。

以 debug 模式构建

对于单一构建模式的生成器 (single-configuration generators),你可以使用参数 -DCMAKE_BUILD_TYPE=Debug 来构建项目,以获得调试标志 (debugging flags)。对于支持多个构建模式的生成器 (multi-configuration generators),像是多数 IDE,你可以在 IDE 里打开调试模式。这种模式有不同的标志(变量以 _DEBUG 结尾,而不是 _RELEASE 结尾),以及生成器表达式的值 CONFIG:DebugCONFIG:Release

如果你使用了 debug 模式构建,你就可以在上面运行调试器了,比如 gdb 或 lldb。