不合格的程序猿
分类: C/C++
2015-12-14 16:05:48
原文地址:makefile(3) 作者:linux_wuliqiang
make书写规则包含两个部分,一个是依赖关系,一个是生成目标的方法。在makefile文件中,规则的顺序是很重要的。因为makefile文件中只应该有一个最终目标,其他的目标都是被这个目标所连带出来的,所以一定要让make知道最终目标是什么。一般来说,定义在makefile文件中的目标可能会有很多,但是第一条规则中的目标将被确立为最终的目标。make所完成的也就是这个目标。
实例5-9
foo.o : foo.c defs.h # foo模块
gcc -c -g foo.c
看到这个例子,应该不是很陌生了,前面也已说过,foo.o是目标,foo.c和defs.h是目标所依赖的源文件,此处可使用命令gcc -c -g foo.c (以Tab键开头)。这个规则说明两件事:
● 文件的依赖关系。foo.o依赖于foo.c和defs.h文件,如果foo.c和defs.h文件的日期比foo.o文件的日期新,或者foo.o不存在,则发生依赖关系。
● 如果生成(或更新)foo.o文件,要用到gcc命令。
如果想定义一系列比较类似的文件,很自然地就想起使用通配符。make支持3种通配符:“*”、“?”和“[...]”。波浪号(“~”)字符在文件名中也有比较特殊的用途。如果是“~/test”,就表示当前用户的$HOME目录下的test目录。而“~hchen/test”则表示用户hchen的宿主目录下的test目录(这些都是Linux下的常识,make也支持)。而在Windows或是MS-DOS下,用户没有宿主目录,波浪号所指的目录则根据环境变量HOME而定。
通配符代替了一系列的文件,如“*.c”表示后缀为c的文件。一个需要注意的是,如果文件名中有通配符,如“*”,可以用转义字符“\”,如“\*”来表示真实的“*”字符,而不是任意长度的字符串。
下面还是先来看几个例子:
实例5-10
clean:
rm -f *.o
实例5-10是操作系统Shell所支持的通配符。这是在命令中的通配符。
实例5-11
print: *.c
lpr -p $?
touch print
实例5-11说明了通配符也可以在规则中,目标print依赖于所有的[.c]文件。其中的“$?”是一个自动化变量,将会在后面作详细介绍。
实例5-12
objects = *.o
实例5-12表示了,通配符同样可以用在变量中。makefile文件中的变量其实就是C/C++中的宏。如果要让通配符在变量中展开,也就是让objects的值成为所有[.o]的文件名的集合,可以像实例5-13这样:
实例5-13
objects := $(wildcard *.o)
这种用法由关键字wildcard指出。
在一些大的工程中,有大量的源文件,通常的做法是把这许多的源文件分类,并存放在不同的目录中。所以,当make需要去找寻文件的依赖关系时,可以在文件前加上路径,但最好的方法是把一个路径告诉make,让make自动去找。makefile文件中的特殊变量VPATH就是完成这个功能的。如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件;如果定义了这个变量,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件。
实例5-14
VPATH = src:../headers
实例5-14的定义指定两个目录:src和../headers。make会按照这个顺序进行搜索。目录由“冒号”分隔(当然,当前目录永远是最高优先搜索的位置)。另一个设置文件搜索路径的方法是使用make的vpath关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,而这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能,它的使用方法有三种:
● vpath
为符合模式
● vpath
清除符合模式
● vpath
清除所有已被设置好了的文件搜索目录。
vapth使用方法中的
实例5-15
vpath %.h ../headers
该语句表示,要求make在../headers目录下搜索所有以.h结尾的文件(如果某文件在当前目录没有找到的话)。可以连续地使用vpath语句,以指定不同搜索策略。如果连续的vpath语句中出现了相同的
实例5-16
vpath %.c foo
vpath % blish
vpath %.c bar
其表示.c结尾的文件,先在foo目录,然后在blish目录,最后在bar目录中进行搜索。
实例5-17
vpath %.c foo:bar
vpath % blish
实例5-17的语句则表示.c结尾的文件,先在foo目录,然后在bar目录,最后才在blish目录中进行搜索。
在前面的例5-1中,提到过一个clean的目标,这是一个“伪目标”。
实例5-18
clean:
rm *.o temp
正像前面例子中的clean一样,既然生成了许多编译文件,也应该提供一个清除它们的“目标”以备完整地重新编译时用(以make clean来使用该目标)。因为并不生成clean这个文件,“伪目标”并不是一个文件,只是一个标签。由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行,只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
为了避免和文件重名这种情况,可以使用一个特殊的标记.PHONY来显式地指明一个目标是“伪目标”,向make说明不管是否有这个文件,这个目标就是“伪目标”。
PHONY : clean
只要有这个声明,不管是否有clean文件,要运行clean这个目标,整个过程可以这样写:
.PHONY: clean
clean:
rm *.o temp
伪目标一般没有依赖的文件,但是,也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在最前面即可。比如,如果makefile文件需要连续生成若干个可执行文件,而只想简单地输入一个make就让其执行,并且所有的目标文件都写在一个makefile文件中,可以使用“伪目标”这个特性,如实例5-19。
实例5-19
all : prog1 prog2 prog3
.PHONY : all
prog1 : prog1.o utils.o
gcc -o prog1 prog1.o utils.o
prog2 : prog2.o
gcc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
gcc -o prog3 prog3.o sort.o utils.o
makefile文件中的第1个目标会被作为其默认目标,声明了一个all的伪目标,其依赖于其他3个目标。由于伪目标的特性是总会被执行,所以其依赖的那3个目标就总不如all这个目标新。所以,其他3个目标的规则总是会被采纳,也就达到了一下子生成多个目标的目的。.PHONY : all声明了all这个目标为“伪目标”。
从实例5-19可以看出,目标也可以成为依赖关系。所以,伪目标同样也可成为依赖关系,如实例5-20。
实例5-20
.PHONY: cleanall cleanobj cleandiff
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
make clean将清除所有需要被清除的文件。cleanobj和cleandiff这两个伪目标有点像“子程序”的意思。可以输入make cleanall和make cleanobj以及make cleandiff命令来达到清除不同种类文件的目的。
makefile文件规则中的目标可以不止一个,其支持多目标。有可能多个目标同时依赖于一个文件,并且其生成的命令大体类似,于是就能把其合并起来。当然,多个目标的生成规则的执行命令是同一个,这可能会带来麻烦,不过可以使用一个自动化变量“$@”。这个变量表示目前规则中所有目标的集合,这样说可能很抽象,如实例5-21。
实例5-21
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
上述规则等价于实例5-22:
实例5-22
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
其中,-$(subst output”$@)中的“$”表示执行一个makefile文件的函数,函数名为subst,后面的为参数。“$@”表示目标的集合,就像一个数组,“$@”依次取出目标,并执行命令。
静态模式可以更加容易地定义多目标的规则,可以让规则变得更加灵活和有弹性。语法如下:
实例5-23
...
● targets定义了一系列的目标文件,可以有通配符,表示目标的一个集合。
● target-pattern指明了targets的模式,也就是目标集模式。
● prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。
如果
所以,“目标模式”或是“依赖模式”中都应该有“%”这个字符,如果文件名中有“%”,可以使用反斜杠“\”进行转义,以标明真实的“%”字符。
看一个例子:
实例5-24
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(gcc) -c $(CFLAGS) $< -o $@
实例5-24中,指明了目标从$object中获取,“%.o”代表所有以“.o”结尾的目标,也就是foo.o bar.o,即变量$object集合的模式,而依赖模式“%.c”则取模式“%.o”的“%”,也就是foo bar,并为其加上“.c”的后缀,于是,依赖目标就是foo.c bar.c。而命令中的“$<”和“$@”是自动化变量,“$<”表示所有的依赖目标集(也就是foo.c bar.c),“$@”表示目标集(也就是foo.o bar.o)。于是,上面的规则展开后等价于实例5-25的规则:
实例5-25
foo.o : foo.c
$(gcc) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(gcc) -c $(CFLAGS) bar.c -o bar.o
试想,如果“%.o”有几百个,只要用这种很简单的“静态模式规则”就可以写完一堆规则,简化多了。“静态模式规则”的用法很灵活,如果用得好,将是一个很强大的功能。再看一个例子:
实例5-26
files = foo.elc bar.o lose.o
$(filter %.o,$(files)): %.o: %.c
$(gcc) -c $(CFLAGS) $< -o $@
$(filter %.elc,$(files)): %.elc: %.el
emacs -f batch-byte-compile $<
$(filter %.o,$(files))表示调用makefile文件的filter函数,过滤$filter集,这个例子展示了makefile文件更大的弹性。
在makefile文件中,依赖关系可能会需要包含一系列的头文件,比如,如果main.c中有一句#include "defs.h",依赖关系应该是:
main.o : main.c defs.h
但是,如果是一个比较大型的工程,必须清楚哪些C文件包含了哪些头文件,并且,在加入或删除头文件时,也需要小心地修改makefile文件,这是一项繁琐的工作。为了避免这种繁琐而又容易出错的工作,可以使用GCC的一个-MM的选项,即自动寻找源文件中包含的头文件,并生成一个依赖关系。例如,如果执行下面的命令:
gcc -M main.c
其输出是:
main.o : main.c defs.h
于是由编译器自动生成依赖关系,而不必再手动书写若干文件的依赖关系,并由编译器自动生成。
编译器的这个功能如何与makefile文件联系在一起呢?因为makefile文件也要根据这些源文件重新生成,让makefile文件自已依赖于源文件。这样并不现实,不过可以用其他手段来迂回地实现这一功能。GNU组织建议把编译器为每一个源文件自动生成的依赖关系放到一个文件中,为每一个name.c的文件都生成一个name.d的makefile文件,“.d”文件中就存放了对应“.c”文件的依赖关系。
于是,可以写出“.c”文件和“.d”文件的依赖关系,让make自动更新或生成“.d”文件,并把其包含在主makefile文件中,就可以自动地生成每个文件的依赖关系了。这里,给出了一个模式规则来产生“.d”文件:
%.d: %.c
@set -e; rm -f $@; \
$(gcc) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
这个规则的意思是,所有的“.d”文件依赖于“.c”文件。rm -f $@的意思是删除所有的目标,也就是“.d”文件。第二行的意思是,为每个依赖文件“$<”,也就是“.c”文件生成依赖文件,“$@”表示模式“%.d”文件,如果有一个C文件是name.c,“%”就是name。“$$$$”意为一个随机编号,第2行生成的文件有可能是name.d.12345,第3行使用sed命令作了一个替换,第4行就是删除临时文件。
总之,这个模式要做的事就是在编译器生成的依赖关系中加入“.d”文件的依赖,即把依赖关系:
main.o : main.c defs.h
转成:
main.o main.d : main.c defs.h
于是,“.d”文件也会自动更新,并会自动生成。当然,在这个“.d”文件中加入的不只是依赖关系,生成的命令也可一并加入,让每个[.d]文件都包含一个完赖的规则。一旦完成这个工作,接下来就要把这些自动生成的规则放进主makefile文件中。可以使用makefile文件的include命令来引入别的makefile文件(前面讲过),例如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述语句$(sources:.c=.d)中的“.c=.d”的意思是作一个替换,把变量$(sources)中所有[.c]的字串都替换成[.d],关于这个替换的内容,在后面会有更为详细的讲述。当然,使用时应注意次序,因为include是按次序来载入文件,最先载入的[.d]文件中的目标会成为默认目标。