makefile(05)_自动生成依赖关系

11.自动生成依赖关系_上

11.0. 实验原料

本节实验所需的源文件和头文件:
原文件:func.c

网站建设哪家好,找创新互联公司!专注于网页设计、网站建设、微信开发、小程序设计、集团企业网站建设等服务项目。为回馈新老客户创新互联还提供了八宿免费建站欢迎大家使用!

#include "stdio.h"
#include "func.h"
void foo()
{
    printf("void foo() : %s\n", HELLO);
}

原文件:main.c

#include 
#include "func.h"
int main()
{
    foo();

    return 0;
}  

头文件func.c

#ifndef FUNC_H
#define FUNC_H

#define HELLO "Hello D.T."

void foo();

#endif

11.1.问题和方案

问题:

  1. 目标文件.o是否只依赖于源文件.c?编译器如何编译源文件和头文件?
    编译器处理头文件中的代码直接插入源文件中,编译器只通过预处理后的原文件产生目标文件,因此,规则中以源文件为依赖,命令可能无法执行。
  2. 下面Makefile有没有问题?
    makefile(05)_自动生成依赖关系
OBJS := func.o main.o

hello.out : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File ==> $@"

$(OBJS) : %.o : %.c
    @gcc -o $@ -c $<

此时看似可以编译成功,但存在潜在隐患。
存在问题:目标文件只依赖于.c文件,而没有关注.h文件,这样当.h文件的内容更新时,不会重新编译.c文件。
解决方案:
我们将.h文件也作为依赖写到Makefile中。

OBJS := func.o main.o

hello.out : $(OBJS)
    @gcc -o $@ $^
    @echo "Target File ==> $@"

$(OBJS) : %.o : %.c func.h
    @gcc -o $@ -c $<

上述解决方案问题:
头文件作为依赖出现于每一个目标文件对应的规则中,当头文件改动,任何源文件都会被重新编译(编译低效),而且当项目中头文件数量巨大时,Makefile件很难维护。

11.2.实现自动依赖

通过命令自动生成对头文件的依赖,将生成的依赖自动包含进入Makefile中,当头文件改动后,自动确认需要重新编译的文件。
预备工作:
1.Linux命令sed,sed时一个流编辑器,用于流文本的修改(增、删、查、改),文件替换,格式为:sed ‘s/abc/xyz/g’;
Sed可以支持正则表达,sed ‘s/(.).o[ :]/objs/\1.o : /g’ 正则匹配目标((.).o[ :]),替换值(objs/\1.o : )
2.编译器选项,生成依赖关系
gcc -MM 获取目标的完整依赖关系
gcc -M 获取目标的部分依赖关系
3.Makefile中目标拆分技巧,将目标的完整依赖拆分为多个部分依赖
makefile(05)_自动生成依赖关系

.PHONY : test a b c

test : a b

test : b c

test : 
        @echo "$^"

输出结果:a b c

思考:如果使用上面的预备工作实现头文件的自动依赖?

12.自动生成依赖关系_中

12.1.Include

Make中的include关键字,类似于C语言中的关键字,在处理是将所包含的文件的内容原封不动的搬到当前文件。
语法:include filename
Eg: include foo.make *.mk $(var)
Make对include关键字的处理方式,在当前目录搜索或者指定目录搜索目标文件,搜索成功:将文件内容搬入当前Makefile中;搜索失败,以文件名作为目标查找并执行对应规则。当文件名对应的规则不存在时,产生错误。
下面的代码怎么执行,为什么?

.PHONY : all

include test.txt

all : 
    @echo "this is all"

test.txt :
    @echo "test.txt"
    @touch test.txt

初次执行文件,自然搜索不到test.txt文件,然后会test.txt文件名作为目标查找并执行对应规则,输出结果:
makefile(05)_自动生成依赖关系
注意:在include关键字前面加上-,可以消除警告。

12.2.命令执行机制

1.Makefile中的命令执行时,每一条命令默认都是一个新的进程;(这样当我们希望使用上一个命令的执行结果,继续执行命令时往往得不到结果,譬如下面的代码);

