自动生成依赖关系(十)

        我们在之前的 makefile 学习中,其目标文件(.o)只依赖于源文件(.c)。那么如果在源文件中还包含有头文件,此时编译器如何编译源文件和头文件呢?我们来看看编译行为带来的缺陷:1、预处理器将头文件中的代码直接插入源文件;2、编译器只通过预处理后的源文件产生目标文件;3、规则中以源文件为依赖,命令就可能无法执行。

成都创新互联主要从事做网站、网站建设、网页设计、企业做网站、公司建网站等业务。立足成都服务景谷,10年网站建设经验,价格优惠、服务专业,欢迎来电咨询建站服务:028-86922220

        我们来看看下面的 makefile 有没有问题

makefile 源码

OBJS := func.o main.o

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

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

func.h 源码

#ifndef _FUNC_H_
#define _FUNC_H_

#define HELLO "Hello D.T."

void foo();

#endif

func.c 源码

#include 
#include "func.h"

void foo()
{
    printf("void foo() : %s\n", HELLO);
}

main.c 源码

#include 
#include "func.h"

int main()
{
    foo();

    return 0;
}

        我们来看看编译结果

自动生成依赖关系(十)

        我们看到已经正确实现了字符串的打印,那么我们接下来在 func.h 源文件中想要改掉这个字符串为 Software 呢?试试看能不能修改成功

自动生成依赖关系(十)

        我们看到在重新编译的时候,它并没有因为头文件的改变而改变,我们在 makefile 中又没有进行头文件的相关添加,改掉头文件中的内容肯定是不动的。下来我们在模式规则中加上头文件,在 %.c 后加上 func.h,再来看看编译结果

自动生成依赖关系(十)

        我们看到直接添加之后,编译出错了。因为 -c 后面的目标中含有头文件,所以不能直接进行编译。我们可以只编译 %.o 后面的第一依赖 %.c,这样就不会去编译 func.h 头文件了,将下面的 $^ 改为 $< ,我们来看看效果

自动生成依赖关系(十)

        我们看到已经正确改过来了。经过上面的实验,我们看到:头文件作为依赖条件出现于每个目标对应的规则中,当头文件改动时,任何源文件都将会被重新编译(编译低效);当项目中头文件巨大时,makefile 将很难维护。那么我们的头脑中不禁会冒出这么个想法:通过命令对自动生成对头文件的依赖;将生成的依赖自动包含进 makefile 中;当头文件改动后,自动确认需要重新编译的文件。那么此时我们还需要知道一个命令,Linux 中的 sed 命令。sed 是一个流编辑器,用于流文本的修改(增、删、查、改);它可用于流文本中的字符串替换,其字符串替换方式为:sed 's:src:des:g',具体格式如下

自动生成依赖关系(十)

        sed 同样也支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果。格式如下

自动生成依赖关系(十)

        下来我们以代码为例来看看 sed 命令是如何使用的

自动生成依赖关系(十)

        再来看看 gcc 关键编译选项,获取目标的完整依赖关系:gcc -M test.c;获取目标的部分依赖关系:gcc -MM test.c。makefile 如下

.PHONY : test

test :
    gcc -M main.c

        编译结果如下

自动生成依赖关系(十)

        我们看到 -M 是获取了它的所有依赖关系,再来试试 -MM 呢

自动生成依赖关系(十)

        我们看到 -MM 后,它只依赖与 main.c func.h。我们可以拆分目标的依赖,即将目标的完整依赖差分为多个部分依赖。格式如下

自动生成依赖关系(十)

        我们来做个实验

.PHONY : a b c

test : a b

test : b c

test :
    @echo "$^"

        我们来打印看看目标 test 的依赖都有哪些,编译结果如下

自动生成依赖关系(十)

        那么我们思考下:如何将 sed 和 gcc -MM 用于 makefile,并自动生成依赖关系呢?

        我们再来看看 makefile 中的 include 关键字,它类似于 C 语言中的 include,是将其它文件的内容原封不动的搬入当前文件。make 对 include 关键字的处理方式是在当前目录下搜索或指定搜索目标文件。如果搜索一成功,便将文件内容搬入当前 makefile 中;如果搜索失败,将会产生警告,以文件名作为目标查找并执行对应规则,当文件名对应的规则不存在时,最终产生错误。格式如下

自动生成依赖关系(十)

        下来还是以代码为例来进行说明

.PHONY : test

include test.txt

all :
    @echo "this is $@"

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

        我们在第 3 行包含 test.txt,可是当前目录下并没有 test.txt,然后触发 test.txt 的规则。因而会打印出 test.txt,然后再创建 test.txt,我们来看看编译结果

