用 GCC 和 Makefile 自动处理依赖关系

Make 是一个非常好的工具,但是 Makefile 的编写却不是很方便,尤其是编译依赖关系的确定上。

假设一个 C/C++ 项目有许多源文件、许多头文件,每个源文件里都引用了一些头文件。这时如果修改了其中的一个头文件,为了加快工程编译速度,应该只有引用了被改动头文件的源文件需要重新编译,其他源文件不需要再次编译。但是,Make 并不知道哪些源文件引用了这个头文件,要确定哪些源文件需要重新编译,只能根据 Makefile 中写定的规则进行判断。

先来看一下 Makefile 中一条规则的语法:

sample.o: sample.c header1.h header2.h
    CC -c -o $@ $<

这里面,header1.hheader2.h 便是 sample.c 所需的头文件,如果 header1.h 改了,Make 就能根据这条规则自动判断出,sample.c 需要重新编译。

但是,这种方式需要在 Makefile 中显式写出引用了哪些头文件,每当 sample.c 增加或删去了一个头文件的引用,还需要更改 Makefile。如果项目规模比较大,头文件之间还会相互引用,这是将会是一件非常痛苦的事情。

生成依赖列表

幸好,GCC(以及 Clang)提供了自动生成依赖关系的功能,而且生成的依赖关系可以直接用在 Makefile 中。

最简单的方法是使用参数 -MM,在上面的例子中,使用命令 gcc -MM sample.c 就可以在终端内输出依赖列表:

sample.o: sample.c header1.h header2.h

可以看到,格式正是 Makefie 需要的,完全可以把这个命令的输出结果直接复制到 Makefile 中去。

其实生成依赖列表还有一个参数 -M,但是 -M 会把所有头文件输出,包括 stdio.h 这类的系统标准头文件,而 -MM 则会忽略这些标准头文件。由于在实际项目中,只有自定义头文件才会修改,因此 -MM 更加合适。

默认情况下,使用 -M-MM 生成的规则的目标名就是源文件明,将后缀替换为 .o,如果希望使用其他的目标名,可以增加一个 -MT 参数,例如:

$ gcc -MM -MT target.o sample.c
target.o: sample.c header1.h header2.h

还有两个参数,-MD-MMD,它们会让 GCC 在编译程序的同时生成依赖列表,并保存成文件。默认的文件名就是规则的目标,将后缀替换成 .d,如果要指定生成的文件名,可以使用参数 -MF

还有一个非常有用的参数是 -MP,它会给每个依赖的头文件生成一条规则,内容为空,就像这样:

$ gcc -MM -MP -MT target.o sample.c
target.o: sample.c header1.h header2.h

header1.h:

header2.h:

生成这些多余的规则是非常有用的,如果删除了某些头文件而没有更新 Makefile,这些空规则可以避免因找不到头文件而报错退出。

自动化

用上面介绍的这些参数,能够自动生成依赖规则,但是仍然需要对每一个文件单独执行一遍。然而,若能和 Make 的模糊匹配结合起来,就可以实现真正的自动化管理。

GNU Make 支持规则的正则表达式匹配,例如:

%.o: %.c
    CC -c -o $@ $<

可以匹配所有 .c 文件生成 .o 文件的规则。如果在编译命令中加上 -MMD 参数,就能在编译的同时生成依赖列表文件:

%.o: %.c
    CC -c -MT $@ -MMD -MP -MF $*.d -o $@ $<

这样,在编译的同时,就会生成后缀名是 .d 的依赖文件。在 Makefile 文件末尾,可以用 include 命令包含这些依赖列表文件:

-include $(patsubst %, %.d, $(basename $(sources)))

其中变量 $(sources) 是全部的源文件,patsubst 函数的作用是将所有的源文件后缀名换成 .d,于是就成了刚刚生成的依赖列表文件。在 include 命令前加一个减号,可以避免找不到某个文件而报错。

用这种方法,执行 Make 的时候会自动寻找依赖文件,如果没有就会在编译的同时生成依赖文件。如果找到了依赖列表文件,就会自动包含在 Makefile 中。