初見殺しなMakefileが生まれる過程を7ステップに分けて解説する


はじめに

今のご時世でもMakefileをなんらかの形で触れる機会は多いと思います。
いちいち長ったらしいコンパイルコマンドを打ったりすることなく、

$ make

で必要な実行環境が整うとなかなかカッコ良いですね。

ただ、Makefileの書き方のルールがたくさんありややこしいなーと思って理解を後回しにしていました。

私もなんとなくノリで便利コマンドをまとめたものぐらいの使い方以上に使っていなかったのですが、「これじゃいかん!」ということで、いつもより凝った使い方をしようとしました。
そのときに予想通り「意味わからん」などしっかりハマりはしたものの、調べていく中で 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ファイルの中身は次のようなものです。

graph_00.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にしたいというのもあり、数十〜数百個に及ぶことを想定しています。ベタ書きだと苦しくなります。

なお、環境をキレイにするために各実行前に

Makefile
clean:
    rm output/*.png
$ make clean

を実行しています。以下のMakefileには clean は省略しています。

1. 超ベタ書き

まずは、一番何も考えなくてできるシンプルなバージョンで実行してみましょう。

Makefile

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にも詳しいわけではない私ですが前知識として、ターゲットと依存ファイルのようなものがあり、ソースファイルが更新されている場合のみコマンドの実行してくれるという機能があるということぐらいは知っていました。
確か、こんな感じ。

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

これを愚直に書いていくと、次のようになりました。

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の気持ちは次の通りです。

  1. imageターゲットをmakeするために、まず image ファイルと、依存ファイルである .png の更新日時を比較する( image ファイルはないので必ず実行)
  2. .png をターゲットとするコマンドが呼ばれる
  3. 依存ファイルであるdotファイルが更新されていればdotコマンドが実行される
  4. imageターゲットを作るためのコマンドは何もないので、結局imageターゲットは作られずに終わる

いや、Makefileがさっきより長くなってるやんけ・・・

とツッコミを入れた方は鋭いですね。
ここからMakefileの便利な使い方を導入していきます。

3. ターゲットと依存ファイルを変数で置き換える

プログラマとしては、重複する記述はなるべく避けたいところですね。
Makefileのコマンド内で、ターゲットは $@ 、依存ファイル(の先頭)は $< という変数名で表すことができます。

覚え方としては次のような感じでしょうか。

  • ターゲットだからat(@)だから $@
  • ファイルからの標準入力を受け取るときには コマンド < ファイル のようにする。依存ファイルは入力ファイルと考えられるから $<

Makefile

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 のように書くことができます。
これまでのように、 ターゲット: 依存ファイル の関係になっていてわかりやすいですね。

今回の場合、二つのファイルに対してターゲットを書いていたのを次のようにまとめることができます。

Makefile
%.png: %.dot
    dot -Tpng $< > $

(注)サフィックスルールは使わない

.c.o のような形で記述するサフィックスルールなどもありますが、公式のSuffix-Rules - GNUでは Old-Fashioned だと言われているので使うのはやめておきましょう。

Makefile

Makefile
image: output/graph_00.png output/graph_01.png

%.png: %.dot
    dot -Tpng $< > $@

下のターゲットは具体的なファイル名が入っておらず、汎用的で再利用しやすい形です。

パターンマッチは、今回のような拡張子に対して行うものだけではなく、特定のprefixを持つファイルのパターンも次のように書くことができます。

Makefile
graph_%.png: graph_%.dot
    dot -Tpng $< > $@

さて、残るは image ターゲットの依存するファイル群をどうにかするだけです。実はここが案外わかりにくいんですよね。。

5. まずは出力ファイル名を変数に置く

image ターゲットの依存ファイルをベタ書きではなく、ワイルドカードなどで取得できるようにしたいですね。
その前段階として、 pngs という変数をMakefileの最初で定義しておきます。

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) という記法がありますので、これを使います。

Makefile
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 というワイルドカードを使ってファイル名を取ってこれます。

Makefile
dots = $(wildcard output/*.dot)
pngs = $(dots:%.dot=%.png)

image: $(pngs)

%.png: %.dot
    dot -Tpng $< > $@

wildcardを利用したシンプルな汎用Makefileや先ほどのCheetSheetを参考にしています。

このようなMakefileの関数が何もないところから出てくると、ビビってしまうものだと思います(自分がそう)。
ですが、一個一個細かくみると、単にターゲットより新しいソースファイルがあればコマンドを実行するというプログラムになっていることがわかります。

完成!

Makefile
dots = $(wildcard output/*.dot)
pngs = $(dots:%.dot=%.png)

image: $(pngs)

%.png: %.dot
    dot -Tpng $< > $@

あえて初見殺しなMakefileにしてみる

驚かすために大文字にし、ついでにdotファイルとpngファイルのディレクトリを異なるものにすると、あっという間にわかりにくくなります。
やっぱり大文字は読みにくいです。できれば変数名を大文字で書くのをやめてほしい。

Makefile
SRCDIR = output
PNGDIR = output
DOTS = $(wildcard $(SRCDIR)/*.dot)
PNGS = $(DOTS:$(SRCDIR)/%.dot=$(PNGDIR)/%.png)

image: $(PNGS)

%.png: %.dot
    dot -Tpng $< > $@

まとめ

「Makefileマジでわかりにくいよなぁ」とずっと思ってきた私でしたが、今回の画像出力の一件でかなり理解が進みました。
以前は、「依存関係?何それおいしいの?」状態で、ターゲットが本当はファイル名を表すものだということすらあまりわかっていませんでした。

こうやって記事を書くことで、あやふやな知識がだんだん固まっていくので、言語化は大事だなと痛感します。

この記事が、初見殺しなMakefileに悩まされている人の助けになれば幸いです。
ありがとうございました!

参考URL