自动生成依赖关系(十)

        我们看到确实是创建了一个 test.txt 文件。那么在 makefile 中命令的执行是:1、规则中的每个命令默认是在一个新的进程中执行(Shell);2、可以通过接续符(;)将多个命令组合成一个命令;3、组合的命令依次在同一个进程中被执行;4、set -e 指定发生错误后立即退出执行。那么我们看看下面的代码会实现想要的功能吗?

.POHONY : all

all :
    mkdir test
    cd test
    mkdir subtest

        我们来看看编译结果自动生成依赖关系(十)

        我们看到在当前目录下创建了目录,但是 subtest 目录却不是在 test 目录下创建的,这是怎么回事呢?在第一条命令执行时创建了目录 test,此时这个进程已经关闭了;在第二条命令执行时,执行的是另一个进程,虽然它已经进入到目录 test 中,但是随着这个进程的关闭,又回到了当前目录;第三个进程是重新创建了目录 subtest。那么如何解决这个问题呢?直接利用 set -e 和 接续符来解决

.PHONY : test

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

        看看编译结果

自动生成依赖关系(十)

        那么我们之前思考问题的初步思路是:1、通过 gcc -MM 和 sed 得到 .dep 依赖文件(目标的部分依赖),技术点是规则中命令的连续执行;2、通过 include 指令包含所有的 .dep 依赖文件。技术点是当 .dep 依赖文件不存在时,使用规则自动生成。下面我们来看看解决方案是怎样的

ONY : 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)

        我们来看看编译结果

自动生成依赖关系(十)

        我们先来分析下,在执行 make all 前,它先通过 include 包含 $(DEPS),通过 $(DEPS) 触发模式规则,进而创建文件夹。我们看到在前面出现两个没有文件夹的信息,其实这条信息是可以隐藏的。我们在 include 前面加上 - 就 OK,来看看效果

自动生成依赖关系(十)

        我们看到并没打印出前面的两条信息了。那么我们再来思考下:如何组织依赖文件相关的规则与源码编译相关的规则,进而形成功能完整的 makefile  程序呢?我们如何在 makefile 中组织 .dep 文件到指定目录呢?初步想法是当 include 发现 .dep 文件不存在时:1、通过规则和命令创建 deps 文件;2、将所有 .dep 文件创建到 deps 文件夹;3、.dep 文件中记录目标文件的依赖关系。

        我们下来看看初步的代码设计是怎样的

.PHONY : all clean

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

DIR_DEPS := deps

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

include $(DEPS)

all :
    @echo "all"

$(DIR_DEPS) :
    $(MKDIR) $@

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

clean :
    $(RM) $(DIR_DEPS)

         我们来看看编译结果,是不是都将所有的 .dep 文件放入一个 deps 文件中

自动生成依赖关系(十)

        我们看到已经实现效果了。我们仔细看看 make 有一个警告,说 main.dep 被修改了,也就是说 main.dep 被重新创建了。那么我们来分析下,为什么一些 .dep 依赖文件会被重复创建多次呢?deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,于是乎就触发相应的规则重新解析和执行命令。那么我们知道了原因,此时这个方案该如何优化呢?我们可以使用 ifeq 动态决定 .dep 目标的依赖,具体 makefile 如下

.PHONY : all clean

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

DIR_DEPS := deps

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


all : 
    @echo "all"

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

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

$(DIR_DEPS) :
    $(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) $(DIR_DEPS)

        我们再次编译看看

自动生成依赖关系(十)

        我们看到它还是报了这样的错误,有可能是编译器的优化造成的。思路是正确的。下来我们来看看 include 的一些鲜为人知的秘密。

        A、 使用减号(-)不但关闭了 include 发出的警告,同时将关闭了错误;当错误发生时 make 将忽略这些错误!以代码为例来进行分析说明

.PHONY : all

include test.txt

all :
    @echo "this is all"

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

        我们来编译看看

自动生成依赖关系(十)

        我们看到不但发出警告,而且报错了。下来我们来在 include 前面加上 - 试试

自动生成依赖关系(十)

        这样它也不报错了,直接就通过了,我们还以为 makefile 写的对着呢。这便是第一个暗黑操作。下来看看第二个暗黑操作

        B、如果 include 触发规则创建了文件,之后还会发生什么?以代码为例来进行分析说明

.PHONY : all

include test.txt

all :
    @echo "this is all"

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

        看看编译结果

自动生成依赖关系(十)

        我们进行直接 make 的时候,发现它输出的 this is other,并不是我们所期望的 this is all。这是为什么呢?因为在 include 的时候,直接将 test.txt 铺开在这,此时会触发规则。makefile 就变成了下面这样

.PHONY : all

other : 
    @echo "creating $@ ..."
    @echo "this is other"

