欢迎您访问365答案网,请分享给你的朋友!
生活常识 学习资料

自动生成依赖关系

时间:2023-08-23
值得思考的问题

目标文件 (.o) 是否只依赖于源文件 (.c)?

编译器如何编译源文件和头文件?

编译行为带来的缺陷

预处理器将头文件中的代码直接插入源文件

编译器只通过预处理后的源文件产生目标文件

因此,

规则中以源文件为依赖,命令可能无法执行 下面的 makefile 有没有问题?

问题的提出 

makefile

TARGET := hello.outCC := gccSRCS := $(wildcard *.c)OBJS := $(SRCS:.c=.o)$(TARGET) : $(OBJS)$(CC) -o $@ $^$(OBJS) : %.o : %.c $(CC) -o $@ -c $<

当前的目录文件

 当我们修改了 foo.h 中的内容后,结果如下

并没有再次进行编译,这导致最终可执行文件没有重新生成。 

实验中的解决方案

头文件作为依赖条出现于每个目标对应的规则中

当头文件改动,任何源文件都将被重新编译 (编译低效)

当项目中头文件数量巨大时,makefile 将很难维护

疯狂的想法

通过命令自动生成对头文件的依赖

将生成的依赖自动包含进 makefile 中

当头文件改动后,自动确认需要重新编译的文件

预备工作 (原材料)

Linux 命令 sed

编译器依赖生成选项 gcc -MM (gcc -M)

Linux 中的 sed 命令

sed 是一个流编辑器,用于流文件的修改 (增/删/查/改)

sed 可用于流文本中的字符替换

sed 的字符串替换方式为 sed 's:src:des:g'

sed 的正则表达式支持 

在 sed 中可以用正则表达式匹配替换目标

并且可以使用匹配的目标生成替换结果

gcc 关键编译选项 

生成依赖关系

获取目标的完整依赖关系

gcc -M test.c

获取目标的部分依赖关系

gcc -MM test.c

小技巧:拆分目标的依赖

将目标的完整依赖拆分为多个多个部分依赖

预备工作 

makefile

.PHONY : test a b ctest : a btest : b ctest : @echo "$^"

运行结果如下所示:

我们将 test 目标的依赖拆分成两部分,和直接将 test 目标的依赖写成一起是一样的。 

makefile 中的 include 关键字

类似 C 语言中的 include

将其它文件的内容原封不动的搬入当前文件

make 对 include 关键字的处理 

在当前目录搜索或指定目录搜索目标文件

搜索成功:将文件搬入当前 makefile 中搜索失败:产生警告

以文件名作为目标查找并执行对应规则当文件名对应的规则不存在时,最终产生了错误

初探 include 关键字 

makefile

.PHONY : allinclude test.txtall : @echo "this is all"test.txt :@echo "this is test.txt"@touch test.txt

test.txt

other :@echo "this is $@"

执行结果如下所示:

首先 make 会查找当前目录中是否存在 test.txt 文件,当前目录中存在该文件,所以 make 会将 test.txt 中的内容插入到 include 的地方,other 成为了顶层目标,所以我们在执行 make 的时候,其实是在执行 other 所对应的命令。

我们将当前目录下的 test.txt 文件删除,再次执行 make,运行结果如下所示: 

此时 make 查找不到对应的文件,所以首先会产生一个警告,然后去查找当前 makefile 中是否存在对应的目标,当前 makefile 中存在 test.txt 目标,所以会执行目标所对应的命令,这时顶层的目标为 all,所以又会去执行 all 所对应的命令。

makefile 中命令的执行机制

规则中的每个命令默认是在一个新的进程中执行 (Shell)

可以通过接续符 (;) 将多个命令组合成一个命令

组合的命令依次在同一个进程中被执行

set -e 指定发生错误后立即退出执行

下面的代码想要实现功能?有没有问题?

每个命令都是 Shell 开启一个新的进程执行的,进程之间不会相互影响,所以 subtest 是创建在当前目录下的,而不是 test 目录下的。 

makefile 的命令执行

makefile

.PHONY : allall :set -e; mkdir test; cd test; mkdir subtest

