最高のサンプルとともに学ぶMakefile
(makefiletutorial.com)- Makefile は、ファイルの変更を基準に必要なビルド手順だけを再実行し、大規模プロジェクトの C/C++ コンパイル を自動化するために主に使われる
- Make は対象と依存関係の ファイルシステムのタイムスタンプ を比較し、対象が存在しないか依存関係のほうが新しい場合にだけコマンドを実行する
- 基本ルールは
targets: prerequisitesと、タブでインデントされたコマンドで構成され、スペースでのインデントはmakeの失敗につながることがある - 変数、ワイルドカード、自動変数、パターンルール、条件文、関数、再帰 make、
include、.PHONY、.DELETE_ON_ERRORによってビルドフローを拡張できる - Cookbook の例では
src/の C/C++ ソースからbuild/の生成物を作り、-MMD -MPと-includeで 自動依存関係管理 を構成する
Makefile が必要な場面
- Makefile は、大きなプログラムでどのファイルを再コンパイルすべきか判断するために使われる
- チュートリアルの中心的な例は C/C++ ファイルのコンパイル
- ファイル変更の有無に応じて一連のコマンドを実行する必要がある作業にも、Make をコンパイル以外の用途で活用できる
- C/C++ の他のビルドツールとして SCons、CMake、Bazel、Ninja が紹介されている
- Java には Ant、Maven、Gradle があり、Go、Rust、TypeScript のような言語は独自のビルドツールを持つ
- Python、Ruby、純粋な JavaScript のようなインタプリタ言語は、ファイルが変わっても再コンパイルが不要なため、Makefile のようなツールが必須ではない
Make の基本実行モデル
- 例では、
makeがインストールされたターミナルでMakefileファイルを作成し、そのディレクトリでmakeを実行する形で動作する - 基本ルールは以下の形式に従う
targets: prerequisites- タブで始まる
command
- 対象 (target) は通常ファイル名であり、コマンドはその対象を作るための手順
- 依存関係 (prerequisites) はコマンド実行前に存在していなければならないファイルで、dependencies とも呼ばれる
- Makefile のコマンド行は必ず タブ文字 でインデントする必要があり、スペースを使うと
makeが失敗する
対象と依存関係の時間比較
make helloはhello対象に結び付いたコマンドを実行するが、helloファイルがすでに存在する場合はコマンドを実行しない- 対象とファイル名は直接結び付いており、通常はその対象のコマンドが対象と同名のファイルを作成する
blah: blah.cルールでは、blahが存在しないか、blah.cがblahより新しい場合にだけコンパイルコマンドが実行される- Make は変更履歴を直接追跡せず、ファイルシステムのタイムスタンプ をヒューリスティックとして利用する
- ファイルを修正した後で更新時刻を過去に戻すと、Make がそのファイルは変更されていないと誤判定することがある
デフォルトターゲットと clean
makeに対象を指定しない場合、Makefile の 最初の対象 がデフォルトの対象として実行されるall対象は複数の対象を一度に作るときに使われ、最初のルールに置けばmakeだけで全体をビルドできるcleanは通常ビルド生成物を削除する対象として使われるが、Make の特別な予約語ではないcleanが最初の対象でもなく、他の対象の依存関係でもない場合は、make cleanのように明示的に呼び出す必要がある- 実際に
cleanというファイルが存在するとclean対象が実行されないことがあり、これは後述の.PHONYで解決する
変数と文字列処理
- Make の変数は文字列しか持てず、チュートリアルでは一般に
:=の使用を推奨している - 変数参照は
$(name)または${name}形式を使う - シングルクォートとダブルクォートは Make 自体では特別な意味を持たず、単に変数に入る文字である
- 変数定義の方法は大きく 2 つある
=: 再帰変数で、利用時に変数を探して展開する:=: 単純展開変数で、定義時点で既に定義されている値だけを展開する
?=はまだ設定されていない変数にだけ値を設定する+=は変数に値を追加する- 未定義の変数は 空文字列 として扱われる
- 行末の空白は削除されず、1 文字分の空白変数を作るには
$(nullstring)のようなテクニックを使える
ワイルドカードと自動変数
- Make では
*と%のどちらもワイルドカードと呼ばれるが、意味は異なる *はファイルシステム上で一致するファイル名を探し、変数定義に直接使うより$(wildcard *.o)のように wildcard 関数 で包むほうが安全*がどのファイルにも一致しない場合、wildcard関数の外ではそのまま残ることがある%はルールや特定の関数で使われ、一致した部分は stem と呼ばれる- よく使われる自動変数は以下の通り
$@: 対象名$?: 対象より新しい依存関係$^: すべての依存関係$<: 最初の依存関係
暗黙ルールとパターンルール
- Make には C コンパイルのための 暗黙ルール があり、明示的にコンパイルコマンドを書かなくてもビルドが進むことがある
- 代表的な暗黙ルールは以下の形式
n.oはn.cから$(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@の形で生成されるn.oはn.ccまたはn.cppから$(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@の形で生成されるnはn.oからリンカコマンドで生成される
- 暗黙ルールで重要な変数は
CC、CXX、CFLAGS、CXXFLAGS、CPPFLAGS、LDFLAGS - 静的パターンルールは
targets...: target-pattern: prereq-patterns ...の形式で、対象の%に一致した stem を依存関係パターンに代入する - パターンルールは
%.o: %.cのように対象に%を含み、自作の暗黙ルールのように使える - 二重コロンルール
::は同じ対象に複数のルールを定義できるが、使われることはまれ
コマンド実行とエラー処理
- コマンドの前に
@を付けると、そのコマンド自体は表示されない make -sを実行すると、各行の前に@を付けたのと同じように動作する- 各コマンド行は新しいシェルで実行されるのと同じ効果を持つため、1 行の
cd ..は次の行に影響しない - 同じシェル状態を維持したいなら、
cd ..; echo \pwd`` のように 1 行で書くか、バックスラッシュでつなぐ必要がある - デフォルトシェルは
/bin/shで、SHELL=/bin/bashのように変更できる - シェル変数の
$を Makefile 内で使うには$$を使う - エラー処理は実行方法によって異なる
make -k: エラーがあっても可能な他の作業を続けて実行する- コマンドの前の
-: そのコマンドのエラーを無視する make -i: すべてのコマンドにエラー無視を適用する
ctrl+cで Make を中断すると、Make はたった今作成した最新の対象を削除する
再帰 make と環境変数
- Makefile の中で再び make を呼び出すときは、
makeではなく特殊変数$(MAKE)を使う $(MAKE)は make フラグを引き継ぎ、その影響を適切に処理する- Make が起動すると、実行時に設定されていた環境変数から Make 変数が自動的に作られる
exportディレクティブは、変数をすべての recipe のシェル環境に設定する- 下位の make でも変数を使うには、その変数を
exportする必要がある .EXPORT_ALL_VARIABLESを使うと、すべての変数を export できるmake clean run testのように複数の対象を一度に指定すると、clean、run、testの順に実行される
条件文と関数
- 条件文は
ifeq、ifneq、ifdef、ifndef、else、endifで構成される ifdefは変数参照を展開せず、変数が定義されているかどうかだけを確認する$(MAKEFLAGS)とfindstringを使って、make -iのようなフラグが渡されたかどうかを確認できる- Make の関数は主に テキスト処理 に使われ、
$(fn, arguments)または${fn, arguments}の形式で呼び出す - 主な関数は以下の通り
subst: 文字列置換patsubst: パターンに一致する単語の置換foreach: 空白区切りの単語リストを変換if: 1 つ目の引数が空でなければ 2 つ目、そうでなければ 3 つ目の引数を使うcall: 変数を基本関数のように呼び出し、$(1)、$(2)のような引数を使うshell: シェルを呼び出すが、改行は空白に変換するfilter: リストから特定パターンに一致する要素を選ぶfilter-out: 特定パターンに一致しない要素を選ぶ
include、vpath、.PHONY、.DELETE_ON_ERROR
include filenames...は、1 つ以上の別の Makefile を読み込むincludeは、gccの-Mのようなオプションで生成した Makefile を読み込むときに便利vpath <pattern> <directories>は、特定の依存ファイルを探すディレクトリを指定するVPATH変数で、同様の検索パス動作をグローバルに指定できる- バックスラッシュ
\は、長いコマンドを複数行に分けて書くために使う .PHONYを対象に追加すると、Make はその対象を同名のファイルと混同しない.DELETE_ON_ERRORは、ルール実行中にコマンドが 0 以外の終了ステータスを返した場合、そのルールの対象を削除する.DELETE_ON_ERRORはPHONYのように直前の対象にだけ適用されるのではなく、すべての対象に適用される
Makefile Cookbook の例
- Cookbook の例は、中規模プロジェクト向けの Makefile テンプレート を提供する
src/フォルダに C/C++ ファイルを置くと、Makefile がコンパイル対象と依存関係を自動で決定する- 主な変数構成は以下の通り
TARGET_EXEC := final_programBUILD_DIR := ./buildSRC_DIRS := ./src
SRCSはfindと$(shell ...)を使って*.cpp、*.c、*.sファイルを探すOBJSは各ソースファイルの前にBUILD_DIRを付け、.oを追加してビルド生成物のパスを作るDEPSは.oを.dに置き換えて依存関係 Makefile の一覧を作るINC_DIRSはsrc/配下のすべてのディレクトリを探し、INC_FLAGSは各ディレクトリに-I接頭辞を付けるCPPFLAGS := $(INC_FLAGS) -MMD -MPは、GCC が.d依存ファイルを作成するように設定する- C と C++ のソースはそれぞれ別のパターンルールで
build/配下に.oファイルを作成し、必要なディレクトリはmkdir -p $(dir $@)で作る cleanは.PHONYとして宣言され、rm -r $(BUILD_DIR)でビルドディレクトリを削除する-include $(DEPS)は.dファイルを取り込むが、初回ビルドのようにファイルが存在しないときに発生するエラーは抑制する
1件のコメント
Hacker News のコメント
あまり知られていないが便利な
makeフラグがいくつかある。出力同期は、ターゲットが終わってから stdout/stderr を出力するようにして、並列ビルド時にログが混ざって読みにくくなる問題を軽減してくれるmake --output-sync=recurse -j10忙しいマルチユーザーシステムでは、
-jのジョブ数だけでは不十分な場合があるため、ロードアベレージを基準に並列性を制限することもできる:make -j10 --load-average=10ターゲットのスケジュール順序をランダム化する
--shuffleも、CI で Makefile の抜けている依存関係を見つけるのに役立つ:make --shuffle # or --shuffle=seed/reverse-Bで、無条件に全体を再ビルドさせるオプションmake -jがマシンを何度も DoS 状態にするのを見てきたので、ほとんどバグのように捉えている1985年ごろ、Boston University Graphics lab で Makefile を使ってアニメーション用の 3D レンダラー生成を動かしていた人を見た。Lisp 系の人で、初期の手続き型生成と 3D 俳優システムを扱っていたのだが、Makefile は全体で10行ほどで、とても優雅だった
単純なファイル日付の依存関係だけで数百本のアニメーションを生成し、Lisp が各フレームの 3D 形状を作ると、Make がフレームを生成していた
1985年だったので、今の 3D やアニメーションで当然だと思われているものがほとんどなかった時代で、皆を驚かせていた。その後 Iron Giant の 3D レンダラーを書き、Coraline でも重要な役割を果たしたと記憶している。Brian Gardner だった
Make はキャリアの初期に学べて本当によかったと感じるツールの一つだ。今ではあまり使っていないが、命令型システムよりも宣言型システムが持つ力を示してくれたからだ
このアイデアが他の作業にも自然に拡張できることに気づいた。このサイト上部の図を見ると、筆者も同じように感じているようで、従来のレシピのようにスクリプト的な命令の順序で書くよりも、Makefile のように宣言的に見るほうが料理レシピをよりよく理解できる
関連記事: https://blog.gpkb.org/posts/cooking-with-make/
レシピはいつも Makefile のように読める形で書いて台所に持っていくのだが、こういう組版や表示を試した人がいるのか気になる。新しいレシピを読むときに、スクリプトを頭の中で Makefile に変換しなくて済むので、かなり時間を節約できそうだ
もちろん代償は、依存関係グラフを自分で解かなければならないことだ。単に事前に直列化された順次ステップに従いたい場合には、より多くの作業を背負うことになる
記事では、ほとんどのレシピは
.PHONYとしてマークしないと言っており、チュートリアルであえて扱わない理由のように書いているが、それは弱い言い訳だ。ツールは正しい使い方を教えるべきだチームでは make をタスクランナーとして使っているので、すべてのレシピに
.PHONYを追加して維持していることについて小言を言われたClark Grubb の Makefile スタイルガイドは良い: https://clarkgrubb.com/makefile-style-guide
このスタイルガイドを使っている人がいるのか気になる。phony レシピは宣言する箇所でマークする派なのか、ファイル上部に巨大な一覧として置く派なのかも気になる。これを強制するリンターがあるといい
-o pipefailを惰性で使うのはあまりよくない。Pipefail は有用だが、パイプラインで人々が最もよく行う作業であるgrepによる出力フィルタリングを壊してしまう。必要なレシピにだけ個別に入れるほうがよいファイルではないターゲットを
.PHONYとしてマークするのは厳密には正しいが、たいていは必要ない。ターゲットが多いと Makefile に不要な冗長さを増やすので、必要なときだけ追加するほうがよいと思う複数の出力ファイルを作るレシピでは、以前はパターンルールが合わないときにダミーファイル/フラグファイルを使うのが標準だったが、GNU Make 4.3 からはグループターゲットをネイティブにサポートしている。Ubuntu 22.04 LTS にも入っている: https://www.gnu.org/software/make/manual/html_node/Multiple-...
最近興味深い点として、CMake は C++20 モジュールを使うプロジェクトには Makefile は不適切で、Ninja が適していると判断していることが挙げられる。https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules....
基本的に、ターゲットの依存関係を静的に定義するのが難しすぎる、あるいは不可能だと見ているようだ。今では
clang-scan-depsのようなツールで動的に処理する: https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...実際、Ninja はすべての依存関係を静的に定義する必要があるため、この点では通常の Make より機能が少ない
Make は大規模な C コードベースのビルドツールとしては居場所がある。しかし、しばしば汎用の「プロジェクト別タスクランナー」のように扱われるが、その用途にはあまり向いていない。単純な条件分岐ですら難しいからだ
たとえば Terraform を Make でラップしようとする善意の試みを何度も見たが、良い結末にはならなかった
Makefile が散らかりがちな部分を置き換える優れた現代的ツールとして just がある: https://github.com/casey/just
まったく別の概念として、8か月前に HN で取り上げられた Makedown もある: https://news.ycombinator.com/item?id=41825344
Makefile は素晴らしいが、のめり込みすぎないほうがいい。数年前、純粋な GNU Make フレームワークを作ろうとして、実質的に autoconf を再発明していることに気づき、そのとき初めて GNU autotools がなぜ作られたのか理解した
Makefile は妙に Lisp 的なチューリングターピットだ。GNU Make にはメタプログラミング機能まであるので、Makefile の中に冒涜的なシステムをメタプログラミングしたくなる衝動を抑えるのが難しい。GNU Make が非常に広く入っていることも、その誘惑を強めている
Make の代替である Task を作った人間であり、メンテナーの一人でもある。8年以上存在していて今も発展中なので、新しいものを探しているなら一度試してみるといい。質問も気軽にどうぞ
https://taskfile.dev/
https://github.com/go-task/task
tup を使ったことがある人がいるか気になる: https://gittup.org/tup/ex_dependencies.html
ファイルシステムへのアクセスに基づいて依存関係を自動判定するビルドシステムなので、どんなコンパイラやツールとも一緒に使える