热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

学习日记:如何写Makefile(二)——规则篇(下)

接前面两篇学习日记:如何写Makefile(二)——规则篇(中)和学习日记:如何写Makefile(二)——规则篇(上)五、隐含规则数据库GNUmake3.80拥有90多个内建隐含规则。隐含规

接前面两篇学习日记:如何写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编译相关的两个内建规则删掉:
%.o: %.l
%.c: %.l
这样的模式规则不带有任何的命令,就可以将他们从make的数据库删除。尽管在实际操作中,这种规则导致的错误非常罕见,但是知道有这样一种情况总是会在不经意的时候对你有所帮助。
make的另一个强大之处在于,对于每一个符合模式匹配的目标文件,make会为它寻找相应的依附条件。如果找到了符合依附条件模式的源文件,这条规则才会生效。但当找不到时,make会再次查找所有的规则,并假设符合依附关系的源文件是另外的一个需要被生成的目标文件。这样,make会递归式的找到一个规则链用以更新目标文件(就像前面的例子一样,make可以根据规则链从lexer.l生成到lexer.o)。 例如一个名为a.o的文件的源文件可能是.c,.cpp,.cc,.p,.f,.r,.s,.mod等等。

规则结构

为了方便用户自定义,内建规则库都有标准的结构。以从C程序生成目标文件的规则为例:
%.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找不到头文件的位置,而造成编译失败。

帮助命令

大型的makefile会包含大量的目标文件,并且非常不容易被记住,一个简单的解决方式就是为默认的目标文件设置帮助命令,然后手工方式维护这些命令又是相当复杂和繁琐的。因此,make的规则数据库提供了命令用于直接使用,下面的例子是使用了这些命令按顺序输出了所有目标列表(每行四个):
.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
执行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的头文件声明:
echo “#include " > stdio.c
然后,运行gcc的编译命令:
gcc -M stdio.c
屏幕会输出相关的头文件的路径:
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
这样就可以将这些需要的路径复制粘贴到makefile中了。但是,这种方法有点笨,对吧。 聪明的方法是:在makefile中添加一个include指示,但目前大多数版本的make都已经有include指示了,因此小技巧是设置一个depend目标
depend: count_words.c lexer.c counter.c
$(CC) -M $(CPPFLAGS) $^ > $@
include depend
在运行make之前,先执行make depend命令。
如果我们把每一个源文件的依赖关系都写入它自己的依赖关系文件中,例如.d为后缀的同名文件,并将.d文件作为这个依赖规则的目标文件。当源文件改变时,make就会知道.d文件需要被更新了。以下代码可以实现这个规则:
%.d: %.c
$(CC) -M $(CPPFLAGS) $<> $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' <$@.$$$$ > $@; \
rm -f $@.$$$$
在shell中$$表示当前进程的进程号,它会确立独一无二的文件名。先将依赖关系保存到这个特殊的文件中,然后使用 "sed" 命令将.d文件作为一个目标文件添加到规则中。sed命令包含了一个查找部分  \($*\)\.o[ :]*  和一个替代部分 \1.o $@,它们都被用逗号隔开。查找部分的文件名为$*,它被包含在括号的正则表达式之中,并要求后缀名为.o。后面的[  :]*表示零到多个空格或者冒号。替代部分前面的正则表达式替换为\1.o,并把当前目标文件添加到依赖文件。
于是,我们的makefile就变成了:
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

注意1: include必须放到手动设置的依赖关系之后,以防被手动设置的依赖关系覆盖。
注意2: include里面使用了subst函数,这是make的一个函数,它将$(SOURCE)文件里的所有.c替换成了.d,这两个后缀之间用逗号隔开且不能有空格。
运行make --just-print 会得到如下结果
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; \
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
前三行并不是报错,而是make的warning,make在所有路径和include里都找不到这几个文件。这些warning可以通过在include前面添加-(减号)消除掉。后面,make开始执行gcc -M来自动生成各种头文件依赖关系。

八、 库管理 档案库(archive library)是一类特殊类型的文件,它用来归类相关的目标文件。make提供了对库文件的专门支持,包括创建,维护和引用。继续使用上面的例子,将counter.o和lexer.o作为库文件,创建的命令为:
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

创建和更新库

在makefile中,库的创建与一般文件没有什么区别,简单的方式例如:
libcounter.a: counter.o lexer.o
$(AR) $(ARFLAGS) $@ $^
这里使用了make对ar程序的内置定义和标准参数选项 rv。但是,每次编译库的时候都会将所有的依赖文件进行编译,为了节省时间,可以将$^改成$?,这样就只会更新比ar库新的目标文件。但是,部分更新库文件所要付出的时间成本通常是远远高于整个库文件的更新,尤其是当库文件的数量比较多的时候,完整更新库会显得更加划算。 在GNU make中,引用库文件里面的成员可以用以下方式:
libcounter.a(counter.o): counter.o
$(AR) $(ARFLAGS) $@ $<;
将上面的内容综合起来,makefile变成了下面的样子:
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

执行make的输出为:
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
注意到生成库文件时使用的“$@”表示的是libcounter.a 而不是libcounter.a(lexer.o) 当然,我们也可以将内建规则应用在这里,使makefile更加精简:
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

库文件作为依赖关系

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

双冒号规则

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


