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

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はコマンドランナーだよ。

これはMakeの「短いシェルスクリプトのリスト」の部分を置き換えるけど、「再実行が必要なルールだけを実行する」という実際に役立つ部分は置き換えないんだよね。

Makeをコマンドランナーとして使う一番の利点は、どこにでもインストールされてる標準ツールってことだね。これらの代替ツールは使いやすそうだけど、わざわざ別のツールをインストールするほどの価値があるとは思えないんだよね。

自分もコマンドランナーとして just を使ってるけど、ここで他の人たちに同意するよ。just はコマンドランナーとして正確に説明されるべきで、make はビルドシステムだからね。特に C/C++ プロジェクトをビルドしたことがない人たちが make を使う場面もあって、そういう場合は just に置き換えるのが理にかなってる。make には余計なものがあるし、実際にファイルを作るために使ってるわけじゃないからね。彼らはおそらく、私たちが「make install」で期待することのような慣習も知らないだろうし、make の慣習を学ばないことを支持するよ。別の何かを使う限りはね。:) make の他の使い方には、Cmake や Bazel のような現代的な代替品が必要になるだろうね。最近の子たちは、誰かが make を教えようとしても「いらないよ」って言えるかもしれないし、make の未来は私たちおじさんたちが文句を言うようなものになるかも。昔はこうだった、みたいな。

注意:MakefileはTABでインデントしないといけなくて、スペースだとmakeが失敗するよ。ああ、やばい。Makefileを使ったことはないけど、それが原因で苦労する人が多いんだろうな。YAMLファイルの余計なスペースで何時間も無駄にしたから、最近チームでSpring BootのコードベースからYAMLを排除することに決めたんだ。

Makefileを理解しているエディタがあるとめっちゃ助かるよ。TABキーがちゃんと機能するからね。Emacsはこれが得意だよ。もちろん、本当の解決策は「CMakeを使え」ってことだ、ダサい奴。

これは実際には問題にならないよ。だって、どのエディタも自動的にMakefileにはタブを使うから。

ちゃんとしたエディタを使えば問題なし。今のPythonと同じだよ。

エディタの話は置いといて。僕は普通、Tabでインデントしてるよ :) Makefileをちょっと学んで、小さなプロジェクトで使ったことがあるけど、その後Autotoolsを見て、頭の中がこの面倒なワークアラウンドを学ぶのを拒否したんだ。そしたらMesonが登場して、ビルド、依存関係、テストの問題が解決したよ :) [1] https://mesonbuild.com 追記: Mesonの依存関係管理は素晴らしいね。

実際にタブが必要なのは神の恵みだね。スペースのオフバイワンエラーがなくなるから。

IDE でスペースやタブの違う記号がないの?混合したりトレーリングホワイトスペースをコミットしてる人をよく見るけど、使ってきたエディタはどれもスペースとタブをはっきり表示してるよ。yaml については同意するけど、多行文字列を整列させる方法を毎回調べないといけないのが面倒だな。

Makeは大規模なCコードベースのビルドツールとしての役割がある。人々は時々これを一般的な「プロジェクト特有のジョブランナー」として扱うけど、実際にはあまり合わないんだよね。簡単な条件分岐ですら難しいし。例えば、Terraformをラップしようとした善意の試みをいくつか見たけど、どれもひどい結果になってた。

いい感じの汎用ジョブランナーってある? 追記: ごめん、「ジョブランナー」の意味を完全に誤解してたみたい。

これは汎用のジョブランナーじゃないよ。線形のシェルスクリプトを宣言的な依存関係に変換するための一般的な方法なんだ。シェル用の一般的なツールだよ。

