makeの:=と=を使い分けると便利

正直な話をすると今まで常に := だけを使っていて、= を使うことはあまりなかった。 使い分けると便利な状況が分かってきたのでここにメモしておく。

要約

ちょっとした実験用のタスクランナーとして使っていたMakefileの書き方が変わる1

今まではこう書いていた。 ( $(shell ...) はシェルで実行した結果(標準出力)を返す関数。 $@ はターゲット名(実行されるタスクの名前))

00:
  python $(shell echo $@*.py) | sort | uniq -c
01:
  mypy --strict $(shell echo $@*.py)

これからはこう。

PYFILE = $(shell echo $@*.py)

00:
  python $(PYFILE) | sort | uniq -c
01:
  mypy --strict $(PYFILE)

ここでは := を使ってはいけない(しかし通常代入としてもっぱら使うのは := の方)。

# これはだめ。即時で展開されるので $(shell echo *.py) が評価された結果が格納される
# つまり `echo *.py` の実行結果が格納される
PYFILE := $(shell echo $@*.py)

詳細

以下詳細。

:== ってそもそも何?

その前に :== について説明しておく。これらはMakefile中で変数の代入のために使うもの。 大雑把に言えば := はcall by value的な評価で = はcall by name (not call by need)2

詳しくは以下などを参考に

これだけだとさすがに意味がわからなそう。

実行例

実行例を書いてみる。

とりあえず使ってみる

foo,bar,booという変数を使ってみる。$(info ...)デバッグプリント的な事ができるのでこれを使って表示する。

00.mk

foo = xxx
bar = $(foo) @ $(foo)
boo = [ $(bar) ]

# or
# foo := xxx
# bar := $(foo) @ $(foo)
# boo := [ $(bar) ]

$(info * $$(foo) is $(foo))
$(info * $$(bar) is $(bar))
$(info * $$(boo) is $(boo))

default:

こういう感じ。変数と代入ですね。

$ make -f 00.mk -s
* $(foo) is xxx
* $(bar) is xxx @ xxx
* $(boo) is [ xxx @ xxx ]

この実行例だけだと :== も結果は変わらずで違いが確認し辛い。

評価回数を数えてみる

違いを明らかにするために評価回数を数えてみる。call by nameなら何度も何度も評価されることが確認されるはず。以下のようなスクリプト$(info ...) を使ってあげると挙動がわかりやすそう。

echo.pyは渡されたコマンドライン引数を標準出力と標準エラー出力に出力する。

echo.py

import shlex
import sys

print(shlex.join(sys.argv[1:]))
print("\t --", shlex.join(sys.argv), file=sys.stderr)

echo.pyの実行例

$ python echo.py hello world
hello world
     -- echo.py hello world

= を使った場合

= を使った場合はこういう感じ。

01.mk

foo = $(shell python echo.py xxx)
bar = $(shell python echo.py $(foo) @ $(foo))
boo = $(shell python echo.py [ $(bar) ])

$(info $$(foo) is $(foo))
$(info $$(bar) is $(bar))
$(info $$(boo) is $(boo))

default:

call by nameなので何度も何度も呼ばれる。

$ make -f 01.mk -s
     -- echo.py xxx
$(foo) is xxx
     -- echo.py xxx
     -- echo.py xxx
     -- echo.py xxx @ xxx
$(bar) is xxx @ xxx
     -- echo.py xxx
     -- echo.py xxx
     -- echo.py xxx @ xxx
     -- echo.py '[' xxx @ xxx ']'
$(boo) is '[' xxx @ xxx ']'

:= を使った場合

:= を使った場合はこういう感じ。

02.mk

foo := $(shell python echo.py xxx)
bar := $(shell python echo.py $(foo) @ $(foo))
boo := $(shell python echo.py [ $(bar) ])

$(info $$(foo) is $(foo))
$(info $$(bar) is $(bar))
$(info $$(boo) is $(boo))

default:

call by valueなので即時評価。評価された値が使われる。

$ make -f 02.mk -s
     -- echo.py xxx
     -- echo.py xxx @ xxx
     -- echo.py '[' xxx @ xxx ']'
$(foo) is xxx
$(bar) is xxx @ xxx
$(boo) is '[' xxx @ xxx ']'

補足

そもそも冒頭のMakefileでなぜわざわざ $(shell .. ) を使うのかの補足説明もしておくと良いかも。

なぜ %.py などでルールを定義しないの?

それは同じ拡張子であっても実行したい内容が異なるから。

python 00*.py x y z かもしれないし DEBUG=1 python 01*.py かもしれない。

もっと言えば、make 00pythonスクリプトmake 01 はrustか何かの実行かもしれない。あるいは、 make 02 は複数のスクリプトかもしれない。

なぜ直接 python 00*.py などではだめなの?

make -n で良い感じの出力が欲しいから。

例えば以下の様な状況で、python 00*.py を実行したいとする。

$ ls
00goto.py   01newtype.py    Makefile    before.mk

githubのissueなどに残したいときに、以下の様に出力されて欲しい。

$ make -n 00
python 00goto.py

# こうなってほしくない
# $ make -n 00
# python 00*.py

というわけで。Makefileはこう書くと嬉しい。

00:
    python $(shell echo $@*.py)

gist

参考


  1. ちなみにこれはその日のうちに書いて捨てるようなちょっとした実験のためのタスクランナーとしてのMakefileの例。いわゆる通常の使い方であるようなビルドツール(ビルドシステム)としての用法ではないし、常用されるようなタスクランナーとしての用法でもない。何らかの調整を加えた後の結果を確認するという行為を make <n> という形にまとめたいときの用法。

  2. call by name と call by needの違いは値がキャッシュされた遅延評価ではないという点。通常遅延評価といった場合にはcall by needを指す場合が多い。call by valueはいわゆる普通の言語での評価戦略。即時評価。call by valueが正格評価で、call by name, call by needは実行が遅延される非正格評価。