21.4 自动处理头文件的依赖关系
现在我们的Makefile写成这样:
- all: main
-
- main: main.o stack.o maze.o
- gcc $^ -o $@
-
- main.o: main.h stack.h maze.h
- stack.o: stack.h main.h
- maze.o: maze.h main.h
-
- clean:
- -rm main *.o
-
- .PHONY: clean
按照惯例,用all做缺省目标。还有一个问题比较麻烦,在写main.o、stack.o和maze.o这三个目标的规则时要查看源代码,找出它们依赖于哪些头文件,这很容易出错,一是因为有的头文件包含在另一个头文件中,在写规则时很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile的规则。
在源代码中已经包含了目标文件和头文件之间的依赖关系,这种依赖关系是以#include的形式描述的,如果在Makefile中以规则的形式再描述一遍,就存在重复信息了。手工维护重复信息很容易出错,应该想办法自动维护。以前我们讲过定义一个宏然后在代码中多处引用它可以避免硬编码,讲过写一个头文件然后包含在很多.c文件中可以避免在这些.c文件中重复声明,道理都是一样的:如果某信息在多处重复出现,要修改它应该只在一个地方手工修改,而在其他地方自动获得更新后的信息,这称为DRY(Don't Repeat Yourself)原则。现在我们要想办法把源代码中的依赖关系信息抽取出来自动转换成Makefile中的规则。第一步,用gcc的-M选项自动分析目标文件和源文件的依赖关系,以Makefile规则的格式输出:
- $ gcc -M main.c
- main.o: main.c /usr/include/stdio.h /usr/include/features.h \
- /usr/include/bits/predefs.h /usr/include/sys/cdefs.h \
- /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
- /usr/include/gnu/stubs-32.h \
- /usr/lib/gcc/i486-linux-gnu/4.4.3/include/stddef.h \
- /usr/include/bits/types.h /usr/include/bits/typesizes.h \
- /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
- /usr/lib/gcc/i486-linux-gnu/4.4.3/include/stdarg.h \
- /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \
- stack.h maze.h
注意在Makefile的规则中也可以使用类似C语言的续行符\。gcc -M的输出结果不仅包括我们自己写的头文件main.h、stack.h和maze.h,还包括stdio.h和其他系统头文件,因为我们的程序中包含了stdio.h,而后者又包含了其他系统头文件。系统头文件通常不需要随我们的程序一起维护,所以通常不用gcc的-M选项而是用-MM选项,输出结果中只包括我们自己写的头文件:
- $ gcc -MM *.c
- main.o: main.c main.h stack.h maze.h
- maze.o: maze.c maze.h main.h
- stack.o: stack.c stack.h main.h
接下来的问题是怎么把这些规则添加到Makefile中,Scott McPeak提供了一个很好的解决方案(http://scottmcpeak.com/autodepend/autodepend.html):
- all: main
-
- main: main.o stack.o maze.o
- gcc $^ -o $@
-
- clean:
- -rm main *.o *.d
-
- .PHONY: clean
-
- OBJS = main.o stack.o maze.o
-
- -include $(OBJS:.o=.d)
-
- %.o: %.c
- gcc -c $(CFLAGS) $*.c -o $*.o
- gcc -MM $(CPPFLAGS) $*.c > $*.d
- mv -f $*.d $*.d.tmp
- sed -e 's|.*:|$*.o:|' < $*.d.tmp > $*.d
- sed -e 's/.*://' -e 's/\\$$//' < $*.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$$/:/' >> $*.d
- rm -f $*.d.tmp
OBJS变量的值是我们要编译生成的.o文件的列表,$(OBJS:.o=.d)是变量值的替换语法,把OBJS变量中每一项的.o后缀替换成.d后缀,所以include这一句相当于:
- -include main.d stack.d maze.d
类似于C语言的#include预处理指示,在Makefile中include main.d stack.d maze.d表示把这三个文件包含到当前的Makefile中,这三个文件也应该符合Makefile的语法。我们在include前面加了一个-,表示如果它要包含的文件列表中有的文件不存在,则忽略不存在的文件,只包含存在的文件。如果include前面没有-,而它要包含的文件又不存在,则make会报错。
事实上,include不只是把文件包含进来这么简单,还要做一个特殊处理:对于要包含进来的每一个文件,make会把文件名当做目标来尝试更新(如果有规则匹配该目标则执行该规则下的命令,如果没有规则能匹配该目标就算了),如果make检查完所有要包含进来的文件后,其中确实有些文件被更新了,则make会从头开始执行,重新包含更新后的文件。详见参考文献[27]的3.7节。对于我们这个例子不必考虑这种复杂的情况,因为我们没有规则能匹配以.d为后缀的目标,不会更新main.d、stack.d或maze.d。
下面我们分几个不同的场景来分析当.c和.h文件的依赖关系发生变化时make如何更新规则来适应这些变化。
1.如果你的工作目录是干净的,只有.c文件、.h文件和Makefile文件,执行make命令的结果是:
- $ make
- gcc -c main.c -o main.o
- gcc -MM main.c > main.d
- mv -f main.d main.d.tmp
- sed -e 's|.*:|main.o:|' < main.d.tmp > main.d
- sed -e 's/.*://' -e 's/\\$//' < main.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$/:/' >> main.d
- rm -f main.d.tmp
- gcc -c stack.c -o stack.o
- gcc -MM stack.c > stack.d
- mv -f stack.d stack.d.tmp
- sed -e 's|.*:|stack.o:|' < stack.d.tmp > stack.d
- sed -e 's/.*://' -e 's/\\$//' < stack.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$/:/' >> stack.d
- rm -f stack.d.tmp
- gcc -c maze.c -o maze.o
- gcc -MM maze.c > maze.d
- mv -f maze.d maze.d.tmp
- sed -e 's|.*:|maze.o:|' < maze.d.tmp > maze.d
- sed -e 's/.*://' -e 's/\\$//' < maze.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$/:/' >> maze.d
- rm -f maze.d.tmp
- gcc main.o stack.o maze.o -o main
由于main.d、stack.d和maze.d一个都不存在,所以include忽略它们,不包含任何文件。然后根据%.o: %.c规则,我们要更新目标main.o、stack.o和maze.o,它们的处理过程是一样的,我们以main.o为例解释一下具体的步骤:
执行gcc -c main.c -o main.o,由main.c编译出main.o。
执行gcc -MM main.c > main.d,把main.c的依赖关系写入文件main.d,其内容是main.o: main.c main.h stack.h maze.h。这条规则在本次make的处理过程中不起作用,但下次执行make时会把这个main.d包含进来,这条规则就会起作用了。上一条命令生成main.o,而这一条命令生成main.d,也就是说:只要生成一个.o文件,就要配合它生成一个.d文件,以便下次make时可以根据.d文件中的规则检查这个.o文件需不需要更新。
由gcc -MM生成的main.d还不能完全满足我们的要求,接下来的几条命令把main.d中的规则改成这样:
- main.o: main.c main.h stack.h maze.h
- main.c:
- main.h:
- stack.h:
- maze.h:
我们稍后会分析为什么要改成这样。sed和fmt命令的用法是另一个复杂的话题,在这里就不细讲了,sed的作用是在文件中做编辑、查找、替换,fmt的作用是段落排版。
2.如果修改了头文件stack.h再重新make,执行结果是:
- $ make
- gcc -c main.c -o main.o
- gcc -MM main.c > main.d
- mv -f main.d main.d.tmp
- sed -e 's|.*:|main.o:|' < main.d.tmp > main.d
- sed -e 's/.*://' -e 's/\\$//' < main.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$/:/' >> main.d
- rm -f main.d.tmp
- gcc -c stack.c -o stack.o
- gcc -MM stack.c > stack.d
- mv -f stack.d stack.d.tmp
- sed -e 's|.*:|stack.o:|' < stack.d.tmp > stack.d
- sed -e 's/.*://' -e 's/\\$//' < stack.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$/:/' >> stack.d
- rm -f stack.d.tmp
- gcc main.o stack.o maze.o -o main
由于Makefile中包含了main.d和stack.d,也就是包含了规则main.o: main.c main.h stack.h maze.h和stack.o: stack.c stack.h main.h,所以修改stack.h将导致main.o和stack.o被更新,但这两条规则没有命令列表,怎么更新呢?注意到它们和%.o: %.c规则属于"一个目标拆开写多条规则"的情况,所以会执行%.o: %.c规则的命令列表。最后,main.o和stack.o被更新又导致可执行文件main被更新。
3.如果添加了一个头文件foo.h,并且在main.c中加了一行#include "foo.h",再重新make,执行结果是:
- $ make
- gcc -c main.c -o main.o
- gcc -MM main.c > main.d
- mv -f main.d main.d.tmp
- sed -e 's|.*:|main.o:|' < main.d.tmp > main.d
- sed -e 's/.*://' -e 's/\\$//' < main.d.tmp | fmt -1 | \
- sed -e 's/^ *//' -e 's/$/:/' >> main.d
- rm -f main.d.tmp
- gcc main.o stack.o maze.o -o main
根据规则%.o: %.c,修改main.c导致main.o被更新,在执行命令列表时重新生成了main.d,其内容为:
- main.o: main.c main.h stack.h maze.h foo.h
- main.c:
- main.h:
- stack.h:
- maze.h:
- foo.h:
把foo.h也包含到规则里了,下次如果修改foo.h并重新make,也会导致main.o被更新。
4.如果删除头文件foo.h再重新make,执行结果是:
- $ make
- gcc -c main.c -o main.o
- main.c:6:17: error: foo.h: No such file or directory
- make: *** [main.o] Error 1
我们知道Makefile中包含了规则main.o: main.c main.h stack.h maze.h foo.h,但foo.h已经不存在了,make不是应该报错吗?为什么还会执行%.o: %.c的命令列表呢?
在这种情况下另外一条规则foo.h:起作用了,像这种既没有依赖条件也没有命令列表的规则是这样处理的:如果以该目标作为文件名的文件不存在,则认为该目标需要更新,由于没有命令列表,执行该规则并不会生成文件,但会认为该目标已被更新,因此依赖该目标的其他目标也要被更新。详见参考文献[27]的4.6节。因此,foo.h不存在将导致main.o被更新。