makefileのforeachのハマりどころ

スクランナーとしてのmakeのN回目。あんまり深追いしたくはないという気持ちもありつつ。

以前書いたようにmakefile中ではbashの関数などの定義ができない。そんなわけでdefineとcallを使う。これはこれで便利。ただforeachまで使おうとするとちょっとハマるかもというポイントがあるのでメモ。

define,callを使って処理をまとめる

# この定義はforeachを使う所で上手くいかなくなるので注意(良くない例)
define F =
   echo $(1)
   echo $(1)
endef

f0:
  $(call F,foo)
  $(call F,bar)

これは以下の様に展開される。

$ make -n f0
echo foo
echo foo
echo bar
echo bar

なるほど。良い。

foreachを使いたい

さて、Makefileで複数の対象を候補に同じ処理を呼びたいということもある。つまるところ対象としてlistを指定したい。このようなときにはforeachを使う。使うのだけれど。これに少しだけハマりどころが存在する(こういうところがあるのでMakeの知識はバッドノウハウっぽい。本当は代替のタスクランナーがあれば乗り換えたい。ただしできればインストールにパッケージマネージャが不要なものが良い(LLなどはこのあたりで候補から外れる))。

以下の様にリスト(xs)を用意して、先程定義したFをそのリストに対して使おうとしてみる。

# この定義はforeachを使う所で上手くいかなくなるので注意(良くない例)
define F =
   echo $(1)
   echo $(1)
endef

xs := foo bar
f1:
  $(foreach x,$(xs),$(call F,$(x)))

-n で実際に実行されるスクリプトを見てみると、一部期待通りではない形になっている。もちろん当然ではあるけれど。このようなコードではfooでの末尾を実行したタイミングで不用意に引数としてbarの先頭行が渡されるということになり。エラーが出てしまう。

$ make f1 -n
echo foo
echo foo        echo bar
echo bar

Makefileのdefineはテキストを定義するというだけの意味なので。endefの手前までの部分、つまるところ改行を含まない定義がforeachによって呼ばれるのでダメ。

期待通りに書くには以下の様にする必要がある。

# こちらは期待通りに動く
define F =
   echo $(1)
   echo $(1)

endef

xs := foo bar
f2:
  $(foreach x,$(xs),$(call F,$(x)))

最後に1行改行が必要。こうすると上手くいく。

$ make f2 -n
echo foo
echo foo
echo bar
echo bar

foreachが使えるようになるとできること

ちなみにforeachが使えるようになるとできることが色々ある。wildcardとbasename,addsuffixあたりを組み合わせると便利。 (あんまりmakeのことを覚えても仕方がないというところもあるので$(shell ...) やバッククォートによるコマンドの実行を使った形の方が共有はしやすいかもしれない)

例えば、setup.pyを持つものをpython packageだとすると。これらの全部のテストを実行するには以下で良い。

define testT =
   (cd $(dir $(1)) && python setup.py -q test)

endef

testall:
  $(foreach p,$(wildcard */setup.py),$(call testT,$(p)))

実際に実行してみる

$ make -n
(cd foo/ && python setup.py -q test)
(cd bar/ && python setup.py -q test)

$ make
(cd foo/ && python setup.py -q test)

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK
(cd bar/ && python setup.py -q test)

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

注意点として、makeでのコマンドは1行毎に違うシェルで実行されるので(正確な表現ではないけれど)以下ではダメ。

define testT =
    cd $(dir $(1))
    python setup.py -q test  # cdする前の位置で実行される
    cd ..

endef