287

In my GNUmakefile, I would like to have a rule that uses a temporary directory. For example:

out.tar: TMP := $(shell mktemp -d)
        echo hi $(TMP)/hi.txt
        tar -C $(TMP) cf $@ .
        rm -rf $(TMP)

As written, the above rule creates the temporary directory at the time that the rule is parsed. This means that, even I don't make out.tar all the time, many temporary directories get created. I would like to avoid my /tmp being littered with unused temporary directories.

Is there a way to cause the variable to only be defined when the rule is fired, as opposed to whenever it is defined?

My main thought is to dump the mktemp and tar into a shell script but that seems somewhat unsightly.

Emil Sit
  • 22,894
  • 7
  • 53
  • 75

4 Answers4

449

In your example, the TMP variable is set (and the temporary directory created) whenever the rules for out.tar are evaluated. In order to create the directory only when out.tar is actually fired, you need to move the directory creation down into the steps:

out.tar : 
    $(eval TMP := $(shell mktemp -d))
    @echo hi $(TMP)/hi.txt
    tar -C $(TMP) cf $@ .
    rm -rf $(TMP)

The eval function evaluates a string as if it had been typed into the makefile manually. In this case, it sets the TMP variable to the result of the shell function call.

edit (in response to comments):

To create a unique variable, you could do the following:

out.tar : 
    $(eval $@_TMP := $(shell mktemp -d))
    @echo hi $($@_TMP)/hi.txt
    tar -C $($@_TMP) cf $@ .
    rm -rf $($@_TMP)

This would prepend the name of the target (out.tar, in this case) to the variable, producing a variable with the name out.tar_TMP. Hopefully, that is enough to prevent conflicts.

Max Alibaev
  • 681
  • 7
  • 17
e.James
  • 116,942
  • 41
  • 177
  • 214
  • 54
    Be careful with this solution! `$(eval $@_TMP := $(shell mktemp -d))` will happen when the Makefile is first evaluated *not* in order of the rule procedures. In other words, `$(eval ...)` happens sooner than you think. While that might be okay for this example, this method will cause problems for some sequential operations. – JamesThomasMoon Jun 11 '15 at 02:53
  • two (off the subject) details: I believe a `>` sign is missing in the echo line `@echo hi > $(TMP)/hi.txt`, or is there a system where that sign is not needed?. And, my tar command did not accept `cf` options without the leading dash `-cf`, but I have seen versions which do accept this. – boclodoa Jul 24 '15 at 17:42
  • 3
    @JamesThomasMoon1979 do you know how to overcome those limitations, e.g. eval some variables that should be a result of steps executed earlier in the rule? – Vadim Kotov Jun 05 '17 at 13:36
  • 2
    Answering my own question: the workaround for me was creating files during 1st rule execution, and trying to eval them in the first step of 2nd rule. – Vadim Kotov Jun 05 '17 at 13:59
  • Why use `:=` instead of `=` for variable assignment inside a recipe? – CMCDragonkai Jul 03 '18 at 06:01
  • 2
    @JamesThomasMoon1979, if you run `make` in this Makefile, it doesn't seem to execute when the Makefile is first evaluated. Saying it differently, the folder isn't created with `make`, only with `make TEST` as expected. https://gist.github.com/jsign/c7313f359daa99c02bafb222becacad3 – Ignacio Hagopian Jul 29 '19 at 01:06
  • IMO this is not a good solution. Using `eval` inside a recipe is almost always wrong. The simple, and portable, way to manage this is to write this all in a single make recipe line. E.g., `@TMP=$$(mktemp -d) && echo hi > $$TMP/hi.txt && tar -C $$TMP cf $@ . && rm -rf $$TMP` as shown in Brent Bradburn's answer below (although I'm using `&&` instead of `set -e`... I find `set -e` to have some odd corner cases). – MadScientist Oct 08 '21 at 13:00
114

A relatively easy way of doing this is to write the entire sequence as a shell script.

