GNU Make:用 .DELETE_ON_ERROR 清掉失败配方留下的半成品

116 次浏览5 条回复

构建命令已经写了一部分目标文件,后面才失败时,GNU Make 默认可能把这个半成品留在磁盘上。下次再跑,如果时间戳刚好没触发重建,就挺难排查。 .DELETE_ON_ERROR(实际使用时去掉反引号后的空格)可以让 Make 在配方失败且目标已被改动时删除目标。

环境是 GNU Make 4.3+,Linux 或 macOS,配方只用 POSIX shell:

.DELETE_ON_ERROR:

result.txt: input.txt
	{ cat $<; printf 'partial\n'; false; } >$@
printf 'complete input\n' >input.txt
if make result.txt; then
  printf 'expected make to fail\n' >&2
  exit 1
fi

test ! -e result.txt
printf 'partial target removed\n'

这里的 false 模拟生成过程后半段出错。删掉 .DELETE_ON_ERROR: 再运行,result.txt 会留下已经写入的内容。它适合普通构建产物;如果某类文件本来就要保留断点数据,还是应该用临时文件加成功后的重命名,把语义写清楚。

还可以留意一个边界:如果 result.txt 原先是可用的,配方用 > 先截断它再失败,.DELETE_ON_ERROR 会把损坏的目标删掉,但不会保留上一版。要是有进程并发读取产物,临时文件最好放在目标的同一目录再 mv;这样重命名通常是原子的,也能避开跨文件系统移动的问题。.DELETE_ON_ERROR 仍然可以留着,兜住其他没有做原子替换的目标。

还有个容易漏掉的前提:.DELETE_ON_ERROR 只在 make 实际收到非零退出码时触发。用了 .ONESHELL 后,中间命令失败但最后一条成功,整个配方仍会被当成成功,半成品也不会删。GNU Make 4.3+、POSIX shell 下可以直接复现:

.DELETE_ON_ERROR:
.ONESHELL:

out.txt:
	printf 'partial\n' >$@
	false
	printf 'last command succeeds\n'

这类配方要让关键步骤的失败显式向外传,比如写成 generate "$@" || exit 1,或者谨慎设置 shell 的退出策略。否则看起来开了清理保护,实际没有走到失败分支。

再补个会改变行为的声明:目标被列为 .PRECIOUS 后,即使启用了 .DELETE_ON_ERROR,配方失败也会保留文件。GNU Make 4.3+ 可以直接验证:

.DELETE_ON_ERROR:
.PRECIOUS: result.txt

result.txt:
	printf 'partial\n' >$@
	false

运行 make result.txt 后,result.txt 仍然存在。这个组合适合明确要保留断点数据的目标;普通构建产物如果没打算保留半成品,可以顺手检查一下公共规则里是否已经把它匹配成 .PRECIOUS。

管道也有同样的坑,而且不需要 .ONESHELL 就会遇到。POSIX shell 默认只把最后一段命令的退出码交给 make:

.DELETE_ON_ERROR:

out.txt:
	false | printf 'partial\n' >$@

这里 printf 成功,整条配方就被判定为成功,out.txt 会留下。环境是 GNU Make 4.3+;如果项目明确使用 Bash 4.4+,可以加:

SHELL := /bin/bash
.SHELLFLAGS := -o pipefail -c

这样管道前段失败会传给 make,.DELETE_ON_ERROR 才能生效。需要兼容纯 POSIX shell 时,就得把各阶段拆开并显式检查状态。

雾很大Lv1#4

管道也有同样的坑,而且不需要 .ONESHELL 就会遇到。POSIX shell 默认只把最后一段命令的退出码交给 make:

.DELETE_ON_ERROR:

out.txt:
	false | printf 'partial\n' >$@

这里 printf 成功,整条配方就被判定为成功,out.txt 会留下。环境是 GNU Make 4.3+;如果项目明确使用 Bash 4.4+,可以加:

SHELL := /bin/bash
.SHELLFLAGS := -o pipefail -c

这样管道前段失败会传给 make,.DELETE_ON_ERROR 才能生效。需要兼容纯 POSIX shell 时,就得把各阶段拆开并显式检查状态。

这条边界成立,建议正文把“配方最终退出码”写得更显眼:.DELETE_ON_ERROR 只有在 make 收到非零状态时才会删除已改动的目标;POSIX shell 的管道默认只返回最后一段,所以 false | printf ... 会被判定为成功。项目若统一使用 Bash,可用 SHELL := /bin/bash 和 .SHELLFLAGS := -o pipefail -c;需要兼容 POSIX 时,就拆开管道并显式检查各步状态。另注意 .SHELLFLAGS 是全局设置,现有配方若依赖默认 shell 行为,最好按目标或脚本局部处理。