.PHONY : all

all :
    set -e;
    mkdir test;
    cd test;
    mkdir subtest

输出结果:
makefile(05)_自动生成依赖关系
很显然,没有达到我们与其的目的(在test文件夹中创建subtest文件夹)
2.可以通过接续符(;)将多个命令组合成为一个命令,组合的命令一次在同一个进程中被执行;
3.可以使用set -e指定发生错误时立即退出。

.PHONY : all

all :
        set -e; \
        mkdir test; \
        cd test; \
        mkdir subtest

输出结果:
makefile(05)_自动生成依赖关系

12.3.实现自动生成依赖

1.通过gcc -MM 和sed命令得到.dep文件(目标的部分依赖),并使用接续符使得命令可以连续执行;
2.通过include指令包含所有的.dep依赖文件(当.dep文件不存在时,查找与.dep文件同名的规则并执行)

.PHONY : all clean

MKDIR := mkdir
RM := rm -fr
CC := gcc

SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)

-include $(DEPS)

all :
        @echo "all"

%.dep : %.c
        @echo "Creating $@ ..."
        @set -e; \
        $(CC) -MM -E $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@

clean :
        $(RM) $(DEPS)

输出结果:
makefile(05)_自动生成依赖关系
我们此时已经成功的生成了依赖文件main.dep和func.dep并在文件中记录了目标和依赖的关系。
思考:如果组织依赖文件相关的规则与源码编译相关的规则,进而形成功能完整的Makefile?

13.自动生成依赖关系_下

13.1.遗留问题

如何在makefile中组织.dep文件到指定目录?
解决思路:
当include 发现.dep文件不存在时,通过规则和命令创建deps文件夹,将所有的.dep文件创建到deps文件夹,并在.dep文件中记录目标文件的依赖关系。

$(DIR_DEPS) :
    $(MKDIR) $@

$(DIR_DEPS)/%.dep : $(DIR_DEPS)  %.c
    @echo "Creating $@ ..."
    @set -e; \
    $(CC) -MM -E  $^  | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@

这样做确实解决了上述问题,生成了deps文件夹:
makefile(05)_自动生成依赖关系
但同时我们看到两个问题:
1.因为依赖中包含deps文件夹,以deps文件夹作为 gcc -MM 的输入时没有意义的,会报告warning,所以使用下面的方法过滤掉deps文件夹

$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@

2.func.dep被重复创建了多次?
问题本质分析:
deps文件夹的时间属性会因为依赖文件创建而发生改变,make发现deps文件夹比对于的目标更新时,会触发相应规则的重新解释和命令的执行。
解决方案:使用ifeq动态决定.dep目标的依赖;

ifeq ("$(wildcard $(DIR_DEPS))", "")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif

13.2.Include黑暗操作

1.使用- 不但关闭了include发出的警告,同时关闭了错误,当发生错误时,make将忽略这些错误。
2.如果include 触发规则创建了文件则会发生下面的事情:

// 使用include 时的暗黑操作
if(如果目标文件不存在)
{
    //以文件名为规则查找并执行,
    if(查找到的规则中创建了文件)
    {
        //将创建成功的目标文件包含进当前makefile
    }
}
else // 如果目标文件存在
{
    // 将目标文件包含进当前makefile
    if(以目标文件名查找是否有相应的规则)
    {
        if(比较规则的依赖关系,决定是否执行规则的命令)
        {
            // (依赖文件更新,则执行)
        }
        else
        {
            // 无操作
        }
    }
    else
    {
        // 无操作
    }
}

实验1:include包含的目标文件不存在,并且以文件名为目标的规则存在,并在规则中创建了文件

.PHONY : all

-include test.txt

all : 
    @echo "this is all"

test.txt :
    @echo "creating $@ ..."
    @echo "other : ; @echo "this is other" " > test.txt

