现在我们的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的规则。为了解决这个问题,可以用gcc
的-M
选项自动生成目标文件和源文件的依赖关系:
$ gcc -M main.c main.o: main.c /usr/include/stdio.h /usr/include/features.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.3.2/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.3.2/include/stdarg.h \ /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \ stack.h maze.h
-M
选项把stdio.h
以及它所包含的系统头文件也找出来了,如果我们不需要输出系统头文件的依赖关系,可以用-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中,GNU make
的官方手册建议这样写:
all: main main: main.o stack.o maze.o gcc $^ -o $@ clean: -rm main *.o .PHONY: clean sources = main.c stack.c maze.c include $(sources:.c=.d) %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
sources
变量包含我们要编译的所有.c
文件,$(sources:.c=.d)
是一个变量替换语法,把sources
变量中每一项的.c
替换成.d
,所以include
这一句相当于:
include main.d stack.d maze.d
类似于C语言的#include
指示,这里的include
表示包含三个文件main.d
、stack.d
和maze.d
,这三个文件也应该符合Makefile的语法。如果现在你的工作目录是干净的,只有.c
文件、.h
文件和Makefile
,运行make
的结果是:
$ make Makefile:13: main.d: No such file or directory Makefile:13: stack.d: No such file or directory Makefile:13: maze.d: No such file or directory set -e; rm -f maze.d; \ cc -MM maze.c > maze.d.$$; \ sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \ rm -f maze.d.$$ set -e; rm -f stack.d; \ cc -MM stack.c > stack.d.$$; \ sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \ rm -f stack.d.$$ set -e; rm -f main.d; \ cc -MM main.c > main.d.$$; \ sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \ rm -f main.d.$$ cc -c -o main.o main.c cc -c -o stack.o stack.c cc -c -o maze.o maze.c gcc main.o stack.o maze.o -o main
一开始找不到.d
文件,所以make
会报警告。但是make
会把include
的文件名也当作目标来尝试更新,而这些目标适用模式规则%.d: %c
,所以执行它的命令列表,比如生成maze.d
的命令:
set -e; rm -f maze.d; \ cc -MM maze.c > maze.d.$$; \ sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \ rm -f maze.d.$$
注意,虽然在Makefile中这个命令写了四行,但其实是一条命令,make
只创建一个Shell进程执行这条命令,这条命令分为5个子命令,用;
号隔开,并且为了美观,用续行符\
拆成四行来写。执行步骤为:
set -e
命令设置当前Shell进程为这样的状态:如果它执行的任何一条命令的退出状态非零则立刻终止,不再执行后续命令。
把原来的maze.d
删掉。
重新生成maze.c
的依赖关系,保存成文件maze.d.1234
(假设当前Shell进程的id是1234)。注意,在Makefile中$
有特殊含义,如果要表示它的字面意思则需要写两个$,所以Makefile中的四个$传给Shell变成两个$,两个$在Shell中表示当前进程的id,一般用它给临时文件起名,以保证文件名唯一。
这个sed
命令比较复杂,就不细讲了,主要作用是查找替换。maze.d.1234
的内容应该是maze.o: maze.c maze.h main.h
,经过sed
处理之后存为maze.d
,其内容是maze.o maze.d: maze.c maze.h main.h
。
最后把临时文件maze.d.1234
删掉。
不管是Makefile本身还是被它包含的文件,只要有一个文件在make
过程中被更新了,make
就会重新读取整个Makefile以及被它包含的所有文件,现在main.d
、stack.d
和maze.d
都生成了,就可以正常包含进来了(假如这时还没有生成,make
就要报错而不是报警告了),相当于在Makefile中添了三条规则:
main.o main.d: main.c main.h stack.h maze.h maze.o maze.d: maze.c maze.h main.h stack.o stack.d: stack.c stack.h main.h
如果我在main.c
中加了一行#include "foo.h"
,那么:
1、main.c
的修改日期变了,根据规则main.o main.d: main.c main.h stack.h maze.h
要重新生成main.o
和main.d
。生成main.o
的规则有两条:
main.o: main.c main.h stack.h maze.h %.o: %.c # commands to execute (built-in): $(COMPILE.c) $(OUTPUT_OPTION) $<
第一条是把规则main.o main.d: main.c main.h stack.h maze.h
拆开写得到的,第二条是隐含规则,因此执行cc
命令重新编译main.o
。生成main.d
的规则也有两条:
main.d: main.c main.h stack.h maze.h %.d: %.c set -e; rm -f $@; \ $(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \ sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$
因此main.d
的内容被更新为main.o main.d: main.c main.h stack.h maze.h foo.h
。
2、由于main.d
被Makefile包含,main.d
被更新又导致make
重新读取整个Makefile,把新的main.d
包含进来,于是新的依赖关系生效了。
%.d: %.c
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
对上述代码进行稍微详细的分析:
1 第一行 目标与依赖定义
%.d: %.c
按照make对规则的分类,这行是一个“模式规则”,它表示的含义是:所有的.d文件依赖于对应的.c文件。而模式规则中的“%”的匹配和替换发生在make执行时。一个模式规则的格式为:
%.o : %.c ; COMMAND...
这个模式规则指定了如何由文件“*.c”来创建文件“*.o”,文件“*.c”应该是已存在的或者可被创建的。
因此,对当前搜索目录下的每一个c源文件,make进行模式替换,相应的自动化变量也会赋值。相当于:
for each *.c fsource file
{
% <---基本文件名,不包含.c ## 如foo.c,则 % = foo
$* = % = 基本的文件名
$@ = %.d ## $@ = foo.d
$< = %.c ## 即$< = foo.c
......
}
假设当前处理的文件为foo.c,那么就有S* = foo, $@ = foo.d, $< = foo.c
2 第二行 gcc 自动分析依赖文件
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
需要将第一步的自动化变量展开代入,得到
$(CC) -MM $(CPPFLAGS) foo.c > foo.d.$$$$; \
$$$$代表了一个随机数,假设为35,即执行
gcc -MM $(CPPFLAGS) foo.c
结果存入文件foo.d.35
这个文件的内容可能是这样的
foo.o: foo.c foo.h defs.h common.h
它说明了foo.c真实的依赖关系。(注意由gcc产生的,make还并不知道这个依赖关系),为了让make也知道这个依赖关系,我们的目标是根据这个结果产生一个“依赖文件”foo.d,加入到Makefile中。即在Makefile中增加类似这样的规则:
foo.o: foo.c foo.h defs.h common.h
但是要直接把这个包含“目标:依赖”的新规则加入到当前的Makefile文件,并不是一件容易的事,还是需要借助于include。因此,可以把上面的结果存入到另一个foo.d的文件中,在Makefile中再通过:
sources = foo.c bar.c
sinclude $(sources:.c=.d)
就可以加入新的目标规则了。
但这样做还不够....因为这样一来,又产生了新的foo.d的文件,那么foo.d的依赖关系呢??显然,foo.d的依赖关系和foo.o是一致的!所以,在foo.d文件中还需要定义foo.d的依赖关系。用简单的多目标规则改写成下面的这样就可以:
foo.o foo.d: foo.c foo.h defs.h common.h
这也是为什么上面保存的文件是foo.d.35而不是最终的foo.d的原因,我们还需要编辑这个文件。要编辑,当然是要用到那个强大的sed工具了。
3 第三行 sed的使用
到这一步,我们的任务就十分明确了:将foo.d.35文件中的"foo.o:foo.c foo.h defs.h common.h"改写成"foo.o foo.d:foo.c foo.h defs.h common.h",并存为foo.d文件。
看看第三行发生了什么:
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
首先对自动变量替换:
sed 's,\(foo\)\.o[ :]*,\1.o foo.d : ,g' < foo.d.35 > foo.d; \
即:从文件foo.d.35读入,结果保存在foo.d中。
sed命令为 sed 's\(foo\)\.o[ :]*,\1.o foo.d : ,g'
这是s替换命令,这里用逗号","作分隔符(注意:s后跟的第一个字符被看作分隔符),其一般格式为:
sed 's/regexp/replacement/'
所以第一段"\(foo\)\.o[ :]*"被当成是正则表达式处理,它表示的模式是foo.o,后面可以有空格,或者冒号,如下面这些模式:
"foo.o" "foo.o:" "foo.o: " "foo.o : "foo.o : : " 都可以匹配。(当然,这里是用贪婪模式匹配)。
对我们给出的例子,匹配情况是这样的:
foo.o: foo.c foo.h defs.h common.h --(注,匹配包含冒号后一个空格,死新浪老自动改,以下也类似)
替换字符串为"\1.o foo.d : ",其中\1 = foo(sed中的replacement可以使用\1抓字串的),代入\1,于是有:\1.o foo.d : =foo.o foo.d :
至此,原来的内容变成了:
foo.o foo.d : foo.c foo.h defs.h common.h
正是我们所想要的结果, 大功告诫了~~~~