jozxyqk
jozxyqk

Reputation: 17294

Best way to handle pipes and their exit status in a makefile

If a command fails in make, such as gcc, it exits...

gcc
gcc: fatal error: no input files
compilation terminated.
make: *** [main.o] Error 4

However, if I have a pipe the exit status of the last command in the pipe is taken. As an example, gcc | cat, will not fail because cat succeeds.

I'm aware the exit codes for the whole pipe are stored in the PIPESTATUS array and I could get the error code 4 with ${PIPESTATUS[0]}. How should I structure my makefile to handle a piped command and exit on failure as normal?


As in the comments, another example is gcc | grep something. Here, I assume the most desired behavior is still for gcc and only gcc to cause failure and not grep if it doesn't find anything.

Upvotes: 17

Views: 7268

Answers (4)

Sylvain Leroux
Sylvain Leroux

Reputation: 52030

I would go for pipefail. But if you really don't want (or if you want to only fail on the first process -- not in case of failure from the rest of the pipe):

SHELL=bash

all:
        gcc | cat ; exit "$${PIPESTATUS[0]}"

Upvotes: 11

DrBeco
DrBeco

Reputation: 11785

Just add in the begin of your makefile the command:

SHELL=/bin/bash -o pipefail

Now you can, for example, generate the errors.err file from objects (1st rule) without being worried it would be overwritten by the executable (2nd rule).

%.o : %.c
    gcc $(CFLAGS) $(CPPFLAGS) $^ -o $@ 2>&1 | tee errors.err

%.x : %.o $(OBJECTS)
    gcc $(LDLIBS) $^ -o $@ 2>&1 | tee errors.err

Without it, make get no errors from rule 1, and run rule 2, overwriting it. You will end up with only a single line in errors.err stating that there are no object file to run gcc

gcc: error: program.o: No such file or directory

Upvotes: 1

tripleee
tripleee

Reputation: 189597

A reasonable and portable approach is to refactor your build jobs to use files instead of pipes. For example:

foo:
    gcc >[email protected]
    grep success [email protected]
    cat [email protected]
    rm [email protected]

Removing the log file after printing it is obviously not necessary; this is just a general template. The beef is the redirection to replace the pipeline. You could even refactor it to multiple recipes:

foo: foo.tmp foo.log
    grep success [email protected]
    mv $< $@
%.tmp %.log:
    gcc -o $*.tmp >$*.log

Properly cleaning up the temporary artefacts and generally managing them is an obvious drawback of this approach.

Upvotes: 1

Etan Reisner
Etan Reisner

Reputation: 80969

You should be able to tell make to use bash instead of sh and get bash to have set -o pipefail set so it exits with the first failure in the pipeline.

In GNU Make 3.81 (and presumably earlier though I don't know for sure) you should be able to do this with SHELL = /bin/bash -o pipefail.

In GNU Make 3.82 (and newer) you should be able to do this with SHELL = /bin/bash and .SHELLFLAGS = -o pipefail -c (though I don't know if adding -c to the end like that is necessary or if make will add that for you even when you specify .SHELLFLAGS.

From the bash man page:

The return status of a pipeline is the exit status of the last command, unless the pipefail option is enabled. If pipefail is enabled, the pipeline's return status is the value of the last (rightmost) command to exit with a non-zero status, or zero if all commands exit successfully. If the reserved word ! precedes a pipeline, the exit status of that pipeline is the logical negation of the exit status as described above. The shell waits for all commands in the pipeline to terminate before returning a value.

Upvotes: 18

Related Questions