2026-03-18-0903-how-make-works
make 是如何实现监控源文件不变就不编译这样一个策略的?
靠的是文件时间戳 + 依赖图,不是“监控”。
make 并不会常驻盯着文件变化。它每次启动时,读取 Makefile,比较目标文件和依赖文件的修改时间,然后决定要不要执行规则。
最核心的逻辑是:
app: main.o util.o
cc -o app main.o util.o
main.o: main.c
cc -c main.c
util.o: util.c
cc -c util.c
当你执行 make app 时,它会这么判断:
-
先看目标
app是否存在 如果不存在,就构建。 -
如果
app存在,就看它依赖的main.o、util.o只要有任意一个依赖比app更新,app就要重建。 -
对
main.o也是一样 如果main.c比main.o新,就重新编译main.o。
所以本质规则就是:
如果某个依赖文件的 mtime 比目标文件新,则目标过期,需要重建。
可以理解成伪代码:
build(target):
for dep in dependencies(target):
if dep needs rebuild:
build(dep)
if target does not exist:
rebuild target
else if any dep.mtime > target.mtime:
rebuild target
else:
skip
这就是为什么它能做到“源文件没变就不编译”:
因为 .c 没变,.o 的时间戳就仍然比 .c 新,make 判断目标还是最新的,于是跳过。
有几个关键点。
第一,.PHONY 会禁用这套机制。
比如:
.PHONY: clean
clean:
rm -f *.o app
clean 不是一个真实文件,所以每次都会执行。
如果你把正常构建目标也写成 .PHONY,那 make 就没法用时间戳优化了。
第二,依赖必须声明完整。
如果 main.o 实际还依赖 main.h,但你没写:
main.o: main.c main.h
那你改了 main.h,make 不知道,自然不会重编。
所以 C/C++ 项目常用 -MMD -MP 自动生成头文件依赖。
第三,这不是内容比较,是时间比较。
make 通常不看文件内容是否真的变了,只看修改时间。哪怕你把文件内容改回去,只要时间更新了,通常也会触发重建。
第四,时钟和拷贝会影响结果。 比如:
cp重新拷贝文件可能更新时间- Git checkout 可能改变 mtime
- 不同机器时钟不一致会导致误判
所以它是个简单高效的策略,不是绝对精确的内容感知策略。
你问的“监控”如果更准确地说,应该是:
make通过声明依赖关系,在执行时基于文件修改时间做增量构建判断。
不是:
- 文件系统 watcher
- 常驻 daemon
- 内容 hash 检测
如果你愿意,我可以接着给你画一张 make -> 递归检查依赖 -> 比较时间戳 -> 决定是否重编 的流程图。