Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I clean up after an error in a Makefile?

Tags:

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?

like image 536
Matt Avatar asked Feb 19 '15 02:02

Matt


People also ask

How do I clean my makefile?

The Cleanup Rule clean: rm *.o prog3 This is an optional rule. It allows you to type 'make clean' at the command line to get rid of your object and executable files. Sometimes the compiler will link or compile files incorrectly and the only way to get a fresh start is to remove all the object and executable files.

How do I ignore an error in makefile?

To ignore errors in a recipe line, write a ' - ' at the beginning of the line's text (after the initial tab). The ' - ' is discarded before the line is passed to the shell for execution.

What does $< mean in makefile?

The $@ and $< are called automatic variables. The variable $@ represents the name of the target and $< represents the first prerequisite required to create the output file.

What is Gmake in Linux?

GNU Make is a tool which controls the generation of executables and other non-source files of a program from the program's source files. Make gets its knowledge of how to build your program from a file called the makefile, which lists each of the non-source files and how to compute it from other files.


1 Answers

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:

  1. Use .ONESHELL: to have all shell commands executed in a single shell.
  2. Install a trap for EXIT to get your cleanup done.
  3. 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.

like image 64
Christian Hujer Avatar answered Sep 22 '22 13:09

Christian Hujer