接前面两篇学习日记:如何写Makefile(二)——规则篇(中)和 学习日记:如何写Makefile(二)——规则篇(上)
五、 隐含规则数据库 GNU make 3.80拥有90多个内建隐含规则。隐含规则即是模式匹配规则又是后缀规则。这些规则支持的语言有很多: C++, Pascal, FORTRAN, ratfor, Modula, Texinfo, TEX (包括Tangle 和 Weave), Emacs Lisp, RCS, SCCS等。但如果你想要编译JAVA或者XML,你可以自己编写规则。(别担心,事实上它们非常简单) 你可以通过--print-data-base或者-p参数来查看make的内建规则数据库(小心,输出有n多行)。%.o: %.l这样的模式规则不带有任何的命令,就可以将他们从make的数据库删除。尽管在实际操作中,这种规则导致的错误非常罕见,但是知道有这样一种情况总是会在不经意的时候对你有所帮助。 make的另一个强大之处在于,对于每一个符合模式匹配的目标文件,make会为它寻找相应的依附条件。如果找到了符合依附条件模式的源文件,这条规则才会生效。但当找不到时,make会再次查找所有的规则,并假设符合依附关系的源文件是另外的一个需要被生成的目标文件。这样,make会递归式的找到一个规则链用以更新目标文件(就像前面的例子一样,make可以根据规则链从lexer.l生成到lexer.o)。 例如一个名为a.o的文件的源文件可能是.c,.cpp,.cc,.p,.f,.r,.s,.mod等等。
%.c: %.l
%.o: %.c $(COMPILE.c) $(OUTPUT_OPTION) $<用户自定义的部分完全取决于变量的使用,事实上这两个变量也是由其他多个变量和参数决定的:
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -cCC = gccOUTPUT_OPTION = -o $@需要注意的是,在makefile中设置参数时需要避免将这些变量赋值,如果在makefile中设置:
CPPFLAGS = -I include/那么,当需要在生成过程中加入命令行参数
make CPPFLAGS=-DDEBUG则-I选项和它的参数就会被取消掉。因为在命令行里面的变量将重写其他所有对变量的设置。因此,这样的设置将最终导致make找不到头文件的位置,而造成编译失败。
.PHONY: help执行make help后就会在屏幕上看到所有的目标文件。 简单解释一下这个命令: 首先,使用--print-data-base查找出规则数据库的内容;然后使用awk命令从内容中抓取到目标文件信息,去掉以百分号和点号开头的文件(模式匹配规则和后缀规则文件),并删掉这一行多余的内容;最后将列表排序并按四个一行输出到屏幕。 六、 特殊目标文件 特殊目标文件是一种改变make默认方式的内建伪目标。例如,.PHONY会声明一个文件不会依赖任何其他真实的文件,并且永远都需要更新。伪文件.PHONY是最常见的特殊目标文件,但是还有些其他特殊文件。特殊文件也遵循着target: prerequisite的语法规则,但目标文件并不是一个文件,他们更像是修改make内部算法的指令。 特殊文件共有十二个,分为三类:一类是为了改变make在更新目标时的动作;还有一类是作为全局标志的形式,编译或忽略他们的目标文件;最后一类是后缀名特殊目标,当指明了旧的后缀规则时使用。 最常用的目标修饰符有: .INTERMEDIATE
help:
make --print-data-base --question | \
awk '/^[^.%][-A-Za-z0-9_]*:/ \
{ print substr($$1, 1, length($$1)-1) }' | \
sort | \
pr --omit-pagination --width=80 --columns=4
这个特殊目标文件的依赖关系被视为中间文件,当make更新其他文件时创建了列表中的文,make会在结束时删除这些文件;但如果更新前这个文件已经存在,则make不会删除它。.SECONDARY
依赖列表中的文件会被当作中间文件,但不会被自动删除。这个特殊目标最常见的地方是针对一些库文件,为了方便调试过程,开发期间使用的库文件尽管也是中间文件,但保留着它可以减少调试中的重复编译过程。.PRECIOUS
当make在执行过程中被中断时,它会将所有这次更新过的目标文件删除。因此,make不会将半成品文件遗留在编译路径中。但是,当某些生成的文件相当大或者运算非常费时的结果。因此,如果将这类文件定义为PRECIOUS,则它们就不会在中断时被删除掉了。尽管.PRECIOUS不太常见,但是它经常会在需要的时候起到意想不到的效果。 注意:make不会在发生错误时自动删除文件,只有当它被信号中断时才会。.DELETE_ON_ERROR
这个正好和.PRECIOUS相反,它会使依赖关系列表中的文件在发生错误时被删除。
七、 自动生成依赖关系 在通常情况下,手动添加目标文件和头文件之间的依赖关系几乎是不可能完成的。以C语言中最常见的stdio.h的头文件为例,它包含了15个其他的头文件,因此一一添加这些头文件以及它们互相之间的依赖关系必须依赖程序来实现。好在gcc提供了这样的一种方式。首先创建一个stdio.c的文件,包含了stdio.h的头文件声明:
echo “#include然后,运行gcc的编译命令:" > stdio.c
gcc -M stdio.c屏幕会输出相关的头文件的路径:
stdio.o: stdio.c /usr/include/stdio.h /usr/include/features.h \这样就可以将这些需要的路径复制粘贴到makefile中了。但是,这种方法有点笨,对吧。 聪明的方法是:在makefile中添加一个include指示,但目前大多数版本的make都已经有include指示了,因此小技巧是设置一个depend目标
/usr/include/x86_64-linux-gnu/bits/predefs.h \
/usr/include/x86_64-linux-gnu/sys/cdefs.h \
/usr/include/x86_64-linux-gnu/bits/wordsize.h \
/usr/include/x86_64-linux-gnu/gnu/stubs.h \
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
/usr/lib/gcc/x86_64-linux-gnu/4.6/include/stddef.h \
/usr/include/x86_64-linux-gnu/bits/types.h \
/usr/include/x86_64-linux-gnu/bits/typesizes.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/lib/gcc/x86_64-linux-gnu/4.6/include/stdarg.h \
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h
depend: count_words.c lexer.c counter.c在运行make之前,先执行make depend命令。 如果我们把每一个源文件的依赖关系都写入它自己的依赖关系文件中,例如.d为后缀的同名文件,并将.d文件作为这个依赖规则的目标文件。当源文件改变时,make就会知道.d文件需要被更新了。以下代码可以实现这个规则:
$(CC) -M $(CPPFLAGS) $^ > $@
include depend
%.d: %.c在shell中$$表示当前进程的进程号,它会确立独一无二的文件名。先将依赖关系保存到这个特殊的文件中,然后使用 "sed" 命令将.d文件作为一个目标文件添加到规则中。sed命令包含了一个查找部分 \($*\)\.o[ :]* 和一个替代部分 \1.o $@,它们都被用逗号隔开。查找部分的文件名为$*,它被包含在括号的正则表达式之中,并要求后缀名为.o。后面的[ :]*表示零到多个空格或者冒号。替代部分前面的正则表达式替换为\1.o,并把当前目标文件添加到依赖文件。 于是,我们的makefile就变成了:
$(CC) -M $(CPPFLAGS) $<> $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' <$@.$$$$ > $@; \
rm -f $@.$$$$
VPATH = src include
CPPFLAGS = -I include
CC = gcc
SOURCES = count_words.c \
lexer.c \
counter.c
count_words: counter.o lexer.o -lfl
count_words.o: counter.h
counter.o: counter.h lexer.h
lexer.o: lexer.h
include $(subst .c,.d, $(SOURCES))
%.d: %.c
$(CC) -M $(CPPFLAGS) $<> $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' <$@.$$$$ > $@; \
rm -f $@.$$$$
# help - The default goal
.PHONY: help
help:
make --print-data-base --question | \
awk '/^[^.%][-A-Za-z0-9_]*:/ \
{ print substr($$1, 1, length($$1)-1) }' | \
sort | \
pr --omit-pagination --width=80 --columns=4
.PHONY: clean
clean:
rm *.o lexer.c count_words *.d
makefile:12: count_words.d: No such file or directory前三行并不是报错,而是make的warning,make在所有路径和include里都找不到这几个文件。这些warning可以通过在include前面添加-(减号)消除掉。后面,make开始执行gcc -M来自动生成各种头文件依赖关系。
makefile:12: lexer.d: No such file or directory
makefile:12: counter.d: No such file or directory
cc -M -I include src/counter.c > counter.d.$$; \
sed 's,\(counter\)\.o[ :]*,\1.o counter.d : ,g'counter.d; \
rm -f counter.d.$$
lex -t src/lexer.l > lexer.c
cc -M -I include lexer.c > lexer.d.$$; \
sed 's,\(lexer\)\.o[ :]*,\1.o lexer.d : ,g'lexer.d; \
rm -f lexer.d.$$
cc -M -I include src/count_words.c > count_words.d.$$; \
sed 's,\(count_words\)\.o[ :]*,\1.o count_words.d : ,g'count_words.d; \
rm -f count_words.d.$$
rm lexer.c
cc -I include -c -o count_words.o src/count_words.c
cc -I include -c -o counter.o src/counter.c
cc -I include -c -o lexer.o lexer.c
cc count_words.o counter.o lexer.o /usr/lib/x86_64-linux-gnu/libfl.so -o count_words
ar rv libcounter.a counter.o lexer.o参数rv表示我们想用列表里面的目标文件替换掉库文件里面的相同内容,如果库中不存在则添加进去,并要求ar显示整个过程。即使库文件不存在,这个参数也可以使用。参数后面的第一个文件名就是库文件名(有些其他版本的ar需要一个参数C,来显式的创建库文件)。 使用库文件的方式十分简单,通常就是在加在命令的编译列表里面,编译器和连接器会自动根据后缀名识别:
cc count_words.o libcounter.a /lib/libfl.a -o count_words事实上,cc会自动识别出libcounter.a 和 /lib/libfl.a 是库文件,并且也会根据定义的库文件位置搜索它们,因此,也可以使用编译器的-l 参数,直接引用库文件:
cc count_words.o -lcounter -lfl -o count_words这样可以省略掉前面表示库文件的部分和后缀名。-l参数可以使编译器搜索系统的库文件路径,并且对于不同的系统都适用。此外,对于支持共享库的系统(在UNIX中扩展名为.so的库文件),连接器会自动的先查找共享库,而不需要明确指出(GNU 的编译器有这种效果)。查找路径可以通过添加-L参数进行修改,修改后的路径会在系统库之前加载,并可以被所有-l参数使用。 事实上,上面一条命令是不能执行的,因为当前工作路径并不是cc的搜索路径,它找不到counter这个库文件。因此,需要做以下修改
cc count_words.o -L. -lcounter -lfl -o count_words
libcounter.a: counter.o lexer.o这里使用了make对ar程序的内置定义和标准参数选项 rv。但是,每次编译库的时候都会将所有的依赖文件进行编译,为了节省时间,可以将$^改成$?,这样就只会更新比ar库新的目标文件。但是,部分更新库文件所要付出的时间成本通常是远远高于整个库文件的更新,尤其是当库文件的数量比较多的时候,完整更新库会显得更加划算。 在GNU make中,引用库文件里面的成员可以用以下方式:
$(AR) $(ARFLAGS) $@ $^
libcounter.a(counter.o): counter.o将上面的内容综合起来,makefile变成了下面的样子:
$(AR) $(ARFLAGS) $@ $<;
VPATH = src include
CPPFLAGS = -I include
CC = gcc
count_words: libcounter.a -lfl
libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)
libcounter.a(lexer.o): lexer.o
$(AR) $(ARFLAGS) $@ $<
libcounter.a(counter.o): counter.o
$(AR) $(ARFLAGS) $@ $<
count_words.o: counter.h
counter.o: counter.h lexer.h
lexer.o: lexer.h
# help - The default goal
.PHONY: help
help:
make --print-data-base --question | \
awk '/^[^.%][-A-Za-z0-9_]*:/ \
{ print substr($$1, 1, length($$1)-1) }' | \
sort | \
pr --omit-pagination --width=80 --columns=4
.PHONY: clean
clean:
rm *.o lexer.c count_words *.d
gcc -I include -c -o count_words.o src/count_words.c注意到生成库文件时使用的“$@”表示的是libcounter.a 而不是libcounter.a(lexer.o) 当然,我们也可以将内建规则应用在这里,使makefile更加精简:
lex -t src/lexer.l > lexer.c
gcc -I include -c -o lexer.o lexer.c
ar rv libcounter.a lexer.o
ar: creating libcounter.a
a - lexer.o
gcc -I include -c -o counter.o src/counter.c
ar rv libcounter.a counter.o
a - counter.o
gcc count_words.o libcounter.a /usr/lib/x86_64-linux-gnu/libfl.so -o count_words
rm lexer.c
VPATH = src include
CPPFLAGS = -I include
CC = gcc
count_words: libcounter.a -lfl
libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)
count_words.o: counter.h
counter.o: counter.h lexer.h
lexer.o: lexer.h
# help - The default goal
.PHONY: help
help:
make --print-data-base --question | \
awk '/^[^.%][-A-Za-z0-9_]*:/ \
{ print substr($$1, 1, length($$1)-1) }' | \
sort | \
pr --omit-pagination --width=80 --columns=4
.PHONY: clean
clean:
rm *.o lexer.c count_words *.d
count_words: -lcounter -lfl如果是第一次编译,这里面的-lcounter是无法被make找到的,因为对于在makefile中生成的库文件,他们的名字在make过程中还不能被查找到。但如果当前目录已经存在了这个库文件,或者使用库文件的全名时,就不会出现这个问题了。
libcounter.a: counter.o lexer.o
$(AR) $(ARFLAGS) $@ $^