在程序编译过程中,Makefile的主要作用就是:构建生成可执行文件的依赖关系树。一个可执行文件需要哪些源文件、哪些库,我们都可以在Makefile里指定。在程序编译阶段,make工具会首先解析这个Makefile,根据Makefile里的指定,构建出编译可执行文件所需要的完整依赖关系。
根据这个依赖关系,make工具接下来就可以分别去编译需要的文件;通过时间戳,可以动态识别哪些是新添加的文件,哪些是已经编译过的文件,哪些文件做了新的修改,这样就不用每次都重新编译了,只编译那些新修改或新添加的源文件就可以了,大大提升了编译的效率。
那Makefile是如何构建可执行文件编译所依赖的关系树的呢?很简单,通过规则。
1.规则
规则是Makefile的基本组成单元。一个规则通常由目标、目标依赖和命令三部分构成:
目标:目标依赖
命令
a.out:hello.c
gcc -o a.out hello.c
如上所示,a.out就是我们要生成的目标,目标一般是一个可执行文件。目标依赖是指生成这个可执行文件所依赖的源文件,如 hello.c。而命令则是如何将这些目标依赖生成对应的目标,一般是gcc命令、链接命令、objcopy命令,一些shell命令等。命令必须使用 tab 键进行缩进,否则Makefile就会报错。
一个规则中的三个部分并不是都必须要有的。一个Makefile文件中可能包含多个规则,有的规则可能无目标依赖,仅仅是为了实现某种操作。如下面的clean命令:
clean:
rm -f a.out hello.o
当我们使用make clean命令清理编译的文件时,会调用这个规则中的命令,不需要什么依赖,仅仅是执行删除操作,所以这个规则中并没有目标依赖。
当然,一个规则中也可以没有命令,仅仅包含目标和目标依赖,仅仅用来描述一种依赖关系。但一个规则中一定要有一个目标。
2.默认目标
一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。正常情况下,当你想编译生成a.out时,要使用make a.out命令,但为什么直接使用make,没有指定参数,也能生成a.out的呢?这是因为在上面的Makefile文件中,a.out是文件中的第一个目标,当我们在make编译时没有给make指定要生成的目标,make就会选择Makefile文件中的第一个目标作为默认目标。
3.多目标
一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。如一个目标文件%.o都是由其对应的源文件%.c编译生成的,生成命令也是相同的:
*.o: *.c
gcc -o *.o *.c
4.多规则目标
多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。
a.out: hello.c
gcc -o a.out hello.c
a.out:
module.c
Make在解析Makefile构建依赖关系树时,会将这两个规则合并为:
a.out: hello.c module.c
gcc -o a.out hello.c other.c
5.伪目标
有时候我们设置一个目标,并不是真正生成这个文件,如上面的clean目标,而是仅仅为了执行某个操作。当我们执行make clean时,clean这个目标并没有生成对应的目标文件clean,因此,clean也可以设置为伪目标。伪目标并不是一个真正的文件,可以看做是一个标签。
.PHONY: clean
a.out: hello.o
gcc -o a.out hello.o
hello.o: hello.c
gcc -c -o hello.o hello.c
clean:
rm -f a.out hello.o
伪目标一般没有依赖关系,也不会生成对应的目标文件,可以无条件执行,纯粹是为了执行某一个命令,如clean执行清理工作。
make第一次编译某个项目时,会依次编译所有的源文件。但是当我们修改程序后,再次使用make编译,make并不会重新编译整个源文件,而是只编译你新添加或修改了的源文件,那make是如何做到的呢?
很简单,make是根据时间戳来判断一个规则中的目标依赖文件是否有更新。make在编译程序时,会依次检查依赖关系树中的所有源文件的时间戳,如果发现某个文件的时间戳有更新,会认为这个文件有改动过,会重新编译这个源文件。如果发现文件的时间戳没有更新,就不会再重新编译一次。
但是还有一种情况我们没有考虑到:在Makefile的规则中,一般不会把头文件添加到目标依赖中。当一个.c文件中包含多个头文件时,如果对应的头文件发生了变化,因为头文件没有包含在依赖关系树中,所以这个.c文件就不会重新编译。如下面的程序:
//hello.c
#include <stdio.h>
#include "module.h"
int main(void){
printf("PI = %f\\n", PI);
func();
printf("hello QST!\\n");
return 0;
}
//module.c
#include <stdio.h>
void func(void){
printf("hello func!\\n");
}
//module.h
#ifndef __MODULE_H__
#define __MODULE_H__
#define PI 3.14
#endif
//Makefile
.PHONY: clean
a.out: hello.o module.o
gcc -o a.out hello.o module.o
hello.o: hello.c
gcc -c -o hello.o hello.c
module.o: module.c
gcc -c -o module.o module.c
clean:
rm -f a.out hello.o
第一次使用make编译生成a.out运行无误后,然后修改module.h中的宏定义PI值为3.1415,再次使用make编译程序,你会发现make并没有重新编译:
wit@pc:/home/makefile# makemake: 'a.out' is up to date.
这是因为module.h并没有添加到Makefile的规则依赖目标中,所以无论你怎么修改module.h,都不会重新编译helloworld.c源文件。
6.头文件依赖
其中一个解决方法是将头文件module.h添加到规则的目标依赖列表中:
//Makefile
.PHONY: clean
a.out: hello.o module.o module.h
gcc -o a.out hello.o module.o
将头文件添加到规则的目标依赖中,虽然能解决依赖关系的问题,但是也有弊端,当helloworld.c包含几十个头文件时,把包含的这些头文件都手工添加进去,工作量还是蛮大的。如果一个项目中有成百上千个C文件,每个C文件包含几十个头文件,按照这种手工添加的方法,996无疑。
7.自动生成头文件依赖关系
一个更高效的解决方法是:使用gcc -M 命令自动生成头文件依赖关系
wit@pc:/home/makefile# gcc -M hello.c
hello.o: hello.c /usr/include/stdc-predef.h \\
/usr/include/stdio.h \\
/usr/include/x86_64-linux-gnu/bits/libc-header-start.h \\
/usr/include/features.h /usr/include/x86_64-linux-gnu/sys/cdefs.h \\
/usr/include/x86_64-linux-gnu/bits/wordsize.h \\
/usr/include/x86_64-linux-gnu/bits/long-double.h \\
/usr/include/x86_64-linux-gnu/gnu/stubs.h \\
/usr/include/x86_64-linux-gnu/gnu/stubs-64.h \\
/usr/lib/gcc/x86_64-linux-gnu/9/include/stddef.h \\
/usr/lib/gcc/x86_64-linux-gnu/9/include/stdarg.h \\
/usr/include/x86_64-linux-gnu/bits/types.h \\
/usr/include/x86_64-linux-gnu/bits/timesize.h \\
/usr/include/x86_64-linux-gnu/bits/typesizes.h \\
/usr/include/x86_64-linux-gnu/bits/time64.h \\
/usr/include/x86_64-linux-gnu/bits/types/__fpos_t.h \\
/usr/include/x86_64-linux-gnu/bits/types/__mbstate_t.h \\
/usr/include/x86_64-linux-gnu/bits/types/__fpos64_t.h \\
/usr/include/x86_64-linux-gnu/bits/types/__FILE.h \\
./usr/include/x86_64-linux-gnu/bits/types/FILE.h \\
/usr/include/x86_64-linux-gnu/bits/types/struct_FILE.h \\
/usr/include/x86_64-linux-gnu/bits/stdio_lim.h \\
/usr/include/x86_64-linux-gnu/bits/sys_errlist.h module.h
如上所示,hello.c源文件中包含了两个头文件:stdio.h和module.h,通过gcc -M命令,我们就可以自动生成一个hello.o目标文件的依赖关系,就不需要我们手动将头文件添加到规则中了。