如何写Makefile(二)——规则篇(下)

接前面两篇学习日记:如何写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多行)。

使用隐含规则

当处理一个目标文件没有发现显式规则时,make就会调用隐含规则。其实,只要不在makefile中目标文件的命令行程序部分添加任何内容,就可以调用隐含规则。
这种方式通常很好,在极特殊的情况下会导致一些问题。例如,在混编过程中使用Lisp和C两种语言,同一个路径下分别有editor.l和editor.c两个文件,使用make隐含规则编译的时候,make有可能将editor.l认做flex的文件,并将它编译成editor.c(正如前面(上)部分的例子)。于是,真正的editor.c就会被覆盖掉。要想避免这个问题,就需要将flex编译相关的两个内建规则删掉:
[plain] view plaincopy
  1. %.o: %.l  
  2. %.c: %.l  
这样的模式规则不带有任何的命令,就可以将他们从make的数据库删除。尽管在实际操作中,这种规则导致的错误非常罕见,但是知道有这样一种情况总是会在不经意的时候对你有所帮助。
make的另一个强大之处在于,对于每一个符合模式匹配的目标文件,make会为它寻找相应的依附条件。如果找到了符合依附条件模式的源文件,这条规则才会生效。但当找不到时,make会再次查找所有的规则,并假设符合依附关系的源文件是另外的一个需要被生成的目标文件。这样,make会递归式的找到一个规则链用以更新目标文件(就像前面的例子一样,make可以根据规则链从lexer.l生成到lexer.o)。
例如一个名为a.o的文件的源文件可能是.c,.cpp,.cc,.p,.f,.r,.s,.mod等等。

规则结构

[plain] view plaincopy
  1.   
为了方便用户自定义,内建规则库都有标准的结构。以从C程序生成目标文件的规则为例:
[plain] view plaincopy
  1. %.o: %.c  
  2.         $(COMPILE.c) $(OUTPUT_OPTION) $<  
用户自定义的部分完全取决于变量的使用,事实上这两个变量也是由其他多个变量和参数决定的:
[plain] view plaincopy
  1. COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c  
  2. CC = gcc  
  3. OUTPUT_OPTION = -o $@  
需要注意的是,在makefile中设置参数时需要避免将这些变量赋值,如果在makefile中设置:
<span style="font-family:Microsoft YaHei;">CPPFLAGS = -I include/
</span>
那么,当需要在生成过程中加入命令行参数
<span style="font-family:Microsoft YaHei;">make CPPFLAGS=-DDEBUG
</span>
则-I选项和它的参数就会被取消掉。因为在命令行里面的变量将重写其他所有对变量的设置。因此,这样的设置将最终导致make找不到头文件的位置,而造成编译失败。

帮助命令

大型的makefile会包含大量的目标文件,并且非常不容易被记住,一个简单的解决方式就是为默认的目标文件设置帮助命令,然后手工方式维护这些命令又是相当复杂和繁琐的。因此,make的规则数据库提供了命令用于直接使用,下面的例子是使用了这些命令按顺序输出了所有目标列表(每行四个):
[plain] view plaincopy
  1. .PHONY: help  
  2. help:  
  3.         make --print-data-base --question |             \  
  4.         awk '/^[^.%][-A-Za-z0-9_]*:/                    \  
  5.         { print substr($$1, 1, length($$1)-1) }' |      \  
  6.         sort |                                          \  
  7.         pr --omit-pagination --width=80 --columns=4   
执行make help后就会在屏幕上看到所有的目标文件。
简单解释一下这个命令: 首先,使用--print-data-base查找出规则数据库的内容;然后使用awk命令从内容中抓取到目标文件信息,去掉以百分号和点号开头的文件(模式匹配规则和后缀规则文件),并删掉这一行多余的内容;最后将列表排序并按四个一行输出到屏幕。

六、 特殊目标文件

