PK
パーネルカニック研究所
今日もどこかでkernel panic。
root@pk-lab:~#

makeとは何か

連載ナビ

この連載は「Linuxを使えるようになる」ではなく、「Linuxがどう動いているのか」を理解するための実践シリーズです。

今回は Linuxを理解する 〜 LFSで学ぶLinux内部構造 〜 の Part 4 / 第2回として、仕組みのつながりを意識しながら学びます。

—

makeは「依存関係を見て、必要な処理だけを実行する」ビルドツール

前回、./configure がMakefileを生成するところまで見ました。今回はそのMakefileを実行する make の番です。

結論から言うと、make はMakefileに書かれた「ターゲット(作りたいもの)」「依存ファイル(材料)」「コマンド(作り方)」の関係を読み取り、ファイルの更新時刻を比較して、作り直しが必要なものだけをビルドするツールです。単なる「コマンドをまとめて実行するスクリプト」ではなく、「何を再実行すべきかを判断するエンジン」である点が本質です。

毎回ゼロからビルドしていたら、開発が成立しないから

なぜ「必要な処理だけ」を判断する仕組みが要るのでしょうか。理由は、ビルドには時間がかかり、しかも大半のビルドは「一部のファイルだけ変更した後」に行われるからです。

たとえばLFSでGCCをビルドすると、マシン性能にもよりますが数十分から数時間かかります。数百のソースファイルのうち1つだけ直したのに毎回全ファイルをコンパイルし直すのは、明らかな無駄です。

makeはこれをタイムスタンプの比較で解決します。Makefileのルールは次の形をしています。


ターゲット: 依存ファイル1 依存ファイル2 ...
	コマンド        # 行頭は必ずタブ文字

makeは「ターゲットが存在しない」または「依存ファイルのどれかがターゲットより新しい」ときだけコマンドを実行します。依存ファイル自身が別のルールのターゲットなら、そちらを先に処理します。つまりMakefile全体は「依存関係のグラフ」であり、makeはそのグラフを末端からたどって、更新が必要な経路だけを再実行するのです。

3行のMakefileで、makeの判断を観察する

理屈を実物で確認します。空のディレクトリで、Cファイルとその横に Makefile を作ってください。


/* hello.c */
#include <stdio.h>
int main(void) { printf("hello, make\n"); return 0; }

hello: hello.o
	gcc -o hello hello.o

hello.o: hello.c
	gcc -c hello.c

注意点が1つだけあります。コマンド行の先頭はスペースではなくタブ文字です。スペースにすると、makeはこの有名なエラーを出します。


Makefile:2: *** missing separator.  Stop.

正しく書けたら、makeの判断を3段階で観察します。


make        # 1回目: hello.o も hello もないので、2つのルールが両方実行される
make        # 2回目: 何も変わっていないので → make: 'hello' is up to date.
touch hello.c
make        # 3回目: hello.c が hello.o より新しくなったので、2つとも再実行される

2回目の 'hello' is up to date. が「makeはコマンド実行ツールではなく、実行の要否を判断するツールである」ことの証拠です。3回目では、hello.c の更新が hello.o の再生成を引き起こし、それがさらに hello の再リンクを引き起こす——依存関係のグラフが連鎖的に評価される様子が見えます。

実務で効く3つのオプションと、失敗時の読み方

configure後の実際のビルドで頻繁に使うのは次の3つです。


make -n           # dry run: 実行せずに、実行「されるはずの」コマンドを表示する
make -j4          # 4並列でビルド(依存関係が独立なルールを同時に実行する)
make V=1          # 多くのプロジェクトで、省略表示ではなく実際のコマンド全文を表示

-j の並列数はCPUコア数に合わせるのが定番で、make -j$(nproc) と書けます。ビルド時間が大きく縮む一方、失敗時のログは並列に混ざるので、エラー原因を探すときは make -j1 で再実行して、最初に出た Error を見るのが確実です。


make -j1 2>&1 | tee build.log
grep -n -m1 "Error" build.log

makeのエラー表示には癖があります。たとえば make: [Makefile:2: hello] Error 1 という行は「Makefileの2行目のルールでコマンドが終了コード1で失敗した」という意味で、本当の原因はその直前にあるコンパイラのエラーメッセージです。最後の行だけ見ても原因は分かりません。また、Makefileのないディレクトリで実行したときの make: No targets specified and no makefile found. Stop. は、「まだ ./configure を実行していない」サインであることが多いです。

makeが作り、次の工程が配る

makeはMakefileの依存関係グラフをたどり、タイムスタンプ比較で必要な部分だけを再ビルドする実行エンジン——これが今回の結論です。LFSでは巨大なパッケージを何度もビルドし直すため、この「差分ビルド」の恩恵と、-j 並列化の使いどころを体で覚えることになります。

ただし、makeが終わった時点では、できあがったバイナリはまだビルドディレクトリの中に転がっているだけです。これをシステムの正しい場所(/usr/bin など)へ配置するのが次の工程。次回は make install が「何をどこへ、なぜそこへ」コピーするのかを見ていきます。

図解テキスト


Makefile のルール:
  target: prerequisites
  <TAB>command

依存グラフの例:
  hello  <-  hello.o  <-  hello.c
  (リンク)    (コンパイル)   (ソース)

makeの判断:
  target が無い、または prerequisites の方が新しい → コマンド実行
  それ以外 → 何もしない ('up to date')

この回で実行するコマンド

上から順番に実行すれば、この回の内容を体験できます。

まずはそのままコピペして実行し、次に1行ずつ意味を確認してください。


mkdir -p ~/make-lab && cd ~/make-lab
printf '#include <stdio.h>\nint main(void){ printf("hello, make\\n"); return 0; }\n' > hello.c
printf 'hello: hello.o\n\tgcc -o hello hello.o\n\nhello.o: hello.c\n\tgcc -c hello.c\n' > Makefile
make
make
touch hello.c
make -n
make -j$(nproc 2>/dev/null || sysctl -n hw.ncpu)
./hello

ハンズオン手順

1. ターミナルを開いて、上のコマンドを1行ずつ実行する

2. 2回目の make で up to date が出る理由を、タイムスタンプの言葉で説明してみる

3. make -n の出力と、その後の make -j... で実際に実行されたコマンドを見比べる

4. Makefileのタブを意図的にスペースに変えて make し、missing separator エラーを一度自分の目で見る(見たら元に戻す)

5. エラーが出たら、そのエラーメッセージをそのまま残す

チェックポイント

  • コマンドが最後まで実行できた
  • 「ターゲット・依存ファイル・コマンド」の3要素を説明できる
  • makeが再実行の要否を何で判断しているか言える
  • 次の回に進む前に、分からない単語を1つ調べた

今回理解できたこと

  • makeはビルドの実行エンジンで、依存グラフとタイムスタンプで差分ビルドする
  • コマンド行の先頭はタブ必須で、違反すると missing separator になる
  • -n で実行予定を確認、-j で並列化、失敗調査は -j1 に戻す
  • エラーの本当の原因は *** Error 行ではなくその手前にある