推荐阅读
  • 在稀疏直接法视觉里程计中,通过优化特征点并采用基于光度误差最小化的灰度图像线性插值技术,提高了定位精度。该方法通过对空间点的非齐次和齐次表示进行处理,利用RGB-D传感器获取的3D坐标信息,在两帧图像之间实现精确匹配,有效减少了光度误差,提升了系统的鲁棒性和稳定性。 ... [详细]
  • 深入解析 Django 中用户模型的自定义方法与技巧 ... [详细]
  • 在CentOS上部署和配置FreeSWITCH
    在CentOS系统上部署和配置FreeSWITCH的过程涉及多个步骤。本文详细介绍了从源代码安装FreeSWITCH的方法,包括必要的依赖项安装、编译和配置过程。此外,还提供了常见的配置选项和故障排除技巧,帮助用户顺利完成部署并确保系统的稳定运行。 ... [详细]
  • Spring Boot 实战(一):基础的CRUD操作详解
    在《Spring Boot 实战(一)》中,详细介绍了基础的CRUD操作,涵盖创建、读取、更新和删除等核心功能,适合初学者快速掌握Spring Boot框架的应用开发技巧。 ... [详细]
  • 结语 | 《探索二进制世界:软件安全与逆向分析》读书笔记:深入理解二进制代码的逆向工程方法
    结语 | 《探索二进制世界:软件安全与逆向分析》读书笔记:深入理解二进制代码的逆向工程方法 ... [详细]
  • 本文深入探讨了在Android应用开发中常见的相机连接故障问题,特别是在RK3288平台和Android 6.0系统上。通过分析具体案例,本文提供了详细的解决方案和应对策略,旨在帮助开发者有效解决相机连接问题,提升应用的稳定性和用户体验。 ... [详细]
  • 如何构建基于Spring MVC框架的Java Web应用项目
    在构建基于Spring MVC框架的Java Web应用项目时,首先应创建一个新的动态Web项目。接着,需将必要的JAR包导入至WebContent/WEB-INF/lib目录下,确保包括Spring核心库及相关依赖。如遇缺失的JAR包,可向社区求助或通过Maven等工具自动下载。正确配置后,即可开始搭建应用结构与功能模块。 ... [详细]
  • PHP中元素的计量单位是什么? ... [详细]
  • 本文详细探讨了Java集合框架的使用方法及其性能特点。首先,通过关系图展示了集合接口之间的层次结构,如`Collection`接口作为对象集合的基础,其下分为`List`、`Set`和`Queue`等子接口。其中,`List`接口支持按插入顺序保存元素且允许重复,而`Set`接口则确保元素唯一性。此外,文章还深入分析了不同集合类在实际应用中的性能表现,为开发者选择合适的集合类型提供了参考依据。 ... [详细]
  • BZOJ4240 Gym 102082G:贪心算法与树状数组的综合应用
    BZOJ4240 Gym 102082G 题目 "有趣的家庭菜园" 结合了贪心算法和树状数组的应用,旨在解决在有限时间和内存限制下高效处理复杂数据结构的问题。通过巧妙地运用贪心策略和树状数组,该题目能够在 10 秒的时间限制和 256MB 的内存限制内,有效处理大量输入数据,实现高性能的解决方案。提交次数为 756 次,成功解决次数为 349 次,体现了该题目的挑战性和实际应用价值。 ... [详细]
  • 本文深入解析了 Apache 配置文件 `httpd.conf` 和 `.htaccess` 的优化方法,探讨了如何通过合理配置提升服务器性能和安全性。文章详细介绍了这两个文件的关键参数及其作用,并提供了实际应用中的最佳实践,帮助读者更好地理解和运用 Apache 配置。 ... [详细]
  • 微信支付授权目录配置详解及操作步骤
    在使用微信支付时,若通过WeixinJSBridge.invoke方法调用支付功能,可能会遇到“当前页面URL未注册”的错误提示,导致get_brand_wcpay_request:fail调用微信JSAPI支付失败。为解决这一问题,需要正确配置微信支付授权目录,确保支付页面的URL已成功注册。本文将详细介绍微信支付授权目录的配置步骤和注意事项,帮助开发者顺利完成支付功能的集成与调试。 ... [详细]
  • 在Spring Boot项目中,通过YAML配置文件为静态变量设置值的方法与实践涉及以下几个步骤:首先,创建一个新的配置类。需要注意的是,自动生成的setter方法默认是非静态的,因此需要手动将其修改为静态方法,以确保静态变量能够正确初始化。此外,建议使用`@Value`注解或`@ConfigurationProperties`注解来注入配置属性,以提高代码的可读性和维护性。 ... [详细]
  • 本文详细介绍了如何在Linux系统中搭建51单片机的开发与编程环境,重点讲解了使用Makefile进行项目管理的方法。首先,文章指导读者安装SDCC(Small Device C Compiler),这是一个专为小型设备设计的C语言编译器,适合用于51单片机的开发。随后,通过具体的实例演示了如何配置Makefile文件,以实现代码的自动化编译与链接过程,从而提高开发效率。此外,还提供了常见问题的解决方案及优化建议,帮助开发者快速上手并解决实际开发中可能遇到的技术难题。 ... [详细]
  • 【前端开发】深入探讨 RequireJS 与性能优化策略
    随着前端技术的迅速发展,RequireJS虽然不再像以往那样吸引关注,但其在模块化加载方面的优势仍然值得深入探讨。本文将详细介绍RequireJS的基本概念及其作为模块加载工具的核心功能,并重点分析其性能优化策略,帮助开发者更好地理解和应用这一工具,提升前端项目的加载速度和整体性能。 ... [详细]
author-avatar
北斗七星
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有