1985年の暗黒時代に、ボストン大学のグラフィックスラボでMakefileを使ってアニメーション用の3Dレンダラーを生成していた人に出会ったことがある。彼はLispを使っていて、初期の手続き型生成や3Dアクターシステムをやってた。彼のMakefileは非常にエレガントで、合計10行くらいだった。シンプルなファイル日付の依存関係に基づいて、何百ものアニメーションを生成していたんだ。彼はLispで各フレームの3D形状を生成して、Makeがフレームを生成してた。この時代は1985年で、今当たり前になっている3Dやアニメーションのほとんどが存在しなかったから、彼はみんなの頭を吹き飛ばしてた。彼は『アイアン・ジャイアント』の3Dレンダラーを書いたし、『キャロライン』にも関わってたと思う。ブライアン・ガードナー。

コララインのことだよね?

この人? http://3d-consultant.com/bio.html

小規模なCプロジェクトでは、「.cファイルが依存している.hファイルはどれか?」って質問に対して、一番簡単な方法は「全部」って言うことだと思ったよ。 SOURCE_FILES := $(wildcard $(SRC_DIR)/.c) HEADER_FILES := $(wildcard $(SRC_DIR)/.h) OBJ_FILES := $(patsubst $(SRC_DIR)/%.c,$(BUILD_DIR)/%.o,$(SOURCE_FILES)) .PHONY: build clean build: $(BUILD_DIR)/$(TARGET) clean: rm -rf $(BUILD_DIR) $(BUILD_DIR): mkdir $(BUILD_DIR) $(BUILD_DIR)/$(TARGET): $(OBJ_FILES) | $(BUILD_DIR) $(LINK.o) $^ $(LDLIBS) -o $@ $(BUILD_DIR)/%.o: $(SRC_DIR)/%.c $(HEADER_FILES) | $(BUILD_DIR) $(COMPILE.c) $ だから、インターファイルや共有インターフェースをいじらなければ、インクリメンタルビルドができるよ。いじるとフルビルドになるけどね。理想的ではないけど、僕の経験では大体大丈夫だよ。追記: Makeのビルトイン変数の名前の付け方が大好きなんだ。出力は明らかに$@だけど、$^や$、$∨が何をするかすぐにわかる?

以前は「depend」ターゲットを使って依存関係を明示的にして、ビルド時間を最小限に抑えるのが好きだったけど、Makefileの内容にちょっと手を加えることになるね(詳しい議論は https://wiki.c2.com/?MakeDepend にある)。その作業をするスタンドアロンの makedepend(1) は、Ubuntu の xutils-dev パッケージに入ってるよ。

gccやclangを使うと、Makefileに含められる依存ファイルを出力できるよ。それは、ソースファイルがどのヘッダーに依存しているかを教えてくれるターゲットなんだ。

記事にはほとんどの人がレシピを .PHONY としてマークしないって書いてあって、それを理由にチュートリアルで教えないみたい。これは弱い言い訳だと思うし、ツールの正しい使い方を教えるべきだよね。チームメイトには、タスクランナーとして make を使ってるから、すべてのレシピに .PHONY を追加・維持することで結構からかわれた。Clark Grubb のページには make ファイルのスタイルガイドが詳しく説明されてるよ: https://clarkgrubb.com/makefile-style-guide 誰かこのスタイルガイド使ってる人いる?それとも、レシピ宣言で phony をマークするのと、ファイルの先頭に巨大なリストを置くの、どっちがいいと思う?これを強制するリンターがあったらいいな…

2025年には、makefile はせいぜい C プロジェクト専用になるね。タスクランニングには just か mise を使おう。

この「make」が適切なビルドツールになる場合の箇条書きにはちょっと驚いたよ: > ビルドシステムは高いポータビリティを必要としない。確かに「高い」っていうのは曖昧な表現だけど、GoプロジェクトではほぼいつもMakefileをデフォルトにしてるし、Linux、macOS、Windows(WSLなしで、Windows用のMakeだけ)でElectronアプリをビルドするのに使ってきた。実行可能なパスを正しく設定するためにちょっとした工夫が必要だけど、私の目的には十分に機能してる。ある程度、Makeが嫌われる理由はわかるけど、シンプルに保てば、package.jsonのスクリプトの制限を乗り越える素晴らしい方法を提供してくれるよ(例えば、コメントを追加することとか)。