Makefileはタスクを整理する方法として便利なため、本連載ではそうした使い方について解説している。だが本来、Makefileは主にソフトウエアのビルドに使われるものであり、そのための機能も多数用意されている。今回は、そうした機能の1つとして「複数のターゲット」を扱う方法を取り上げる。
Makefileのルールは、行頭から始まる「ターゲット : 依存先」という記述で構成されている。このターゲットの部分に、これまでは1つのターゲットしか書いてこなかったが、実はここには複数のタスクを書くことができる。
つまり、次のようなMakefileを書くことができるのだ。
all: task1 task2
task1 task2:
echo "Here!"
.PHONY: task1 task2
実行すると次のようになる。
% make
echo "Here!"
Here!
echo "Here!"
Here!
%
このように、複数のターゲットが書いてあるMakefileを直接見ることはあまりない。実際には、次のように変数を使って書かれていることが多いからだ。これだと一見、シングルターゲットのように見える。しかし、実際には複数のターゲットが指定されていることになる。
TASKS:= task1 task2
all: $(TASKS)
$(TASKS):
echo "Here!"
.PHONY: $(TASKS)
実行結果は次の通りだ。当たり前だが、変数に収納しただけなので結果は先ほどと同じになる。
% make
echo "Here!"
Here!
echo "Here!"
Here!
%
では、複数のターゲットを指定したときに実際にどのように動作しているのか、探ってみよう。以下では、自動変数「$@」を出力するようにMakefileを書き換えた。
TASKS:= task1 task2
all: $(TASKS)
$(TASKS):
echo $@
.PHONY: $(TASKS)
実行すると次のようになる。
% make
echo task1
task1
echo task2
task2
%
自動変数「$@」の値が、「task1」「task2」と異なっていることを確認できる。
つまり、「複数のターゲットを指定する」というのは、それぞれが単一のターゲットに展開されているのと同じことになる。
TASKS:= task1 task2
all: $(TASKS)
task1:
echo $@
task2:
echo $@
.PHONY: $(TASKS)
実行すると次のようになる。当然、結果は同じだ。
% make
echo task1
task1
echo task2
task2
%
変数に複数のターゲットを収めておき、それらを同じコマンドで処理する。そういったケースはソースコードのコンパイルという作業においても頻出する。そういった場合にこの「複数のターゲット」が必要になるわけだ。
実際のプログラミングではどうなるか?
実際にこの複数のターゲットがどんな感じで使われているかを確認しつつ、陥りがちなミスなども見ていこう。
まず、次のようにプログラミングのビルド処理ではパターンを使ってルールを書くことが多い。「%.o: %.c」という部分がパターンを使ったルールの記述だ。複数パターンとは少しばかり機能が違うのだが、今は似たような機能だと考えておいてもらって良いと思う。
OBJS:= src1.o src2.o src3.o
all: $(OBJS)
%.o: %.c
@echo Build $@ from $<
@touch $@
clean:
rm *.o
.PHONY: all clean
実行すると次のようになる。
% ls -l
合計 1
-rw-r--r-- 1 daichi daichi 133 4月 24 16:27 Makefile
-rw-r--r-- 1 daichi daichi 0 4月 24 16:25 src1.c
-rw-r--r-- 1 daichi daichi 0 4月 24 16:25 src2.c
-rw-r--r-- 1 daichi daichi 0 4月 24 16:25 src3.c
% make
Build src1.o from src1.c
Build src2.o from src2.c
Build src3.o from src3.c
% ls -l
合計 1
-rw-r--r-- 1 daichi daichi 133 4月 24 16:27 Makefile
-rw-r--r-- 1 daichi daichi 0 4月 24 16:25 src1.c
-rw-r--r-- 1 daichi daichi 0 4月 24 16:27 src1.o
-rw-r--r-- 1 daichi daichi 0 4月 24 16:25 src2.c
-rw-r--r-- 1 daichi daichi 0 4月 24 16:27 src2.o
-rw-r--r-- 1 daichi daichi 0 4月 24 16:25 src3.c
-rw-r--r-- 1 daichi daichi 0 4月 24 16:27 src3.o
% make
make: 'all' に対して行うべき事はありません.
%
パターンに一致するターゲットが複数ターゲットのようなものだと考えると、その機能を使って処理が行われていることになる。
これまでに紹介した機能を使い、これを明示的に複数のターゲットを使うように記述してみよう。何も考えずに書いてみたのが次のサンプルだ。
SRCS= *.c
OBJS= $(patsubst %.c,%.o,$(SRCS))
all: $(OBJS)
$(OBJS):
@echo Build $@
@touch $@
clean:
rm *.o
.PHONY: all clean
実行すると次のようになった。
% make
Build *.o
%
思ったように機能していない。
これはシェルに詳しい場合にはまりがちなケースだ。シェルでは「*」がパスのワイルドカードとして機能し、自動的に展開対象となるが(グロブ展開)、Makefileではそういったことは行われない。そのため、「*」を指定しても思っているようなファイル名展開は行われないのだ。
さらにシンプルに次のようなMakefileを用意してみよう。
OBJS= src1.s src2.s src3.s
all: $(OBJS)
*.s:
@echo Build $@
@touch $@
clean:
rm *.s
.PHONY: all clean
実行すると次のようになる。
% make
make: *** 'all' に必要なターゲット 'src1.s' を make するルールがありません. 中止.
%
ターゲットに書いた「*.s」も当然ながらシェルのグロブ展開のようにパス展開は行われずに「*.s」のままなので、思ったようには機能してくれないのだ。
GNU makeには、「*」をシェルのグロブ展開のようにパス展開させるための「wildcard」という関数が用意されている。この関数を使うと、ファイル名パスを展開して複数ターゲットとして利用することができるようになる。
具体的には、次のようなMakefileを書けばよい。ワイルドカードや関数については今後取り上げるので、今は「こう書くものなんだ」と思っておいてもらえれば大丈夫だ。
SRCS= $(wildcard *.c)
OBJS= $(patsubst %.c,%.o,$(SRCS))
all: $(OBJS)
$(OBJS):
@echo Build $@
@touch $@
clean:
rm *.o
.PHONY: all clean
実行すると次のようになる。
% make
Build src1.o
Build src2.o
Build src3.o
%
思っていた通りの動作になった。複数ターゲットを使うことで、「ビルド処理を書く」というイメージをつかんでもらえたのではないかと思う。
複数ターゲットはプログラミング時に便利
今回取り上げたように、複数ターゲットは変数などを介して使っていることが多い。実際には複数ターゲットを書いているのだが、見た目はシングルターゲットなので気が付いていないパターンだ。実際には複数のターゲットはそれぞれがシングルのターゲットに展開して実行されているのと同じであり、試してみればその動きが見えてくると思う。
Makefileで使われているルールは全部合わせてもそれほど多いものではないのだが、どうも難解な記述をしているように思われがちだ。しかし、実際にはそれほど複雑なものではなく、必要な挙動をさせるための機能が実装されているだけである。仕組みが分かれば、とても便利なツールなのだ。
「たくさんのターゲットを1つずつ全部書くのは大変なので、複数ターゲットのような書き方が用意されている」と考えておけばよいだろう。処理対象のファイルが増減する度にMakefileを書き換えるのが面倒、といった場合も、複数ターゲットを使うと便利になるはずだ。