特殊目标文件是一种改变make默认方式的内建伪目标。例如,.PHONY会声明一个文件不会依赖任何其他真实的文件,并且永远都需要更新。伪文件.PHONY是最常见的特殊目标文件,但是还有些其他特殊文件。特殊文件也遵循着target: prerequisite的语法规则,但目标文件并不是一个文件,他们更像是修改make内部算法的指令。
特殊文件共有十二个,分为三类:一类是为了改变make在更新目标时的动作;还有一类是作为全局标志的形式,编译或忽略他们的目标文件;最后一类是后缀名特殊目标,当指明了旧的后缀规则时使用。
最常用的目标修饰符有:
.INTERMEDIATE
这个特殊目标文件的依赖关系被视为中间文件,当make更新其他文件时创建了列表中的文,make会在结束时删除这些文件;但如果更新前这个文件已经存在,则make不会删除它。
.SECONDARY
依赖列表中的文件会被当作中间文件,但不会被自动删除。这个特殊目标最常见的地方是针对一些库文件,为了方便调试过程,开发期间使用的库文件尽管也是中间文件,但保留着它可以减少调试中的重复编译过程。
.PRECIOUS
当make在执行过程中被中断时,它会将所有这次更新过的目标文件删除。因此,make不会将半成品文件遗留在编译路径中。但是,当某些生成的文件相当大或者运算非常费时的结果。因此,如果将这类文件定义为PRECIOUS,则它们就不会在中断时被删除掉了。尽管.PRECIOUS不太常见,但是它经常会在需要的时候起到意想不到的效果。注意:make不会在发生错误时自动删除文件,只有当它被信号中断时才会。
.DELETE_ON_ERROR
这个正好和.PRECIOUS相反,它会使依赖关系列表中的文件在发生错误时被删除。

七、 自动生成依赖关系

在通常情况下,手动添加目标文件和头文件之间的依赖关系几乎是不可能完成的。以C语言中最常见的stdio.h的头文件为例,它包含了15个其他的头文件,因此一一添加这些头文件以及它们互相之间的依赖关系必须依赖程序来实现。好在gcc提供了这样的一种方式。首先创建一个stdio.c的文件,包含了stdio.h的头文件声明:
<span style="font-family:Microsoft YaHei;">echo “#include <stdio.h>" > stdio.c
</span>
然后,运行gcc的编译命令:
<span style="font-family:Microsoft YaHei;">gcc -M stdio.c
</span>
屏幕会输出相关的头文件的路径:
<span style="font-family:Microsoft YaHei;">stdio.o: stdio.c /usr/include/stdio.h /usr/include/features.h \
 /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
</span>
这样就可以将这些需要的路径复制粘贴到makefile中了。但是,这种方法有点笨,对吧。
聪明的方法是:在makefile中添加一个include指示,但目前大多数版本的make都已经有include指示了,因此小技巧是设置一个depend目标
[plain] view plaincopy
  1. depend: count_words.c lexer.c counter.c  
  2.         $(CC) -M $(CPPFLAGS) $^ > $@  
  3. include depend  
在运行make之前,先执行make depend命令。
如果我们把每一个源文件的依赖关系都写入它自己的依赖关系文件中,例如.d为后缀的同名文件,并将.d文件作为这个依赖规则的目标文件。当源文件改变时,make就会知道.d文件需要被更新了。以下代码可以实现这个规则:
[plain] view plaincopy
  1. %.d: %.c  
  2.         $(CC) -M $(CPPFLAGS) $< > $@.$$$$;              \  
  3.         sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \  
  4.         rm -f $@.$$$$  
在shell中$$表示当前进程的进程号,它会确立独一无二的文件名。先将依赖关系保存到这个特殊的文件中,然后使用 "sed" 命令将.d文件作为一个目标文件添加到规则中。sed命令包含了一个查找部分  \($*\)\.o[ :]*  和一个替代部分 \1.o $@,它们都被用逗号隔开。查找部分的文件名为$*,它被包含在括号的正则表达式之中,并要求后缀名为.o。后面的[  :]*表示零到多个空格或者冒号。替代部分前面的正则表达式替换为\1.o,并把当前目标文件添加到依赖文件。
于是,我们的makefile就变成了:
[plain] view plaincopy
  1. VPATH = src include  
  2. CPPFLAGS = -I include  
  3. CC = gcc  
  4. SOURCES = count_words.c \  
  5.           lexer.c       \  
  6.           counter.c  
  7.   
  8. count_words: counter.o lexer.o -lfl  
  9. count_words.o: counter.h  
  10. counter.o: counter.h lexer.h  
  11. lexer.o: lexer.h  
  12.   
  13. include $(subst .c,.d, $(SOURCES))  
  14. %.d: %.c  
  15.         $(CC) -M $(CPPFLAGS) $< > $@.$$$$;              \  
  16.         sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \  
  17.         rm -f $@.$$$$  
  18.   
  19. # help - The default goal  
  20. .PHONY: help  
  21. help:  
  22.         make --print-data-base --question |             \  
  23.         awk '/^[^.%][-A-Za-z0-9_]*:/                    \  
  24.         { print substr($$1, 1, length($$1)-1) }' |      \  
  25.         sort |                                          \  
  26.         pr --omit-pagination --width=80 --columns=4   
  27. .PHONY: clean  
  28. clean:   
  29.         rm *.o lexer.c count_words *.d  

