Makefileとワイルドカード「*」
Makefileは、プログラミングのみならずさまざまな用途で使える便利なツールであるため、その書き方について基本的なところは押さえておきたい。それほど難しいものではないのだが、現在の主要なプログラミング言語とは記述形式がかなり異なるので、慣れるまで少し時間が必要かもしれない。
今回はMakefileの機能のうち、ファイルパスに自動展開されるワイルドカード「*」について取り上げる。ワイルドカードはシェルでも使われるし、ファイルパスに展開されるという点も同じだ。基本的には、シェルの「*」と同じようなものだと思っておいて問題ないと思う。
しかしながら、Makefileにおける「*」は使う場所によって意味が異なる。また、「*」を使う際にやってしまいがちなミスもあるので、今回はこの辺りについて説明していく。
「*」はターゲットと依存先で使える
Makefileでは、基本的にルール(ターゲットと依存先)の部分でワイルドカードを使うことができる。例えば、次のMakefileをご覧いただきたい。
all: *.o
*.o: *.c
@echo "target: $@ depends on: $^"
update:
touch *.c
allターゲットの依存先が「*.o」になっているほか、ターゲット自体が「*.o」になっており、さらに「*.o」の依存先が「*.c」になっている。ワイルドカードで展開されまる書き方だ。
実行すると次のようになる。
> ls
Makefile src1.c src1.o src2.c src2.o src3.c src3.o
> make
target: src1.o depends on: src1.c src2.c src3.c
target: src2.o depends on: src1.c src2.c src3.c
target: src3.o depends on: src1.c src2.c src3.c
>
このMakefileが置いてあるのと同じディレクトリに「src1.c」「src1.o」「src2.c」「src2.o」「src3.c」「src3.o」というファイルを用意してある。また、依存関係が機能するように*.cファイルの方の最終更新時刻を新しくしてある。
この状態で実行結果が先ほどのようになったということは、実行する段階で「*.o」や「*.c」が次のように展開されていることになる。
all: src1.o src2.o src3.o
src1.o src2.o src3.o: src1.c src2.c src3.c
@echo "target: $@ depends on: $^"
update:
touch *.c
複数ターゲットは単一のターゲットに展開できるので、上記は下記と同じということになる。
all: src1.o src2.o src3.o
src1.o: src1.c src2.c src3.c
@echo "target: $@ depends on: $^"
src2.o: src1.c src2.c src3.c
@echo "target: $@ depends on: $^"
src3.o: src1.c src2.c src3.c
@echo "target: $@ depends on: $^"
update:
touch *.c
自動変数は実行前に展開されて次のようになる。
all: src1.o src2.o src3.o
src1.o: src1.c src2.c src3.c
@echo "target: src1.o depends on: src1.c src2.c src3.c"
src2.o: src1.c src2.c src3.c
@echo "target: src2.o depends on: src1.c src2.c src3.c"
src3.o: src1.c src2.c src3.c
@echo "target: src3.o depends on: src1.c src2.c src3.c"
update:
touch *.c
ワイルドカード「*」を使って複数のファイルを指定できていることがお分かりいただけるだろう。
ポイントは、「ワイルドカードはルール(ターゲットと依存先)で展開される」ということだ。
例えば、先程のMakefileを次のように書き換えたとする。
OBJS=*.o
SRCS=*.c
all: $(OBJS)
$(OBJS): $(SRCS)
@echo "target: $@ depends on: $^"
update:
touch $(SRCS)
動作はこれまでと同じだ。
一見すると、このMakefileにおける「*.o」と「*.c」は変数に代入した時点で展開されるような気がする。シェルではその動作になるので、シェルから見れば自然な発想だ。
しかし、Makefileでは「OBJS=*.o」や「SRCS=*.c」ではワイルドカードは展開されない。ただの文字列として変数に格納されるだけだ。ルールに記載され展開されたときに初めて展開される。「Makefileの*はターゲットと依存先にあるときに展開される」ということを基本として押さえておこう。
コマンドの「*」は展開されない
ということは、コマンドラインに書いてある「*」はどうなるのかと言えば、Makefileは何もしない。
先ほどのMakefileをもう一度確認してみよう。
all: *.o
*.o: *.c
@echo "target: $@ depends on: $^"
update:
touch *.c
このMakefileのupdateターゲットを実行すると次のようになる。
> make update
touch *.c
>
エコーバックの内容をみるとわかるように、makeは「touch *.c」を「touch *.c」のまま実行している。「touch *.c」はそのままシェルに渡される。シェルは「touch *.c」を受け取ると、「*.c」の部分をグロブ展開の対象であると判断し、この段階で「src1.c src2.c src3.c」へ展開される。結果的にシェルは「touch src1.c src2.c src3.c」というコマンドを実行することになる。
このようにMakefileではルール(ターゲットと依存先)以外の「*」は基本的にただの文字列でなにも行われない。コマンド部分に書いてあるのはシェルに渡されてからシェルが展開している。同じ表記なので混乱してしまうが、まったく処理が違うことを把握しておこう。
「*」の誤った使い方サンプル
「*」によるファイルパス展開は便利なので、特にプログラミングに使う場合には多用しがちなのだが、この書き方にはいくつか陥りがちな「誤った書き方」がある。典型的な例は、存在しないファイルを想定して書いてしまうというものだ。
次のMakefileを見てみよう。
all: *.o
*.o: *.c
touch $@
update:
touch *.c
clean:
rm -f *.o
偽ターゲットについて取り上げた際にも説明したが、cleanターゲットのこうした使い方は適切だ。実行すると次のようになる。
> make clean
rm -f *.o
>
同じMakefileのまま、今度は次のように実行してみよう。
> make clean
rm -f *.o
> make
touch *.o
> ls
'*.o' Makefile src1.c src2.c src3.c
>
Makefileを書いたユーザーの意図としては、ここで「obj1.o」「obj2.o」「obj3.o」というファイルが生成される、ないしは、更新されてほしいのだが、実際にはそうなっていない。
make cleanを実行した段階で*.oファイルは全部消えている。よって、Makefileにおける解釈は次のような感じになる。
all: *.o ← cleanした後だと*.oファイルがない!
*.o: *.c ← cleanした後だと*.oファイルがない!
touch $@← 「*.o」という文字のまま処理される
update:
touch *.c
clean:
rm -f *.o
makeは「*.o」が展開できないので、そのままシェルに渡す。シェルは渡ってきた「*.o」を展開しようとするのだが、ここでも展開対象となるファイルは存在していないので、touchコマンドに「*.o」という文字列がそのまま引き渡される。
そのため、最終的に「*.o」という不思議な名前のファイルが生まれてしまったわけだ。
今回のようなケースでは、ワイルドカード「*」ではなくパターン「%」を使って処理を書くのが通常の書き方ということになる。なお、「$(wildcard )」という関数を使うと、関数が処理される段階で「*」がファイルパス展開されるので、この機能を使って処理を行う、という方法もある。
makeが処理する「*」とシェルが処理する「*」を区別しよう
今回のポイントをまとめると次のようになる。
- Makefileの「*」は、ルール(ターゲットと依存先)に書いてあるものが実行時に展開される。強制的に展開させたい場合には$(wildcard )関数を使う
- コマンドの「*」は、makeからは展開されない。そのままシェルに渡され、以降の処理はシェルによって行われる
Makefileはシェルとよく似たシンタックスを採用しているので便利である反面、シェルが機能しているのかmakeが処理をしているのかわからなくなることがある。しかし、それらはまったく別の実装であるため、どちらが処理を行っているのか把握しておくことが大切だ。この辺りの切り分けがきっちりできるようになっているなら、Makefileをかなり深く理解できていると言えるだろう。