code_fodder
code_fodder

Reputation: 16361

makefile: Reassign variable in each recipie to store the step name

I have a makefile that runs a rule when it fails. I have written out a small example below:

temp = "start"

run:
    make run_all || make run_failed

run_all: step1
run_all: step2
run_all: step3
run_all: ;

step1:
    echo temp = "step1"

step2:
    echo temp = "step2"
    $(error fail test)

step3:
    echo temp = "step3"

run_failed:
    @echo "failed on step ${temp}"

I force this little example to fail in step 2 with the $(error ...) command. The problem I have is that I want to set my variable temp at each step. But I don't know how to do that.

If it remove the "echo" before temp = in each step I just get a shell error (can't find temp).

How can I set my variable so that I can print out the step that the makefile failed on?

Upvotes: 2

Views: 1118

Answers (1)

Renaud Pacalet
Renaud Pacalet

Reputation: 29240

Note: you shouldn't use make in your recipes. Use $(MAKE) instead. There are several very good reasons for this that you will find in the GNU make manual.

Make variables and shell variables are different. When you write:

temp = "start"

it is make syntax and temp is a make variable. When you write:

step1:
    temp = "step1"

temp is a shell variable and you get a syntax error because shell variables assignment is temp="step1" (no spaces around =).

But this is not the whole story. Each line of a recipe is run by a separate shell (unless the special target .ONESHELL appears in the makefile) and different recipes can't be run by the same shell instance (regardless of .ONESHELL). So, you cannot expect the recipe of your run_failed rule to get access to the same shell variable temp as the one that has been assigned by one line of your step2 recipe.

Anyway, when make fails on a recipe it tells you what went wrong, including what rule failed. If it is not enough the best option would be to equip each recipe with an error message:

$ cat fail
#!/usr/bin/env bash
exit 1

$ cat Makefile
run: step1 step2 step3

step1 step3:
    @echo "doing $@" || echo "$@ failed"

step2:
    @./fail || echo "$@ failed"

$ make step1
doing step1

$ make run
doing step1
step2 failed
doing step3

If, for any reason, this is not possible, you will have to:

  1. pass failure information from one recipe (step2) to another (run),
  2. pass failure information from one sub-make invocation (run-all) to another (run_failed).

All this will be extremely difficult, unless you use a log file and tell make not to run in parallel mode:

$ cat Makefile
.NOTPARALLEL:

FAILED  := failed.txt

run:
    @{ rm -f $(FAILED) && $(MAKE) run_all; } || $(MAKE) run_failed

run_all: step1 step2 step3

step1 step3:
    @echo "doing $@" || { echo "$@ failed" >> $(FAILED) && exit 1; }

step2:
    @./fail || { echo "$@ failed" >> $(FAILED) && exit 1; }

run_failed:
    @cat $(FAILED)

$ make --no-print-directory
doing step1
Makefile:14: recipe for target 'step2' failed
make[1]: *** [step2] Error 1
step2 failed

Upvotes: 2

Related Questions