从 GNU Make 到 CMake 快速入门
GNU
Make用于控制如何从程序的源代码文件编译并链接为可执行文件,通过make命令从名称为makefile的文件中获取构建信息,该文件定义了一系列规则来指定源文件的编译先后顺序、是否需要重新编译、甚至于进行更为复杂的操作。通过makefile文件可以方便的实现工程的自动化编译,只需要执行make命令即可完成编译动作,从而极大的提高了开发人员的工作效率。

CMake 3.17是一款源代码构建管理工具,最初作为各种 Makefile 方言的生成器,后来逐步发展为现代化的构建系统,广泛用于 C 和 C++ 工程源代码的构建。官方提供的《CMake Tutorial》 为开发人员提供了一个循序渐进的指南,涵盖了 CMake 构建过程中常见问题的解决方案。如果需要构建从第三方发布的源代码包,则可以参考《User Interaction Guide》。而《Using Dependencies Guide》则主要针对需要使用第三方库的开发人员。
GNU Make
make是一款用于解释makefile文件当中命令的工具,而makefile关系到整个工程的编译规则。许多
IDE 集成开发环境都整合了该命令,例如:Visual C++
里的nmake,Linux 里的 GNU
make,本章节主要讲解 GNU make
相关的内容。开始进一步讲解之前,需要先了解一下 C/C++
源代码的编译过程,具体内容可参见笔者的《基于 Linux 的 GCC 与 GDB 应用调试》 -
编译步骤一文:

