1

Makefile简明介绍

 3 years ago
source link: https://note.qidong.name/2017/08/makefile-introduction/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

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,其实只做了两件事。

  1. 管理编译依赖,决定编译顺序。
  2. 根据时间戳变化与依赖关系,实现增量编译。

以下举一个简单的示例:

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_bfile_ctouch 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_bfile_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的约定,还有uninstallclean等,需要在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_bfile_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的生成工具。 以下是使用这套工具的流程图,从中可以感受到智慧与岁月。

Autotools的流程图

然后是CMake,一个跨平台的Makefile生成工具。 目前,它已经成为这类需求的主流工具。

这类工具的作用,是在更高的层级上去控制编译及相关流程,以实现复杂的操作。 而Makefile,只是它们的产物。

有趣的是,为什么一定要产生Makefile呢? Makefile真正核心的功能,其实并不复杂。 既然已经做了一个复杂的上层工具,为什么不干脆另起炉灶? GNU的思路是从UNIX哲学出发,用简单的工具来组合出复杂的用途,这个可以理解。 而另起炉灶的思路,也已经走通了,代表作就是Ninja


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK