世界を動かす技術を、日本語で。

Makefileの学習

概要

  • Makefile の基本構造と動作原理を分かりやすく解説
  • C/C++ コンパイルを中心に、依存関係やビルドの仕組みを説明
  • 変数・自動変数・ワイルドカード などの使い方を具体例で紹介
  • cleanターゲット などの運用パターンや注意点もカバー
  • GNU Make を前提にした説明で、実際に動かせるサンプル付き

Makefile入門ガイド

  • Makefile は、大規模プログラムのどの部分を再コンパイルすべきかを決定するためのツール
  • 主に C/C++ のビルドで利用されるが、他の用途や言語にも応用可能
  • 依存関係グラフ を定義し、ファイルの更新状況に応じて必要な処理のみ実行
  • Make の代替としては、SCons、CMake、Bazel、Ninjaなどが存在
  • JavaやGo、Rustなど他言語にはそれぞれ独自のビルドツールがある

Makefileの基本構造

  • ルール(rule) は「ターゲット: 依存ファイル コマンド」の形で記述
  • コマンドは タブ文字 で始める必要あり(スペース不可)
  • 依存ファイルが更新されていれば、ターゲットのコマンドが実行される
  • ターゲット名と生成されるファイル名は一致することが多い

例:Hello World

  • 最小のMakefile例
    • hello: echo "Hello, World"
  • make helloを実行すると、helloファイルがなければコマンドが実行される

例:Cプログラムのビルド

  • blah.cファイル(内容:int main() { return 0; })を用意
  • Makefile例
    • blah: blah.c cc blah.c -o blah
  • make実行時、blahがなければコンパイル
  • blah.cを更新すれば再コンパイルされる

Makefileの本質

  • 依存ファイルのタイムスタンプ を元に、再ビルドが必要か自動判定
  • ファイルを改ざんし、タイムスタンプを古くすると、正しく検知できない場合もある

依存関係の連鎖例

  • 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
  • 依存ファイルを削除・変更・touchすることで、どのターゲットが再実行されるか実験可能

常に実行されるルール

  • 依存ファイルが生成されない場合、ターゲットは常に実行される
    • 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"

cleanターゲット

  • clean はビルド成果物の削除用ターゲット
  • デフォルトターゲットや依存関係に含まれないため、明示的にmake cleanで実行
  • cleanファイルが存在すると実行されないため、.PHONY指定推奨

変数の利用

  • 変数は := で定義(=も可)
    • files := file1 file2
    • some_file: $(files) echo "Look at this variable: " $(files) touch some_file
  • 変数参照は$(変数名)または${変数名}

allターゲットと複数ターゲット

  • デフォルトで複数ターゲットをビルドしたい場合、 all ターゲットを利用
    • all: one two three
    • one: touch one
    • two: touch two
    • three: touch three

ワイルドカードの使い方

  • * はファイル名のマッチに利用、wildcard関数推奨
    • 例:$(wildcard *.c)
  • % はパターンマッチや置換に利用。ルール定義や特定関数で活躍

自動変数

  • $@(ターゲット名)、$^(すべての依存ファイル)、$<(最初の依存ファイル)、$?(ターゲットより新しい依存ファイル)など
    • hey: one two echo $@ echo $? echo $^ echo $< touch hey

暗黙ルール(implicit rules)

  • CコンパイルなどはMakeの 暗黙ルール で自動的に処理される場合あり
  • 暗黙ルールの利用は便利だが、混乱の元になることもあるため注意

このガイドを参考に、 Makefile の仕組みや記述方法を実際に手を動かしながら学習することを推奨

Hackerたちの意見

役に立つけどあまり知られてないmakeのフラグがいくつかあるよ。出力の同期を取るやつで、ターゲットが終わった時だけmakeがstdout/stderrを一度だけ表示するようにするんだ。普通はインターリーブされてて追いかけるのが大変だからね。使い方はこう:make --output-sync=recurse -j10 それから、忙しいマルチユーザーシステムでは、ジョブのための-jフラグはあまり良くないかも。代わりに負荷平均に基づいて並列処理を制限することもできるよ:make -j10 --load-average=10 ターゲットのスケジュール順をランダムにするのもいいね。CIでMakefileを強化したり、ターゲット間の依存関係を見逃してないか確認するのに便利だよ:make --shuffle # または --shuffle=seed/reverse

役に立つmakeのフラグがいくつかあるけど、ポータブルじゃないから注意してね。自分の配布しないおもちゃプロジェクト以外では使わないで。

一番よく使うのは、全てを無条件にビルドするための-Bだね。

「make -j」を使ったマシンを何度も見てきたから、これをバグだと思ってる。

おそらくあまり知られてないことだけど、作成者たちがどこかに選択肢のリストをまとめて、そのプログラムと一緒に配布すれば、ユーザーが読めるようになるんじゃないかな?テキストファイルとか、何かの組版言語を使って。そうすれば、その知識がもっと身近になると思う。

多忙な / 複数ユーザーのシステムでは、OS スケジューラがそれを処理できないの?

最近面白いのは、CMakeがC++20モジュールを使うプロジェクトにはMakefileが不適切だと判断して、ninjaがベストだってこと。基本的に、ターゲットの依存関係を静的に定義するのは難しすぎるか、ほぼ不可能と見なされてるんだ。今はclang-scan-depsみたいなツールを使って動的に行われているよ。 [1] https://cmake.org/cmake/help/latest/manual/cmake-cxxmodules.... [2] https://llvm.org/devmtg/2019-04/slides/TechTalk-Lorenz-clang...

モジュールは正直言って大惨事だね。

Makefileがごちゃごちゃする部分のための素晴らしい現代的な代替品だよ: https://github.com/casey/just

それか、- Task (Go): https://github.com/go-task/task - Cake (C#): https://github.com/cake-build/cake - Rake (Ruby): https://github.com/ruby/rake あるいは全く異なるコンセプトのMakedownについて、8ヶ月前にHNで話題になったよ: https://news.ycombinator.com/item?id=41825344

Task*は別の選択肢だけど、正直言って私はシンプルな趣味プロジェクトでCを使うときにしか使ったことがないから、スケールするかどうかは分からないな。*https://taskfile.dev/

彼らはMakeの代替として自分たちを位置づけているけど、個人的には全く異なるもので、比較にならないと思う。Makeはアーティファクトを作成することに重点を置いていて、すでにビルドされたものを再ビルドすることには向いてない。Justはコマンドランナーだよ。

Hacker Newsで議論の続きを見る