Makefileの基本アイデア

Makefileは基本的に「生成されるファイル」「元ファイル」「生成されるファイルを作るコマンド」の3つで構成されており、これらを組み合わせて1つの「ブロック」とする。ブロックは複数書くことができる。基本的な書き方は、次の通りだ。

生成されるファイル1: 元ファイル1
    ファイル1を作るコマンド
    ファイル1を作るコマンド
    ファイル1を作るコマンド

生成されるファイル2: 元ファイル2
    ファイル2を作るコマンド
    ファイル2を作るコマンド
    ファイル2を作るコマンド

生成されるファイル3: 元ファイル3
    ファイル3を作るコマンド
    ファイル3を作るコマンド
    ファイル3を作るコマンド

そして前回、「生成されるファイル」と「元ファイル」の最終更新時刻を比較することで、それに続く「ファイルを作るコマンド」が実行されるかどうかが決まる、ということを説明した。こういった仕組みにすることで、必要最小限のビルド処理だけを行うようにするのがmakeおよびMakefileの基本的な考え方となる。

ただし、これはあくまでもMakefileの最も基本的な考え方の一つに過ぎず、実際にはもっとほかの使い方もできる。今回はその例として、ファイルが存在しないケースと、依存関係、それにデフォルトのブロックについて説明しよう。

「生成されるファイル」はなくてもOK

Makefileにおいて最も重要なのは、「生成されるファイル」と「元ファイル」という関係性なのだが、実はこの「生成されるファイル」というのは存在しなくてもよい(作られる必要もない)。

次のMakefileを例に考えてみよう。

clean:
    echo "ここはcleanです"

最初に説明した原則に従えば、上記の書き方では「clean」というファイルが生成されることになり、そのファイルの元となるファイルは存在しない、ということになる。しかし、それでは最終更新時刻の比較をすることはできないため、前回までの仕組みでは成り立たない。

実際にはそんな比較はできないわけで、上記のように「元ファイル」の存在しない書き方そのものができるようになっている。上記の書き方であればmakeコマンドに対して「clean」という引数を指定でき、そして「clean」を指定した場合には「clean:」に続くコマンドが実行される。

実行すると次のようになる。

% ls -l
合計 1
-rw-r--r-- 1 daichi なし 38  2月 21 08:58 Makefile
% make clean
echo "ここはcleanです"
ここはcleanです
%ls -l
合計 1
-rw-r--r-- 1 daichi なし 38  2月 21 08:58 Makefile
% make clean
echo "ここはcleanです"
ここはcleanです
%

前回の例では「make file_output」と実行することで「file_output」というファイルが生成された。そのため、もう一回「make file_output」と実行しても、同じコマンドが実行されることはなかった。「file_input」の最終更新時刻を更新するか、「file_output」というファイルを削除する処理をしないと、再び同じコマンドが実行されることはなかったわけだ。

だが、ここで示したように「元ファイル」を書かないと、何度でも名前を指定してコマンドを実行することができる。今回のケースなら何度でも「make clean」と実行して、そこに書いてある「echo "ここはcleanです"」を実行できるわけだ。これも、Makefileの基本的な書き方の一つである。

「ターゲット」と呼ぶ

「clean:」という名前から始まるブロックに、ほかのブロックに書いてあるコマンドによって生成されたファイルや一時ファイルなどを削除するコマンドを書く、という手法は、ビルド用のMakefileを作る際などによく見られる。

例えば次のMakefileを見てみよう。

file_output: file_input
    file_outputを生成するコマンド

clean:
    rm -f file_output

こう書いておけば、「make file_output」で「file_output」というファイルを生成することができ、「make clean」で「file_output」というファイルを削除できる。「make clean」のほうは削除する処理を書きたいので、ファイルの依存関係を書くことはできないのだ。

生成されないファイルを書いておくことができるので、「生成されるファイル」という呼び方だと都合が悪い。実のところ、「生成されるファイル」の部分は通常は「ターゲット」と呼ばれている。以降は、この部分を指し示す言葉として「ターゲット」を使用する。

makeにターゲットを指定しないと1つ目のブロックが呼ばれる

次に覚えておきたい挙動が、「デフォルト」のターゲットだ。

Makefileには複数のブロックを書いておくことができ、「make ターゲット」でそのブロックを実行することができる。

例えば、次のように複数のターゲットを持ったMakefileがあるとしよう。

first:
    echo "ここはfirstです"

second:
    echo "ここはsecondです"

third:
    echo "ここはthirdです"

実行すると次のようになる。

makeの実行サンプル

% make first
echo "ここはfirstです"
ここはfirstです
% make second
echo "ここはsecondです"
ここはsecondです
% make third
echo "ここはthirdです"
ここはthirdです
% 

この状態で引数を指定せずに「make」と実行してみよう。次のようになる。

% make
echo "ここはfirstです"
ここはfirstです
% make
echo "ここはfirstです"
ここはfirstです
% 

引数に何も指定しない状態でmakeコマンドが実行された場合、一番最初のブロックのターゲットが指定されたのと同じ動作をしている。要するに、一番最初に書いたターゲットがデフォルトのターゲットということになる。