set -e 指定发生错误后立即退出执行,并通过接续符将四个命令组合成一个命令,Shell 此时只会创建一个进程去执行这些命令,所以 subtest 是创建在 test 目录下的。

解决方案的初步思路

通过 gcc -MM 和 sed 得到 .dep 依赖文件 (目标的部分依赖)

技术点:规则中命令的连续执行

通过 include 指令包含所有的 .dep 依赖文件

技术点:当 .dep 依赖文件不存在时,使用规则自动生成 解决方案原型

makefile

.PHONY : all cleanCC := gccMKDIR := mkdirRM := rm -rfSRCS := $(wildcard *.c)DEPS := $(SRCS:.c=.dep)-include $(DEPS)all :@echo "this is $@"%.dep : %.c@echo "creating $@ ..."@set -e; $(CC) -MM -E $^ | sed 's,(.*).o[ :]*,objs/1.o : ,g' > $@clean :$(RM) $(DEPS)

.dep 依赖文件记录了一个目标文件所对应的依赖关系;如果对应的依赖文件不存在,则通过 include 去执行目标所对应的命令,gcc -MM 生成目标文件的依赖关系,通过 sed 将这些依赖关系都添加 objs/ 前缀,并重定向到对应的依赖文件中。

问题

如何在 makefile 中组织 .dep 文件到指定目录?

解决思路

当 include 发现 .dep 文件不存在时

通过规则和命令创建 deps 文件夹将所有 .dep 文件创建到 deps 文件夹.dep 文件中记录目标文件的依赖关系 初步的代码设计

不是问题的问题

为什么一些 .dep 依赖文件会被重复创建多次?

问题本质分析

deps 文件夹的时间属性会因为依赖文件创建而发生改变

make 发现 deps 文件夹比对应的目标更新

触发相应的规则重新解析和命令的执行

解决方案的优化

使用 ifeq 动态决定 .dep 目标的依赖

 

makefile

.PHONY : all cleanCC := gccMKDIR := mkdirRM := rm -rfDIR_DEPS := depsSRCS := $(wildcard *.c)DEPS := $(SRCS:.c=.dep)DEPS := $(addprefix $(DIR_DEPS)/, $(DEPS))all :@echo "this is $@"ifeq ("$(MAKECMDGOALS)", "all")-include $(DEPS)endififeq ("$(MAKECMDGOALS)", "")-include $(DEPS)endif$(DIR_DEPS) :$(MKDIR) $@ifeq ("$(wildcard $(DIR_DEPS))", "")$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.celse$(DIR_DEPS)/%.dep : %.cendif@echo "creating $@ ..."@set -e; $(CC) -MM -E $(filter %.c, $^) | sed 's,(.*).o[ :]*,objs/1.o : ,g' > $@clean :$(RM) $(DIR_DEPS)

当 deps 文件夹不存在时,.dep 文件依赖于 deps 文件夹和对应的 .c 源文件;当 deps 文件夹被创建出来时,.dep 文件就仅仅依赖于 .c 源文件了,就不会因为文件夹时间属性的更新而重新创建目标了。

当 make 的命令行参数为 空 或者 all 时,将依赖关系包含进 makefile 中。

include 暗黑操作一:

使用了减号 (-) 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时,make 将忽略这些错误!

 

include 暗黑操作二:

如果 include 触发规则创建了文件,之后还会发生什么?

 

include 暗黑操作三:

如果 include 包含的文件存在,之后还会发生什么?

预备实验 

makefile.1

.PHONY : allinclude text.txtall :@echo "this is $@"

当前目录下仅有 makefile,执行结果如下所示:

由于当前目录下不存在 test.txt,所以在 include 的时候会产生一个错误,make 终止执行。

在 include 前加上 '-' 后,执行结果如下所示:

尽管当前目录中不存在 test.txt,由于 include 前加上了 '-' ,它会屏蔽 include 产生的所有警告和错误,并继续向下执行。

 

makefile.2

.PHONY : all-include test.txtall :@echo "this is $@"test.txt :@echo "creating test.txt ..."@echo "other : ;@echo "this is other"" > test.txt

执行结果如下所示:

