使用GCC编译器生成头文件依赖
复杂的C/C++工程中的头文件比较多,在编写GNU Makefile时,手动指出其源代码文件的头文件依赖关系是不可行的,需要通过编译工具自动生成头文件的依赖关系。GNU GCC的-MXX 命令行选项用于生成某个代码文件的依赖关系,通常使用的命令行选项为:
-MT object.o -MP -MMD -MF object.d
其中,-MT 用于指定与源文件对应的目标文件名(此处为object.o );-MP 指示GCC编译器为依赖的头文件增加伪目标规则;-MMD 不会隐含增加-E 编译选项,一次编译操作可以生成头文件依赖文件(上面的object.d )和目标文件,且不引入系统头文件的依赖。最后-MF object.d 指定生成的依赖文件名称为object.d 。这四个选项不会干扰正常的编译、链接流程,仅仅是告知编译器需要生成object.d ;也就是说,依赖规则文件object.d 在每次编译时都会随目标文件的生成而重新生成。
GNU Make读取依赖文件
依赖文件(或者依赖规则文件)是由GCC编译器在编译链接过程中,通过上面的-MXXX 选项生成的。那么第一次编译C/C++代码时,这些依赖文件是不存在的;那么问题就来了:GNU Make如何包含一个尚未生成的规则文件?官方文档给出了解决方案:
-include FILENAMES
sinclude FILENAMES
GNU Make的-include 指令可以忽略规则文件时文件不存在的错误。不过,这一功能特点并不能保证依赖文件会被创建。上面第一节中说明了,依赖文件会在编译过程被生成;这利用了GCC编译器的功能特性。某些情况下,还必须强制生成依赖文件,以便下次编译时能包含到。事实上,只要通过include 命令包含的头文件,GNU Make都会尝试进行更新,不论其是否已经存在,这一点后面会提到。
笔者编写的简单C/C++代码共有4个文件,hello.c 文件内容为:
#include <stdio.h>
#include "header0.h"
#include "header1.h"
int main(int argc, char *argv[])
{
printf("%s %s!\n", HELLO, WOLRD);
return 0;
}
header0.h 的内容为:
#ifndef AD_HEADER0_H
#define AD_HEADER0_H 1
#define HELLO "hello"
#endif
header1.h 的内容为:
#ifndef AD_HEADER1_H
#define AD_HEADER1_H 1
#include "header2.h"
#define WOLRD "world"
#endif
header2.h 的内容为:
#ifndef AD_HEADER2_H
#define AD_HEADER2_H 1
/* nothing to define here */
#endif
最后,编写的Makefile脚本如下:
MAKEFLAGS += -r -R
CC := gcc
CFLAGS := -Wall -O2 -fPIC
SOURCES := $(wildcard *.c)
OBJS := $(SOURCES:%.c=%.o)
DEPENDS := $(SOURCES:%.c=%.d)
TARGETS := $(OBJS) hello
.PHONY: all clean
all: $(TARGETS)
%: %.o
$(CC) -o $@ $<
%.o %.d: %.c
$(CC) -c $(CFLAGS) -MT $*.o -MP -MMD -MF $*.d -o $*.o $<
define include_depend
sinclude $(1)
endef
$(foreach depend,$(DEPENDS),$(eval $(call include_depend,$(depend))))
clean:
rm -rf $(TARGETS)
自动生成的依赖文件
以上源代码文件保存后,执行make 可以自动生成依赖文件hello.d :
$ make
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
gcc -o hello hello.o
$ cat hello.d
hello.o: hello.c header0.h header1.h header2.h
header0.h:
header1.h:
header2.h:
查看其内容,可以了解到是标准的Makefile依赖规则;此外,该规则还列出了其间接依赖的header2.h 头文件。这正是自动生成大型C/C++工程的头文件依赖关系的基本机制。当仅更新头文件,执行make 会重新编译生成hello.o :
$ touch header1.h
$ make
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
gcc -o hello hello.o
这一点正是我们需要达到的效果。
强制生成依赖文件
注意到上面编写的Makefile没有将hello.d 文件删除。如果在clean 目标中,将该文件删除会怎么样?首先看一下不删除hello.d 的效果:
$ make clean
rm -rf hello.o hello
$ make clean
rm -rf hello.o hello
接下来修改Makefile:
diff --git a/Makefile b/Makefile
index 5ef6129..74853be 100644
--- a/Makefile
+++ b/Makefile
@@ -24,4 +24,4 @@ endef
$(foreach depend,$(DEPENDS),$(eval $(call include_depend,$(depend))))
clean:
- rm -rf $(TARGETS)
+ rm -rf $(TARGETS) *.d
多次执行make clean 结果如下:
$ make clean
rm -rf hello.o hello *.d
$ make clean
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
rm -rf hello.o hello *.d
$ make clean
gcc -c -Wall -O2 -fPIC -MT hello.o -MP -MMD -MF hello.d -o hello.o hello.c
rm -rf hello.o hello *.d
出现这样的结果是因为,依赖文件hello.d 是Makefile需要包含的文件;而对于包启的脚本文件,GNU Make会强制更新;如果未找到更新的规则,就会报错(此处不报错是因为使用了sinclude ),或者不更新(当文件已存在时);官方文档对此做了详细的说明。因此,上面的编译脚本不删除依赖文件hello.d 是有正当理由的。
最后,推荐使用CMake,已集成了自动生成头文件依赖文件的功能,可以省去这些烦恼。
|