【发布时间】:2015-02-19 02:38:52
【问题描述】:
foobar 可能会在失败的情况下创建输出文件,所以我需要在这种情况下删除它。
我可以这样做:
foo: bar baz
foobar $^ -o $@ || (rm -f $@ && exit 1)
但这不会传播foobar 返回的相同退出代码(然后由make 输出)。有什么方法可以在 Makefile 中而不是在 shell 中捕获错误?
【问题讨论】:
foobar 可能会在失败的情况下创建输出文件,所以我需要在这种情况下删除它。
我可以这样做:
foo: bar baz
foobar $^ -o $@ || (rm -f $@ && exit 1)
但这不会传播foobar 返回的相同退出代码(然后由make 输出)。有什么方法可以在 Makefile 中而不是在 shell 中捕获错误?
【问题讨论】:
如果 DELETE_ON_ERROR 没有解决问题,而您正在寻找的有点像 Java/JUnit 中的 tearDown、@After 或 finally,您可以这样做:
.ONESHELL: 让所有shell 命令在一个shell 中执行。EXIT 安装trap 以完成清理工作。errexit 确保shell 在出现任何错误时退出。在此过程中,我们还可以立即设置pipefail。例如,假设你要启动一个 docker 容器,做一个测试,无论如何都要停止 docker 容器,但是得到测试的结果。这是怎么做的:
export SHELL:=/bin/bash
export SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit
.ONESHELL:
.PHONY: test
test:
function tearDown {
docker stop test-image
}
trap tearDown EXIT
docker run --name test-image …
testStep1…
testStep2…
testStep3…
…
export SHELL 导出告诉 GNU make 使用 bash 作为 shell,它比默认的 sh 占用空间更大,但功能更多。export SHELLOPTS 为bash shell 设置pipefail 和errexit 标志。
pipefail 确保管道的退出状态不是最后一个命令,而是最后一个具有非零退出状态的命令。因此,false | true 将返回 1 而不是 0。errexit 确保命令序列的退出状态不会是最后一个命令,而是具有非零退出状态的最后一个命令,并且不会执行后续命令。因此,false ; true 将返回 1 而不是 0 并且 true 不会被执行。.ONESHELL: 告诉 GNU make 在单个 shell 中运行所有命令。这意味着,你的食谱现在真的是一个 shellscript。 (需要 GNU make 3.82 或更高版本。)function tearDown { docker stop test-image } 定义了一个名为 tearDown 的 shell 函数。在本例中,它将停止 docker 容器。trap tearDown EXIT 是本例中最关键的部分。它告诉为配方调用的 shell 在退出时运行 tearDown 函数,也就是说,无论命令是成功还是失败。这类似于 Java 中的finally。跨多个目标/测试重用是不可能的。它绝对不像 JUnit 中的 @AfterClass / @AfterAll 或 tearDown() / @After / @AfterEach。
但你可以这样做,以备不时之需。比如说,你想在同一个 docker 容器上运行多个测试,然后无论如何都要拆除它。这类似于 JUnit 中的 @AfterClass / @AfterAll。那么它可能看起来像这样:
export SHELL:=/bin/bash
export SHELLOPTS:=$(if $(SHELLOPTS),$(SHELLOPTS):)pipefail:errexit
.ONESHELL:
.PHONY: start
start:
docker run --name test-image …
.PHONY: stop
stop:
docker stop test-image
.PHONY: test
test: start
function tearDown {
$(MAKE) stop
}
trap tearDown EXIT
$(MAKE) -k testImpl
.PHONY: testImpl
testImpl: testCase1 testCase2 testCase3
.PHONY: testCase1
testCase1:
…
.PHONY: testCase2
testCase2:
…
.PHONY: testCase3
testCase3:
…
这将运行所有测试,即使第一个测试失败,在所有测试完成后进行清理,并在任何测试失败时报告错误。
免责声明:这需要 GNU make 的.ONESHELL 功能,该功能在 GNU make 3.82 中引入。截至本次编辑的当前 GNU make 版本是 GNU make 4.2.1,Mac OS X 仍然附带 GNU make 3.81。
【讨论】:
SHELLOPTS 的那一行是至关重要的。 .ONESHELL 不是默认值是有原因的,原因是 errexit 不是 shell 的默认值,并且没有标准的设置方法,它取决于 shell。
set -e,食谱的行为也可能有所不同,例如false ; true,其中true 将不再被执行。完全没有反对您的解决方案,但应该知道.ONESHELL 的含义。
.DELETE_ON_ERROR: 在这里做你想做的事吗?
通常当配方行失败时,如果它完全更改了目标文件,则该文件已损坏并且无法使用 - 或者至少没有完全更新。然而文件的时间戳表明它现在是最新的,所以下次 make 运行时,它不会尝试更新该文件。情况与shell被信号杀死时的情况相同;请参阅中断。因此,通常正确的做法是在开始更改文件后如果配方失败,则删除目标文件。如果 .DELETE_ON_ERROR 显示为目标,make 将执行此操作。这几乎总是你想做的,但这不是历史惯例;所以为了兼容性,你必须明确地请求它。
如果不是,或者如果你只需要它用于那个目标,那么你想要的 shell 行是:
foobar $^ -o $@ || (ret=$$?; rm -f $@ && exit $$ret)
【讨论】:
make 回应的简单干净的线条,而不是冗长的shell hack。除非有办法抑制 shell 命令的 part 上的回显,我对此表示怀疑。
@echo foobar $^ -o $@; ... 之类的前缀,但这可能会使查看 make 输出并想知道文件是如何被删除的人感到困惑。