Makefileの変数展開はレシピの実行前に行われる

makeなんてよく使うものだから分かっているつもりだったけど実はよく分かっていなかったのが、変数展開がどのタイミングで行われるかということ。

itchyny.hatenablog.com

Makefileでの := は simply expanded variable といって一度しか展開されないが、 = は参照するたびに展開される。

DATE = $(shell date)

.PHONY: all
all:
   @echo $(DATE)
   @$(shell sleep 3)
   @echo $(DATE)
   @$(shell sleep 3)
   @echo $(DATE)

これは、 $(DATE) を参照するたびに展開されるから、値はどんどん変わっていく。

Thu Apr 4 20:13:21 JST 2019
Thu Apr 4 20:13:24 JST 2019
Thu Apr 4 20:13:27 JST 2019

ここまでは知っていたのだけど、どのタイミングで展開されるのかというのを知らなくて、次のようなケースで悩んでしまった。

FILE := /tmp/example.txt
$(shell echo 'Old contents' > $(FILE))
CONTENTS = $(shell cat $(FILE))

.PHONY: all
all:
  echo $(CONTENTS)
  echo 'New contents' > $(FILE)
  echo $(CONTENTS)

ファイルに書き込んだ後に $(CONTENTS) を参照しているから、最後の行は New contents と表示する… わけではない。

echo Old contents
Old contents
echo 'New contents' > /tmp/example.txt
echo Old contents
Old contents

こうなる理由だが、Makefile$(...) の展開はレシピを実行する前に行われるからだ。上の例だと、レシピ内のすべての $(CONTENTS)$(FILE) が展開されてから、3つのコマンド実行が行われる。

次の例を考えてみよう。

FILE := /tmp/sample.txt

.PHONY: all
all:
   @echo $(shell date)
   @$(shell sleep 3)
   @date > $(FILE)
   @$(shell sleep 3)
   @echo $(shell date)
   @$(shell sleep 3)
   @cat $(FILE)

レシピ3行目の date > $(FILE) (正確にはすでに展開されているので date > /tmp/sample.txt) が実行されるのは、全ての sleep (2, 4, 6行目) が行われた後なので、ファイルに書かれた日付が最も (5行目の echo $(shell date) よりも) 新しくなる。

では、ファイル書き込みがあってその最新の内容を取りたいときはどうすればいいか。レシピが実行される前に展開されても、まだ cat は実行されない状態にするには次のようにすれば良い。

FILE := /tmp/example.txt
$(shell echo 'Old contents' > $(FILE))
CONTENTS = $$(cat $(FILE)) # instead of $(shell cat $(FILE))

.PHONY: all
all:
  echo $(CONTENTS)
  echo 'New contents' > $(FILE)
  echo $(CONTENTS)

そう、シェルの展開に変えてしまうのだ。 (もうこうなると := を使っても同じになる)

echo $(cat /tmp/example.txt)
Old contents
echo 'New contents' > /tmp/example.txt
echo $(cat /tmp/example.txt)
New contents

多くの場合は $(shell ...) が使えるのでこれを使っていたが、これが使えないケースがあることに気がついて少しワクワクした。

stackoverflow.com めっちゃ混乱してstackoverflowに投げたら解決した。

GNU Make 第3版

GNU Make 第3版