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.