all :
    @echo "this is all"

        我们在直接 make 的时候,它默认执行的是第一个目标,因此便会输出 this is other,只有当我们 make all 的时候才会输出 this is all。这便是 include 的第二个暗黑操作了,下面继续看看第三个

        C、如果 include 包含的文件存在,之后会发生什么呢?以代码为例来进行分析说明

.PHONY : all

-include test.txt

all :
    @echo "this is all"

test.txt : b.txt
    @echo "this is $@"

        在当前目录下创建一个 b.txt 文件,看看编译结果

自动生成依赖关系(十)

        我们看到同样也执行了 test.txt 的相应的规则。看看下面这个 makefile 将会输出什么

.PHONY : all

-include test.txt

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

        看看结果

自动生成依赖关系(十)

        我们看到它最后输出的 all 的依赖是 c.txt,不应该觉得奇怪吗?我们明明在 all 后面没有依赖啊。再来看看生成的 test.txt 文件,它的内容是 all : c.txt,因此输出的结果是我们意想不到的。那么我们关于 include 便有了这几条总结:1、当目标文件不存在时,以文件名查找规则并执行;2、当目标文件不存在时且查找到的规则中创建了目标文件,将创建成功的目标文件包含进当前的 makefile 中;3、当目标文件存在,将目标文件包含进当前 makefile,以目标文件名查找是否有相应规则,YES 的话则比较规则的依赖关系来决定是否执行规则的命令,NO 的话则 NULL(无操作)。4、当目标文件存在且目标名对应的规则被执行,规则中的命令更新了目标文件,make 重新包含目标文件,替换之前包含的内容。目标文件未被更新,便是 NULL(无操作)。

        经过了这么多的知识点的探索,此时已经具备实现之前的想法的能力了。想要实现的具体格式如下

自动生成依赖关系(十)

        下面我们就根据这个来编写相关的 makefile。

func.h 源码

#ifndef FUNC_H
#define FUNC_H

#define HELLO "hello Makefile"

#endif

func.c 源码

#include 
#include "func.h"

void foo()
{
    printf("void foo() : %s\n", HELLO);
}

main.c 源码

#include 
#include "func.h"

int main()
{
    foo();
    
    return 0;
}

makefile 源码

.PHONY : all clean

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

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

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 $^

$(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)

        编译结果如下

自动生成依赖关系(十)

        我们看到已经自动生成了,并且最后的结果也是我们想要的,那么我们如果在 func.h 中改变字符串,看看结果是否也会改变

自动生成依赖关系(十)

        我们看到在编译的时候报错了,原因是只能编译 .c 文件,.h 头文件不参与编译,这时我们便要用到预定义函数 filter 了。因此我们需要在 makefile 第37 行将它改为 $(CC) -o $@ -c $(filter %.c, $^);再来看看效果

自动生成依赖关系(十)

        我们看到也成功的替换掉了。这时我们基本上已经完成我们之前的想法了,那么在实际开发中,肯定需要时不时的添加头文件,我们再来在 func.h 中包含一个头文件 define.h,在 define.h 文件中定义字符串 hello-makefile,看看结果是否会跟着改变

自动生成依赖关系(十)

        我们看到字符串并没有发生改变,再来看看 func.dep 和 main.dep 中是否包含了 define.h

自动生成依赖关系(十)

        也没有包含,按理说不应该,因为我们在 func.h 中包含了 define.h,那么在 func.c 和 main.c 中肯定也就包含了 define.h。下来我们来分析下这个,当 .dep 文件生成后,如果动态的改变头文件间的依赖关系,那么 make 可能无法检测到这个改变,进而做出错误的编译决策。解决方案便是:1、将依赖文件名作为目标加入自动生成的依赖关系中;2、通过 include 加载依赖文件时判断是否执行规则;3、在规则执行时重新生成依赖关系文件;4、最后加载新的依赖文件。解决方法是在 sed 命令后加上 $@,看看编译效果,顺便我们再来加上 rebuild。

自动生成依赖关系(十)

        我们看到已经正确实现了,我们来看看在 deps 文件下的 .dep 文件是否包含 define.h 呢?

自动生成依赖关系(十)

        确实是包含了 define.h,我们再来加上 new.h,看看是否还会有效

自动生成依赖关系(十)

        我们看到 new.h 同样也包含进去了。通过对综合示例的学习,总结如下:1、makefile 中可以将目标的依赖拆分写到不同的地方;2、include 关键字能够触发相应规则的执行;3、如果规则的执行导致依赖更新,可能导致再次解释执行相应规则;4、依赖文件也需要依赖于源文件得到正确的编译决策;5、自动生成文件间的依赖关系能够提高 makefile 的移植性。

        欢迎大家一起来学习 makefile,可以加我QQ:243343083。


名称栏目:自动生成依赖关系(十)
网站地址:http://ybzwz.com/article/pcgpoo.html