版权声明:本文章参考了陈皓先生的《跟我一起写makefile》,并根据最新的《GNU make手册》(截止2018年5月),以及《Linux man pages》做了修改,增添了一部分内容。未经作者允许,严禁用于商业出版,否则追究法律责任。网络转载请注明出处,这是对原创者的起码的尊重!!!
1 工程管理器make
1.1 make简介
make是什么?
Make是GCC提供的一种半自动化的工程管理器。所谓的半自动化是指在使用工程管理器之前需要人工编写程序的编译规则,所有的编译规则都保存在Makefile文件中,全自动化工程管理器会在编译程序前自动生成Makefile文件。为什么要有工程管理器Make?
因为在实际的开发过程中,仅仅通过gcc命令对程序进行编译时非常低效的,原因主要有两点:- 程序往往是由多个源文件组成的,源文件的个数越多,那么gcc的命令行就会越长。此外,各种编译规则也会加大gcc命令行的复杂度,所以在开发调试的过程中,通过输入gcc命令行来编译程序是很麻烦的。
- 在程序的整个开发过程中,调试的工作量占到了整体工作量的70%以上。在调试程序的过程中,每次调试一般只会修改部分源文件。而在使用gcc命令行编译程序时,gcc会把那些没有被修改的源文件一起编译,这样就会影响变异的总体效率。
工程管理器Make有哪些优越性?
- 使用方便。通过命令“make”就可以启动MAKE工程管理器对程序进行编译,所以不再需要每次都输入GCC命令行。MAKE启动后会根据Makefile文件中的编译规则命令自动对源文件进行编译和链接,最终生成可执行文件。
- 调试效率高。为了提高编译程序的效率,Make会检查每个源文件的修改时间(时间戳)。只有在上次编译之后被修改的源文件才会在接下来的编译过程中被编译和链接,这样就能避免多余的编译工作量。为了保证源文件具有正确的时间戳,必须保证操作系统时间的准确性。
1.2 make的退出码
退出码 | 描述 |
---|---|
0 | 表示成功执行。 |
1 | 使用make的“-q”选项时,如果指定目标需要更新,make退出码1,否则为0。 |
2 | 如果make运行时出现任何错误 |
1.3 make命令详解
命令:make [options] [target] …
描述:根据Makefile自动编译。默认执行第一个目标。
短选项 | 长选项 | 描述 |
---|---|---|
-b, -m | 为了兼容性,忽略这些选项 | |
-B | –always-make | 无条件重建所有目标,即使目标是最新的 |
-C dir | –directory=dir | 切换到dir目录,读取并执行makefile文件。默认当前目录。使用多个-C命令行选项时,后一个指定的目录是前一个指定的目录的子目录。 |
-d | 打印大量调试信息。调试信息指出正在考虑重新创建哪些文件,正在比较哪些文件时间以及结果如何,哪些文件真的需要重编译,哪些隐式规则会被考虑并应用 | |
–debug[=FLAGS] | 打印各种调试信息。如果FLAGS被省略,则与-d相同。FLAGS为b适用于基本调试,v适用于更详细的基本调试,i适用于显示隐式规则,j适用于调用命令的详细信息,m适用于调试时重新生成文件。n禁用所有以前的调试标志。 | |
-e | –environment-overrides | 系统环境变量将覆盖 makefile 中定义的同名变量 |
-f FILE | –file=file, –makefile=FILE | 读取FILE作为一个makefile。默认读取GNUmakefile、makefile、Makefile |
-h | 打印帮助消息。 | |
-i | –ignore-errors | 执行某个命令时如果发生错误则忽略并继续往下执行,不指定该选项则停止 |
-I dir | –include-dir=dir | 指定makefile中引入的其它makefile文件的搜索目录,使用多个-I 选项时,make 将按照顺序依次在 dir目录下搜索 |
-j [N] | –jobs[=jobs] | 同时允许N个任务;无参数表明允许无限个任务。如果多个-j选项,则最后一个有效 |
-k | –keep-going | 当某些目标无法创建时尽可能多的继续。 |
-l [N] | –load-average[=load] | 不开始新的作业除非系统负载低于N。省略N则删除之前ed负载限制 |
-L | –check-symlink-times | 在符号链接和目标之间使用最新的mtime。 |
-n | –just-print, –dry-run, –recon | 仅显示要执行的命令,但不执行命令。 |
-o FILE | –old-file=file, –assume-old=file | 强制将FILE认作非常老,不要重新 make 它。 |
-O[type] | –output-sync[=type] | 当与-j并行运行多个作业时,确保每个作业的输出都被收集在一起,而不是散布在其他作业的输出中。如果type被省略或是target,则将每个目标的输出组在一起。如果type是line,每个命令的输出将被组在一起。如果type是recurse,则整个递归make的输出被组在一起。如果type是none,则禁用输出同步。 |
-p | –print-data-base | 打印读取makefile所产生的数据库(规则和变量值) |
-q | –question | 不运行任何命令;退出状态为0则目标已全部更新。 |
-r | –no-builtin-rules | 禁用内置隐含规则。 同时清除后缀规则的默认后缀列表。 |
-R | –no-builtin-variables | 禁用内置变量设置。 |
-s | –silent, –quiet | 执行时不打印执行的命令。 |
-S | –no-keep-going, –stop | 关闭 -k。除非在递归make中-k可能通过MAKEFLAGS从顶层make继承,或者在环境中的MAKEFLAGS中设置-k,否则该选项是不需要的。 |
-t | –touch | 使用touch将目标的时间标记为最新,而不是重新创建它们。 |
–trace | 打印每个目标的处置信息(为什么要重建目标以及运行哪些命令来重建目标)。 | |
-v | –version | 打印 make 的版本号并退出。 |
-w | –print-directory | 在其它处理前后打印工作目录。 |
–no-print-directory | 即使 -w 隐式开启,也要关闭 -w。 | |
-W FILE | –what-if=file, –new-file=file, –assume-new=file | 将 FILE 认作无限新。 |
–warn-undefined-variables | 当引用未定义的变量打印警告信息。 | |
2 Makefile简介
Makefile存储着工程管理器Make进行工作所需的编译规则命令。
2.1 简单示例
例如,有 Makefile 文件,内容如下:
main.exe:main.o func.o //有头文件时要加入头文件
g++ -o main.exe main.o func.o
main.o:main.cpp
g++ -c main.cpp
func.o:func.cpp
g++ -c func.cpp
对于该 Makefile 文件,程序 make 处理过程如下:
1. make程序首先读到第1行的目标文件main.exe
和它的两个依赖文件main.o
和func.o
;然后比较文件main.exe
和main.o
、func.o
的产生时间,如果 main.exe
比 main.o
或func.o
旧的话,则执行第2行命令,以产生目标文件main.exe
。
2. 在执行第2行的命令前,它首先会查看makefile中的其他定义,看有没有以第1行main.o
和 func.o
为目标文件的依赖文件,如果有的话,继续按照1、2的方式匹配下去。
3. 根据2的匹配过程,make 程序发现第3行有目标文件main.o
依赖于main.cpp
,则比较目main.o
与它的依赖文件 main.cpp
的文件新旧,如果main.o
比main.cpp
旧,则执行第4行的命令以产生目标文件main.o
。
4. 在执行第 4 条命令时,main.cpp
在文件makefile不再有依赖文件的定义,make程序不再继续往下匹配,而是执行第4条命令,产生目标文件main.o
。目标文件 func.o
按照上面的同样方式判断产生。
5. 执行3、4产生完main.o
和 func.o
以后,则第 2 行的命令可以顺利地执行了,最终产生了第 1 行的目标文件main.exe
。
2.2 Makefile组成
- 规则
- 显式规则。显式规则说明了,如何生成一个或多个目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
- 隐式规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。
- 变量定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指令。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。
- 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“#”。
2.3 MakeFile长行分割
- Makefile使用
\
来进行长行分割。
2.4 Makefile文件名
默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“gnumakefile”,“makefile”、“Makefile”的文件。
当然,你可以使用别的文件名来书写Makefile,比如:“xxxx”,但要用用make的“-f FILE”选项指定该文件为Makefile文件,如:make -f xxxx
。
2.5 引入其它的Makefile
在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是:
include filename1 filename2... //filename可以是当前操作系统Shell的文件名(可以保含路径和通配符)
make命令开始时,会把找寻include所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的#include一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
(1)如果make执行时,有-I
或--include-dir
参数,那么make就会在这个参数所指定的目录下去寻找。
(2)如果目录<prefix>/include
(一般是:/usr/local/bin
或/usr/include
)存在的话,make也会去找。
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”,表示无论include过程中出现什么错误,都不要报错继续执行,如 -include <filename>
。
2.6 环境变量MAKEFILES
如果你的当前环境中定义了环境变量MAKEFILES,那么,make会把这个变量中的值做一个类似于include的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和include不同的是,从这个环境变中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。
2.7 覆盖其它Makefile的部分
有两种方式:
- 对于
inlclude
引入的其它makefile,相同的规则(目标与依赖都相同),在后面展开的的会覆盖在前面展开的() - GNUmakefile,gnumakefile,makefile,Makefile。优先级由高到低,因此前面的会覆盖后面的文件。
2.8 make如何读取Makefile
GNU的make工作时的执行步骤入下:
- 第一阶段:
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 第二阶段
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
展开:将变量、函数的值放在对应的位置。
- 立即展开:在第一段中发生的展开。
延后展开:直到构建上下文出现或者第二阶段才发生的展开。
变量赋值
immediate = deferred
immediate ?= deferred
immediate := immediate
immediate ::= immediate
immediate += deferred or immediate //右值是简单变量立即展开,否则延后展开
immediate != immediate
define immediate
deferred
endef
define immediate =
deferred
endef
define immediate ?=
deferred
endef
define immediate :=
immediate
endef
define immediate ::=
immediate
endef
define immediate +=
deferred or immediate
endef
define immediate !=
immediate
endef
- 条件指令:immediate
- 规则定义:显式规则,模式规则,后缀规则,静态模式规则和简单的先决条件定义
immediate : immediate ; deferred
deferred
2.9 再次展开
make可对依赖项在立即展开后再次展开,再次展开发生在第二阶段前。一般是因为第一次展开后make将其解释为字符串而不是make语法。.SECONDEXPANSION
关键字表示将后面的内容多次展开。
.SECONDEXPANSION:
...
#不能运行
objs1:=main.o
objs2:=add.o
main.exe:$(objs1) $$(objs2)
gcc -o $@ $^
#可以运行
.SECONDEXPANSION:
objs1:=main.o
objs2:=add.o
main.exe:$(objs1) $$(objs2)
gcc -o $@ $^
3 显式规则
target: dependency //目标项:依赖项
command1
command2 //必须以Tab键开头,command为更新目标的命令,支持shell脚本
#或
target: dependency_files; command1
command2
规则规定了两件事:
- 如何判断目标项是否过期:如果目标不存在或者依赖项新于目标项
- 怎样更新目标项。
- 依赖关系中最好加入头文件。原因在于头文件改变,实现文件未改变时,不会重新编译目标。
3.1 目标项和依赖项
3.1.1 依赖项的类型
- 普通依赖项
当普通依赖项新于目标项时,默认情况下目标项要更新。 - 顺序依赖项
用普通依赖项|顺序依赖项
表示,其仅表示一个先执行的操作,当顺序依赖项新于目标项时,目标项不需要更新。例如:将目标文件放在单独的目录中,因此需要先创建目录,但是任何对目录项的操作都会引起目录时间更新,如果目录不是顺序依赖项,则会导致目标由于目录的更新而重新编译。
OBJDIR := objdir OBJS := objdir/add.o objdir/main.o