CMake 进阶
written by SJTU-XHW
Reference: CMake Tutor
本人知识浅薄,文章难免有问题或缺漏,欢迎批评指正!
本文目标:在前文(GNU Tutor)初步了解 CMake、C++ 编译过程后,继续深入学习 CMake 在 C++ 构建中的使用;
⚠ 劝退警告:如果你只想用 IDE 一键编译运行,而不想了解构建和编译细节,那么这篇文章不是为你准备的!
Chapter 0. Make 介绍
对 Make 没兴趣的这章可以跳过!
CMake 生成的 Makefile 究竟是什么?语法是怎样?为什么要有它?
由于现在是 21 世纪 20 年代,所以像
make
这样底层的古董就点到为止;;
地位:GNU 计划的一个开源程序;
作用
- 制定整个项目的编译规则(利用
Makefile
定义整个编译流程以及各个目标文件与源文件之间的依赖关系),自动化编译步骤,以此提高开发效率; - 二次编译时,仅重新编译你的修改会影响到的部分,从而降低编译的时间;
- 制定整个项目的编译规则(利用
劣势:为什么上面说 “点到为止”?因为它比较底层,导致抽象层级不高,不能跨平台,每个平台有各自的 make 程序,导致编写
Makefile
较为繁琐;例如在 前文 “GNU Tutor” 中提到的 MinGW 编译器中的 make 和 Unix 系统下的 make 就有所差别;
这一劣势将由 CMake 进行弥补,之后讨论;
0.1 Makefile 的规则
0.1.1 显式规则
定义:显式规则说明了如何生成一个或多个目标文件。这是由 Makefile 的书写者明显指出要生成的文件、文件的依赖文件和生成的命令;
基本语法:
1
2
3
4
5
6
7<target> : <prerequisites>
<command>
# And <-- Makefile 的行注释是 “#” 字符
<pTarget> :
<command>target
:可以是一个object file(*.o
),也可以是一个执行文件(最终目标);pTarget
:在一个 makefile 中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等;被称为伪目标;pTarget
部分在运行make
的时候不会自动调用,外界可以使用make [pTargetName]
来手动调用,例如:make clean
,make install
(如果 Makefile 中定义了的话)prerequisites
:生成该target
所依赖的文件和target
;command
:该target
或motion
要执行的命令(任意的 shell 命令)
这指明了文件的依赖关系,即:
target
这一个或多个的目标文件依赖于prerequisites
中的文件,其生成规则定义在command
中。有有同学会问,Make 是怎么做到 上面的第二条作用(仅重新编译修改的部分)的呢?
很简单,如果
prerequisites
文件的日期要比targets
文件的日期要新,或者target
不存在的话,那么,make 就会执行后续定义的command
;示例:这个例子可以不需要了解项目依赖关系;废话少说,上栗子🌰:
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# 假设有一个项目包含 3 个头文件、8个源文件(名称如下):
# 最终目标 edit
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
# 中间目标
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
# 伪目标
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
backup :
cp edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o /opt/
install :
# ...中间目标一般都是
*.o
;最终目标视需求而定,可以是可执行文件,也可以是动/静态链接库;prerequisites
是目标的依赖,一般是*.h/c/cpp
;command
是获得目标 / 完成动作的操作;上面的例子中,cc
是 C 的编译器命令,cp
和rm
是 Unix 系统的命令;- 必须以制表符(或者说
\t
、Tab 键)开头; - 默认工作目录为 Makefile 所在目录;
- 必须以制表符(或者说
(规范)伪目标声明:对于伪目标而言,应该使用
.PHONY
关键字声明,更符合规范;例如上面例子的规范写法应该加上:
1
.PHONY : clean backup install
另外,规范来说,请将所有伪目标写在后面,以防读者误认为最终目标;
作用:防止与文件名/最终目标重名,增强可读性;
特点:伪目标可以有依赖,这样相当于是委托调用,例如:
1
2
3
4
5
6
7
8
9
10.PHONY : cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
指定文件查找目录:
VPATH
关键字(知道就行);
0.1.2 隐晦规则
作用:make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile;
- 生成
*.o
时,默认将同名的*.c/cpp
加入依赖中,同时省去相应的编译命令; - 当 make 在 Makefile 中找不到与目标同名的
*.c/cpp
,那么 make 认为这个目标为伪目标;
因此,利用隐晦规则,最开始的例子可以简化为:
1 | objects = main.o kbd.o command.o display.o \ |
注:上面的 “
-rm edit
” 前面有个 “-”,表示中间如果出现错误,也请继续进行的意思(忽略错误);这里正好补充一下 Makefile 的变量使用:
- 定义:
name=value
;调用:${variableName}
;- 类似 C++ 中的宏替换(后面介绍赋值运算符);
0.1.3 文件指示 和 编译设置
文件指示一共包含 3 个部分:
- 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 #include 一样;
- 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样;
- 还有就是定义一个多行的命令,不会深入介绍,感兴趣请查看 教程;
这里就介绍第一个:
1 | include foo.make *.mk ${bar} |
上面这行代码的含义是:包含 foo.make
,所有后缀为 mk
的,和在变量 ${bar}
中的文件作为 Makefile;
此外,以 UNIX 为例,make 自动包含了 /usr/local/bin
和 /usr/include
中的文件;
还可以加上 “-” 表示读取错误全部忽略:
1 | -include foo.make *.mk ${bar} |
编译设置 最常用的是设置 C++ 编译器,可以由修改内置变量完成:
1 | CC := clang |
上面的代码含义是:将 C 编译器改为
clang
,将 C++ 编译器改为clang++
;
0.1.4 赋值运算符
看到上面的例子,大家可能有些疑惑,
:=
和=
赋值有区别吗?答案是,有的。
=
是保留计算式的赋值;:=
是立刻计算结果并覆盖原来的值;?=
是如果没有被赋值过就赋予等号后面的值;+=
是添加等号后面的值;
举个例子体会一下:
1 | x = foo |
1 | x := foo |
上面一段 Makefile 运行 make all
会输出:xyz bar
;
而下面一段会输出:foo bar
;
0.1.5 小结:Make 的工作方式
读入所有的 Makefile;
读入被 include 的其它 Makefile;
初始化文件中的变量;
推导隐晦规则,并分析所有规则;
为所有的目标文件创建依赖关系链;
根据依赖关系,决定哪些目标要重新生成;
执行生成命令;
0.2 Make 命令的使用
- Windows 上如果安装
MinGW
编译器套件,那么应该使用mingw32-make
来进行;不讨论MSVC
编译器套件 ~ 用它的大多数是用了微软的 Visual Studio 的套件; - Linux 本身就是 GNU 产物,直接安装
make
就能用了;
Chapter 1. CMake 命令使用
众所周知,CMake 可以完成 2 步:① 将 CMakeLists.txt 翻译生成 Makefile;② 代替 make 完成编译构建;
下面我们特指 ① 为 生成,② 为 编译/构建;
1.1 生成指令
总体语法:cmake [options] <projectDir>
(projectDir
需含有 CMakeLists.txt
);
下面介绍 [options]
:
指定生成 Makefile 等中间文件的目录(生成时会将所有文件放入该目录):
-B <dirName>
(dirName
默认当前目录,下同);指定生成的 Makefile 种类:
-G <Makefile-Type>
;这里 Unix 类系统(macOS 和 Linux)会默认
"Unix Makefiles"
,大多数情况下无需更改,因为使用 Unix 内置的 GNU/Make 程序和 GNU/GCC 编译器;这里对于 Windows 用户很重要,因为 Windows 的 make 工具种类很多,针对您所安装的编译器对应的
make
,需要进行合理选择,取值有(有空格,命令行里记得加双引号):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
39Visual Studio 17 2022 <--- 使用 VS IDE 的同学不需要指定,IDE 会自动设置
Visual Studio 16 2019
Visual Studio 15 2017 [arch]
Visual Studio 14 2015 [arch]
Visual Studio 12 2013 [arch]
Visual Studio 11 2012 [arch]
Visual Studio 9 2008 [arch]
Borland Makefiles
NMake Makefiles <--- 使用 nmake 的同学选这个
NMake Makefiles JOM
MSYS Makefiles
MinGW Makefiles <--- 使用正宗 MinGW 编译器的同学选这个
Green Hills MULTI
Unix Makefiles <--- Unix 系统的同学不需要指定,系统会自动设置
Ninja <--- 使用 Ninja 的同学选这个
Ninja Multi-Config
Watcom WMake
CodeBlocks - MinGW Makefiles <--- 你的 codeblocks 装了什么编译器?
CodeBlocks - NMake Makefiles
CodeBlocks - NMake Makefiles JOM
CodeBlocks - Ninja
CodeBlocks - Unix Makefiles
CodeLite - MinGW Makefiles
CodeLite - NMake Makefiles
CodeLite - Ninja
CodeLite - Unix Makefiles
Eclipse CDT4 - NMake Makefiles
Eclipse CDT4 - MinGW Makefiles
Eclipse CDT4 - Ninja
Eclipse CDT4 - Unix Makefiles
Kate - MinGW Makefiles
Kate - NMake Makefiles
Kate - Ninja
Kate - Ninja Multi-Config
Kate - Unix Makefiles
Sublime Text 2 - MinGW Makefiles
Sublime Text 2 - NMake Makefiles
Sublime Text 2 - Ninja
Sublime Text 2 - Unix Makefiles
1.2 编译构建指令
构建编译(可代替
make
):cmake --build <dirName>
;和 Make 一样,可以仅重新编译你的修改会影响到的部分,从而降低编译的时间;
Chapter 2. CMakeList 常见函数
注:CMake 的函数名不区分大小写;
以下函数分为 3 个等级:optional 可选、recommended 推荐书写、necessary 必要;
众所周知,C++ 项目的编译和构建的目标可以是 可执行文件,也可以是静/动态链接库;以下函数如果仅用于某个特定目标,那么会标注 in EXE Project 或 in LIB Project;
【optional】指定 CMake 版本,可选低于版本产生致命错误:
1
2
3
4# ver 建议是当前系统中的 CMake 版本
CMAKE_MINIMUM_REQUIRED (VERSION <ver> [FATAL_ERROR])
# 例如我的 CMake 版本是 3.12,并且想让其他人生成时,CMake低于这个版本就报错,那么这么写:
CMAKE_MINIMUM_REQUIRED (VERSION 3.12 FATAL_ERROR)【recommended】指定 Project 名称、使用编译器语言(不填写也可自动识别):
1
PROJECT (<projectName> [LANGUAGES CXX])
注意,这里指定项目名称后,变量
${PROJECT_NAME}
就被设定了,可以在后面使用;【optional】告诉编译器从哪里寻找非标准 C++ 的头文件:
1
INCLUDE_DIRECTORIES (<dir>)
可以理解为:
Makefile 中的 include 关键字:
1
include <dir>/*
或者 g++ 中的
-I
参数:1
g++ [...] -I<dir> [...]
【optional】向指定目标规定寻找非标准 C++ 头文件路径:
1
2
3
4TARGET_INCLUDE_DIRECTORIES (<targetName> [INTERFACE | PRIVATE | PUBLIC] <dir>)
# PUBLIC 表示这个项目的外部使用者能看到
# PRIVATE 则对外部完全隐藏,即不希望调用这个项目目标的使用者知道“这个项目引入了该头文件”INCLUDE_DIRECTORIES
和TARGET_INCLUDE_DIRECTORIES
比较:前者是向整个项目(包括子目录和库)添加了寻找头文件的寻找路径;
后者是向特定目标添加了寻找头文件的寻找路径,同时可以指定暴露级别;
这就和不同范围设置 C++ 标准异曲同工:
1
2
3SET (CMAKE_CXX_STANDARD 11) # 全局设置 C++ 规范
# ---------------------------------------------------------------------
TARGET_COMPILE_FEATURES (<targetName> [...] cxx_std_11) # 设置特定库的规范从项目规范性上说,建议使用
TARGET_INCLUDE_DIRECTORY
而非INCLUDE_DIRECTORY
;【necessary in EXE Project】将指定源文件加入构建为 可执行文件 的目标中:
1
ADD_EXECUTABLE (<exeName> <sourceFileNames>)
位于
TARGET_LINK_LIBRARIES
前,在其他函数之后;注意,和 Make 的隐晦规则恰好相反,如果这个头文件被某一源文件引入的话,可以省略对应的头文件;
【recommended】查找指定目录下的所有源文件,并将文件名存入变量:
1
AUX_SOURCE_DIRECTORY (<dir> <variableName>)
此后就能使用变量:
${<variableName>}
;【optional】给项目加入子目录(即读取这个目录下的
CMakeLists.txt
):1
ADD_SUBDIRECTORY (<dir>)
书写这个函数后,在主
CMakeLists.txt
中就可以直接指明子目录下生成的目标名,这在导入并链接自己编写的 动/静态链接库 时用的较多;【necessary in LIB Project】将指定源文件加入构建为 链接库 的目标中:
请大家复习一下什么是 “静态链接库”、什么是 “动态链接库”:
静态链接库是 运行前、编译时可以链接进入到目标文件中,优点是分发时文件个数少,不依赖外部文件;缺点是修改了静态链接库的内容的话程序整体需要重新编译;
动态链接库是 运行时才链接到目标文件中,优点是只要 API 不变,修改动态链接库可以单独进行编译,并且节省内存(仅在调用库函数时才加载到内存中);缺点是文件分发数多,不便管理;
注:C++ 的库的隐含规则——链接库文件名是 “lib” + 库名 + 后缀;
目标为静态链接库(后缀名可能是
*.a
、*.lib
,和操作系统、编译器类型都有关系):1
ADD_LIBRARY (<libName> [STATIC] <sourceFileNames>) # 默认 STATIC
目标为动态链接库(后缀名可能是
*.so
、*.dll
、*.dylib
):1
2# 动态链接库一般不和主程序一起编译,因为这样还不如用静态链接库
ADD_LIBRARY (<libName> SHARED <sourceFileNames>)
【optional】告诉编译器从哪里寻找非标准 C++ 的动态链接库,一般也同时指定非标准头文件查找目录(include_directories 或 target_include_directories):
1
LINK_DIRECTORIES (<dir>)
可以理解为 g++ 的
-L
参数:1
g++ [...] -L <dir> [...]
【optional】向 可执行文件目标 或 链接库目标 链接一些库:
众所周知,想要链接库,必须要有 头文件、动/静态链接库文件,并且把它们都引入自己的项目中
要链接的库是自己编写的 / 第三方静态链接库:
1
2
3
4
5
6# 如果是自己编写的静态链接库,请确保使用了 add_subdirectory 引入该库的 CMakeLists
# 或者在当前 CMakeLists 中指定编译的库,需要 add_library
# 如果是第三方静态链接库,则建议引入头文件,使用 include_directory
TARGET_LINK_LIBRARIES (<targetName> <libName/libFileName>)要链接的库是动态链接库:
1
2
3
4
5
6INCLUDE_DIRECTORIES (<dir>) # 引入非标准头文件查找目录
LINK_DIRECTORIES (<dir>) # 引入非标准链接库查找目录
# 如果最终目标是可执行文件,那么 add_executable 应该写在这里
TARGET_LINK_LIBRARIES (<targetName> <libName/libFileName>)要链接的库是标准 C++ 库(一般这种情况是这个标准 C++ 库不在标准位置,最常见的是官方的 C++ 扩充,例如 Qt 的库):
1
2
3
4
5
6# 指定标准库名,如果它在环境变量中,那么就不需要后面的 PATHS 参数
FIND_PACKAGE (<stdLibDirName> [REQUIRED] [PATHS <dir>])
# 指定标准库以何种形式链接到目标中
# PUBLIC 和 PRIVATE 含义和之前的 TARGET_INCLUDE 的含义相同
TARGET_LINK_LIBRARIES (<targetName> [PUBLIC | PRIVATE] <stdLibDirName::stdLibName>)
Chapter 3. CMakeLists 变量控制
3.1 常用内置变量
1 | ${CMAKE_CURRENT_SOURCE_DIR} # 这是 CMakeLists.txt 所在目录 |
3.2 赋值和使用
CMake 变量赋值函数 SET
:
1 | SET (<variableName> <value> [CACHE] [STRING | BOOL] [Description]) |
使用直接:${<variableName>}
;
3.3 编译时宏定义
假设程序中有一个量,不希望其他人知道,但其他代码都可以开源——那么需要实现:仅编译时将这个量传入;假设这个量在源码中以
APP_ID
表示,那么 CMakeLists.txt 应该这么写:
1 | # ... |
这是传入临时值的方法是 cmake 的 -D<env=value>
参数,例如上面的生成指令应该这么写:
1 | cmake -D_APP_ID="XXX" <Dir> |
Chapter 4. CMakeLists 常见模板
下面模板的项目依赖极其简单,但不能照抄,需要根据项目实际依赖情况进行调整;
4.1 普通 C++ 项目
项目结构如下:
1 | . # C++ 标准 11 |
模板:
1 | CMAKE_MINIMUM_REQUIRED (VERSION 3.12) |
4.2 多个目录的 C++ 项目
多个目录可以考虑使用静态链接库,下面展示一种自定义静态链接库的 CMakeLists.txt 写法
项目结构如下:
1 | . # 这个项目需要 Debug 模式构建,并且增添编译参数 “-Wall” 和 “-ggdb” |
在项目根目录下的 CMakeLists.txt:
1 | CMAKE_MINIMUM_REQUIRED (VERSION 3.12) |
在 src/
目录下的 CMakeLists.txt:
1 | AUX_SOURCE_DIRECTORY (. LIB_SRC) |
4.3 使用动态链接库的 C++ 项目
项目结构如下:
1 | . # 项目使用 C++ 11 规范 |
项目根目录下的 CMakeLists.txt 写法:
1 | CMAKE_MINIMUM_REQUIRED (VERSION 3.12) |
或者不用 LINK_DIRECTORIES
,直接改为单独设置库的代码:
1 | CMAKE_MINIMUM_REQUIRED (VERSION 3.12) |