侧边栏壁纸
博主头像
一笑痕

人生若只如初见,
是可喜亦或者是可悲?

  • 累计撰写 229 篇文章
  • 累计收到 7 条评论

Makefile 里已有 build 文件,为什么配方没跑

2026-10-22 / 0 评论 / 5 阅读

Makefile 里已有 build 文件,为什么配方没跑

在 Makefile 里写下 build:,本意通常是执行一串构建命令。可如果当前目录里刚好有个叫 build 的文件,make build 可能只回一句「up to date」,命令一行也没执行。看上去像 Make 忘了配方,实际是它按文件目标的规则判断:目标已经存在,且没有比依赖旧,就没有理由重做。

我觉得这个坑比拼错目标名更难发现。终端没有报错,退出状态还是成功;构建文件却可能是上一次留下的旧产物。特别是目标名叫 clean、test 或 deploy,它们本来表达动作,一旦同名路径出现,文件时间戳就会参与判断。

Make 为什么会看磁盘上的文件?

Make 最初处理的是文件之间的生成关系。规则里的目标通常对应一个文件,依赖更新后,目标文件需要重新生成。比如 app: main.c 表示 app 依赖 main.c;当源文件比目标新,配方就有执行的理由。目标文件不存在时也一样,Make 会尝试生成它。

这套机制对编译产物很合适。麻烦出在「伪目标」上:有些目标只代表一项动作,本身并不打算生成同名文件。clean 是最常见的例子,配方只删除产物;test 则运行测试。若目录里碰巧出现 clean 或 test 这个路径,Make 不会凭名字猜它是动作,仍把它当普通文件目标处理。

要标出这种差别,Makefile 使用 .PHONY:

.PHONY: build
build:
    @./scripts/build.sh

.PHONY 声明的是 build 这个目标不代表真实文件。这样即使同名文件已经存在,Make 仍会执行配方。它不是「强制重跑所有东西」的开关,只改变列出的目标如何参与文件时间戳判断。

一个同名文件能拦住配方吗?

本机在临时目录里造了一个内容为 stale artifact 的 build 文件。普通规则没有依赖项,配方把每次运行次数追加到日志。第一次执行 make build,Make 报告目标已是最新,运行次数仍为 0。然后只给规则加上 .PHONY,重复两次调用,运行次数才从 0 变成 1,再变成 2。

make_version=GNU Make 3.81
$ make -f Makefile build
make: `build' is up to date.
recipe_runs=0
$ make -f Makefile.phony build
recipe_runs=1
$ make -f Makefile.phony build
recipe_runs=2

这个结果没有用 touch 去制造先后时间差,也不依赖系统时钟精度。文件存在就足以让没有依赖项的普通目标被认为已经满足;.PHONY 让这条判断不再适用。实验用的是机器自带的 GNU Make 3.81,结论限于这次本地复现,不代表测过所有 Make 实现和版本。

脚本 tools/verify-make-phony-target.py 在系统临时目录建文件和 Makefile,运行完自动清理;日志 tools/verify-make-phony-target.log 留下本次输出。脚本没有碰当前仓库内容,也没有访问网络。换一台机器复跑时,Make 版本和提示文本可能不同,但两种规则的执行次数应当能直接观察。

哪些目标该写进 .PHONY?

该不该声明,先看它是否需要生成一个真实产物。目标名像不像动词并不重要:all、clean、test 这类纯动作通常适合声明为伪目标;app、bundle.tar 这类由配方生成的文件则应该保留普通文件目标。把真实产物也标成 .PHONY,会让 Make 每次都执行相关配方,增量构建便失去意义。

伪目标声明最好靠近相应规则,读到目标时不用往文件上方翻很远。也别把 .PHONY 写成无范围的装饰:它只对列出的目标生效,漏写一个就仍按普通目标判断。项目规模变大时,可以先检查哪些规则的配方是动作,哪些规则确实要产出同名文件,再逐项补声明。

还有一个边界值得分清。.PHONY 解决的是目标和文件重名造成的跳过,不会替构建系统检查脚本是否正确,也不会保证输出文件内容最新。生成 app 的规则仍要把源文件列成依赖;如果依赖关系漏了,Make 照样可能保留过期产物。伪目标和依赖图各管一件事。

目录里出现意外的 test 文件时,最直接的诊断是看 make -n test 有没有把配方列出来,再检查规则是否声明了 .PHONY。不过 -n 只展示计划,不等于命令真的执行过。需要确认实际动作时,还是要在临时目录里做一个同名文件跑一遍。对纯动作目标,声明 .PHONY;对真实文件目标,保留依赖关系。两者别混着写。

    🤞 分享