我们期望了输出结果因该是:this is all,因为all是第一个(默认)目标。
运行结果:
makefile(05)_自动生成依赖关系
原因在于当出现上面的情况时:以文件名为规则查找并执行,同时如果查找到的规则中创建了文件,将创建成功的目标文件包含进当前makefile,此时在makefile中第一个目标变成了other
实验2:

.PHONY : all

-include test.txt

all : 
    @echo "this is all"

test.txt : b.txt
    @echo "creating $@ ..."

当不存在b.txt时的运行结果:
makefile(05)_自动生成依赖关系
当存在b.txt,但b.txt文件比test.txt文件旧时的运行结果:
makefile(05)_自动生成依赖关系
当存在b.txt,但b.txt文件比test.txt文件新时的运行结果:
makefile(05)_自动生成依赖关系
结论:如果目标文件存在:将目标包含进当前makefile,以目标文件名查找是否有相应的规则
如果有则比较规则的依赖关系,决定是否执行规则的命令(依赖文件更新,则执行),如果规则中的命令更新了目标文件,替换之前包含了的内容。未更新,则无操作。
以目标文件名查找是否有相应的规则,不能找到,则无操作
实验3:

.PHONY : all

-include test.txt

all : 
    @echo "$@ : $^"

test.txt : b.txt
    @echo "creating $@ ..."
    @echo "all : c.txt" > test.txt

a.txt内容:

all : a.txt

当该文件中所需的所有文件都存在,并且test.txt的内容为最新时,make all输出结果:
makefile(05)_自动生成依赖关系
当b.txt文件最新时,make all输出结果:
makefile(05)_自动生成依赖关系

14.自动生成依赖关系_续

经过前面的技巧学习,我们现可以去完成这个自动生成依赖关系的想法了
注意:
思考:我们在13节中最终创建出来的makefile是否存在问题?
当.dep文件生成后,如果动态的改变文件间的依赖关系,那么make可能无法检测到这个改变,进而做出错误的判断。
实例:

输出结果:

解决方案:
将依赖文件的文件名作为目标加入自动生成的依赖关系中,通过include加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。
举个栗子:当我们前面编译过之后(生成了依赖文件),又添加了新的头文件,这时根据include的暗黑操作,要去检查与include所包含的依赖文件同名的规则是否存在,如果存在,则检查这个目标所对应的依赖是否被更新,如果更新,则执行相应规则。
最终方案:

.PHONY : all clean rebuild

MKDIR := mkdir
RM := rm -fr
CC := gcc

DIR_DEPS := deps
DIR_EXES := exes
DIR_OBJS := objs

DIRS := $(DIR_DEPS) $(DIR_EXES) $(DIR_OBJS)

EXE := app.out
EXE := $(addprefix $(DIR_EXES)/, $(EXE))

SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/, $(OBJS))
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))

all : $(DIR_OBJS) $(DIR_EXES) $(EXE)

ifeq ("$(MAKECMDGOALS)", "all")
include $(DEPS)
endif

ifeq ("$(MAKECMDGOALS)", "")
include $(DEPS)
endif

$(EXE) : $(OBJS)
    $(CC) -o $@ $^
    @echo "Success! Target => $@"

$(DIR_OBJS)/%.o : %.c
    $(CC) -o $@ -c $(filter %.c, $^)
#   $(CC) -o $@ -c $(filter %.c, $^)

$(DIRS) :
    $(MKDIR) $@

ifeq ("$(wildcard $(DIR_DEPS))", "")
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
    @echo "Creating $@ ..."
    @set -e; \
    $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@ : ,g' > $@

clean :
    $(RM) $(DIRS)

rebuild :
    @$(MAKE) clean
    @$(MAKE) all

总结:
Makefile中可以将目标的依赖拆分写到不同的地方;
include关键字能够触发相应的规则的执行;
如果规则的执行导致依赖更新,可能导致再次解释执行相应的规则;
依赖文件可需要依赖源文件得到正确的编译决策
自动生成文件的依赖关系能够提高Makefile的移植性。


当前标题:makefile(05)_自动生成依赖关系
分享路径:http://pwwzsj.com/article/pdsidh.html