- 预处理 Preprocessing:解析各种预处理命令,包括头文件包含、宏定义的扩展、条件编译的选择等;
- 编译 Compiling:对预处理之后的源文件进行翻译转换,产生由机器语言描述的汇编文件;
- 汇编 Assembly:将汇编代码转译成为机器码;
- 链接 Link:将机器码中的各种符号引用与定义转换为可执行文件内的相应信息(例如虚拟地址);
makefile 文件
基本规则
执行make命令时,实际会解析当前目录下的makefile文件,该文件用于告知make命令如何对源代码进行编译与链接,一个
makefile 的基本编写规则如下所示:
1 | target ... : prerequisites ... |
target:即可以是 1 个目标文件,也可以是 1 个执行文件,甚至还可以是 1 个标签;prerequisites:生成该target所依赖的文件或者其它target;command:该target所要执行的 Shell 命令,需要保持 1 个【Tab】的缩进;
上述的基本编写规则最终会形成一套依赖关系,其中target依赖于prerequisites,而生成规则定义在command;如果prerequisites中的文件比target上的文件要新,则command所定义的命令就会被执行。
观察下面的例子,其中的反斜杠\表示换行,将其保存为一个makefile或者Makefile文件,然后在当前目录执行make命令,就可以生成可执行文件app。如果需要删除可执行文件以及中间生成的目标文件,则执行make clean命令即可。
1 | app : main.o kbd.o command.o display.o insert.o search.o files.o utils.o |
输入make命令之后,就会开始执行上述的makefile文件,具体执行流程如下所示:
make会在当前目录下查找Makefile或者makefile文件;- 找到后将当中定义的第 1
个
target作为最终的目标文件; - 如果
app文件不存在,或者其依赖的.o文件修改时间要比app执行文件更新。那么,他就会执行command定义的命令来生成app文件; - 如果
app依赖的.o文件也不存在,那么查找.o文件对应的依赖规则生成.o文件; - 最后,基于工程中
.c和.h源文件生成.o依赖文件,然后再基于这些.o文件生成app执行文件;
定义变量
上面示例中app生成规则中的一系列.o文件反复出现,这里我们可以将其声明为一个变量:
1 | objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o |
自动推导
GNU Make
可以自动识别并推导目标与依赖关系之后的command命令,只要make发现
1
个.o文件,就会自动将对应的.c文件添加至依赖关系当中,同时也会将对应的gcc -c命令推导出来。
1 | objects = main.o kbd.o command.o display.o \ |
这种方法被称为make的隐含规则,上述代码中.PHONY表示clean是一个伪目标文件,关于隐晦规则和伪目标文件的内容后续将会进行更为详细的介绍。
通过隐含规则可以进一步简化上面的makefile,这样虽然可以最大幅度减少代码,但是文件的依赖关系显得较为凌乱,所以这种风格较少被采用。
1 | objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o |
清理中间文件
习惯上,每个makefile文件都应该编写一个用于清理中间文件的规则,这样不仅便于重新编译,也有利于保持工程的整洁。
1 | clean: |
之前代码采用了上面较为简单粗暴的方式,但是更为稳健的方法是采用下面这样的风格:
1 | .PHONY : clean |
.PHONY关键字用于表标识clean是一个伪目标,rm命令前的小减号-表示忽略操作出现问题的文件,习惯上会将clean放置在makefile的最后。
Makefile 组成
Makefile 文件主要包含显式规则、隐式规则、变量定义、文件指示、注释。
- 显式规则:由
Makefile编写者明确指定,用于描述如何生成target; - 隐式规则:利用
make命令的自动推导功能,简略书写Makefile; - 变量的定义:通常为字符串,当
Makefile被执行时,其中的变量会扩散到相应的引用位置; - 文件指示:在
Makefile当中引用另外的Makefile,类似于 C 语言里的#include。或者根据条件指定Makefile的有效部分,类似于 C 语言中的#if。除此之外,还可以用于定义一条拥有多行的命令; - 注释:注释采用
#字符,需要时可以采用反斜杠进行转义\#;
注意:
Makefile中的命令command必须以【Tab】键开始。
引用其它 Makefile
使用include关键字可以将其它Makefile包含进来,类似于
C
语言中的#include预处理语句,被包含的文件会自动替换至包含位置。
1 | include <filename> |
filename可以是当前操作系统 Shell
命令或者文件(可以包含路径和通配符),include关键字之前可以存在空字符,但是绝不允许出现【Tab】键。
例如:存在 4 个
Makefilea.mk、b.mk、c.mk、foo.make以及
1 个变量$(bar)(包含e.mk和f.mk)
,那么下面 2 条语句就是等价的:
1 | include foo.make *.mk $(bar) |
make命令开始执行时,会查找include的其它Makefile,如果没有指定绝对或者相对路径的话,make会首先在当前目录下查找,如果没有查询到则会进入如下目录:
- 如果
make命令执行时,带有-I或者--include-dir参数,那么make就会在该参数指定的目录下查找; - 此外,
make还会去查找<prefix>/include目录(通常为/usr/local/bin或者/usr/include);
最后,如果文件未能找到,make将会生成警告信息,然后继续载入其它文件,一旦makefile读取完成,make会再次进行查询,如果依然未能找到,则报出一条致命错误信息。如果想让make忽略读取错误,则可以在include前添加减号-。
1 | -include <filename> |
注意:其它版本
make采用的兼容命令是sinclude,其作用与-include相同。
这里,重新再来总结一下 GNU Make 的工作步骤:
- 读取所有
Makefile文件; - 查找被
include的其它Makefile; - 初始化
Makefile文件当中定义的变量; - 分析并且推导隐式规则;
- 创建
target目标文件的依赖关系; - 根据依赖关系,决定哪些
target需要重新生成;
MAKEFILES 环境变量
如果当前定义了MAKEFILES环境变量,其值为采用空格分隔的其它Makefile,执行make时会将这个该环境变量的值include进来。但是与include所不同的是,该环境变量引入的Makefile的target不会生效,其定义的文件如果发现错误,make也会不理会。
日常开发环境,不建议使用MAKEFILES环境变量,因为定义后会影响到所有make命令的执行。反而是在makefile文件出现一些莫名其妙错误的时候,需要检查当前是否定义了这个环境变量。
规则
规则描述了Makefile文件的依赖关系以及如何生成目标文件。定义在
Makefile 中的target可以有很多,但是第 1
条规则中的target会被确立为最终的目标。
1 | # 一种格式 |
targets:目标文件名称,以空格分隔,可以使用通配符;prerequisites:目标文件的依赖,如果某个依赖文件比目标文件要新,那么就会重新进行生成;command:Shell 命令行,如果不与target : prerequisites在一行,那么必须以【Tab】开头;如果保持在一行,则可以采用分号;进行分隔;
注意:如果
prerequisites和command过长,可以使用反斜杠\进行换行。通常make会以 Bash Shell 也就是/bin/sh来执行命令。
通配符
make支持*、?、~三个通配符。~字符在
Linux 下表示当前用户的$HOME目录,在 Windows
下则根据环境变量HOME设置而定。
通配符可以应用在command当中,下面代码会在清除所有.o文件之前,查看一下main.c文件。
1 | clean: |
通配符还可以应用于prerequisites,下面代码中的print目标依赖于所有.c文件,其中的$?是后续将会讲到的自动化变量。
1 | print: *.c |
通配符同样可以应用在变量中,但是并不会因此而自动展开,下面代码里变量objects的值就是*.o。
1 | objects = *.o |
如果需要让通配符在变量当中展开,即让objects的值是所有.o文件名的集合。
1 | objects := $(wildcard *.o) |
Autoconf
Automake
CMake
CMake
教程提供了一个循序渐进的指南,涵盖了常见的构建系统问题。本文涉及的示例代码可以在
CMake
源码树的Help/guide/tutorial目录下找到,每个步骤都拥有其相应的子目录,循序渐进直至提供完整的解决方案。
基本出发点
最为基础的项目是从源代码构建可执行文件,这样只需要一个 3
行的CMakeLists.txt文件,这将是整个教程的起点。在【Step1】目录当中创建如下CMakeLists.txt文件:
1 | cmake_minimum_required(VERSION 3.10) |
CMake
支持大写、小写、混合大小写的命令,上面的CMakeLists.txt文件使用了小写命令。教程源代码Step1目录中提供了用于执行数字平方根计算的cxx文件。
1 | /* 用于执行数字平方根计算的简单程序 */ |
添加版本号和配置头文件
我们要添加的第一个特性是为项目提供 1
个版本号。虽然源代码中也可以完成这件事,但是使用CMakeLists.txt可以提供更好的灵活性。首先,修改CMakeLists.txt文件,使用project()命令设置项目名称和版本号。
1 | cmake_minimum_required(VERSION 3.10) |
然后,继续编写配置,把一个头文件上保存的版本号传递到源代码:
1 | configure_file(TutorialConfig.h.in TutorialConfig.h) |
由于配置文件将会被写入到二叉树,所以必须将该目录添加至搜索包含文件的路径列表当中,在CMakeLists.txt文件的末尾添加以下行:
1 | target_include_directories(Tutorial PUBLIC |
在当前目录下创建TutorialConfig.h文件,并且包含如下内容:
1 | /* 配置主、副版本号 */ |
当 CMake
配置该头文件以后,上述的@Tutorial_VERSION_MAJOR@和@Tutorial_VERSION_MINOR@的值将会被替换。
接下来修改tutorial.cxx来包含上面的TutorialConfig.h头文件,并最终通过修改后的tutorial.cxx打印版本号。
1 |
|
指定 C++ 标准
接下来,将tutorial.cxx文件中的atof替换为std::stod,从而为项目添加一些
C++11 特性。同时,删除#include <cstdlib>。
1 | const double inputValue = std::stod(argv[1]); |
CMake 中启用特定 C++
标准支持的最简单方法是使用CMAKE_CXX_STANDARD变量,这里将CMakeLists.txt文件里的CMAKE_CXX_STANDARD变量设置为11,并将CMAKE_CXX_STANDARD_REQUIRED设置为True:
1 | cmake_minimum_required(VERSION 3.10) |
编译与测试
从命令行导航到 CMake 源代码树的 Help/guide/tutorial 目录,并运行以下命令:
CMake GUI
从 GNU Make 到 CMake 快速入门




