makefile生成*.d依赖文件,解决“只修改.h头文件,包含了该头文件的.c文件不重新编译的问题”
最近在使用makefile的过程中发现,当我只更改.h文件,不改任何.c文件的情况下,执行编译操作。.h的改动不会被编译器识别,也就没有编译到最终的可执行文件中。
接下来,会通过一个简单的demo,来说明.h文件的改动无法被编译器识别的原因,以及如何解决此问题。
demo中一共包含4个源文件,printHello.h,printHello.c,main.c,config.h。其目的是通过配置config.h中的宏定义,来控制main.c中的输出。源文件如下:
printHello.h
//printHello.h
#ifndef _PRINTHELLO_H_
#define _PRINTHELLO_H_
void printHello1();
void printHello2();
#endif
printHello.c
//printHello.c
#include <stdio.h>
#include "printHello.h"
void printHello1() {
printf("hello1\n");
}
void printHello2() {
printf("hello2\n");
}
main.c
//main.c
#include "printHello.h"
#include "config.h"
void main() {
#ifdef HELLO1
printHello1();
#endif
#ifdef HELLO2
printHello2();
#endif
}
config.h
//config.h
#define HELLO1
#define HELLO2
makefile的代码如下:
CC = gcc
output_path = output
all_objects := $(output_path)/main.o
all_objects += $(output_path)/printHello.o
printHello: $(all_objects)
$(CC) -o $@ $^
$(all_objects):$(output_path)/%.o:%.c
$(CC) -c $< -o $@
.PHONY: clean
clean:
-rm printHello.exe
del /Q /S output\*
在接下来的操作中,我会通过改动config.h中的宏定义,来控制main.c中的输出。
首先,config.h配置如下:
//config.h
#define HELLO1
#define HELLO2
然后更改config.h中的宏定义,如下:
//config.h
//#define HELLO1
#define HELLO2
我们预期的结果是,只输出hello2.
但是我们发现,在更改了config.h后,再次运行make时,包含了config.h的main.c并没有被重新编译。所以最后我们的改动,没有更新的到最终的可执行文件中。
分析原因:
makefile中的目标文件更新的规则是:如果依赖文件的修改时间比目标文件的修改时间新,那么就会执行后面的命令来生成目标文件。
实际上,main.o的依赖文件应该是:main.c printHello.h config.h。但在我们前面的makefile中,main.o的依赖文件只有main.c(makefile的9~10行),config.h并没有体现在我们的依赖关系中,所以编译器识别不了config.h的修改。所以我们的优化思路就应该是,想办法在makefile中体现完整的依赖关系。
解决方案:
GCC给我提供了自动生成依赖关系的方法。就是在生成目标文件的过程中,自动生成依赖关系文件(*.d)。不过生成的这些依赖关系文件也需要我们将其添加到makefile中。
自动生成依赖文件的方法主要是给gcc编译器传递相关的参数。
参数定义:
- -M
生成文件的依赖关系,同时也把一些标准库的头文件包含了进来。本质是告诉预处理器输出一个适合 make 的规则,用于描述各目标文件的依赖关系。对于每个源文件,预处理器输出 一个 make 规则,该规则的目标项 (target) 是源文件对应的目标文件名,依赖项 (dependency) 是源文件中 “#include” 引用的所有文件,生成的规则可以是单行,但如果太长,就用’'换行符续成多行。规则显示在标准输出,不产生预处理过的 C 程序。
注意:该选项默认打开了 -E 选项, -E 参数的用处是使得编译器在预处理结束时就停止编译 - -MM
生成文件的依赖关系,和 -M 类似,但不包含标准库的头文件。 - -MD
等同于 -M -MF File,但是默认关闭了 -E 选项。其输出的文件名是基于 -o 选项,若给定了 -o 选项,则输出的文件名是 -o 指定的文件名,并添加 .d 后缀,若没有给定,则输入的文件名作为输出的文件名,并添加 .d 后缀,同时继续指定的编译工作。
注意:-MD 不会像 -M 那样阻止正常的编译任务,因为它默认关闭了 -E 选项。比如命令中使用了 -c 选项,其结果要生成 .o 文件。若使用了 -M 选项,则不会生成 .o 文件,若使用的是 -MD 选项,则会生成 .o 文件 - -MF File
当使用了 “-M” 或者 “-MM” 选项时,则把依赖关系写入名为 “File” 的文件中。若同时也使用了 “-MD” 或 “-MMD”,“-MF” 将覆写输出的依赖文件的名称,File中可以包含路径,使得依赖文件可以输出到指定的路径中 。
例如:
gcc -MD -MF main.d main.c
-MD所输出的关于main.c的依赖关系的文件,输出在main.d中。
- -MMD
类似于 “-MD”,但是输出的依赖文件中,不包含标准头文件 - -MT Target
在生成的依赖文件中,指定依赖规则中的目标
导入依赖文件:
makefile中可以使用include关键字把其他文件包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。include的语法是:
include <filename>
可以在include前加一个减号-。如:
-include <filename>
其表示,无论include过程中出现什么错误,都不要报错继续执行。
修改后的makefile:
CC = gcc
output_path = output
all_objects := $(output_path)/main.o
all_objects += $(output_path)/printHello.o
all_o_depds = $(patsubst %.o, %.o.d, $(all_objects))
printHello: $(all_objects)
$(CC) -o $@ $^
$(all_objects):$(output_path)/%.o:%.c
# $(CC) -c $< -o $@
$(CC) -MMD -MF $(output_path)/$(notdir $@).d -MT $@ -c $< -o $@
-include $(all_o_depds)
.PHONY: clean
clean:
-rm printHello.exe
del /Q /S output\*
编译演示:
首先,config.h配置如下:
//config.h
#define HELLO1
#define HELLO2
然后更改config.h中的宏定义,如下:
//config.h
//#define HELLO1
#define HELLO2
我们可以看到,在只更改了config.h后,运行make时,编译器重新生成了main.o目标文件,所以我们对.h文件的修改,就更新到了最后的可执行文件中。
最后,介绍下demo中的文件结构:
makefile会把编译中生成的目标文件(*.o)和依赖文件(*.d)放到output文件夹下。
其中main.o.d是main.o的依赖文件。其中的内容如下: