GNU Makeの使い方

make(メイク)は、ビルドツールと呼ばれるソフトウェアで、その名の通り、主にプログラムのビルドの自動化に使われる。プログラムは、通常、複数のコンポーネントから構成されている。これらのコンポーネントには「あるファイルを修正したら、このファイルとこのファイルをビルドしなおさなければならない」といった「依存関係」がある。一般に「〇〇したら〇〇しなければならない」もしくは「〇〇する前には〇〇しなければならない」といった状況は危険信号だ。依存関係が複雑になったら、自分の修正がどこまで波及するのかがわかりにくくなり、必ずミスが起きる。そのため、依存関係を認識し、ソフトウェアを正しくビルドするためのツールがビルドツールだ。makeは古典的なビルドツールで、better makeとしてのCMake、RubyによるRakeや、PythonのSConsなど、多くのビルドツールがあるが、まずはmakeを使えるようになってみよう。makeにも方言があるが、ここでは広く使われているGNU Makeの使い方を学ぶ。

makeの基本的な使い方

makeは、makefileというファイルに「ルール」と呼ばれる 依存関係処理をまとめたもの記述する。

ターゲット: 依存するファイル1 依存するファイル2...
    コマンド

という形になる。ルールは

から構成される。

まずは、簡単なmakefileを書いてみよう。まずはgithubディレクトリに入り、練習用のリポジトリkaityo256/make_tutorialをcloneせよ。

cd github
git clone https://github.com/kaityo256/make_tutorial.git

cloneできたら、リポジトリのhelloというディレクトリに入ろう。そこにはhello.txtというテキストファイルが置いてある。

$ cd make_tutorial
$ cd hello
$ cat hello.txt
Hello Make!

このcat hello.txtをmakeにやらせてみよう。vim でmakefileを開き、以下の内容を入力せよ

all:
    cat hello.txt

これは

というルールである。

記述の際、以下の点に注意せよ。

入力が終わったら、実行してみよう。端末でmakeを実行せよ。

$ make
cat hello.txt
Hello Make!

この動作について説明しよう。

なお、コマンドに@をつけるとそのコマンドは表示されない。

all:
    @cat hello.txt
$ make
Hello Make!

なお、もう一度makeすると、もう一度コマンドが実行される。

$ make
Hello Make!

makeは「ターゲットが存在しない」もしくは「ターゲットが依存するファイルが存在しないか、ターゲットより新しい」場合にコマンドを実行する。今回はターゲットallのコマンドを実行しても、allが作成されないので、何度でも実行される。

依存関係の記述

次に、依存関係を記述してみよう。同じディレクトリ(hello)で、makefileを以下のように書き直そう。

all: result.txt

result.txt: hello.txt
    cat hello.txt > result.txt

これは、

ことを表している。makeしてみよう。

$ make
cat hello.txt > result.txt

コマンドが実行され、result.txtが作成された。catで確認せよ。

$ cat result.txt
Hello Make!

さて、前回と違って、今回はもう一度makeを実行すると、コマンドは実行されない。

$ make
make: `all' に対して行うべき事はありません.

実行されると、makeは以下のように考える。

以上から、makeは何もしない。

次に、依存関係の処理について見てみよう。上記でmakeが何もしなかったは、result.txtのタイムスタンプがhello.txtより新しかったからだ。そこで、hello.txtのタイムスタンプを新しくしてみよう。タイムスタンプを変えるにはtouchコマンドを使う。

$ touch hello.txt
$ make
cat hello.txt > result.txt

makeすると、hello.txtからresult.txtが作り直された。このように、makeはターゲットが依存するファイルのタイムスタンプをチェックして、どのターゲットを実行するべきかを決める。

C++の分割コンパイル

次に、もう少し実戦的な例を見てみよう。リポジトリのcppに移動せよ。

cd ..
cd cpp

そこには、以下の三つのファイルがある。

main.cpp

#include "param.hpp"
#include <cstdio>

void show(void);

int main(void) {
  printf("main: N is %d\n", N);
  show();
}

sub.cpp

#include "param.hpp"
#include <cstdio>

void show(void){
  printf("sub:  N is %d\n",N);
}

param.hpp

const int N = 10;

パラメタを定義したparam.hppというヘッダファイルがあり、それをmain.cppsub.cppが依存している状況だ。

まずは手動でビルドしてみよう。C/C++は、分割コンパイルができる。まずはソースファイルからオブジェクトファイルを作成し、それをリンクすることで実行バイナリを作成するのだが、ソースからオブジェクトファイルを作るところをファイル毎に行うことができる。

コンパイラに-cオプションをつけると、コンパイルしてオブジェクトファイルを出力し、リンクしない。

$ g++ -c main.cpp
$ g++ -c sub.cpp
$ ls *.o
main.o  sub.o

作成されたオブジェクトファイルをリンクで「くっつける」と実行バイナリができあがる。

$ g++ main.o sub.o
$ ./a.out
main: N is 10
sub:  N is 10

実際にリンクしているのは「リンカ」と呼ばれるプログラムなのだが、g++が適切にコンパイラやリンカを呼び出して対処しているので、我々はあまり気にしなくて良い。

さて、ここでparam.hppの内容を修正しよう。

const int N = 20;

そして、sub.cppの再コンパイルを忘れ、main.cppのみ再コンパイルして、リンクしてしまったとしよう。

$ g++ -c main.cpp
$ g++ main.o sub.o
$ ./a.out
main: N is 20
sub:  N is 10

この場合でも実行バイナリは更新され、実行できるのだが、本来同じであるべき値がずれてしまっている。このような依存関係を認識し、自動的に必要なファイルを再コンパイルしてくれるのがmakeである。

クリーン

依存関係をどう扱うかは後で説明することにして、まずは分割コンパイル、リンクをするmakefileを書いてみよう。

all: a.out

a.out: main.o sub.o
    g++ main.o sub.o

main.o: main.cpp
    g++ -c main.cpp

sub.o: sub.cpp
    g++ -c sub.cpp

書けたら、実行バイナリやオブジェクトファイルを削除してからmakeしてみよう。

$ rm -f a.out *.o
$ make
g++ -c main.cpp
g++ -c sub.cpp
g++ main.o sub.o
$ ./a.out
main: N is 20
sub:  N is 20

a.outをビルドするための一連の動作をmakeがやってくれた。また、全てゼロからビルドしたから当たり前だが、値が正しく表示される、正しい実行バイナリができている。この、「全てゼロからビルド」するために、中間ファイルやターゲットを削除することを「クリーン」と呼ぶ。makefileでは、慣習としてcleanというターゲット名で「クリーン」のためのルールを記述する。

先ほどのmakefileの一番最後に、以下のルールを追加せよ。

clean:
    rm -f a.out *.o

これにより、make cleanと打つと、実行バイナリやオブジェクトファイルが削除される。

$ make clean
rm -f a.out *.o

これにより

make clean
make

すれば、必ずクリーンな状態からビルドすることができる。

パターンルール

次に、ソースファイルから、オブジェクトファイルを作るコマンドが同じなので、ファイルが増えた時に毎回似たような処理を書くのは面倒だ。まとめてしまおう。

makefileの以下の部分を削除する。

main.o: main.cpp
    g++ -c main.cpp

sub.o: sub.cpp
    g++ -c sub.cpp

削除後に、以下のルールを追加しよう。

%.o: %.cpp
    g++ -c $<

これは「パターンルール」と呼ばれる構文で、「%.oというファイル名にマッチするものは、%.cppから以下のコマンドで作れますよ」ということをmakeに教える。

$<とは、マクロ、もしくは自動変数と呼ばれるもので、「必要条件」に展開される。

最終的に、makefileは以下のようになったはずだ。

all: a.out

a.out: main.o sub.o
            g++ main.o sub.o

%.o: %.cpp
        g++ -c $<

clean:
        rm -f a.out *.o

実際にmakeしてみよう。

$ make clean
rm -f a.out *.o
$ make
g++ -c main.cpp
g++ -c sub.cpp
g++ main.o sub.o

正しくビルドできた。

依存関係の出力とインクルード

ここまでで、makefileはa.outmain.osub.oに、main.omain.cppに、sub.osub.cppに依存することを認識しているが、main.cppsub.cppparam.hppに依存していることは知らない。

例えば、一度makeする。

make

その後、param.hppを更新しても、makeはそれを認識してくれない。

$ touch param.hpp
$ make
make: 'all' に対して行うべき事はありません.

したがって、makefileに正しく依存関係を記述してあげる必要があるのだが、そもそも「人間は複雑な依存関係を処理できなくてミスするよね」というのがスタート地点なのに、人間に「makefileに正しく依存関係を書け」というのもおかしな話である。なので、依存関係の抽出もプログラムにやらせよう。g++には、依存関係を抽出してmakefile用に出力してくれるオプション-MMが存在する。

$ g++ -MM *.cpp
main.o: main.cpp param.hpp
sub.o: sub.cpp param.hpp

これを、リダイレクトでファイルに落とそう。

g++ -MM *.cpp > makefile.dep

できた依存関係記述ファイルを、makefileでインクルードしてやる。makefileのインクルードは-includeで行う。makefileの最後に以下の文を記述せよ。

-include makefile.dep

最終的に、makefileは以下のようになったはずだ。

all: a.out

a.out: main.o sub.o
      g++ main.o sub.o

%.o: %.cpp
  g++ -c $<

clean:
  rm -f a.out *.o

-include makefile.dep

このmakefileが、正しく依存関係を認識しているか調べてみよう。まずはクリーンビルドする。

make clean
make

この状態で、sub.cppだけ更新してからmakeしてみよう。

$ touch sub.cpp
$ make
g++ -c sub.cpp
g++ main.o sub.o

正しく、sub.cppのみ再コンパイルされて、実行できた。

次にparam.hppを更新してからmakeしてみよう。

$ touch param.hpp
$ make
g++ -c main.cpp
g++ -c sub.cpp
g++ main.o sub.o

正しく依存関係を認識し、param.hppに依存するmain.cppsub.cppがどちらも再コンパイルされた。

変数の利用

最後に、変数を使ってみよう。場面によって、コンパイラが違うことがある。違うコンパイラを使う場合、

a.out: main.o sub.o
            g++ main.o sub.o

%.o: %.cpp
        g++ -c $<

の二か所に出現するg++を修正しなければならない。「〇〇したら××しなければならない」は危険信号だ。一か所だけ修正したら全部修正できるように、変数を使おう。

makefileの変数は、変数名=値で宣言し、利用は変数名を$()で囲んだ$(変数名)とする。

まずはmakefileの冒頭で、コンパイラを変数で定義しよう。慣習としてC++コンパイラはCXXとする。

CXX=g++

そして、g++とあるところを$(CXX)に置換する。

最終的に、以下のようなmakefileになるはずだ。

CXX=g++

all: a.out

a.out: main.o sub.o
            $(CXX) main.o sub.o

%.o: %.cpp
        $(CXX) -c $<

clean:
        rm -f a.out *.o

-include makefile.dep

GNU makeは非常に多機能だが、ここまででよく使う機能はだいたいカバーできたはずだ。

並列ビルドと変数置換

makeは依存関係があるものならなんでも使える。特に、変数置換と並列ビルドと組み合わせると、簡単なデータ処理などで便利な時がある。

リポジトリのmakejディレクトリに入ってみよう。

その中には、input0.datからinput9.datまでの10個のインプットファイルと、convert.pyがある。convert.pyにデータを食わせると、変換されたデータが出てくるものとしよう。

例えば

python convert.py < input0.dat > output0.dat

などとする。convert.pyは単に入力をそのまま出力するだけのスクリプトだが、時間のかかる処理を模擬するために、内部で一秒待っている。

さて、これを10個のデータ全部に対してやりたい。もちろん、インプットデータが修正されたら、修正されたところだけアウトプットデータを修正したい。これをmakeにやらせよう。

まず、手元にあるものはinput0.datからinput9.datだ。これを変数INPUTに代入する。

INPUTS=$(shell ls input*.dat)

GNU Makeでは、変数名=$(shell コマンド)とすると、そのコマンドを実行した結果を変数に代入できる。この場合、INPUTSにはinput0.dat input1.dat ... input9.datが代入される。

欲しいのは、これらを全て変換したoutput0.dat output1.dat ... output9.datだ。これをINPUTから作るために、変数の置換を利用する。

OUTPUTS=$(INPUTS:input%=output%)

このようにDEST=$(SRC:パターン=パターン)と記述することで、変換した結果を得ることができる。

今回のケースでは、input0.datinput%にマッチし、%0.datとなる。この%output%を代入するとoutput0.datが得られる。input1.datなども同様である。こうしてOUTPUTSoutput0.dat ... output9.datが代入された。

最終的に欲しいもの(ビルドターゲット)はOUTPUTSであるから、allターゲットは

all: $(OUTPUTS)

と書けばよい。

以上を実装した、以下のようなmakefileが用意されている。

INPUTS=$(shell ls input*.dat)
OUTPUTS=$(INPUTS:input%=output%)


all: $(OUTPUTS)

output%: input%
  python convert.py < $< > $@

clean:
  rm -f $(OUTPUTS)

input?.datからoutput?.datを作るルール

output%: input%
  python convert.py < $< > $@

にある$@は自動変数の一種で、ターゲットに展開される。

早速makeしてみよう。ただし、時間も測ってみる。

$ time make
python convert.py < input0.dat > output0.dat
python convert.py < input1.dat > output1.dat
python convert.py < input2.dat > output2.dat
python convert.py < input3.dat > output3.dat
python convert.py < input4.dat > output4.dat
python convert.py < input5.dat > output5.dat
python convert.py < input6.dat > output6.dat
python convert.py < input7.dat > output7.dat
python convert.py < input8.dat > output8.dat
python convert.py < input9.dat > output9.dat
make  0.33s user 1.66s system 15% cpu 12.898 total

内部で1秒待つので、最低でも10秒かかる。ここでは13秒かかっていた。

では、並列ビルドを試してみよう。並列ビルドはmake -j 並列数で指定する。例えば、5並列で処理してみよう。

$ make clean
$ time make -j 5
python convert.py < input0.dat > output0.dat
python convert.py < input1.dat > output1.dat
python convert.py < input2.dat > output2.dat
python convert.py < input3.dat > output3.dat
python convert.py < input4.dat > output4.dat
python convert.py < input5.dat > output5.dat
python convert.py < input6.dat > output6.dat
python convert.py < input7.dat > output7.dat
python convert.py < input8.dat > output8.dat
python convert.py < input9.dat > output9.dat
make -j 5  0.66s user 1.78s system 90% cpu 2.680 total

5個ずつ処理されたのがわかると思う。なお、makeの-jオプションは並列数を省略すると並列実行可能なタスクを全て同時に実行しようとする。したがって、100個データがある場合は、100個プロセスを立ち上げて100個同時に実行する。非常にシステムに負荷をかけるため、並列ビルドをする時には必ず並列数を指定する癖をつけておくこと。最高でもCPUコア数までとする。

実戦的な例:アニメーション作成

先ほどの並列ビルドは、単に入力をそのまま出力に出すだけだった。もう少し実戦的な例として、シミュレーションデータを可視化して、アニメーションを作成してみよう。

リポジトリのspiralディレクトリに入ろう。

cd ..
cd spiral

まず、シミュレーションをして、途中の結果をダンプする処理をしよう。

$ python3 makedata.py
spiral00.dat
spiral01.dat
spiral02.dat
spiral03.dat
spiral04.dat
spiral05.dat
spiral06.dat
spiral07.dat
spiral08.dat
spiral09.dat
spiral10.dat
spiral11.dat
spiral12.dat
spiral13.dat
spiral14.dat
spiral15.dat

実行するとspiral00.datからspiral15.datまでの16個のデータが出力されたはずだ。これがシミュレーションデータだと思うことにしよう。

このデータを処理して、画像ファイルとして出力するスクリプトconvert.pyが用意してある。実行してみよう。

$ python3 convert.py spiral00.dat
spiral00.png

spiral00.pngが出てきたはずだ。eogで見てみよう。

eog spiral00.png

らせん模様が見えただろうか?これを全て変換したいが、いちいちコマンドを入力するのは面倒だし、シミュレーションの途中でも可視化したいし、シミュレーションが終わったら、可視化していないデータのみ変換したい。こんな時はmakeの出番だ。

DAT=$(shell ls *.dat)
PNG=$(DAT:%.dat=%.png)

all: $(PNG)


%.png: %.dat
        python3 convert.py $<


gif: $(PNG)
        convert -delay 5 -loop 0 -resize 50% spiral*.png spiral.gif

clean:
        rm -f spiral.gif spiral*.dat spiral*.png

もう何をやっているかはわかると思う。早速makeしてみよう。せっかくなので並列ビルドしてしまおう。

make -j

spiral00.pngは作成済みであるから、spiral01.pngからspiral15.pngが作成されたはずだ。せっかく連番ファイルがえきたので、アニメーションgifを作ってみよう。ImageMagickのコマンドを毎回入力するのは面倒なので、それもmakeにやらせよう。

$ make gif
convert -delay 5 -loop 0 -resize 50% spiral*.png spiral.gif

spiral.gifが作成されたはずだ。見てみよう。

eog spiral.gif

うずまきがグルグル回っただろうか。

とにかく「何か依存関係のある処理」を自動化するのにmakeは便利だ。単に便利というのみならず、人為的なミスも防ぐし、自動化しておくと「あれ?このデータからこの画像はどうやって作るんだっけ?」と忘れた時に、makeを見ればやり方を思い出すだろう。論文を書く時にも、データを更新したらmake一発で図も更新してPDFまで作る環境を作っておくことが望ましい。