0 什么是Makeflie
0.1 makefile关系到了整个工程的编译规则
一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
0.2 编译的过程
编译
首先编译产生目标文件(object file/.obj/.o)。编译时,编译器需要的是语法的正确,函数与变量的声明的正确。只要所有的语法正确,编译器就可以编译出中间目标文件。
一般来说,每个源文件都应该对应于一个中间目标文件。链接
链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件 (Object File) ,在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。
1 Makefile介绍
示例:一个具有3个头文件和7个c文件的工程,且makefile要完成如下功能:
- 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
- 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。
1.1 Makefile规则
1 | target... : prerequisites ... |
target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label);
prerequisites是要生成target所需要的文件或是目标;
command也就是make需要执行的命令。(任意的Shell命令)。
三者的关系为:target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。
进一步的:prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。 这就是Makefile的规则。也就是Makefile中最核心的内容。
- 几个符号(自动化变量)
$@ –目标文件
$^ –所有的依赖文件
$< –第一个依赖文件
1.2 示例
1 | # 第一句是链接,后面是编译 |
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个Tab键作为开头。
make并不管命令是怎么工作的,他只管执行所定义的命令。
make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
clean
是一个标签,可以用类似的方法定义命令。以此为例,只要输入make clean
,就会执行冒号后面的命令。
1.3 make是如何工作的
在默认的方式下,也就是我们只输入make命令:
- make会在当前目录下找名字叫
Makefile
或makefile
的文件。 - 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到
edit
这个文件,并把这个文件作为最终的目标文件。 - 如果
edit
文件不存在,或是edit所依赖的后面的.o
文件的文件修改时间要比edit
这个文件新,那么,他就会执行后面所定义的命令来生成edit
这个文件。 - 如果
edit
所依赖的.o
文件也存在,那么make会在当前文件中找目标为.o
文件的依赖性,如果找到则再根据那一个规则生成.o
文件。(这有点像一个堆栈的过程) - 当然,你的C文件和H文件是存在的啦,于是make会生成
.o
文件,然后再用.o
文件声明make的终极任务,也就是执行文件edit了。
make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性。比如,如果命令不是用来编译这个目标文件的,makefile是检测不出来的
makefile的终极要义是依赖性:就是看目标文件的依赖文件是否都存在以及目标文件是否存在,如果目标文件存在则比较文件的生成时间,如果依赖文件不存在则递归这个编译过程,即寻找依赖文件的依赖关系,并执行这个依赖文件下的指令。
于是在我们编程中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如file.c,那么根据我们的依赖性,我们的目标file.o会被重编译(也就是在这个依性关系后面所定义的命令),于是file.o的文件也是最新的啦,于是file.o的文件修改时间要比edit要新,所以edit也会被重新链接了(详见edit目标文件后定义的命令)。
1.4 makefile中使用变量
对于makefile中重复出现的部分,我们可以像C语言的宏一样将其定义为变量。
例如:1
2
3
4
5
6
7
8
9objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
# 依赖关系...
clean:
rm edit $(objects)
在指令中使用$(Variable_name)
,即可代表一串文件的名字。
1.5 让makefile自动推导
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,并且 cc -c [.c] 也会被推导出来。
所以上述makefile语句可以简化为:
1 | objects = main.o kbd.o command.o display.o insert.o search.o files.o utils.o |
- 其中
.PHONY
表示,clean
是个伪目标文件。 - 每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。
- 在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。
1.6 依赖关系的另一种写法
依赖关系可以用下列方式来写,即以被依赖的文件为主体的表达:
1 | $(objects) : defs.h |
2 Makefile总述
2.1 Makefile里有什么
五个部分:显式规则、隐晦规则、变量定义、文件指示和注释。
- 显式规则
显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
隐晦规则
由于make有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写Makefile,这是由make所支持的。变量定义
变量一般都是字符串,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。文件指示
其包括了三个部分:
在一个Makefile中引用另一个Makefile,就像C语言中的include一样;
指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;
定义一个多行的命令。注释
使用#来注释。
值得一提的是,在Makefile中的命令,必须要以[Tab]键开始。
2.2 Makefile的文件名
默认的情况下,make命令会在当前目录下按顺序找寻文件名为GNUmakefile
、makefile
、Makefile
的文件。最好使用Makefile
,而不要使用GNUmakefile
(仅适用于GNU make)。
也可以使用其他任意文件名,使用make -f filename
或make --file filename
即可调用该文件。
2.3 引用其它的Makefile
使用include关键字可以把别的Makefile包含进来,这很像C语言的#include
,被包含的文件会原模原样的放在当前文件的包含位置。
语法为:include<filename>
,例如include ../GNUMakeConfig/defines.qnx.mk
。文件路径符合当前系统shell语法即可。
在include前面可以有一些空字符,但是绝不能是[Tab]键开始。多个文件之间可以用空格隔开。
举个例子,你有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个变量$(bar)
,其包含了e.mk和f.mk,那么,下面的语句:1
include foo.make *.mk $(bar)
等价于1
include foo.make a.mk b.mk c.mk e.mk f.mk
make命令开始时,会把找寻include
所指出的其它Makefile,并把其内容安置在当前的位置。就好像C++的#include
指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
- 如果make执行时,有
-I
或--include-dir
参数,那么make就会在这个参数所指定的目录下去寻找。 - 如果目录
/include
(一般是:/usr/local/bin或/usr/include
)存在的话,make也会去找。
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。
2.4 环境变量MAKEFILES
如果你的当前环境中定义了环境变量MAKEFILES
,那么,make会把这个变量中的值做一个类似于include
的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和include
不同的是,从这个环境变中引入的Makefile的“目标”不会起作用,如果环境变量中定义的文件发现错误,make也会不理。
2.5 make的工作方式
GNU的make工作时的执行步骤入下:
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
3 Makefile书写规则
规则包含两个部分,一个是依赖关系,一个是生成目标的方法。
一般来说,定义在Makefile中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make所完成的也就是这个目标。
3.1 规则的语法
如1.1和1.2节所述,规则如下所示:1
2main.o : main.c defs.h
cc -c main.c
规则告诉我们两件事:
- 文件的依赖关系,main.o依赖于main.c和defs.h的文件,如果main.c和defs.h的文件日期要比main.o文件日期要新,或是main.o不存在,那么依赖关系发生,执行下面的指令。
- 如果生成(或更新)main.o文件。也就是那个cc命令,其说明了,如何生成main.o这个文件。
规则也可以写成:1
main.o : main.c defs.h; cc -c main.c
命令行如果不与target:prerequisites
在一行,那么,必须以[Tab键]开头,如果和prerequisites
在一行,那么可以用分号做为分隔。
3.2 文件搜寻
在一些大的工程中,有大量的源文件,我们通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make在自动去找。
- 特殊变量
VPATH
可以完成这个功能
如果没有指明VPATH
,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当当前目录找不到的情况下,到所指定的目录中去找寻文件了。1
2
3
4
5
6
7
8
9
10
11
12
13
14VPATH = src:../headers
```
上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。
* 关键字`vpath`也可以设置搜索文件路径
它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:
```makefile
vpath < pattern> < directories> #为符合模式< pattern>的文件指定搜索目录<directories>。
vpath < pattern> #清除符合模式< pattern>的文件的搜索目录。
vpath #清除所有已被设置好了的文件搜索目录。
```
`vapth`使用方法中的`<pattern>`需要包含`%`字符。`%`的意思是匹配零或若干字符,例如,`%.h`表示所有以`.h`结尾的文件。`<pattern>`指定了要搜索的文件集,而`<directories>`则指定了的文件集的搜索的目录。例如:
```makefile
vpath %.h ../headers
该语句表示,要求make在../headers
目录下搜索所有以.h
结尾的文件(如果某文件在当前目录没有找到的话)。我们可以连续地使用vpath
语句,以指定不同搜索策略。如果连续的vpath
语句中出现了相同的<pattern>
,或是被重复了的<pattern>
,那么,make会按照vpath
语句的先后顺序来执行搜索。
3.3 伪目标
伪目标不被最终目标所依赖。比如上述的clean。我们并不生成“clean”这个文件。“伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显示地指明这个“目标”才能让其生效。
.PHONY : clean
可以显式地声明这是一个伪目标,即使后面的名称与要编译的文件重名也没关系,make clean
一样能够执行clean
后的命令,这就是伪目标的特性:只要调用,无论有没有依赖关系都总是被执行。
伪目标也可以作为最终目标,也可以作为依赖:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23.PHONY : all
all : prog1 prog2 prog3
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
对于这个makefile文件,只需要一个make命令就可以生成prog1
、prog2
、prog3
三个目标,而cleanall
依赖cleanobj
与cleandiff
因此使用make cleanall
会执行所有指令。
3.4 多目标
如1.6节所述,可以多个目标依赖一个文件:1
2bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于:1
2
3
4
5bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
3.5 静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。
其语法为:1
2<targets...>: <target-pattern>: <prereq-patterns ...>
<commands>
<targets>
定义了一系列的目标文件,可以有通配符。是目标的一个集合。<target-parrtern>
是指明了<targets>
的模式,也就是的目标集模式。<prereq-parrterns>
是目标的依赖模式,它对<target-parrtern>
形成的模式再进行一次依赖目标的定义。
如果我们的<target-parrtern>
定义成%.o
,意思是我们的集合中都是以.o
结尾的,而如果我们的<prereq-parrterns>
定义成%.c
,意思是对<target-parrtern>
所形成的目标集进行二次定义,其计算方法是,取<target-parrtern>
模式中的%
(也就是去掉了.o
这个结尾),并为其加上.c
这个结尾,形成的新集合。
举例来说:1
2
3
4
5objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
其相当于1
2
3
4
5foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
再比如:1
2
3
4
5
6
7files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
3.6 自动生成依赖性
使用gcc -MM filename
或cc -m filename
可以输出文件的依赖项。
GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个name.c
的文件都生成一个name.d
的Makefile文件,.d
文件中就存放对应.c
文件的依赖关系。
于是,我们可以写出.c
文件和.d
文件的依赖关系,并让make自动更新或自成.d
文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依赖关系了。
这里,我们给出了一个模式规则来产生[.d]文件:1
2
3
4
5%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< >; $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ >; $@; \
rm -f $@.$$$$
这个规则的意思是,所有的.d
文件依赖于.c
文件,rm -f $@
的意思是删除所有的目标,也就是.d
文件,第二行的意思是,为每个依赖文件$<
,也就是.c
文件生成依赖文件,$@
表示模式 %.d
文件,如果有一个C文件是name.c,那么%
就是name
,$$$$
意为一个随机编号,第二行生成的文件有可能是 name.d.12345
,第三行使用sed
命令做了一个替换,关于sed
命令的用法请参看相关的使用文档。第四行就是删除临时文件。
总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入[.d]文件的依赖,即把依赖关系:1
main.o : main.c defs.h
转成:1
main.o main.d : main.c defs.h
接下来,我们就要把这些自动生成的规则放进我们的主Makefile中。我们可以使用 Makefile的include
命令,来引入别的Makefile文件,例如:1
2sources = foo.c bar.c
include $(sources:.c=.d)
上述语句中的$(sources:.c=.d)
中的“.c=.d”的意思是做一个替换,把变量$(sources)
所有.c
的字串都替换成.d
,关于这个“替换”的内容,在后面我会有更为详细的讲述。当然,你得注意次序,因为include
是按次来载入文件,最先载入的.d
文件中的 目标会成为默认目标。
4 Makefile书写命令
4.1 显示命令
通常,make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被make显示出来,最具代表性的例子是,我们用这个功能来像屏幕显示一些信息。如:1
@echo 正在编译XXX模块......
当make执行时,会输出“正在编译XXX模块……”字串,但不会输出命令,如果没有“@”,那么,make将输出:
echo 正在编译XXX模块……
正在编译XXX模块……
如果make执行时,带入make参数-n
或--just-print
,那么其只是显示命令,但不会执行命令,这个功能很有利于我们调试我们的Makefile,看看我们书写的命令是执行起来是什么样子的或是什么顺序的。
而make参数-s
或--slient
则是全面禁止命令的显示。
4.2 命令执行
当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。
需要注意的是,如果要让上一条命令的结果应用在下一条命令时,应该使用分号分隔这两条命令。比如第一条命令是cd
命令,你希望第二条命令得在cd
之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:
示例一:1
2
3exec:
cd /home/hchen
pwd
示例二:1
2exec:
cd /home/hchen; pwd
当我们执行make exec
时,第一个例子中的cd
没有作用,pwd
会打印出当前的Makefile目录,而第二个例子中,cd
就起作用了,pwd会打印出/home/hchen
。
4.3 命令出错
每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。
有时命令返回非0并不代表有错误,比如mkdir
的目录已存在。此时,我们在命令前加-
,标记为不管命令出不出错都认为是成功的。
另一种方式为给make加上-i
或是--ignore-errors
参数,那么,Makefile中所有命令都会忽略错误。而如果一个 规则是以.IGNORE
作为目标的,那么这个规则中的所有命令将会忽略错误。
还有一个要提一下的make的参数的是-k
或是--keep-going
,这个参数的意思是,如果某规则中的命令出错了,那么就终目该规则的执行,但继续执行其它规则。
4.4 嵌套执行make
在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模 块编译和分段编译有着非常大的好处。
例如,我们有一个子目录叫subdir
,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:
1 | subsystem: |
其等价于:1
2subsystem:
$(MAKE) -C subdir
定义$(MAKE)
宏变量的意思是,也许我们的make需要一些参数,所以定义成一个变量比较利于维护。这两个例子的意思都是先进入subdir
目录,然后执行make命令。
我们把这个Makefile叫做“总控Makefile”,总控Makefile的变量可以传递到下级的Makefile中(如果你显示的声明),但是不会覆盖下层的Makefile中所定义的变量,除非指定了-e
参数。
如果你要传递变量到下级Makefile中,那么你可以使用这样的声明:1
export <variable ...>;
如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:1
unexport <variable ...>;
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15#示例一
export variable = value
#其等价于:
variable = value
export variable
#其等价于:
export variable := value
#其等价于:
variable := value
export variable
#示例二:
export variable += value
#其等价于:
variable += value
export variable
如果你要传递所有的变量,那么,只要一个export
就行了。后面什么也不用跟,表示传递所有的变量。
需要注意的是,有两个变量,一个是SHELL
,一个是MAKEFLAGS
,这两个变量不管你是否export
,其总是要传递到下层Makefile中,特别是MAKEFILES
变量,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层Makefile中定义了 这个变量,那么MAKEFILES
变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。
4.5 定义命令包
如果Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以define
开始,以endef
结束,如:
1 | define run-yacc |
这里,run-yacc
是这个命令包的名字,其不要和Makefile中的变量重名。在define
和endef
中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc
程序,因为Yacc
程序总是生成y.tab.c
的文件,所以第二行的命令就是把这个文件改改名字。