はじめに
最近はさまざまなビルドツールがあり、古典的な make を使う機会は減っているかもしれませんね。でも古典だけあって、make を使う機会が全くない、という人も少ないのではないでしょうか。
ここでは make でわかりにくいことのひとつ、2種類の変数と評価のタイミングについて整理してみます。
なお make にはいろいろ方言がありますが、ここでは GNU Make を対象にしています。
ふたつの変数
単純展開変数 と 再帰展開変数 の 2種類があります。違いは いつ評価されるか です。
単純展開変数
CC := gcc
のように :=
を使って定義します。文字通り単純で、特に難しいことはありません。
make
がこの行を読んだタイミングで、変数 CC
の値は gcc
である、と決まります。
例えばこの Makefile
FOO := foo
BAR := $(FOO)
FOO := baz
$(info BAR: $(BAR))
all:
で make するとこうなります。
(ちなみに $(info ...)
はログを出すための関数です)
$ make
BAR: foo
make: Nothing to be done for 'all'.
後から代入した baz
ではなく foo
が出力されました。当たり前ですね。
再帰展開変数
こちらは
CC = gcc
のように =
を使って定義します。単純展開変数と異なり、評価されたときに値が決ま ります。
例を見たほうがわかりやすいでしょう。
FOO := foo
BAR = $(FOO)
FOO := baz
$(info BAR: $(BAR))
all:
さっきの例との違いは BAR
への代入に :=
を使うか、 =
を使うか、だけです。実行してみましょう。
$ make
BAR: baz
make: Nothing to be done for 'all'.
$(info BAR: $(BAR))
が読まれたタイミングで BAR
の値が評価されます。BAR
の値は $(FOO)
です。 FOO
の値はこのタイミングでは baz
になっていますから、 baz
が出力されました。
また、再帰展開変数は評価されるたびに展開されます。例えばこうです。
BAR = $(shell date)
all: foo bar
foo:
@echo $(BAR)
@sleep 3
bar:
@echo $(BAR)
$ make
Wed Feb 21 14:37:48 JST 2018
Wed Feb 21 14:37:51 JST 2018
同じように echo $(BAR)
しているのに、異なる時間が表示されました。これはターゲット foo
とターゲット bar
に対応したコマンドの実行時に BAR
の値がそれぞれ展開されているためです。
再帰展開変数のこの性質がトラブルとなることがあります。
- いつの間にか値が変わってしまう
- パフォーマンス劣化の原因になる
- 時間がかかるような処理 (例えば
FILES = $(shell find . -type f -print)
とか) をしてしまうと何度もその処理が実行されてしまう
- 時間がかかるような処理 (例えば
特別な意図がないときは :=
を使って単純展開変数にしておく のがよいでしょう。
逆に再帰展開変数を使いたいのは、「Makefile のその行を読んでいるときにはまだ決まらない値を与えたいとき」です。ビルドの途中経過で値が決まるケースなど。
コマンド中での変数参照で値が決まるタイミング
ところが、単純展開変数なのに後から値が決まるように見えることがあります。こんなケースです。
FOO := foo
all:
@echo $(FOO)
FOO := bar
$ make
bar
単純展開変数なのに、後から代入したはずの bar
が出力されました !
実は、コマンド中での変数の展開は、そのターゲットを作ろうとするタイミングで評価されます。つまり、 Makefile を いったん全部読み終わって、さあ all
を作ろう、というときに 評価されるのです。
この挙動はなかなかわかりにくいのでハマりやすいです。 Makefile の中でひとつの変数に何度も代入するときには気をつけましょう。
環境変数とコマンドラインオプションでの変数設定
「2種類の変数」という話題からちょっと離れますが、変数がらみで環境変数とコマンドラインオプションでの変数指定についても書いておきます。
A. $ CFLAG=-O2 make
B. $ make CFLAGS=-O2
このふたつの違いはわかるでしょうか ? 前者は make
を実行するときの 環境変数、後者は make
に渡す コマンドラインオプション です。
環境変数
こちらは別に make
に限ったやり方ではないですね。シェルから、環境変数を渡しつつコマンドを起動しているだけです。 Makefile 中では、この値も make 変数として参照できます。 (例: $(CFLAGS)
)
コマンドラインオプションによる変数設定
こちらは実は make のコマンドラインオプションです。このように値を与えた場合には、 Makefile 内で他の値が代入されていてもコマンドラインでの指定が優先されます。
CFLAGS := -O0 -g
と書いてあったとしてもコマンドラインで make CFLAGS=-O2
と指定したなら CFLAGS
の値は -O2
となります。
ちなみにこの場合も単純展開変数と再帰展開変数を使い分けることができます。 :=
なら単純展開、 =
なら再帰展開です。
変数の値を make 起動時に柔軟に変更できるようにする
上記の性質を使うと、Makefile 中に例えば
CFLAGS := -O0 -g $(CFLAGS)
などと定義しておけば、
-
CFLAGS=-Wall make
と起動すればCFLAGS
の値は-O0 -g -Wall
になる (追加できる) -
make CFLAGS:=-O2
と起動すればCFLAGS
の値は-O2
になる (上書きされる)
みたいなこともできます。ちょっとトリッキーですが…。
まとめ
- ふたつの make 変数、 単純展開変数 と 再帰展開変数 について説明しました。
- ついでにコマンドラインオプションでの変数の上書きについても紹介しました。
おまけ
オライリーの GNU Make の本の pdf が https://www.oreilly.co.jp/library/4873112699/ からダウンロードできます。 自分である程度の規模の Makefile を書く人は一度目を通しておくことをお勧めします。