外观
05.自动生成依赖关系
约 2717 字大约 9 分钟
自动生成依赖滤波算法个人随笔
2022-06-18
第十一部分 :自动生成依赖关系(上)
11.1 值得思考的问题
- 目标文件(.o )是否只依赖于源文件(.c ) ?
- 编译器如何编译源文件和头文件?

11.2 编译行为带来的缺陷
- 预处理器将头文件中的代码直接插入源文件
- 编译器只通过预处理后的源文件产生目标文件
- 因此,
- 规则中以源文件为依赖,命令可能无法执行
11.3 下面的 makefile有没有问题?

11.4 编程实验:问题的提出
当func.h发生改变,理应重新编译,但makefile只检查两个源文件,发现无改动是最新的,所以不会重新编译,导致错误。
OBJS := func.o main.o
hello.out : $(OBJS)
@gcc -o $@ $^
@echo "Target File ==> $@"
$(OBJS) : %.o : %.c func.h
@gcc -o $@ -c $<
11.5 实验中解决方案的问题
- 头文件作为依赖条出现于每个目标对应的规则中
- 当头文件改动,任何源文件都将被重新编译(编译低效)
- 当项目中头文件数量巨大时, makefile将很难维护
11.6 疯狂的想法
- 通过命令自动生成对头文件的依赖
- 将生成的依赖自动包含进makefile中
- 当头文件改动后,自动确认需要重新编译的文件
11.7 预备工作(原材料)
- Linux命令sed
- 编译器依赖生成选项gcc-MM ( gcc-M )
11.8 Linux中的sed命令
- sed是一个流编辑器,用于流文本的修改(增/删/查/改)
- sed可用于流文本的中的字符串替换
- sed的字符串替换方式为 : sed 's: src:des:g'

11.9 sed的正则表达式支持
- 在sed 中可以用正在表达式匹配替换目标
- 并且可以使用匹配的目标生成替换结果


sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
① 分隔符:既可以是,也可以是:
② \(.*\)\.o[ :]*
\(.*\)\.o[ :]*
- 想匹配.o文件的路径
- \文件夹名字\文件夹名字\.o
- 在这个路径之后,如果有空格有冒号也算匹配成功
- 无论匹配目标后面有多少个空格或者:都转换成空格:空格(sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g')

objs/\1.o :
- 将把前面的匹配结果加上objs/前缀
11.10 gcc 关键编译选项
- 生成依赖关系
获取目标的完整依赖关系
- gcc -M test.c
获取目标的部分依赖关系
- gcc -MM test.c
- gcc -MM -E main.c 其中的-E告诉编译器只要做初步解析即可,无需做进一步的工作,会高效一些


11.11 小技巧:拆分目标的依赖
- 将目标的完整依赖拆分为多个部分依赖

11.12 编程实验:预备工作
.PHONY : test a b c
test : a b
test : c
test :
@echo "$^"
11.13 To be continued ...
思考:如何将sed和gcc -MM用于makefile,并自动生成依赖关系?
第十二部分 :自动生成依赖关系(中)
12.1 makefile 中的include关键字
- 类似C语言中的include
- 将其它文件的内容原封不动的搬入当前文件

- include foo.make 包含 foo.make文件
- include *.mk 包含 所有.mk文件
- include $(var) 包含 var 变量所在的文件(的全部内容)
12.2 make 对include关键字的处理方式
- 在当前目录搜索或指定目录搜索目标文件
- 搜索成功:将文件内容搬入当前makefile 中
- 搜索失败:产生警告
- 以文件名作为目标查找并执行对应规则
- 当文件名对应的规则不存在时,最终产生错误
12.3 下面的代码怎么执行?为什么?
.PHONY : all
include test.txt
all :
@echo "this is $@"
test.txt :
@echo "test.txt"
@touch test.txt12.4 编程实验:初探include关键字

12.5 makefile 中命令的执行机制
- 规则中的每个命令默认是在一个新的进程中执行(Shell)
- 可以通过接续符(;)将多个命令组合成一个命令(就算出错也继续执行下一个)
- 组合的命令依次在同一个进程中被执行
- set -e 指定发生错误后立即退出执行
12.6 下面的代码想要实现功能?有没有问题?
.PHONY : all
all :
mkdir test
cd test
mkdir subtest12.7 编程实验:makefile的命令执行
① 开启了3个进程,没有实现test文件夹下有
② 利用 接续符;和 连接符\ ,得以实现
.PHONY : all
all :
set -e;\
mkdir test;\
cd test;\
mkdir subtest
12.8 解决方案的初步思路
通过gcc -MM和sed 得到.dep依赖文件(目标的部分依赖)
- 技术点:规则中命令的连续执行
通过include指令包含所有的.dep依赖文件
- 技术点:当.dep依赖文件不存在时,使用规则自动生成
12.9 编程实验:解决方案原型
.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)
12.10 To be continued ...
思考:如何组织依赖文件相关的规则与源码编译相关的规则,进而形成功能完整的makefile程序?
第十三部分 :自动生成依赖关系(下)
13.1 如何在makefile中组织.dep 文件到指定目录?
解决思路
当include发现.dep文件不存在:
- 通过规则和命令创建deps文件
- 将所有.dep文件创建到deps文件夹
- .dep文件中记录目标文件的依赖关系
初步的代码设计


