最高のサンプルと一緒に学ぶMakefile
(makefiletutorial.com)- Makefile は C/C++ のビルド自動化と依存関係管理を簡素化するツール
- タイムスタンプを活用した変更ファイル検出 により、必要な場合にのみコンパイルを実行する
- ルール(rule)、コマンド(command)、依存関係(prerequisite) などの 中核構造 をサンプルとともに説明
- 自動変数、パターンルール、変数展開 といった高度な機能も実用的に扱う
- 中規模プロジェクト向けの 実践的な Makefile テンプレート を通じて、拡張性と保守性の重要性を紹介
Makefile チュートリアルガイド紹介
- Makefile はプロジェクトのビルド自動化と依存関係管理を担う中核ツール
- さまざまな暗黙ルールや記号のため、初見では複雑に感じられることがあるが、このガイドでは主要な内容を簡潔かつそのまま実行できるサンプルで整理している
- 各セクションごとに、実習ベースの例を通じて理解できる
はじめに
Makefile の目的
- Makefile は大規模プログラムで 変更された部分だけを再コンパイル するために使われる
- C/C++ 以外にも言語ごとの専用ビルドツールは存在するが、Make は一般的なビルドシナリオ全般で活用される
- 変更されたファイルを検知して必要な作業だけを実行 するロジックが中核
Make の代替ビルドシステム
- C/C++ 系: SCons, CMake, Bazel, Ninja など複数の選択肢がある
- Java 系: Ant, Maven, Gradle など
- Go, Rust, TypeScript なども独自のビルドツールを提供
- Python, Ruby, JavaScript などの インタプリタ言語 はコンパイルが不要なため、Makefile のような別管理の必要性は低い
Make のバージョンと種類
- さまざまな Make 実装があるが、本ガイドは GNU Make(主に Linux、MacOS で使用)向けに最適化されている
- サンプルは GNU Make 3 と 4 の両方に対応
サンプルの実行方法
- ターミナルで make をインストールした後、各サンプルを
Makefileとして保存し、makeコマンドを実行する - Makefile 内のコマンド行は必ず タブ文字 でインデントする必要がある
Makefile の基本構文
ルール(Rule)の構造
-
ターゲット: 依存関係- コマンド
- コマンド
-
ターゲット: ビルド結果のファイル名(通常は1つ)
-
コマンド: 実際に動作するシェルスクリプト(タブで開始)
-
依存関係: ターゲットをビルドする前に必ず準備されているべきファイルの一覧
Make の本質
Hello World サンプル
hello:
echo "Hello, World"
echo "This line will print if the file hello does not exist."
- ターゲット
helloには依存関係がなく、2つのコマンドを実行する make helloを実行したとき、ファイルhelloが存在しなければコマンドが実行される。すでにファイルがある場合は実行されない- 一般的にはターゲット名とファイル名が一致するように書く
C ファイルのコンパイル基本例
blah.cファイルを作成(内容はint main() { return 0; })- 次の Makefile を作成
blah:
cc blah.c -o blah
make実行時にblahターゲットが存在しなければコンパイルが実行され、blahファイルが生成されるblah.cを変更しても自動再コンパイルはされない → 依存関係の追加が必要
依存関係の追加方法
blah: blah.c
cc blah.c -o blah
- これで
blah.cが新しく変更された場合、blahターゲットが再ビルドされる - ファイルの タイムスタンプ を変更検出の基準として使う
- タイムスタンプを任意に操作すると、意図どおりに動かない場合がある
サンプルの追加
連結されたターゲットと依存関係の例
blah: blah.o
cc blah.o -o blah
blah.o: blah.c
cc -c blah.c -o blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
- ツリー構造で依存関係をたどりながら、各段階の生成処理が自動化される
必ず実行されるターゲットの例
some_file: other_file
echo "This will always run, and runs second"
touch some_file
other_file:
echo "This will always run, and runs first"
other_fileは実際のファイルとして生成されないため、some_fileのコマンドは毎回実行される
Make clean
cleanターゲットはビルド成果物を削除する用途でよく使われる- Make の特別な予約語ではなく、自分でコマンドとして定義する必要がある
- もしファイル名が
cleanだと混乱する可能性があるため、.PHONYの使用を推奨
例:
some_file:
touch some_file
clean:
rm -f some_file
変数処理
- 変数は常に 文字列。
- 通常は
:=を推奨し、=,?=,+=などさまざまな代入方法がある - 使用例:
files := file1 file2
some_file: $(files)
echo "Look at this variable: " $(files)
touch some_file
file1:
touch file1
file2:
touch file2
clean:
rm -f file1 file2 some_file
- 変数参照の方法:
$(variable)または${variable} - Makefile 内の引用符は Make 自体には意味がない(ただしシェルコマンドでは必要)
ターゲット管理
all ターゲット
- 複数のターゲットをまとめて実行するには、最初の(デフォルト)ターゲットに役割を持たせる
all: one two three
one:
touch one
two:
touch two
three:
touch three
clean:
rm -f one two three
複数ターゲットと自動変数
- 複数ターゲットに対して、それぞれ個別にコマンドを実行できる。
$@は現在のターゲット名を表す
all: f1.o f2.o
f1.o f2.o:
echo $@
自動変数とワイルドカード
* ワイルドカード
*はファイルシステム上の名前を直接探索する- 必ず
wildcard関数で包んで使うことが推奨される
print: $(wildcard *.c)
ls -la $?
- 変数定義で直接
*を使ってはいけない
thing_wrong := *.o
thing_right := $(wildcard *.o)
% ワイルドカード
- 主に パターンルール で使われ、指定パターンを抽出して展開できる
Fancy Rules
暗黙(Implicit)ルール
- Make には C/C++ ビルドに関する さまざまな隠れたデフォルトルール が組み込まれている
- 代表的な変数:
CC,CXX,CFLAGS,CPPFLAGS,LDFLAGSなど - C の例:
CC = gcc
CFLAGS = -g
blah: blah.o
blah.c:
echo "int main() { return 0; }" > blah.c
clean:
rm -f blah*
Static Pattern Rules
- 同じパターンに従う複数のルールを簡潔に書ける
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all
$(objects): %.o: %.c
$(CC) -c $^ -o $@
all.c:
echo "int main() { return 0; }" > all.c
%.c:
touch $@
clean:
rm -f *.c *.o all
Static Pattern Rules + filter 関数
- filter を活用すると、特定の拡張子パターンに一致する対象だけを選べる
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c
all: $(obj_files)
.PHONY: all
$(filter %.o,$(obj_files)): %.o: %.c
echo "target: $@ prereq: $
1件のコメント
Hacker Newsのコメント
1985年にBoston University Graphicsラボで、ある人物がMakefileを使ってアニメーション用の3Dレンダラーを作っていたのを実際に見た経験がある。その人はLispプログラマーで、初期のプロシージャル生成と3Dアクターシステムに取り組んでおり、10行ほどの本当にエレガントなMakefileを書いていた。単純なファイル日付依存だけで何百ものアニメーションを自動生成する構成だった。各フレームの3D形状をLispで作り、Makeがフレームを生成する方式だった。1985年当時は、今のように3Dやアニメーションを当たり前と見る時代ではなく、皆が驚いていた。その人物が後にIron GiantとCoralineの3Dレンダラーを担当したBrian Gardnerだったことを覚えている
もしかするとこの人は 3d-consultant.com/bio.html に載っている人物なのか、と気になっている様子
Coralineという映画のことを言っているのか確認
Makeを使う際の、あまり知られていない便利なフラグをいくつか紹介
--output-sync=recurse -j10: 各ターゲットの処理が終わるまでstdout/stderrをまとめてから出力するフラグで、これがないとログが混ざって解析しにくい-jの代わりに--load-averageを使って並列実行時のシステム負荷を調整できる (make -j10 --load-average=10)--shuffleオプションは、CI環境でMakefile内の依存関係の問題を見つけるのに有益makeのさまざまなオプションを公式に整理して、テキストや文書形式でプログラムに含めれば、利用しやすくなるのではというアイデアに言及
自分がよく使うオプションとして、全体を強制ビルドする
-Bフラグを説明make -jのせいでDOSマシンで問題が起きるのをよく見たので、その現象をバグだと認識している忙しいシステムやマルチユーザー環境での並列化の問題は、OSスケジューラが処理すべきことではないのかと質問
便利なフラグではあるが、これらのオプションはポータブルではないので、自分専用の非公開プロジェクト以外では使わない方がよいと勧める
.PHONYを使っていないことを理由にチュートリアルで飛ばすのは弱い言い訳だと思う。ツールを正しく使う方法を教えるべきだという意見
-o pipefailを盲目的に適用するのは問題で、パイプでgrepなどを使うと壊れることがあるため、状況ごとに適用するのを勧めるMakeは大規模なCコードベースのビルドに特化したツールだという主張
Makeはジョブランナーというより、線形なシェルスクリプトを宣言的な依存関係の形に変換する汎用シェルツールだという意見
MakeをCコードベース専用のビルドツールとして見る考え方は、もはや正しくないという立場。過去20年で、より堅牢で明確なビルドシステムが開発されてきた現実に触れ、認識をアップデートすべきだと指摘
良いジョブランナーとは何か質問。(自分がジョブランナーという意味を取り違えていたと謝罪を追加)
Makefileが複雑になりがちな部分をモダンに置き換えるツールとして just を推薦
justはshellスクリプトの一覧を置き換えるには良いが、「再実行が必要なルールだけを実行する」というMakeの本質的な機能は置き換えられない
そのほかの代替として
代替ツールは自分たちをMakeの代替だと称しているが、自分にはまったく別物に思え、比較自体が難しい。Makeの核心は成果物の生成と、すでにビルド済みのものを再ビルドしないことにある。一方でjustは単なるコマンド実行器の役割
Makeをコマンド実行器として使う利点は、ほぼどこにでも入っている標準ツールだという安心感にある。代替のほうが出来が良くても、別途インストールの手間があるので、そこまで使う必要性を感じない
Taskは、自分がCでやっている簡単な趣味プロジェクトではうまく使えているが、大規模プロジェクトにも向くかどうかはまだ判断が難しい(Task公式サイト)
最近CMakeが、MakefileはC++20モジュール対応に適していないとして、ninjaをデフォルトに選んだ点が興味深い(CMakeガイド)
clang-scan-depsのようなツールで動的に解析する方式を採っている(技術スライド)実際にはこの制約はCMake側の判断、あるいはMakefile generatorに支援者がいない問題だと思う。ninjaもC++モジュールを直接サポートしていないし(関連イシュー)、ninjaはむしろMakeより機能が少なく、すべての依存関係を静的に明示しなければならないという問題もあると指摘
モジュール導入そのものが複雑で混乱しているという意見
tupを使った経験があるか質問。(公式ドキュメント)
自分がTaskというMake代替ツールの作者兼メンテナーだと紹介。8年以上開発を続けており、今も進化している
justも別のMake代替として推薦(just GitHub)
面白い偶然だが、自分はTaskをよく使っていて、今朝も issueを投稿した
このチュートリアルには危険で微妙な問題がある
ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))各GitHubリポジトリに常にMakefileを含める習慣がある
makeを実行するだけで、別に覚えていなくてもそのプロジェクトで期待される動作をすぐ実行できる