如何在Makefile中清理错误后的文件?

23
< p >< code > foobar < /code >即使失败,也可能创建输出文件,因此我需要在这种情况下将其删除。

我可以这样做:

foo: bar baz
        foobar $^ -o $@ || (rm -f $@ && exit 1)

但是这不会传播与foobar返回的相同退出代码(然后由make输出)。有没有办法在Makefile中捕获错误,而不是在Shell中捕获?

2个回答

29

如果 DELETE_ON_ERROR 无法满足要求,而您正在寻找类似于Java/JUnit中的tearDownfinally的东西,那么您可以这样做:

  1. 使用.ONESHELL:以在单个Shell中执行所有Shell命令。
  2. EXIT安装trap以完成清理工作。
  3. 通过设置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 的 pipefailerrexit 标志。
    • pipefail 确保管道的退出状态码不是最后一个命令的退出状态码,而是最后一个具有非零退出状态码的命令。因此,false | true 将返回 1 而不是 0
    • errexit 确保命令序列的退出状态码不是最后一个命令的退出状态码,而是最后一个具有非零退出状态码的命令,并且不会执行后续命令。因此,false ; true 将返回 1 而不是 0,并且将不执行 true 命令。
  • .ONESHELL: 告诉 GNU make 在单个 shell 中运行所有命令。这意味着,您的配方现在真正是一个 shell 脚本。(需要 GNU make 3.82 或更高版本。)
  • function tearDown { docker stop test-image } 定义一个名为 tearDown 的 shell 函数。在此示例中,它将停止 docker 容器。
  • trap tearDown EXIT 是此示例中的最关键部分。它告诉为配方调用的 shell 在退出时运行 tearDown 函数,也就是说,无论命令成功还是失败。

限制

这类似于 Java 中的 finally。无法在多个目标/测试之间重复使用。它绝对不像 JUnit 中的 @AfterClass / @AfterAlltearDown() / @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的当前版本为4.2.1,而Mac OS X仍带有GNU make 3.81。


警告:.ONESHELL 可能会使您的配方行为不同。请参阅文档的最后一段。 - schieferstapel
@schieferstapel 确实,这就是关于 SHELLOPTS 的那一行至关重要的原因。.ONESHELL 不是默认设置的原因是,对于 shell 而言,errexit 不是默认设置,并且没有标准的方法来设置它,这取决于 shell。 - Christian Hujer
感谢您的详细说明。但是,即使使用 set -e,配方的行为也可能不同,例如 false; true,其中 true 将不再被执行。绝对不反对您的解决方案,但应该意识到 .ONESHELL 的影响。 - schieferstapel

9
.DELETE_ON_ERROR在这里是否能满足你的要求?
食谱中的错误中得知:
通常情况下,当一个食谱行失败时,如果它已经改变了目标文件,则该文件会被破坏并且不能使用,或者至少它没有完全更新。然而文件的时间戳表明它现在是最新的,因此下一次运行make时,它不会尝试更新该文件。这种情况就像shell被信号杀死时一样;请参见中断。因此,通常正确的做法是,在食谱开始更改文件后,如果食谱失败,则删除目标文件。如果将.DELETE_ON_ERROR显示为目标,则make会执行此操作。这几乎总是您希望make这样做的,但这不是历史惯例;因此,出于兼容性考虑,您必须明确请求它。
如果不是这样,或者如果您只需要它用于那个目标,那么您所需的shell行是:
foobar $^ -o $@ || (ret=$$?; rm -f $@ && exit $$ret)

我猜在这种情况下对所有目标使用它不会有影响。但是为了将来的参考,有没有一种方法可以在不污染 shell 命令的情况下完成它?看到 make 打印出简单清晰的行而不是冗长的 shell 代码真是太好了。除非有一种方法可以抑制 部分 shell 命令的回显,但我表示怀疑。 - Matt
1
那个选项是全有或全无的。我不知道有什么方法可以只针对特定规则进行操作。这种壳游戏是我能想到的最好的方法。话虽如此,您可以尝试使用 automake 风格的静默规则来控制您的 make 输出的外观,即使面对“丑陋”的 shell 命令也可以。 - Etan Reisner
@matt 你可以将 shell 代码移动到脚本中,使其更加漂亮。你也可以在前面加上类似 @echo foobar $^ -o $@; ... 的东西,但是这可能会让查看 make 输出并想知道文件是如何被删除的人感到困惑。 - Ross Ridge
我的自动化静默规则功能现在有一个 Github 项目:https://github.com/deryni/silent-make-rules。 - Etan Reisner
“.DELETE_ON_ERROR” 应该是默认设置。我认为这样做几乎总是需要的。我只能想到一种情况,即当您需要查看它以调试损坏的构建时,才需要保留损坏的输出文件。 - bobbogo
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接