何もターゲットを指定しないでmakeコマンドを実行するというのも、よくやる操作だ。ソフトウエア開発では、「ソフトウエアを全てビルドする」という処理がデフォルトのターゲットになっていることが多い。用途に応じて、最もよく使う“問題の発生しないターゲット”をデフォルトにしておくことが多い。

ブロックには依存関係を持たせることができる

前回「生成されるファイル」と「元ファイル」の2つの依存関係を記述するというのが、Makefileの基本的なアイデアであることを説明した。この依存関係は、ブロックそのものに対しても持たせることができる。つまり、ファイル名ではなくターゲットを依存関係の対象にできるということだ。これもMakefileの基本的な使い方であり、強力な機能である。

例えば、次のMakefileを見てみよう。

first:
    echo "ここはfirstです"

second:
    echo "ここはsecondです"

third:
    echo "ここはthirdです"

all:    first second third

「all: first second third」で、「ターゲットallは、ターゲットfirstとターゲットsecondとターゲットthirdに依存している」という指定になる。それに続くコマンドが書いてないが、Makefileではコマンド部分は省略できるので、このように依存関係だけを書いておくことができる。

ここで「make all」と実行すると次のようになる。

% make all
echo "ここはfirstです"
ここはfirstです
echo "ここはsecondです"
ここはsecondです
echo "ここはthirdです"
ここはthirdです
% 

first、second、thirdという依存関係の指定に対して、同名のターゲットが順番に実行されていることがわかる。こうした使い方ができるのだ。

試しに、「all: first second third」という表記を、次のように「all: third second first」と書き換えてみよう。

first:
    echo "ここはfirstです"

second:
    echo "ここはsecondです"

third:
    echo "ここはthirdです"

all:    third second first

ここで「make all」と実行すると次のようになる。

% make all
echo "ここはthirdです"
ここはthirdです
echo "ここはsecondです"
ここはsecondです
echo "ここはfirstです"
ここはfirstです
%

今度はthird、second、firstの順で依存関係にあるターゲットが実行されている。

依存関係は芋づる式に書くこともできる。次のように依存関係がつながっているような書き方だ。

start: first

first: second
    echo "ここはfirstです"

second: third
    echo "ここはsecondです"

third:
    echo "ここはthirdです"

この書き方だと、ターゲットは「start」→「first」→「second」→「third」という依存関係になっている。

実行すると次のようになる。

% make start
echo "ここはthirdです"
ここはthirdです
echo "ここはsecondです"
ここはsecondです
echo "ここはfirstです"
ここはfirstです
% 

依存関係の末端から「third」→「second」→「first」→「start」の順に実行されていることがわかる。ビジネスツールとしてMakefileを使う場合、あまり複雑な依存関係を使うことはないのだが、こうした仕組みになっていることは覚えておこう。

組み合わせるとこんな感じ

前回のMakefileと今回のMakefileを組み合わせて、次のようなMakefileを用意する。

default: file_output

file_output: file_input
    # file_inputからfile_outputを作成
    cat file_input > file_output

    # file_inputとfile_outputの更新日時を表示
    stat -c '%n %z' file_input file_output

clean:
    rm -f file_output

実行してみると次のようになる。

% ls -l
合計 1
-rw-r--r-- 1 daichi なし   0  2月 21 09:17 file_input
-rw-r--r-- 1 daichi なし 247  2月 21 09:17 Makefile
% make
# file_inputからfile_outputを作成
cat file_input > file_output
# file_inputとfile_outputの更新日時を表示
stat -c '%n %z' file_input file_output
file_input 2022-02-21 09:17:24.638679800 +0900
file_output 2022-02-21 09:17:36.411849800 +0900
% make clean
rm -f file_output
% 
  • 実行サンプル

    実行サンプル

makeで「make default」が呼ばれていること、そこから依存関係でfile_outputターゲットが実行されていることがわかる。また、「make clean」で作成したファイルを削除していることも確認できる。

前回は一度makeしたら2度目以降はビルドできなかった。それは今回も同様なのだが、途中で「make clean」を実行してクリーンナップすれば、再度makeを実行して生成処理を行うことができる。

% make
# file_inputからfile_outputを作成
cat file_input > file_output
# file_inputとfile_outputの更新日時を表示
stat -c '%n %z' file_input file_output
file_input 2022-02-21 09:17:24.638679800 +0900
file_output 2022-02-21 19:53:45.540901100 +0900
% make
make: 'default' に対して行うべき事はありません.
% make
make: 'default' に対して行うべき事はありません.
% make clean
rm -f file_output
% make
# file_inputからfile_outputを作成
cat file_input > file_output
# file_inputとfile_outputの更新日時を表示
stat -c '%n %z' file_input file_output
file_input 2022-02-21 09:17:24.638679800 +0900
file_output 2022-02-21 19:53:50.600818600 +0900
%

基本的な機能だけで構成したMakefileだが、これだけでも結構処理を整理することができる。Makefileはそれほど見やすいものでもないのだが、前回と今回の内容を押さえていれば、既存のMakefileがざっくりと読めるようになっているはずだ。どこかでMakefileを見かけたら、一度覗いてみてはいかがだろうか。