Makefileでソースディレクトリを分けてる場合のターゲットの記述方法(foreach, eval)

2023-07-31

Makefileでビルドを管理する場合にソースディレクトリを分けていると同じ記述を何度もするのが煩わしかったが、 まとめる方法があることを知った。

前口上

自分の好みとしてソースを格納するディレクトリとビルドで生成するオブジェクトファイルは別のディレクトリに入れるのが好きなんだけど、ソースをさらにいくつかのサブディレクトリに分けている場合に%を使ったパターンルールを内容は同じでもディレクトリごとに書く必要があるのが面倒だった。

例えばsrc以下にfoobarというディレクトリがあったとして:

$ tree
.
├── Makefile
├── obj # オブジェクトファイル出力先
└── src
├── bar
│   └── bar.c
└── foo
    └── foo.c

Makefileで

# 同じ内容でもディレクトリごとにパターンルールを書く必要がある
obj/%.o: src/foo/%.c
mkdir -p obj
$(CC) -c -o $@ $<

obj/%.o: src/bar/%.c
mkdir -p obj
$(CC) -c -o $@ $<

コンパイルしてオブジェクトファイルを生成するルールが「$(SRC_DIR)以下のすべて」とか書ければいいんだけどその方法を知らなくて、ソースディレクトリごとに書く必要があって面倒だった。

src/**/%.cで書ける、と一瞬喜んだが第2階層だけだった)。

define, foreach, call, eval

Makeにはマクロという機能があるとのことで、defineで定義できる:

define DEFINE_OBJ_TARGET
$(OBJ_DIR)/%.o: $(1)/%.c
mkdir -p $(OBJ_DIR)
$(CC) -c -o $$@ $$<
endef

endef までが内容で、パラメータが$1以降に渡される。 注意点としては$の変数参照がマクロ展開時に行われるので、ルール内で行わせたい場合にはエスケープして二重の$$にする必要がある。

定義したマクロを使うにはeval-callを、そして配列で複数に対して行うためにforeachを使う:

SRC_DIRS:=$(foo_DIR) $(bar_DIR)
$(foreach D, $(SRC_DIRS), $(eval $(call DEFINE_OBJ_TARGET,$(D))))

foreachの第2引数の各要素を第1引数(D)の変数にセットして第3引数の内容が実行される。 callでマクロを呼び出した結果をevalすることで要素ごとのルールが定義される。

追加の情報で処理したい場合

ちょっと変えて例えば実行ファイルfoobarをそれぞれxxx_OBJS変数のオブジェクトファイル群からビルドしたい、という場合にどうするか。 foreachに渡す配列をターゲット名とオブジェクトファイル群の二重配列にして、とか文字列を連結しといたものを分割して、とか考えたがうまくいかず。

がなんのことはない、変数の展開を重ねて、

define DEFINE_EXE_TARGET
$(1): $$($(1)_OBJS) # このブログ上で$$が勝手に変換されるのを避けるため全角にしているが、実際は半角
...
endef

とするだけでできる (内容としては$(1)がマクロcall時に展開され、$$call$になったものがeval時に処理される。 が$$$にしてもちゃんと動く、重ねてもちゃんと展開できる)。 「このディレクトリには特別にコンパイルオプションを足したい」、とかも変数名で引けるようにしておけばできるでしょう (未定義変数の場合は単に空白に置換される)。

Makefileの関数wildcardnotdiraddprefixなどを使えば、ファイル構成と命名に沿ってルールを生成することもできると思う。

参照