Makefile简明介绍
2017-08-02 13:41:27 +08 字数:3109 标签: Makefile现在,只有非常小的项目,才会有人手写Makefile。 当然,例外还是有的,那就是Android(6.0以前)。
不写Makefile,不代表不需要去了解它。 实际上,对Makefile的理解,可以延伸出对整个软件编译流程、乃至持续集成系统的理解; 对Makefile技术的扩展、重构、发展,衍生出了当代实际使用的其它构建工具。
本文仅介绍Makefile最核心的东西,以及一些个人领悟。
Makefile是一个DSL ¶
Makefile其实是make
程序的配置文件,只是比XML、YAML这些东西复杂得多。
Makefile有其独特的语法,可以调用Shell,支持复杂的逻辑判断,但还不是图灵完全的一门编程语言。
所以,Makefile是一个DSL(Domain-Specific Languages,领域特定语言,由于这个通用翻译的水准较低,所以一般还是称为DSL)。
Makefile控制编译流程 ¶
作为一种DSL,Makefile的领域就是控制软件编译流程。
对于一个单文件C语言代码,比如hello.c
,编译是比较简单的。
gcc hello.c -o hello
而如果文件数目增多,互相之间还有复杂的编译依赖关系,手动执行gcc
就变得繁琐、易错。
用Shell脚本来做,是一个直观而且可行的方案。
不过,一个常见的问题是,当一个项目有大量文件,却只有少数几个发生改变时,如何进行增量编译?
难道每个中、大型项目都需要一个复杂的Shell编译脚本,并且都实现一个判断时间戳与依赖的功能?
于是,Makefile应运而生。
Makefile诞生的目标语言,是C语言。 而后,凡是静态类型,或者说需要编译的语言,都可以使用Makefile来组织项目。
值得一提的是,常见的静态类型语言中,Java是个例外。
Java的构建工具,从一开始就超越了Makefile,向着更深层次而发展。
因为,JDK中的编译器javac
,其实已经自带了Makefile的核心功能。
Makefile的核心功能 ¶
专为控制软件编译过程而生的Makefile,其实只做了两件事。
- 管理编译依赖,决定编译顺序。
- 根据时间戳变化与依赖关系,实现增量编译。
以下举一个简单的示例:
file_a: file_b file_c
touch file_a
file_b: file_d
touch file_b
file_c: file_d
touch file_c
file_d:
touch file_d
这就是一个简单的Makefile。
file_a: file_b file_c
就是说,需要得到file_a
,先要有file_b
和file_c
。
touch file_a
是一个实验性质的手段,即产生一个空的file_a
,或更新其时间戳。
总体来看,这是一个菱形依赖,A依赖于B、C,B、C又都分别依赖于D。
执行结果如下:
$ make
touch file_d
touch file_b
touch file_c
touch file_a
$ make
make: 'file_a' is up to date.
$ touch file_b
$ make
touch file_a
解释一下现象:
- 首次执行会先产生
file_d
,依次到file_a
,符合菱形依赖的规则。 - 再次执行时,由于
file_a
及其所有依赖都已经存在,时间戳也未更新,所以不再执行任何操作。 - 如果只更新
file_b
,那么只会重新生成依赖于它的file_a
,实现了增量编译。
Makefile是一门发展多年的古老DSL,扩展出了很多功能。 但万变不离其宗,这就是它的核心功能。
基本概念 ¶
核心功能相关的概念,有四个(中文翻译仅供参考):
- Rule,规则。
- Target,目标。
- Prerequisite,条件。
- Recipe,配方。
以下整个Makefile代码块,可以看做一个Rule。
targets : prerequisites
recipe
…
参照前面的样例,file_a: file_b file_c
里的file_a
就是Target,
file_b
和file_c
就是Prerequisite,
而touch file_a
就是产生file_a
的Recipe。
第一个Target,就是默认Target。
如果需要改变,比如改成file_b
,可以添加一行:
.DEFAULT_GOAL := file_b
默认情况下,Target都是文件的路径,可以是相对路径,也可以是绝对路径。 当然,通过内置的手段,可以让Target是文件夹,或者是非文件的Phony Target(虚假目标)。
另外,Prerequisite分两种。一种是普通,一种是有序。
targets : normal-prerequisites | order-only-prerequisites
在|
后面的Prerequisite,将严格按照排列顺序来产生,而普通的则无所谓顺序。
总之,再复杂的Makefile,都是这种形式的Rule组合而成。 理解了核心功能与基本概念后,Makefile已经没有什么难点,只剩细节了。
make命令 ¶
GNU make是执行Makefile的最常用软件。
通常,一个开源项目只需要两行就可编译、安装。
make
make install
其中,make
是编译整个项目,make install
是安装。
单单一个make
就能编译,这需要Makefile的配合,让默认Target就是项目编译。
install
是一个自定义的Phony Target,默认不存在,却是一种常用约定。
类似install
的约定,还有uninstall
、clean
等,需要在Makefile中自行实现。
除了常用的Target约定,make
本身也有一些常用的参数。
$ make -h
Usage: make [options] [target] ...
Options:
-B, --always-make Unconditionally make all targets.
-k, --keep-going Keep going when some targets can't be made.
-f FILE, --file=FILE, --makefile=FILE
Read FILE as a makefile.
-h, --help Print this message and exit.
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
-p, --print-data-base Print make's internal database.
-q, --question Run no recipe; exit status says if up to date.
...
上面只列出了帮助文档的一部分,完整内容可以自行查看。
-B
是一个不太常用、但能救急的命令。
它强制完整编译整个项目,相当于禁用了增量编译。
在出现一些奇奇怪怪的编译问题时,可以用这一招再试一下。
-k
是在编译出错时,尽量编译更多内容,触发所有错误,而非一出错就停下来。
-f
是指定Makefile。
通常,make
命令会默认使用当前目录下的Makefile
文件,其次是makefile
,如果都没有则会报错。
特殊情况下,可能Makefile文件需要命名为其它名称,或在其它目录,这时就可以以-f
指定。
-j
可以指定同时启动多少条线程来执行并行编译。
而并行编译的前提,就是判断有哪些文件的编译是可以并行的,这一点可以从Makefile自动判断。
前面的例子中,只有file_b
和file_c
是可以并行的,所以最多只需要-j2
即可。
对大型项目来说,可以设置为最大,即CPU的核数。
-q
会不执行Recipe,相当于dry-run。
-p
会打印完整的make
过程到命令行。
打印内容包含了很多默认的规则、环境变量等,至少1000行,不推荐直接输出到终端。
两个命令通常一起使用,make -qp > qp.makefile
,
这样做可以合并多个Makefile,分析其中的细节。
相关资料 ¶
详细的Makefile资料,可以参考GNU make文档。
可以执行Makefile的其它程序 ¶
- NMAKE,全称Microsoft Program Maintenance Utility,是微软的Visual Studio 随附的命令行工具,用来在Windows上执行Makefile。
- mozilla/pymake,一个Python实现的工具,功能是GNU make的子集。
- google/kati,基于GNU make而发展的一个项目,主要目的是把Makefile转换为Ninja,为Android服务。
生成Makefile的程序 ¶
首先是GNU Autotools,一套Unix下的Makefile的生成工具。 以下是使用这套工具的流程图,从中可以感受到智慧与岁月。
然后是CMake,一个跨平台的Makefile生成工具。 目前,它已经成为这类需求的主流工具。
这类工具的作用,是在更高的层级上去控制编译及相关流程,以实现复杂的操作。 而Makefile,只是它们的产物。
有趣的是,为什么一定要产生Makefile呢? Makefile真正核心的功能,其实并不复杂。 既然已经做了一个复杂的上层工具,为什么不干脆另起炉灶? GNU的思路是从UNIX哲学出发,用简单的工具来组合出复杂的用途,这个可以理解。 而另起炉灶的思路,也已经走通了,代表作就是Ninja。