注意1: include必须放到手动设置的依赖关系之后,以防被手动设置的依赖关系覆盖。
注意2: include里面使用了subst函数,这是make的一个函数,它将$(SOURCE)文件里的所有.c替换成了.d,这两个后缀之间用逗号隔开且不能有空格。

运行make --just-print 会得到如下结果
<span style="font-family:Microsoft YaHei;">makefile:12: count_words.d: No such file or directory
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.$$ > 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.$$ > 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.$$ > 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
</span>
前三行并不是报错,而是make的warning,make在所有路径和include里都找不到这几个文件。这些warning可以通过在include前面添加-(减号)消除掉。后面,make开始执行gcc -M来自动生成各种头文件依赖关系。

八、 库管理

档案库(archive library)是一类特殊类型的文件,它用来归类相关的目标文件。make提供了对库文件的专门支持,包括创建,维护和引用。继续使用上面的例子,将counter.o和lexer.o作为库文件,创建的命令为:
<span style="font-family:Microsoft YaHei;">ar rv libcounter.a counter.o lexer.o</span>
参数rv表示我们想用列表里面的目标文件替换掉库文件里面的相同内容,如果库中不存在则添加进去,并要求ar显示整个过程。即使库文件不存在,这个参数也可以使用。参数后面的第一个文件名就是库文件名(有些其他版本的ar需要一个参数C,来显式的创建库文件)。
使用库文件的方式十分简单,通常就是在加在命令的编译列表里面,编译器和连接器会自动根据后缀名识别:
<span style="font-family:Microsoft YaHei;">cc count_words.o libcounter.a /lib/libfl.a -o count_words
</span>
事实上,cc会自动识别出libcounter.a 和 /lib/libfl.a 是库文件,并且也会根据定义的库文件位置搜索它们,因此,也可以使用编译器的-l 参数,直接引用库文件:
<span style="font-family:Microsoft YaHei;">cc count_words.o -lcounter -lfl -o count_words
</span>
这样可以省略掉前面表示库文件的部分和后缀名。-l参数可以使编译器搜索系统的库文件路径,并且对于不同的系统都适用。此外,对于支持共享库的系统(在UNIX中扩展名为.so的库文件),连接器会自动的先查找共享库,而不需要明确指出(GNU 的编译器有这种效果)。查找路径可以通过添加-L参数进行修改,修改后的路径会在系统库之前加载,并可以被所有-l参数使用。
事实上,上面一条命令是不能执行的,因为当前工作路径并不是cc的搜索路径,它找不到counter这个库文件。因此,需要做以下修改
<span style="font-family:Microsoft YaHei;">cc count_words.o -L. -lcounter -lfl -o count_words
</span>

创建和更新库

在makefile中,库的创建与一般文件没有什么区别,简单的方式例如:
<span style="font-family:Microsoft YaHei;">libcounter.a: counter.o lexer.o
        $(AR) $(ARFLAGS) $@ $^
</span>
这里使用了make对ar程序的内置定义和标准参数选项 rv。但是,每次编译库的时候都会将所有的依赖文件进行编译,为了节省时间,可以将$^改成$?,这样就只会更新比ar库新的目标文件。但是,部分更新库文件所要付出的时间成本通常是远远高于整个库文件的更新,尤其是当库文件的数量比较多的时候,完整更新库会显得更加划算。
在GNU make中,引用库文件里面的成员可以用以下方式:
<span style="font-family:Microsoft YaHei;">libcounter.a(counter.o): counter.o
        $(AR) $(ARFLAGS) $@ $<;
