Makefile系列之一 : 书写规则
1. 规则
target : prerequisites
command
2. example
excute 为最终生成的可执行文件。
可以通过命令 make clean来删除所有编译时产生的中间文件。
excute : main.o a.o b.o c.o d.o cc -o excute main.o a.o b.o c.o d.o #命令必须以tab开头 main.o : comm.h main.c cc -c main.c a.o : comm.h a.c a.h cc -c a.c b.o : b.c b.h cc -c b.c c.o : c.h c.c comm.h cc -c c.c d.o : d.h d.c cc -c d.c clean: #无依赖文件,make不执行后续命令 rm excute a.o b.o c.o d.o
3. make 命令的执行
1)输入make命令后,make在当前目录下找名字叫“makefile”,"Makefile"的文件
2)找到后,再去找文件中的第一个目标文件,如例子中的excute.
3) 如果excute文件不存在或其后的依赖文件更新时间比它晚,则执行后面的命令,如 cc -o excute...
4) excute文件存在后,就依次找后面的依赖文件的依赖关系,直到生成所有的中间文件,最后再生成 最终目标文件excute
4. make的自动推导能力
make可以通过目标文件自动推导出部分依赖文件,因此在编写makefile时,可以省去不少篇幅。如make找到一个目标文件 a.o 能够推出的依赖文件为 a.c 及 cc -c a.c. 因此上述例子可以写成下面的样子
excute : main.o a.o b.o c.o d.o cc -o excute main.o a.o b.o c.o d.o #命令必须以tab开头 main.o : comm.h a.o : comm.h a.h b.o : b.h c.o : c.h comm.h d.o : d.h
.PHONY : clean clean: #无依赖文件,make不执行后续命令 rm excute a.o b.o c.o d.o
5. 引用其它的makefile
就像C里面的 include <fname> 一样,makefile 也可以引用其它makefile,其本质就是做了一次简单的文本替换而已。
tab键不能出现在include <fname> 前面,空格可以。include 语句中可以包含文件通配符,变量
如当前有文件a.mk, b.mk, 变量$var = c.mk, 则下面语句
include *.mk $var
等价于
include a.mk b.mk c.mk #仅仅做了一次文本替换
include 文件查找顺序如下:
1)当前目录
2)执行make时,如果有“-I” 或 “--include-dir”参数,则在该参数指定的目录下去寻找
3)如果目录<prefix>/include(一般是:/usr/local/bin或/usr/include)存在,则去这里面查找
include过程中,如果出现找不到文件的情况,make会生成一条warning,但不会报错,直到makefile的读取完成,再去尝试加载那些没有找到的文件,如果仍没有找到,make则报错,停止执行。如果让其忽略,可以这样写
-include <fname> #忽略include过程中出现的错误,继续执行
6. makefile文件里的依赖目标查找顺序
1)首先在当前目录下查找
2)makefile的变量VPATH
如果makefile里定义了变量VPATH,则在该变量指定的目录下查找,变量定义如下语句:
VPATH = src: ../source
它告诉makefile去src 与 ../source目录下去查找,多个目录间用:分隔。
3) make 的关键字vpath
vpath <pattern> <direc>
在direc目录下搜索符合pattern的文件
vpath <pattern>
清除符合模式<pattern>的文件的搜索目录
vpath
清除所有已设置好了的文件搜索目录
上述三种方式中<pattern>可以包含%字符, 如下面这样一句
vpath %.h ../source
表示让make在../source目录下搜索以.h为结尾的文件
7. 伪目标
伪目标也可以有依赖文件,如果伪目标相要成为默认目标时,它必须为第一个目标,如
all : target1 target2 .PHONY : all target1 : a.o b.o cc -o target1 a.o b.o target2 : a.o c.o cc -o target2 a.o c.o
这样做的好处就是只要我们输入make all这一条命令,就可以同时生成多个可执行文件。
伪目标成为依赖的例子
.PHONY : clean cleanobj cleanbak clean : cleanobj cleanbak rm *.hex cleanobj : rm *.o cleanbak: rm *.bak
8. 静态模式更容易定义多个目标
规则:
<targets> : <target-pattern> : <prereq-pattern>
<commond>
targets
定义了一系列的目标文件,是目标集合。
target-pattern
target的模式,即目标模式
prereq-pattern
对target-pattern进行再一次的依赖目标定义,即目标的依赖模式。
如下列几句例子:
obj = foo.o bar.o all : $(obj) $(obj) : %.o : %.c $(CC) -c $(CFLAGS) $< -o $@
上例中,表示目标从$obj中取得,%.o表示要取$obj中以.o为结尾的目标,%.c表示取目标模式中的%再加上.c, 自动化变量 $< 表示所有依赖目标集, $@表示目标集。 上面规则可以展开成以下这种
foo.o : foo.c $(CC) -c $(CFLAGS) foo.c -o foo.o bar.o : bar.c $(CC) -c $(CFLAGS) bar.c -o bar.o
9. 自动生成依赖文件
假设存在依赖关系如下:
main.o : main.c defs.h
但是我们怎么才能知道main.o 的文件呢?当然我们可以去查看main.c里面include的头文件,但是但一个工程很巨大时,我们也一个个去找吗?很显然,这个工作量不但巨大,而且很容易出错,更不用说之后的维护了,幸好强大的C/C++编译器提供了一个自动生成依赖关系的一个功能,即它的“-M”的选项。如输入命令:
cc -M main.c
其输出是:
main.o : main.c defs.h
注意:使用GNU的C/C++编译器,你得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
gcc -M main.c的输出是:
main.o: main.c defs.h /usr/include/stdio.h //很显然,它还输出了系统库头文件
gcc -MM main.c的输出则是:
main.o: main.c defs.h
即使有了编译器为我们提供的自动生成依赖关系的命令,但是我们还得知道要为哪个文件去生成依赖关系,那么有没有更好的办法来帮我们完成makefile的编写呢?
GNU组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name.c”的文件都生成一个
“name.d”的 Makefile文件,[.d]文件中就存放对应[.c]文件的依赖关系。于是,我们可以写出[.c]文件和[.d]文件的依赖关系,并让make自动更新或生成[.d]文件,并把其包含在我们的主Makefile中,这样,我们就可以自动化地生成每个文件的依
赖关系了。
这里,我们给出了一个模式规则来产生[.d]文件:
%.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]文件的依赖,即把依赖关系:
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]文件中的目标会成为默认目标。