当前目录下不存在 test.txt,include 发现不存在对应的文件后,就回去查找有没有 test.txt 这个目标,发现这个目标存在就去执行对应的命令,这个命令将创建 test.txt,并且将 other : ;@echo "this is other" 写入到 test.txt 中,并将 test.txt 的内容包含进 makefile,此时 other 为顶层目标,我们去执行 make 的时候,执行的就是 other 目标。 

当命令和目标、依赖 写在一行后,';' 后面为命令。" 为转义为 " 字符。

makefile.3

.PHONY : all-include test.txtall :@echo "this is $@"test.txt : b.txt@echo "creating test.txt ..."

test.txt

other : ;@echo "this is other"

当前目录下存在 test.txt, b.txt。

执行结果如下所示:

由于当前目录下存在 test.txt,make 将 test.txt 的内容包含进 makefile 中,并且去查看是否存在 test.txt 的目标,查找到有对应的 test.txt 目标后,会查看依赖关系决定是否执行命令,由于这里的 b.txt 比 test.txt 更新,所以会执行对应的命令,打印 creating test.txt ...;最后,执行顶层命令。 

makefile.4

.PHONY : all-include test.txtall :@echo "$@ : $^"test.txt : b.txt@echo "creating test.txt ..."@echo "all : c.txt" > $@

test.txt

other : ;@echo "this is other"

当前目录下存在 test.txt,b.txt,c.txt。

执行结果如下所示:

make 首先会将 test.txt 内容包含进 makefile,然后去查看 test.txt 这个目标,由于 b.txt 比 test.txt 更新,所以会执行 test.txt 所对应的命令,重新创建 test.txt,并将  all : c.txt 写入到 test.txt,此时 include 会将 test.txt 重新包含进来,去执行顶层目标 all。

关于 include 的总结一

当目标文件不存在时

以文件名查找规则并执行

当目标文件不存在,且查找到的规则中创建了目标文件

将创建成功的目标文件包含进当前 makefile 关于 include 的总结二

当目标文件存在

将目标文件包含进当前 makefile以目标文件名查找是否有相应规则

YES:比较规则的依赖关系,决定是否执行规则的命令NO:NULL (无操作) 关于 include 的总结三

当目标文件存在,且目标名对应的规则被执行

规则中的命令更新了目标文件

make 重新包含目标文件,替换之前包含的内容目标文件未被更新

NULL (无操作) 疯狂想法的具体实现

注意事项 

当 .dep 文件生成后,如果动态的改变头文件间的依赖关系,那么 make 可能无法检测到这个改变,进而做出错误的编译决策。

解决方案

将依赖文件名作为目标加入自动生成的依赖关系中

通过 include 加载文件时判断是否执行规则

在规则执行时重新生成依赖关系文件

最后加载新的依赖关系

疯狂的想法

makefile

.PHONY : all clean rebuildCC := gccMKDIR := mkdirRM := rm -rfDIR_EXES := exesDIR_OBJS := objsDIR_DEPS := depsDIRS := $(DIR_EXES) $(DIR_OBJS) $(DIR_DEPS) EXE := hello.outEXE := $(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_EXES) $(DIR_OBJS) $(EXE)ifeq ("$(MAKECMDGOALS)", "all")-include $(DEPS)endififeq ("$(MAKECMDGOALS)", "")-include $(DEPS)endif$(EXE) : $(OBJS)$(CC) -o $@ $^@echo "Success! Target => $@"$(DIR_OBJS)/%.o : %.c$(CC) -o $@ -c $(filter %.c, $^)$(DIRS) :$(MKDIR) $@ifeq ("$(wildcard $(DIR_DEPS))", "")$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.celse$(DIR_DEPS)/%.dep : %.cendif@echo "creating $@ ..."@set -e; $(CC) -MM -E $(filter %.c, $^) | sed 's,(.*).o[ :]*,objs/1.o $@ : ,g' > $@rebuild :$(MAKE) clean$(MAKE) allclean :$(RM) $(DIRS)

执行 make 时,首先查看 include,然后才是执行顶层目标所对应的规则。

Copyright © 2016-2020 www.365daan.com All Rights Reserved. 365答案网 版权所有 备案号:

部分内容来自互联网,版权归原作者所有,如有冒犯请联系我们,我们将在三个工作时内妥善处理。