13.2 编程实验:解决方案
① 将所有生成的.dep文件放到deps文件夹中

- $(filter %.c, $^) 只取依赖中所有的.c文件
② gcc编译时不需要文件夹作为目标,进行过滤
.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)) # 添加路径
include $(DEPS)
all :
@echo "all"
$(DIR_DEPS) :
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c #$(DIR_DEPS) # 利用依赖创建文件夹
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)13.3 为什么一些.dep 依赖文件会被重复创建多次?

13.4 问题本质分析
- deps文件夹的时间属性会因为依赖文件创建而发生改变
- make 发现deps文件夹比对应的目标更新
- 触发相应规则的重新解析和命令的执行
13.5 解决方案的优化:使用ifeq动态决定.dep目标的依赖
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' > $@13.6 编程实验:解决方案的优化
ifeq ("$(MAKECMDGOALS)", "all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)", "")
-include $(DEPS)
endif
Make clean 不需要包含文件13.7 include暗黑操作一
使用减号(-)不但关闭了include 发出的警告,同时关闭了错误;当错误发生时make将忽略这些错误!
.PHONY : all
-include test.txt
all :
@echo "this is all"
13.8 include暗黑操作二
如果include触发规则创建了文件,之后还会发生什么?
.PHONY : all
-include test.txt
all :
@echo "this is all"
test.txt :
@echo "creating $@ ..."
@echo "other : ; @echo "this is other" " > test.txt前提:test.txt不存在
① 执行:-include test.txt
② 执行:test.txt规则 此时生成了test.txt,原来-include test.txt替换成了test.txt
③ 执行:test.txt中的规则,make执行第一条规则
④ 打印:this is other

13.9 include暗黑操作三
如果include包含的文件存在,之后还会发生什么?
.PHONY : all
-include test.txt
all :
@echo "this is all"
test.txt : b.txt
@echo "creating $@ ..."前提:b.txt比test.txt时间戳更新
① 当include文件后,make检查这个文件名,有没有对应的规则
② 如果有,则进步一检查依赖的时间戳是否比文件更新
③ 如果确实更新,就会执行规则下的命令

13.10 编程实验:预备实验
.PHONY : all
-include test.txt
all :
@echo "$@ : $^"
test.txt : b.txt
@echo "creating $@ ..."
@echo "all : c.txt" > test.txt前提:test.txt时间戳最新

touch b.txt,b.txt时间戳最新

13.11 关于include的总结
总结 1
当目标文件不存在
- 以文件名查找规则,并执行
当目标文件不存在,且查找到的规则中创建了目标文件
- 将创建成功的目标文件包含进当前makefile
总结 2
- 当目标文件存在
- 将目标文件包含进当前makefile
- 以目标文件名查找是否有相应规则
- YES : 比较规则的依赖关系,决定是否执行规则的命令
- NO : NULL(无操作)
总结 3
- 当目标文件存在,且目标名对应的规则被执行
规则中的命令更新了目标文件
- make重新包含目标文件,替换之前包含的内容
目标文件未被更新
- NULL(无操作)
第十四部分 :自动生成依赖关系(续)
14.1 疯狂想法的具体实现

14.2 注意事项
当.dep文件生成后,如果动态的改变头文件间的依赖关系,那么make可能无法检测到这个改变,进而做出错误的编译决策。
14.3 解决方案
- 将依赖文件名作为目标加入自动生成的依赖关系中
- 通过include 加载依赖文件时判断是否执行规则
- 在规则执行时重新生成依赖关系文件
- 最后加载新的依赖文件
14.4 编程实验:疯狂的想法
.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, $^)
$(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) all14.5 小结
- makefile 中可以将目标的依赖拆分写到不同的地方
- include关键字能够触发相应规则的执行
- 如果规则的执行导致依赖更新,可能导致再次解释执行相应规则
- 依赖文件也需要依赖于源文件得到正确的编译决策
- 自动生成文件间的依赖关系能够提高makefile的移植性