out.tar:
   set -e ;\
   TMP=$$(mktemp -d) ;\
   echo hi $$TMP/hi.txt ;\
   tar -C $$TMP cf $@ . ;\
   rm -rf $$TMP ;\

I have consolidated some related tips here: Multi-line bash commands in makefile

Brent Bradburn
  • 51,587
  • 17
  • 154
  • 173
  • 5
    This is definitely the simplest and therefore best answer (avoiding `@` and `eval` and doing the same job). Note that in your output you see `$TMP` (e.g. `tar -C $TMP ...`) although the value is correcty passed to the command. – Kalle Richter May 08 '15 at 09:21
  • This is how it's normally done; I hope people see this answer because in practice nobody goes through all the effort of the accepted one. – smheidrich Mar 08 '17 at 14:21
  • 1
    What if you are using shell specific commands ? In that case shell commands shall be in same shell language than the one calling make, right ? I had seen some makefile with shebang. – ptitpion Apr 25 '18 at 13:34
  • 1
    @ptitpion: You may also want [`SHELL := /bin/bash`](https://stackoverflow.com/q/589276/86967) in your makefile to enable BASH-specific features. – Brent Bradburn Apr 25 '18 at 16:18
43

Another possibility is to use separate lines to set up Make variables when a rule fires.

For example, here is a makefile with two rules. If a rule fires, it creates a temp dir and sets TMP to the temp dir name.

PHONY = ruleA ruleB display

all: ruleA

ruleA: TMP = $(shell mktemp -d testruleA_XXXX)
ruleA: display

ruleB: TMP = $(shell mktemp -d testruleB_XXXX)
ruleB: display

display:
    echo ${TMP}

Running the code produces the expected result:

$ ls
Makefile
$ make ruleB
echo testruleB_Y4Ow
testruleB_Y4Ow
$ ls
Makefile  testruleB_Y4Ow
user3124434
  • 431
  • 4
  • 3
  • Just as a reminder, GNU make has a syntax for [order-only prerequisites](http://www.gnu.org/software/make/manual/make.html#Prerequisite-Types) which may be necessary to complement this approach. – anol Jun 05 '14 at 19:24
  • 19
    Be careful with this solution! `ruleA: TMP = $(shell mktemp -d testruleA_XXXX)` and `ruleB: TMP = $(shell mktemp -d testruleB_XXXX)` will happen when the Makefile is first evaluated. In other words, `ruleA: TMP = $(shell ...` happens sooner than you think. While this might work for this particular case, this method will cause problems for some sequential operations. – JamesThomasMoon Jun 11 '15 at 02:55
  • 1
    That's not my experience - rule-local variables are expanded only when the particular rule is requested (either explicitly or as dependcy) – Zaar Hai Oct 23 '20 at 09:54
  • 1
    This should still work if you do `ruleA: private TMP = ...`. See [target-specific variables](https://www.gnu.org/software/make/manual/html_node/Target_002dspecific.html). – Victor Sergienko Jan 22 '21 at 00:23
7

I dislike "Don't" answers, but... don't.

make's variables are global and are supposed to be evaluated during makefile's "parsing" stage, not during execution stage.

In this case, as long as the variable local to a single target, follow @nobar's answer and make it a shell variable.

Target-specific variables, too, are considered harmful by other make implementations: kati, Mozilla pymake. Because of them, a target can be built differently depending on if it's built standalone, or as a dependency of a parent target with a target-specific variable. And you won't know which way it was, because you don't know what is already built.

Victor Sergienko
  • 13,115
  • 3
  • 57
  • 91
  • 1
    local variables would be useful even if they are, well, truly local, i.e. they are not visible downwards to the rules that get executed under us. – Attila Lendvai Aug 06 '20 at 11:59
  • 1
    @AttilaLendvai at the time of writing the answer, I didn't know [you can make target-specific variable private](https://www.gnu.org/software/make/manual/html_node/Suppressing-Inheritance.html). This surely makes them more safe, but I would still discourage their usage. – Victor Sergienko Jan 22 '21 at 00:28