本文原创发布于微信公众号“洛奇看世界”
我在上一篇《Makefile新手?千万别错过了这个入门教程》推荐了李云的《驾驭 Makefile》作为新手入门 Makefile 的推荐教程。
在该教程中存在两个问题:
-
编译时出现死循环,不断反复生成 main.dep 和 foo.dep -
每次编译时都会反复交替生成 foo.o 和 main.o
这两个问题都由同一个原因引起,即目标文件对目录的依赖,而依赖的目录中每次更新文件时都会更新目录的修改时间。
本文追根溯源,详细展示这两个问题的调试流程和解决办法。
全文分为四个部分:
-
问题和源码 -
无限生成依赖文件的分析 -
无限生成依赖文件的解决 -
交替生成文件的原因和解决办法
如果只关注问题的原因分析,请跳转到第2.3节;
如果只关注问题的解决办法,请直接跳转到第3节;
1. 问题和源码
1.1 无限和反复生成文件的问题
这里的两个问题分别指:
-
无限循环生成 foo.dep
和main.dep
文件 -
反复交替生成 foo.o
和main.o
文件
具体来说,就是:
-
编译时出现死循环,不断反复生成 main.dep 和 foo.dep
-
每次编译时都会反复交替生成 foo.o 和 main.o
从第二次 make 开始,先生成 foo.o,然后生成 main.o,再次编译时先生成 main.o,然后生成 foo.o……,不断循环往复。
1.2 源码
如果你想亲自查看下这个问题,代码仓库地址:
https://github.com/guyongqiangx/makefile-study
关于这个问题的调试信息位于 clock-skew-issue
分支上:
$ git clone https://github.com/guyongqiangx/makefile-study -b clock-skew-issue
$ cd makefile-study/Makefile/3-complicated/
makefile-study/Makefile/3-complicated$ ls -lh
total 40K
-rw-rw-r-- 1 rocky rocky 5.7K Dec 24 17:00 debug-log-0-clock-skew-issue.log
-rw-rw-r-- 1 rocky rocky 2.4K Dec 24 17:00 debug-log-fix1-no-depending-deps-dirs.log
-rw-rw-r-- 1 rocky rocky 2.8K Dec 24 17:00 debug-log-fix2-update-modification-time.log
-rw-rw-r-- 1 rocky rocky 87 Dec 24 17:00 foo.c
-rw-rw-r-- 1 rocky rocky 55 Dec 24 17:00 foo.h
-rw-rw-r-- 1 rocky rocky 54 Dec 24 17:00 main.c
-rw-rw-r-- 1 rocky rocky 1017 Dec 24 17:00 Makefile
-rw-rw-r-- 1 rocky rocky 1.1K Dec 24 17:00 Makefile.Fix1
-rw-rw-r-- 1 rocky rocky 1.2K Dec 24 17:00 Makefile.Fix2
makefile-study/Makefile/3-complicated$
2. 无限生成依赖文件的分析
以下是无限循环生成 foo.dep
和 main.o
文件的问题的分析过程。
在 Makefile 中,一个目标是否需要更新,由这个目标和依赖的最后修改时间(last-modification time
)决定。
如果存在以下两种情况之一:
-
依赖的最后修改时间比目标的最后修改时间新 -
目标不存在
make 就会执行相应的命令来更新目标。
第 1 条比较好理解
目标文件 complicated.exe
依赖于另外两个文件 foo.o
和 main.o
,只要后两者有任何一个比目标文件新,那就会更新目标文件 complicated.exe
第 2 条,目标不存在,可以换个思路转换成第 1 条的情况
假想目标存在,但其最后修改时间为 0,即时间戳 1970-01-01 00:00.000
,因为目前电脑 UTC 时间的起始值 0 就对应这个时间点,而时间不会出现负数,因此没有时间能比这个时间更早了。
问题:
有没有电脑显示文件的时间戳早于1970-01-01 00:00.000
呢?
因此,这两条的情况都是目标比依赖新,所以当更新完成后,目标的最后修改时间会比依赖的最后修改时间新,至少一样新。
关于这部分,在官方 GNU Make Manual 的 4.2 Rule Syntax
一节中原话是这么说的:
原文链接:https://www.gnu.org/software/make/manual/make.html#Rule-Syntax
我们可以通过调试来确认是不是这个原因。那如何调试呢?
2.1 比较目标和依赖的时间戳
既然是通过最后修改时间来判断目标是否需要更新,那我们可以在 Makefile 中手动获取 deps
目录和目录下的 *.dep
文件的最后修改时间,通过比较这两个时间来验证我们的想法。
获取文件的时间戳
如何获取一个文件的时间戳呢,度娘一下,发现可以通过stat
命令来搞定。
stat
命令默认的输出信息比较多,如下:
$ stat main.c
File: ‘main.c’
Size: 54 Blocks: 8 IO Block: 4096 regular file
Device: 801h/2049d Inode: 188292 Links: 1
Access: (0664/-rw-rw-r--) Uid: ( 1001/ rocky) Gid: ( 1001/ rocky)
Access: 2020-12-24 17:00:56.114282103 +0800
Modify: 2020-12-24 17:00:56.114282103 +0800
Change: 2020-12-24 17:00:56.114282103 +0800
Birth: -
但其实我们感兴趣的只是倒数第三行的修改时间(Modify)。
通过查看 stat
命令的选项,--format
或 --printf
可以用于自定义输出格式。
在具体的格式上:
-
%Y
用于显示整数时间戳,但不容易阅读 -
%y
用于显示容易识别的时间 -
%n
用于显示文件名
(更多格式请参考命令 man stat
的输出)
我们自定义 stat
获取文件的修改时间如下:
# 使用 '--printf' 选项自定义输出格式
$ stat --printf="%yt%nn" main.c
2020-12-24 17:00:56.114282103 +0800 main.c
获取目标和依赖的最后修改时间
为了检验 deps
目录,以及目录中 *.dep
文件的时间戳变化情况,我们将 Makefile 的 %.dep
规则修改如下:
主要新增了两个白色方框包含的部分,
-
38 行显示改动前 deps
目录的时间戳 -
44 行显示改动后 deps
目录的时间戳 -
45 行显示在改动后 deps
目录下所有.dep
文件的时间戳 -
47 行的 sleep
用于编译时暂停,方便观察时间戳
运行的效果如下:
在上面的红色方框中,deps
目录在生成了 deps/main.dep
文件以后,时间戳发生了变化:
17:47:05.582369639 --> 17:47:05.594369640
在下面的绿色方框中,deps
目录在生成了 deps/foo.dep
文件以后,时间戳也发生了变化:
17:47:05.594369640 --> 17:47:07.638369704
但 deps/main.dep
的时间戳保持了不变。
从而验证了我们的猜想,修改 deps
目录下的文件导致 deps
的目录时间戳被更新了。
再次在 deps
目录下生成 foo.dep
文件时,导致 deps
目录的最后修改时间比先生成的文件 main.dep
最后修改时间新。
换句话说,此时规则 deps/main.dep: deps main.c
的依赖满足不了了,需要重新生成 deps/main.dep
。
到底是不是这样了,我们通过其它方式验证下。
2.2 使用 --debug
选项调试 Makefile
为了调试 Makefile,自然要检查下 make 命令都支持哪些选项。
在 GNU Make Manual 的 9.7 Summary of Options
中列举了所有支持的选项,其中跟调试相关的选项有 -d
和 --debug[=options]
。其中 --debug
的 options 又有多个选择。
make完整的命令行选项参考
man make
输出或下面的官方文档:
https://www.gnu.org/software/make/manual/make.html#Options-Summary
你或许会问,这么多option我到底该用哪个?说实话,一开始我也不知道,好在我们的 Makefile 比较简单,可以一个一个的挨个试,看看哪个的输出信息能满足我们的需要。
最后发现 --debug=m
的输出信息刚好是我们需要的,为了方便查看,我将 Makefile 中添加的打印信息以及make输出的版权信息去掉后如下:
完整 log 请参考代码目录下的文件:
debug-log-0-clock-skew-issue.log
makefile-study/Makefile/3-complicated$ make --debug=m
...
Reading makefiles...
Updating makefiles....
File 'deps/main.dep' does not exist.
File 'deps' does not exist.
Must remake target 'deps'.
...
Successfully remade target file 'deps'.
Must remake target 'deps/main.dep'.
...
Successfully remade target file 'deps/main.dep'.
File 'deps/foo.dep' does not exist.
Must remake target 'deps/foo.dep'.
...
Successfully remade target file 'deps/foo.dep'.
Re-executing[1]: make --debug=m
...
Reading makefiles...
Updating makefiles....
Prerequisite 'deps' is newer than target 'deps/main.dep'.
Must remake target 'deps/main.dep'.
...
Successfully remade target file 'deps/main.dep'.
Re-executing[2]: make --debug=m
...
Reading makefiles...
Updating makefiles....
Prerequisite 'deps' is newer than target 'deps/foo.dep'.
Must remake target 'deps/foo.dep'.
...
Successfully remade target file 'deps/foo.dep'.
Re-executing[3]: make --debug=m
这段 log 一共执行了3遍 make
命令,翻译过来如下:
-
第一遍执行 make --debug=m
: -
读取所有 makefile,并更新; -
文件 deps/main.dep
不存在,文件deps/main.dep
依赖的deps
目录也不存在; -
创建 deps
目录,生成deps/main.dep
文件; -
文件 deps/foo.dep
不存在; -
生成 deps/foo.dep
文件; -
重新执行 make --debug=m
命令; -
第二遍执行 make --debug=m
: -
文件 deps/main.dep
的依赖deps
目录修改时间比deps/main.dep
目录新; -
生成 deps/main.dep
文件; -
重新执行 make --debug=m
命令; -
第三遍执行 make --debug=m
-
文件 deps/foo.dep
的依赖deps
目录修改时间比deps/foo.dep
目录新; -
生成 deps/foo.dep
文件; -
重新执行 make --debug=m
命令;
这里:
-
执行第二遍时发现之前生成的 deps/main.dep
依赖满足不了,重新生成deps/main.dep
; -
执行第三遍时发现另一个文件 deps/foo.dep
的依赖满足不了,重新生成deps/foo.dep
; -
循环往复,真是冤冤相报何时了……
2.3 无限循环生成deps/*.dep
文件的原因
总结一下,通过上面第二步的调试,我们发现导致无限循环生成 deps/*.dep
的根本原因是:
-
编译时先生成了 deps/main.dep
文件,后生成deps/foo.dep
文件,每次生成deps/*.dep
文件的同时都会更新deps
目录的修改时间。后生成deps/foo.dep
文件的结果就是deps
目录的修改时间比deps/main.dep
文件新; -
而包含指令 -include $(DEPS)
的存在,会要求更新了deps/main.dep
和deps/foo.dep
文件以后重新读取makefile,检查依赖关系并再次执行; -
再次执行时发现 deps
目录的修改时间比deps/main.dep
文件新,此时就会重新生成deps/main.dep
文件,结果就是deps
目录的修改时间比deps/foo.dep
文件新; -
而包含指令 -include $(DEPS)
的存在,更新dep/main.deps
文件以后会重新读取makefile,检查依赖关系并再次执行; -
再次执行又发现 deps
的时间比deps/foo.dep
文件新,又重新生成deps/foo.dep
,结果就是deps
目录的修改时间又一次比deps/main.dep
文件新;
事故就这样发生了……
3. 无限生成依赖文件的解决
无限循环生成.dep
文件的解决办法
既然问题的根源是目录下的文件依赖于目录自身,而依赖关系要求目标文件比依赖的目录新(如果不是更新,那至少也要一样新)。
解决思路有两条:
-
去掉文件对对目录的依赖; -
始终保证目标文件比依赖的目录新(至少一样新);
从上面的两个解决思路出发就产生了两个解决方案。
方案一,去掉文件内对目录的依赖
去掉 %.dep
文件对 $(DIR_DEPS)
目录的依赖,在编译时检查 $(DIR_DEPS)
目录是否存在,如果不存在,则手动创建该目录。
#$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
$(DIR_DEPS)/%.dep: %.c
@echo "Making $@..."
@set -e;
if [ ! -e $(DIR_DEPS) ]; then
$(MKDIR) $(DIR_DEPS);
fi;
$(RM) $(RMFLAGS) $@.tmp;
$(CC) -E -MM $(filter %.c, $^) > $@.tmp;
sed 's,(.*).o[ :]*,objs/1.o: ,g' < $@.tmp > $@;
$(RM) $(RMFLAGS) $@.tmp;
方案二,保证文件比目录新
在每次生成 %.dep
文件后,用 touch
命令更新目录下所有文件的修改时间。
$(DIR_DEPS)/%.dep: $(DIR_DEPS) %.c
@echo "Making $@..."
@set -e;
$(RM) $(RMFLAGS) $@.tmp;
$(CC) -E -MM $(filter %.c, $^) > $@.tmp;
sed 's,(.*).o[ :]*,objs/1.o: ,g' < $@.tmp > $@;
$(RM) $(RMFLAGS) $@.tmp;
touch $(DIR_DEPS)/*;
4. 交替生成文件的问题
交替生成 foo.o
和 main.o
的原因和解决办法
和上面2.3节一样,重点还是检查目标和依赖的时间戳,这里不再详细分析调试过程。
原因如下:
objs
目录下的文件 *.o
依赖于目录自身,而每次更新 foo.o
或main.o
都会更新 objs
目录的修改时间。每更新一次 objs
目录下的文件都会导致 objs
目录自身比下面的一些 *.o
文件新。当再次执行时,就会更新那些比 objs
目录旧的文件。
那为什么没有发生无限循环的问题呢?
因为没有 include
指令去包含生成的 objs/*.o
文件,所以即使每次更新 objs/*.o
文件以后出现依赖满足不了的问题,并不会马上执行 makefile。
因此不会有无限的循环编译,但当你下次在命令行再次执行 make
命令时,就会发现会更新 objs/*.o
文件。
解决办法也参考前面的第 3 节的解决办法。
5. 总结
为了找出问题的根本原因,这里使用了两种手段:
-
使用 stat 命令检查目标和依赖的最后修改时间
-
使用 make --debug=m 跟踪 Make 执行流程
整个分析过程有些啰嗦,重点提出假设,通过逐步调试验证假设,找到问题的根本原因,最终才能提出解决的办法。
你有什么调试 Makefile 的好方法吗?欢迎留言或微信交流。
原文始发于微信公众号(哆啦安全):Makefile调试、检查目标和依赖的时间关系
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论