Makefile 构建防坑:用 .DELETE_ON_ERROR 清理失败目标

举报
yd_240403232 发表于 2026/11/02 08:31:18 2026/11/02
【摘要】 构建命令失败或中途退出时,直接写目标文件会留下时间戳更新的半成品,让 make 误判已最新。本文用临时文件加 mv 原子替换,并启用 .DELETE_ON_ERROR,使失败目标被自动删除并触发重建。

问题:失败构建留下半成品

Makefile 常把生成文件的命令直接写到目标上,例如编译器或代码生成器直接覆盖 build/app.txt。如果命令在中途失败,目标文件可能已经被创建或截断,且修改时间比依赖更新。下一次执行 make 时,make 只比较时间戳,可能认为目标已经是最新,从而跳过重建,留下一个内容不完整的文件。

仅靠删除目标并不能覆盖所有失败路径:命令可能在写了一半时退出,也可能在替换目标后才发现内容校验失败。更稳妥的做法是两层配合:生成时先写同目录临时文件,成功后再用 mv 原子替换;同时在 Makefile 中声明 .DELETE_ON_ERROR,让 make 在 recipe 失败且目标已被修改时删除目标。

最小示例

目录中先准备两个输入文件,其中一个包含 FAIL,用于触发失败。下面的 Makefile 可直接保存为 Makefile:

.DELETE_ON_ERROR:
.PHONY: all bad clean

all: build/ok.txt
bad: build/bad.txt

build:
	mkdir -p $@

build/ok.txt: src/ok.in | build
	@tmp=$@.tmp.$$$$; trap 'rm -f $tmp' EXIT; sed 's/^/generated: /' $< > $tmp; mv $tmp $@

build/bad.txt: src/bad.in | build
	@tmp=$@.tmp.$$$$; trap 'rm -f $tmp' EXIT; cp $< $tmp; mv $tmp $@; if grep -q FAIL $@; then echo post-check failed >&2; exit 1; fi

clean:
	rm -rf build

这里每个 recipe 行都是一条完整的 shell 命令。tmp=$@.tmp.$$$$ 经过 make 展开后,shell 看到的是 tmp=目标.tmp.$$,其中 $$ 是当前 shell 的进程号,用来降低并行构建时临时文件重名的概率。trap 在 shell 退出时清理临时文件;mv 成功后再做一次内容检查,检查失败就返回非零。

运行方法

在 Makefile 所在目录执行:

mkdir -p src
printf 'hello\n' > src/ok.in
printf 'FAIL\n' > src/bad.in
make all
make bad
ls build/bad.txt
make bad

预期行为是:make all 生成 build/ok.txt;make bad 在 mv 之后发现 FAIL 并失败,.DELETE_ON_ERROR 删除已经替换上去的 build/bad.txt;因此 ls build/bad.txt 应提示文件不存在;再次执行 make bad 时,make 不会认为目标是最新,而是重新执行生成命令。这样就把失败状态留给了下一次 make。

两层保护的分工

临时文件加 mv 解决的是“命令中途失败”的问题:目标要么保持旧版本,要么完全不被替换,不会出现只写了一半的目标。mv 在同一文件系统内是原子替换,所以临时文件应放在目标文件所在目录,例如 $@.tmp.$$,而不是放到 /tmp 后再跨文件系统移动。

.DELETE_ON_ERROR 解决的是“目标已经被替换,但 recipe 后续步骤失败”的问题。比如示例中的内容检查在 mv 之后执行,此时目标已经更新;make 发现 recipe 返回非零,并且目标已被修改,就会删除该目标。下一次构建不会因为时间戳而误判。

边界与限制

  • .DELETE_ON_ERROR 只在 recipe 失败且目标已发生变化时删除目标;如果命令在创建目标之前就失败,通常没有目标可删,旧目标仍由依赖时间戳决定是否重建。
  • .PRECIOUS 可以阻止 make 删除指定目标;如果目标被声明为 precious,本示例的自动删除不会生效。
  • 该机制不能替代正确的依赖声明。如果输入内容变化但时间戳没有变化,make 仍可能不重建;应通过完整依赖或显式 phony 目标处理。
  • trap 不能覆盖 SIGKILL、断电等场景,临时文件可能残留;可以在 clean 中按明确模式清理,避免误删其他文件。
  • 示例假设路径不含空格。实际使用时建议给 shell 变量加引号,并注意 $@、$< 的展开位置。
  • mv 的原子性依赖源和目标在同一文件系统;跨文件系统时可能退化为复制再删除,不能保证原子。
【版权声明】本文为华为云社区用户原创内容,未经允许不得转载,如需转载请自行联系原作者进行授权。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。