从 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 快速入门