6 ポイント 投稿者 GN⁺ 2025-06-21 | 1件のコメント | WhatsAppで共有
  • 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 ファイルのコンパイル基本例

  1. blah.c ファイルを作成(内容は int main() { return 0; }
  2. 次の 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件のコメント

 
GN⁺ 2025-06-21
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を使っていないことを理由にチュートリアルで飛ばすのは弱い言い訳だと思う。ツールを正しく使う方法を教えるべきだという意見

    • チームではMakeをタスクランナーとして使っており、すべてのレシピに .PHONY を追加して維持することをめぐって議論があった
    • Clark GrubbのMakefileスタイルガイド(clarkgrubb.com/makefile-style-guide)を推薦
    • .PHONY 宣言をレシピごとに書くやり方と、ファイル先頭にまとめて書くやり方の両方を経験しており、linterで強制できたらよいのにと思っている
    • 読んでみた限りでは悪くない文書だが、いくつか同意できない点がある
      • -o pipefail を盲目的に適用するのは問題で、パイプでgrepなどを使うと壊れることがあるため、状況ごとに適用するのを勧める
      • ファイルではないターゲットに .PHONY を付けるのは厳密ではあるが、ほとんどの場合は不要で、Makefileが冗長になるだけなので必要なときだけでよいという見方
      • 複数の出力ファイルを作るレシピは、以前はダミーファイルを使っていたが、最近のGNU Make 4.3以降ではグループターゲットを正式にサポートしている(ここで確認)
  • Makeは大規模なCコードベースのビルドに特化したツールだという主張

    • ある人はプロジェクトごとのジョブランナーとしてよく使っているが、Makeはジョブランナーとしては向いておらず、条件分岐のようなものも扱いにくい構造だという
    • Terraformのようなツールをラップしようとして失敗した例も見たことがある
    • 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を使った経験があるか質問。(公式ドキュメント)

    • tupはファイルシステムアクセスをもとに自動で依存関係を把握し、どんなコンパイラやツールにも適用できるビルドシステム
  • 自分がTaskというMake代替ツールの作者兼メンテナーだと紹介。8年以上開発を続けており、今も進化している

  • このチュートリアルには危険で微妙な問題がある

    • MAKEFLAGSでオプション解析をするとき、長いオプションや空の短いオプションを扱うには次のようにすべき
      ifneq (,$(findstring t,$(firstword -$(MAKEFLAGS))))
    • OS X標準搭載の古いmakeとの互換性が必要なら、かなりの機能が欠けているか微妙に異なる
    • それ以外の問題の大半は誤字やベストスタイル違反なので省略
    • ちなみにloadはguileよりポータブルで、クロスコンパイル環境ではコンパイラ指定を正確に行うべき
    • Paul’s Rules of Makefiles(ここ) とGNU makeマニュアル(ここ)、関連マニュアルは必ず読むことを勧める
    • 簡単なデモ用Makefileプロジェクトも運営している(デモ github)
  • 各GitHubリポジトリに常にMakefileを含める習慣がある

    • 毎回コマンドを忘れがちなので、Makefileに書いておけば簡単に保存でき、複雑なステップも追加しておけるし、make を実行するだけで、別に覚えていなくてもそのプロジェクトで期待される動作をすぐ実行できる