</span>
将上面的内容综合起来,makefile变成了下面的样子:
[plain] view plaincopy
  1. VPATH = src include  
  2. CPPFLAGS = -I include  
  3. CC = gcc  
  4.   
  5. count_words: libcounter.a -lfl  
  6. libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)  
  7. libcounter.a(lexer.o): lexer.o  
  8.         $(AR) $(ARFLAGS) $@ $<  
  9. libcounter.a(counter.o): counter.o  
  10.         $(AR) $(ARFLAGS) $@ $<  
  11.   
  12. count_words.o: counter.h  
  13. counter.o: counter.h lexer.h  
  14. lexer.o: lexer.h  
  15.   
  16. # help - The default goal  
  17. .PHONY: help  
  18. help:  
  19.         make --print-data-base --question |             \  
  20.         awk '/^[^.%][-A-Za-z0-9_]*:/                    \  
  21.         { print substr($$1, 1, length($$1)-1) }' |      \  
  22.         sort |                                          \  
  23.         pr --omit-pagination --width=80 --columns=4   
  24. .PHONY: clean  
  25. clean:   
  26.         rm *.o lexer.c count_words *.d  

执行make的输出为:
<span style="font-family:Microsoft YaHei;">gcc  -I include   -c -o count_words.o src/count_words.c
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
</span>
注意到生成库文件时使用的“$@”表示的是libcounter.a 而不是libcounter.a(lexer.o)
当然,我们也可以将内建规则应用在这里,使makefile更加精简:
[plain] view plaincopy
  1. VPATH = src include  
  2. CPPFLAGS = -I include  
  3. CC = gcc  
  4.   
  5. count_words: libcounter.a -lfl  
  6. libcounter.a: libcounter.a(lexer.o) libcounter.a(counter.o)  
  7. count_words.o: counter.h  
  8. counter.o: counter.h lexer.h  
  9. lexer.o: lexer.h  
  10.   
  11. # help - The default goal  
  12. .PHONY: help  
  13. help:  
  14.         make --print-data-base --question |             \  
  15.         awk '/^[^.%][-A-Za-z0-9_]*:/                    \  
  16.         { print substr($$1, 1, length($$1)-1) }' |      \  
  17.         sort |                                          \  
  18.         pr --omit-pagination --width=80 --columns=4   
  19. .PHONY: clean  
  20. clean:   
  21.         rm *.o lexer.c count_words *.d  

库文件作为依赖关系

在依赖关系中使用库文件的方式有两种,一种是直接使用绝对路径,另一种则是在库文件名前面加上一个-l参数。后者的好处在于,它会优先搜索共享路径,并且可以根据用户自定义搜索相应的路径。用户自定义的模式匹配的内容存放在.LIBPATTERNS中。
然而,并不是所有情况下-l参数都是有效的,例如:
[plain] view plaincopy
  1. count_words: -lcounter -lfl  
  2. libcounter.a: counter.o lexer.o  
  3.         $(AR) $(ARFLAGS) $@ $^  
如果是第一次编译,这里面的-lcounter是无法被make找到的,因为对于在makefile中生成的库文件,他们的名字在make过程中还不能被查找到。但如果当前目录已经存在了这个库文件,或者使用库文件的全名时,就不会出现这个问题了。
注意: 通常情况下,库文件在依赖关系中的顺序很重要,如果其中一个库会引用另外一个库的成员,被引用的一定要在引用它的库之后出现,因为连接器是不会回述链接好的库文件的。例如,库文件A调用了库文件B中的一个成员,于是在依赖列表中一定要保证-lA -lB的顺序,但如果B中的另一个成员又引用了A中的成员,则形成了循环引用。这时需要将依赖关系写作:-lA -lB -lA。这种多次调用库文件的方式在大型工程中经常出现,甚至会重复多次。这提醒了我们,在用$^替代依赖关系时是有问题的,因为它会去掉所有重复的内容,因此,会使用$+作为依赖关系变量使用,它保留了重复的内容。

双冒号规则

这是一种模糊的规则,它允许同一个目标文件被不同的依赖列表更新,更新的依据在于那一个依赖列表比目标文件的修改时间更晚(更加新)。

转自:http://blog.csdn.net/reenigne/article/details/8543844
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值