6 ポイント 投稿者 GN⁺ 2025-06-21 | 1件のコメント | WhatsAppで共有
  • 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++ の他のビルドツールとして SConsCMakeBazelNinja が紹介されている
  • Java には AntMavenGradle があり、Go、Rust、TypeScript のような言語は独自のビルドツールを持つ
  • Python、Ruby、純粋な JavaScript のようなインタプリタ言語は、ファイルが変わっても再コンパイルが不要なため、Makefile のようなツールが必須ではない

Make の基本実行モデル

  • 例では、make がインストールされたターミナルで Makefile ファイルを作成し、そのディレクトリで make を実行する形で動作する
  • 基本ルールは以下の形式に従う
    • targets: prerequisites
    • タブで始まる command
  • 対象 (target) は通常ファイル名であり、コマンドはその対象を作るための手順
  • 依存関係 (prerequisites) はコマンド実行前に存在していなければならないファイルで、dependencies とも呼ばれる
  • Makefile のコマンド行は必ず タブ文字 でインデントする必要があり、スペースを使うと make が失敗する

対象と依存関係の時間比較

  • make hellohello 対象に結び付いたコマンドを実行するが、hello ファイルがすでに存在する場合はコマンドを実行しない
  • 対象とファイル名は直接結び付いており、通常はその対象のコマンドが対象と同名のファイルを作成する
  • blah: blah.c ルールでは、blah が存在しないか、blah.cblah より新しい場合にだけコンパイルコマンドが実行される
  • 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.on.c から $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@ の形で生成される
    • n.on.cc または n.cpp から $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@ の形で生成される
    • nn.o からリンカコマンドで生成される
  • 暗黙ルールで重要な変数は CCCXXCFLAGSCXXFLAGSCPPFLAGSLDFLAGS
  • 静的パターンルールは 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 のように複数の対象を一度に指定すると、cleanruntest の順に実行される

条件文と関数

  • 条件文は ifeqifneqifdefifndefelseendif で構成される
  • 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_ERRORPHONY のように直前の対象にだけ適用されるのではなく、すべての対象に適用される

Makefile Cookbook の例

  • Cookbook の例は、中規模プロジェクト向けの Makefile テンプレート を提供する
  • src/ フォルダに C/C++ ファイルを置くと、Makefile がコンパイル対象と依存関係を自動で決定する
  • 主な変数構成は以下の通り
    • TARGET_EXEC := final_program
    • BUILD_DIR := ./build
    • SRC_DIRS := ./src
  • SRCSfind$(shell ...) を使って *.cpp*.c*.s ファイルを探す
  • OBJS は各ソースファイルの前に BUILD_DIR を付け、.o を追加してビルド生成物のパスを作る
  • DEPS.o.d に置き換えて依存関係 Makefile の一覧を作る
  • INC_DIRSsrc/ 配下のすべてのディレクトリを探し、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件のコメント

 
GN⁺ 2025-06-21
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

    • 「あまり知られていない」なら、make の作者たちがオプション一覧をどこかにまとめてプログラムと一緒に配布してくれるといい。ユーザーが読めるテキストファイルや組版言語のような形式なら、知識へのアクセス性がずっと良くなりそうだ
    • 一番よく使うのは -B で、無条件に全体を再ビルドさせるオプション
    • make -j がマシンを何度も DoS 状態にするのを見てきたので、ほとんどバグのように捉えている
    • 忙しいマルチユーザーシステムなら、OS のスケジューラで処理できないものなのかと思う
    • 便利ではあってもポータブルではない。配布しない個人の遊びプロジェクト以外では使わないほうがいい
  • 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 に変換しなくて済むので、かなり時間を節約できそうだ

    • このアプローチの良い点は、ユーザーが実質的に依存関係グラフを自分で解釈し、実行者になるので、より多くの制御権を持てることにある。料理の例で言えば、宣言型の構造は次に何をするか選択肢が開いている地点をよりよく示し、洗い物がしやすい順番のように 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...

    • 間違っていたら訂正してほしいが、この Makefile に関する制限は完全に CMake の選択であるか、少なくとも Makefile ジェネレーターにサポートを追加するボランティアが不足していた結果のように見える。Ninja 自体も C++ モジュールをサポートしていない: https://github.com/ninja-build/ninja/issues/2457
      実際、Ninja はすべての依存関係を静的に定義する必要があるため、この点では通常の Make より機能が少ない
    • 正直、モジュールは災厄に近い
  • Make は大規模な C コードベースのビルドツールとしては居場所がある。しかし、しばしば汎用の「プロジェクト別タスクランナー」のように扱われるが、その用途にはあまり向いていない。単純な条件分岐ですら難しいからだ
    たとえば Terraform を Make でラップしようとする善意の試みを何度も見たが、良い結末にはならなかった

    • Make は汎用タスクランナーではない。線形のシェルスクリプトを宣言的な依存関係に変換するための汎用的な方法に近い。シェルのための一般的なツールだ
    • 大規模な C コードベース向けのビルドツールという言い方も、今では正しくないと思う。この20年で、より堅牢で明確に定義されたビルドシステムが作られてきたので、アップデートする時期だ
    • 良い汎用タスクランナーはあるのだろうか? 追記: 「タスクランナー」という言葉を完全に誤解していたようだ
  • Makefile が散らかりがちな部分を置き換える優れた現代的ツールとして just がある: https://github.com/casey/just

    • Make の「短いシェルスクリプト一覧」という側面は置き換えるが、本当に有用な部分である「再実行が必要なルールだけを実行する」機能は置き換えない
    • ほかの選択肢もある: Task(Go) https://github.com/go-task/task, Cake(C#) https://github.com/cake-build/cake, Rake(Ruby) https://github.com/ruby/rake
      まったく別の概念として、8か月前に HN で取り上げられた Makedown もある: https://news.ycombinator.com/item?id=41825344
    • これらのツールは自らを make の代替として位置づけてはいるが、個人的には完全に別物で、比較対象でもないと思う。make は成果物の生成と、すでに作られたものを再ビルドしないことに重点があり、just はコマンドランナーだ
    • Make をコマンドランナーとして使う主な利点は、「どこにでも」インストールされている標準ツールである点だ。代替ツールのほうが使いやすそうに見えても、追加ツールのインストールを正当化するほど大きな利点を感じたことはない
    • Task も別の代替だ。ただし個人的には単純な C の趣味プロジェクトでしか使ったことがないので、規模が大きくなっても耐えられるかは何とも言えない: https://taskfile.dev/
  • 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
    ファイルシステムへのアクセスに基づいて依存関係を自動判定するビルドシステムなので、どんなコンパイラやツールとも一緒に使える