はじめに
今のご時世でもMakefileをなんらかの形で触れる機会は多いと思います。
いちいち長ったらしいコンパイルコマンドを打ったりすることなく、
$ make
で必要な実行環境が整うとなかなかカッコ良いですね。
ただ、Makefileの書き方のルールがたくさんありややこしいなーと思って理解を後回しにしていました。
私もなんとなくノリで便利コマンドをまとめたものぐらいの使い方以上に使っていなかったのですが、「これじゃいかん!」ということで、いつもより凝った使い方をしようとしました。
そのときに予想通り「意味わからん」などしっかりハマりはしたものの、調べていく中で Makefileの気持ちがわかってきたので記録として残しておきます。
結論:この記事を読んで書ける初見殺しなMakefile
ベストではないと思いますが、こんな感じになります。
dots = $(wildcard output/*.dot)
pngs = $(dots:%.dot=%.png)
image: $(pngs)
%.png: %.dot
dot -Tpng $< > $@
やろうとしたこと
今回は、複数のdotファイルに対し、ファイルごとにコマンドを実行し、出力をpngで出力しようとしました。
入力したいファイルとしては、
output/graph_00.dot
output/graph_01.dot
...
のような各dot
ファイルから次のような画像ファイルを作りたいです(すでに別プログラムでdotが出力されているので output/
にあります)。
output/graph_00.png
output/graph_01.png
...
dotファイルからpngを出力するのに用いるコマンドは次のようなものです。
$ dot -Tpng output/graph_00.dot > output/graph_00.png
これを全てのdotファイルに対して行いたいです。
↓理想は以下のコマンドで 各dotファイルがpngに変換されることです。
$ make image
ちなみにdotファイルの中身は次のようなものです。
digraph {
a -> b[weight="0.2"];
a -> c[weight="0.4"];
c -> b[weight="0.6"];
}
次のコマンドを実行すると、有向グラフを好きなファイル形式で出力してくれます(ここでは -Tpng
でpngイメージとして出力しています)。
$ dot -Tpng output/graph_00.dot > output/graph_00.png
なんらかの計算をして、大量のdotファイルを出力した後、各dotファイルから画像を出力し、まとめてgifアニメーションにする、なんてことができたら面白そうです。
Makefileを書いてみる
ここからは、ベタ書きから始めて、初見殺しなMakefileっぽい書き方に至るまでのプロセスを、いくつかのステップに分けながら解説をしていきます。
なぜこうしたかというと、Makefileの機能は便利だけど、初見殺しが多すぎるからです。
記事をいろいろ読んだのですが、Makefileの機能が充実しすぎて、結果だけ見せられても「なぜそうしているの?私の知ってるMakefileとちがーう」という気持ちになりました。
ですので、ステップバイステップで、一番馴染みのあるベタ書きなコマンドから、Makefileっぽい書き方のMakefile(変な表現)に至るまで見ていきます。
変換したいdotファイルは最終的にはgifにしたいというのもあり、数十〜数百個に及ぶことを想定しています。ベタ書きだと苦しくなります。
なお、環境をキレイにするために各実行前に
clean:
rm output/*.png
$ make clean
を実行しています。以下のMakefileには clean
は省略しています。
1. 超ベタ書き
まずは、一番何も考えなくてできるシンプルなバージョンで実行してみましょう。
Makefile
image:
dot -Tpng output/graph_00.dot > output/graph_00.png
dot -Tpng output/graph_01.dot > output/graph_01.png
実行結果
$ make image
dot -Tpng output/graph_00.dot > output/graph_00.png
dot -Tpng output/graph_01.dot > output/graph_01.png
そんなにファイル数が多くない時はぶっちゃけこれでも動くので良いとは思うのですが、最終的には数百ファイルから数百枚の画像を出力したいので、こんな書き方ではやってられません。
2. 依存関係を考慮してみる
特にMakefileにも詳しいわけではない私ですが前知識として、ターゲットと依存ファイルのようなものがあり、ソースファイルが更新されている場合のみコマンドの実行してくれるという機能があるということぐらいは知っていました。
確か、こんな感じ。
output/graph_00.png: output/graph_00.dot
dot -Tpng output/graph_00.dot > output/graph_00.png
出力ファイル(ターゲット): 依存ファイル
のような書き方をすると、依存ファイルの更新日時が出力ファイルのものより新しい場合のみ、コマンドを実行します。
$ make output/graph_00.png
dot -Tpng output/graph_00.dot > output/graph_00.png
上のコマンドを実行したあと、依存ファイル( output/graph_00.dot
)を更新しないでmakeコマンドを再実行すると、dotコマンドは up to date
となって実行されません。
$ make output/graph_00.png
make: `output/graph_00.png' is up to date.
Makefile
これを愚直に書いていくと、次のようになりました。
image: output/graph_00.png output/graph_01.png
output/graph_00.png: output/graph_00.dot
dot -Tpng output/graph_00.dot > output/graph_00.png
output/graph_01.png: output/graph_01.dot
dot -Tpng output/graph_01.dot > output/graph_01.png
実行結果
$ make image
dot -Tpng output/graph_00.dot > output/graph_00.png
dot -Tpng output/graph_01.dot > output/graph_01.png
$ make image
make: Nothing to be done for `image'.
makeの気持ちは次の通りです。
- imageターゲットをmakeするために、まず
image
ファイルと、依存ファイルである.png
の更新日時を比較する(image
ファイルはないので必ず実行) -
.png
をターゲットとするコマンドが呼ばれる - 依存ファイルであるdotファイルが更新されていればdotコマンドが実行される
- imageターゲットを作るためのコマンドは何もないので、結局imageターゲットは作られずに終わる
いや、Makefileがさっきより長くなってるやんけ・・・
とツッコミを入れた方は鋭いですね。
ここからMakefileの便利な使い方を導入していきます。
3. ターゲットと依存ファイルを変数で置き換える
プログラマとしては、重複する記述はなるべく避けたいところですね。
Makefileのコマンド内で、ターゲットは $@
、依存ファイル(の先頭)は $<
という変数名で表すことができます。
覚え方としては次のような感じでしょうか。
- ターゲットだからat(@)だから
$@
- ファイルからの標準入力を受け取るときには
コマンド < ファイル
のようにする。依存ファイルは入力ファイルと考えられるから$<
Makefile
image: output/graph_00.png output/graph_01.png
output/graph_00.png: output/graph_00.dot
dot -Tpng $< > $@
output/graph_01.png: output/graph_01.dot
dot -Tpng $< > $@
実行結果
$ make image
dot -Tpng output/graph_00.dot > output/graph_00.png
dot -Tpng output/graph_01.dot > output/graph_01.png
$ make image
make: Nothing to be done for `image'.
$@
や $<
のような自動的に定義される変数はAutomatic Variables - GNU(英語)にまとめられていますので、気になる方は参照してください。
だんだんとMakefileっぽい書き方になってきましたが、まだまだ重複箇所があるので、それをまとめていきます。
以下では、実行結果が全て同じになるので省略します。
4. パターンマッチを使う
先ほどの例では、ターゲットと依存ファイルの関係が .dot
が依存ファイルで、 .png
に出力するというものになっていました。
makeでは、いちいちファイル名を指定しないでも、拡張子やファイル名のパターンがマッチしたときにコマンドを実行できるようにする パターンマッチという機能があります。
.dot
を依存ファイルとして、同名の .png
をターゲットとするようなパターンマッチは %.png: %.dot
のように書くことができます。
これまでのように、 ターゲット: 依存ファイル
の関係になっていてわかりやすいですね。
今回の場合、二つのファイルに対してターゲットを書いていたのを次のようにまとめることができます。
%.png: %.dot
dot -Tpng $< > $
(注)サフィックスルールは使わない
.c.o
のような形で記述するサフィックスルールなどもありますが、公式のSuffix-Rules - GNUでは Old-Fashioned だと言われているので使うのはやめておきましょう。
Makefile
image: output/graph_00.png output/graph_01.png
%.png: %.dot
dot -Tpng $< > $@
下のターゲットは具体的なファイル名が入っておらず、汎用的で再利用しやすい形です。
パターンマッチは、今回のような拡張子に対して行うものだけではなく、特定のprefixを持つファイルのパターンも次のように書くことができます。
graph_%.png: graph_%.dot
dot -Tpng $< > $@
さて、残るは image
ターゲットの依存するファイル群をどうにかするだけです。実はここが案外わかりにくいんですよね。。
5. まずは出力ファイル名を変数に置く
image
ターゲットの依存ファイルをベタ書きではなく、ワイルドカードなどで取得できるようにしたいですね。
その前段階として、 pngs
という変数をMakefileの最初で定義しておきます。
pngs = output/graph_00.png output/graph_01.png
image: $(pngs)
%.png: %.dot
dot -Tpng $< > $@
すると、 image
と %.png
の二つのターゲットが具体的なファイル名に依存しない形にすることができました。
6. dotファイル名からpngファイル名を変換する
今回、pngs
変数は、dotファイルの .dot
を .png
にそのまま置き換えたものです。
Makefileの関数で、パターンで置換する $(変数名:%.dot=%.png)
という記法がありますので、これを使います。
dots = output/graph_00.dot output/graph_01.dot
pngs = $(dots:%.dot=%.png)
image: $(pngs)
%.png: %.dot
dot -Tpng $< > $@
この辺りの書き方は、Makefile CheetSheetを参考にしています。
公式でも、Functions for String Substitution and Analysis (英語)で解説がされています。
7. dotファイル名をワイルドカードで取得する
ここまでくれば、 output/*.dot
というワイルドカードを使ってファイル名を取ってこれます。
dots = $(wildcard output/*.dot)
pngs = $(dots:%.dot=%.png)
image: $(pngs)
%.png: %.dot
dot -Tpng $< > $@
wildcardを利用したシンプルな汎用Makefileや先ほどのCheetSheetを参考にしています。
このようなMakefileの関数が何もないところから出てくると、ビビってしまうものだと思います(自分がそう)。
ですが、一個一個細かくみると、単にターゲットより新しいソースファイルがあればコマンドを実行するというプログラムになっていることがわかります。
完成!
dots = $(wildcard output/*.dot)
pngs = $(dots:%.dot=%.png)
image: $(pngs)
%.png: %.dot
dot -Tpng $< > $@
あえて初見殺しなMakefileにしてみる
驚かすために大文字にし、ついでにdotファイルとpngファイルのディレクトリを異なるものにすると、あっという間にわかりにくくなります。
やっぱり大文字は読みにくいです。できれば変数名を大文字で書くのをやめてほしい。
SRCDIR = output
PNGDIR = output
DOTS = $(wildcard $(SRCDIR)/*.dot)
PNGS = $(DOTS:$(SRCDIR)/%.dot=$(PNGDIR)/%.png)
image: $(PNGS)
%.png: %.dot
dot -Tpng $< > $@
まとめ
「Makefileマジでわかりにくいよなぁ」とずっと思ってきた私でしたが、今回の画像出力の一件でかなり理解が進みました。
以前は、「依存関係?何それおいしいの?」状態で、ターゲットが本当はファイル名を表すものだということすらあまりわかっていませんでした。
こうやって記事を書くことで、あやふやな知識がだんだん固まっていくので、言語化は大事だなと痛感します。
この記事が、初見殺しなMakefileに悩まされている人の助けになれば幸いです。
ありがとうございました!
参考URL
- Makefileの関数
- Make覚書
- GNU make やっぱり公式が一番いいっすわ。英語だけど。