Makefile 目标依赖

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 zhaixue.cc!\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# make
make: 'a.out' is up to date.

这是因为module.h并没有添加到Makefile的规则依赖目标中,所以无论你怎么修改module.h,都不会重新编译helloworld.c源文件。

头文件依赖

其中一个解决方法是将头文件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无疑。

自动生成头文件依赖关系

一个更高效的解决方法是:使用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目标文件的依赖关系,就不需要我们手动将头文件添加到规则中了。

至于在Makefile中如何使用gcc -M命令自动生成头文件依赖关系,如何使用。这个还需要进一步的解析和处理,涉及到正则表达式、模式匹配等,有点小复杂,一句话两句话说不清楚,如有兴趣,可观看Makefile进阶视频教程学习:Makefile工程实践第01季:从零开始一步一步写项目的Makefile

《Makefile工程实践》视频教程,一线开发工程师独家录制,网上首家讲解Makefile的实战课程。从零开始,教你一步一步编写一个工程项目的Makefile,支持使用第三方静态库、动态库,支持指定模块或目录编译生成静态库、动态库,赠送企业级的Makefile模板,学完即可拿来使用,投入项目开发实战,具备独立开展项目开发和管理的能力。详情请点击淘宝链接:Linux三剑客