Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.ostack.omaze.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.dstack.dmaze.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 个子命令,用 ; 号隔开,并且为了美观,用续行符 \ 拆成四行来写。执行步骤为:

  1. set -e 命令设置当前 Shell 进程为这样的状态:如果它执行的任何一条命令的退出状态非零则立刻终止,不再执行后续命令。

  2. 把原来的 maze.d 删掉。

  3. 重新生成 maze.c 的依赖关系,保存成文件 maze.d.1234 (假设当前 Shell 进程的 id 是 1234)。注意,在 Makefile 中 $ 有特殊含义,如果要表示它的字面意思则需要写两个$,所以 Makefile 中的四个$传给 Shell 变成两个$,两个$在 Shell 中表示当前进程的 id,一般用它给临时文件起名,以保证文件名唯一。

  4. 这个 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

  5. 最后把临时文件 maze.d.1234 删掉。

不管是 Makefile 本身还是被它包含的文件,只要有一个文件在 make 过程中被更新了, make 就会重新读取整个 Makefile 以及被它包含的所有文件,现在 main.dstack.dmaze.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.omain.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 包含进来,于是新的依赖关系生效了。