How can I clean up after an error in a Makefile?
gnu-make, makefile
Solution
In case `DELETE_ON_ERROR` doesn't cut it, and what you're looking is a bit like a `tearDown`, `@After`, or `finally` in Java/JUnit, this is what you can do:
- Use `.ONESHELL:` to have all shell commands executed in a single shell.
- Install a `trap` for `EXIT` to get your cleanup done.
- Make sure that the shell exits in case of any errors by setting `errexit`. While we're at it, we can also set `pipefail` right away.
For example, let's say you want to start a docker container, do a test, stop the docker contaner no matter what, but get the result of the test. This is how to do it:
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…
…
How it works
- The `export SHELL` export tells GNU make to use `bash` as shell, which has heavier footprint than the default `sh` but way more features.
- The `export SHELLOPTS` sets the `pipefail` and `errexit` flags for the `bash` shell.
- `pipefail` ensures that the exit status of a pipe will not be the last command but the last with a non-zero exit status. So, `false | true` would return `1` instead of `0`.
- `errexit` ensures that the exit status of a command sequence will not be the last command but the last with a non-zero exit status, and that subsequent commands will not be executed. So, `false ; true` would return `1` instead of `0` and `true` would not be executed.
- The `.ONESHELL:` tells GNU make to run all the commands in a single shell. That means, your recipe really is one shellscript now. (Requires GNU make 3.82 or later.)
- The `function tearDown { docker stop test-image }` defines a shell function named `tearDown`. In this example, it will stop the docker container.
- The `trap tearDown EXIT` is the most crucial part of everything in this example. It tells the shell that was invoked for the recipe to run the `tearDown` function on exit, that is, no matter whether the commands were successful or failed.
Limitations
This is analogous to `finally` in Java. Reuse across multiple targets / tests is not possible. It is definitely not like `@AfterClass` / `@AfterAll` or `tearDown()` / `@After` / `@AfterEach` in JUnit.
Getting it even better
But you could do that, in case you need it. Say, you want to run multiple tests on the same docker container, and tear it down no matter what. That would be analogous to `@AfterClass` / `@AfterAll` in JUnit. Then that could look like this:
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:
…
This would now run all tests, even if the first one failed, clean up after all of them have finished, and report an error if any of the tests failed.
Disclaimer: This requires the `.ONESHELL` feature of GNU make, which was introduced in GNU make 3.82. The current version of GNU make as of this edit is GNU make 4.2.1, and Mac OS X still ships with GNU make 3.81.
Problem
`foobar` may create the output file even when it fails, so I need to delete it in that case. I can do this: ``` foo: bar baz foobar $^ -o $@ || (rm -f $@ && exit 1) ``` But this does not propagate the same exit code returned by `foobar` (which is then outputted by `make`). Is there any way to catch the error in the Makefile rather than in the shell?