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を書き換えるのが面倒、といった場合も、複数ターゲットを使うと便利になるはずだ。

参考