【问题标题】:How can I clean up after an error in a Makefile?Makefile 出错后如何清理?
【发布时间】:2015-02-19 02:38:52
【问题描述】:

foobar 可能会在失败的情况下创建输出文件,所以我需要在这种情况下删除它。

我可以这样做:

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

但这不会传播foobar 返回的相同退出代码(然后由make 输出)。有什么方法可以在 Makefile 中而不是在 shell 中捕获错误?

【问题讨论】:

    标签: makefile gnu-make


    【解决方案1】:

    如果 DELETE_ON_ERROR 没有解决问题,而您正在寻找的有点像 Java/JUnit 中的 tearDown@Afterfinally,您可以这样做:

    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 SHELLOPTSbash shell 设置pipefailerrexit 标志。
      • 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 / @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 版本是 GNU make 4.2.1,Mac OS X 仍然附带 GNU make 3.81。

    【讨论】:

    • 一句警告:.ONESHELL 可能会使您的食谱表现不同。见the documentation的最后一段。
    • @schieferstapel 事实上,这就是为什么关于SHELLOPTS 的那一行是至关重要的。 .ONESHELL 不是默认值是有原因的,原因是 errexit 不是 shell 的默认值,并且没有标准的设置方法,它取决于 shell。
    • 感谢您的详细说明。但即使使用set -e,食谱的行为也可能有所不同,例如false ; true,其中true 将不再被执行。完全没有反对您的解决方案,但应该知道.ONESHELL 的含义。
    【解决方案2】:

    .DELETE_ON_ERROR: 在这里做你想做的事吗?

    来自Errors in Recipes

    通常当配方行失败时,如果它完全更改了目标文件,则该文件已损坏并且无法使用 - 或者至少没有完全更新。然而文件的时间戳表明它现在是最新的,所以下次 make 运行时,它不会尝试更新该文件。情况与shell被信号杀死时的情况相同;请参阅中断。因此,通常正确的做法是在开始更改文件后如果配方失败,则删除目标文件。如果 .DELETE_ON_ERROR 显示为目标,make 将执行此操作。这几乎总是你想做的,但这不是历史惯例;所以为了兼容性,你必须明确地请求它。

    如果不是,或者如果你只需要它用于那个目标,那么你想要的 shell 行是:

    foobar $^ -o $@ || (ret=$$?; rm -f $@ && exit $$ret)
    

    【讨论】:

    • 我想在这种情况下将它用于所有目标不会有什么坏处。但是为了将来参考,有没有办法在不污染shell命令的情况下做到这一点?很高兴看到make 回应的简单干净的线条,而不是冗长的shell hack。除非有办法抑制 shell 命令的 part 上的回显,我对此表示怀疑。
    • 该选项是全有或全无。我不知道仅针对特定规则有任何方法。这种空壳游戏是我最好的。话虽如此,即使面对“丑陋”的 shell 命令,您也可以使用 automake 风格的 silent rules 来控制您的 make 输出的外观。
    • @matt 您可以将 shell 代码移动到脚本中以使其更漂亮。您也可以在其前面加上 @echo foobar $^ -o $@; ... 之类的前缀,但这可能会使查看 make 输出并想知道文件是如何被删除的人感到困惑。
    • @RossRidge 第二个建议是我的无声规则建议,仅供记录。
    • 我的自动生成式静默规则功能现在有一个 github 项目:github.com/deryni/silent-make-rules
    猜你喜欢
    • 2011-09-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2022-01-04
    • 2010-09-19
    • 1970-01-01
    • 2022-01-18
    • 1970-01-01
    相关资源
    最近更新 更多