私はMakefileが好き
(switowski.com)- 新しいビルドツールが次々に登場しても、Makefile はプロジェクトの反復作業をまとめる軽量な基本ツールとして依然として有用
make、make build、make install、make devのような慣習的なコマンドのおかげで、初めて見るプロジェクトでもビルド・インストール・開発サーバーの起動をすばやく試せる- Jekyll、Hugo、11ty、Python の Web フレームワーク、Docker、gulp のようにスタックが異なっても、Makefile は共通の実行インターフェースとして機能する
- 個人プロジェクトでは、条件分岐やフラグよりも
dev、build、deployのような単純なターゲットがいくつかあれば十分なことが多い - GNU Make は広く普及しており、たとえなくても Makefile 内のシェルコマンドを直接実行できるため、制約の多い環境でも負担が小さい
プロジェクトごとに異なるコマンドをひとつの入口にまとめる
- Makefile は、ビルド・実行・デプロイ方法が異なるプロジェクトをなじみのあるコマンド群で包む用途に使われる
- 初めて見るプロジェクトに
Makefileがあれば、たいていまず次のコマンドから試せるmakemake buildmake install
- 昔の個人プロジェクトを開き直すときでも、
make devだけ実行すれば必要なビルド手順と開発サーバー起動をまとめて処理するよう構成できる - Jekyll、Hugo、11ty、さまざまな Python Web フレームワークのように技術スタックが違っても、同じ入口を維持できる
- Docker や gulp を使う場合でも、Makefile はそれらのツールを置き換えるのではなく、オーケストレーション層として動作する
- 例:
make buildがプロジェクトごとの追加引数を渡しながら必要な Docker イメージをビルドする
- 例:
個人プロジェクトでのシンプルな使い方
- Makefile の利用は、複雑な機能よりもシェルコマンドの束ね方に重点を置く
- 条件分岐やフラグのような機能は使わない
- make の作業単位は技術的にはターゲット(target)だが、実際にはタスク(task)のように扱う
- bash スクリプトの関数としても作れるが、Makefile のほうが簡単で速いと考えている
- 個人プロジェクトでよく使う作業は、たいてい 2〜3 個
dev: 開発サーバーを起動build: 必要に応じてプロジェクトをビルドdeploy: プロジェクトをデプロイまたは公開watch: ソースファイルの変更時にビルド作業を自動で再実行する追加タスクとして使用
- 現在のブログの Makefile は
devターゲットひとつだけを持つdev:の下でnpm run devを実行
- より複雑な個人プロジェクトでは、次の作業を Makefile にまとめる
dev:bundle exec jekyll serveで開発サーバーを起動build:npm run gulp buildでアセットをビルドwatch:npm run gulp watchで特定フォルダを監視deploy: 本番環境向けに Jekyll をビルドした後、make encrypt、netlify deploy --prodを実行encrypt:npx staticryptで_siteフォルダの HTML ファイルを暗号化
知っておきたい制約と利点
- 例では phony targets を省略しているが、
dev、build、watch、deploy、encryptといった名前のファイルがあると、Makefile が想定どおりに動作しない可能性がある- Hacker News の読者たち は、このような場合に phony target の追加を勧めている
- GNU Make は makefile を実行するソフトウェアであり、Linux にはすでにインストールされている可能性が高く、MacBook でも別途インストールした記憶がないほど一般的
- Make は多くの追加依存関係を必要としないため、セキュリティ上の理由でパッケージのインストールが難しい、または不可能な制約の多い環境で役立つことがある
- Make がなくても、Makefile 内のコマンドをシェルに直接コピーして実行できる
- 逆に、サーバーに gulp がなければ JavaScript コードをそのままターミナルに貼り付けて実行するのは難しい、という違いがある
- より優れていて高速な新しいビルドツールを見つけたとしても、Make は異なるツールや設定を管理する共通コマンド層として引き続き活用できる
2件のコメント
依存関係を定義していない makefile は、justfile に置き換えるとより良い使い勝手が得られます。
Hacker Newsのコメント
このスレッドで「makeを間違って使っている」と言われても落ち込む必要はない。Make が優れたツールである理由のひとつは、見た目以上にシンプルに始められることだ
.PHONYを使わないと問題が起きることはあるが、小さなプロジェクトなら1年後にようやく踏む落とし穴かもしれないし、その時でも1時間ほど頭をかけば済む可能性が高い。99%の場合、「定石」を気にせずすぐに始められるし、Makeが要求する複雑さも破綻しない程度に収まっているMakeは入力ファイルと依存関係を受け取り、正確に1つの出力ファイルを生成する。
install、all、cleanのように出力を作らないルールや、記事内のすべてのターゲットは.PHONYのような特別なマジックターゲットに依存しなければならないが、.PHONYは2017年のPOSIX(IEEE Std 1003.1-2017, https://pubs.opengroup.org/onlinepubs/9699919799/utilities/m...)にはなく、現行標準(IEEE Std 1003.1-2024, https://pubs.opengroup.org/onlinepubs/9799919799/utilities/m...)でようやく入った。オブジェクトファイルやモジュール、プリコンパイル済みヘッダーのように複数ファイルを作りたいなら、壊れやすい小細工を自分で書かなければならない。すべてのMakeがGNU Makeというわけではなく、BSDやSolaris/Illumosのようなnix系もまだ存在するだからといってMakeが役に立たないわけではない。十分に複雑ではあるが、まだ「より良い」ビルドシステムが必要なほどではないプロジェクトには有用だ。問題は、そういうプロジェクトもコードが増えるにつれて複雑になり、結局Makefileやその一部を生成するスクリプトやプログラムを持つようになって、場当たり的なメタビルドシステムができてしまうことだ
さらに大きな問題は、それを使うこと自体ではなく、それを「みんなに」解決策として勧めることだ。そしてプロジェクトルートに
buildやdevのようなディレクトリやファイルができた瞬間、そのMakefileは動かなくなる最近はpnpmに切り替えてはいるが、それでもそこまで複雑だとは思わない。文法をある程度知っている必要はあるが、他のツールを最初から正しく動かすよりは難しくない
.PHONY周りで問題を経験したことはないシンプルで、読みやすく、修正しやすく、組み合わせやすく、どこにでもすでに入っている。表面に書かれていることをやり、それ以上のことはほとんどしない
参考までに、他のプロジェクトで流行りのビルドシステムや悪夢のようなビルドシステムを扱わなければならないときも、makeで包んで使っている
誰でもどこかから始めなければならないし、いったん動き始めれば段階的な改善は簡単に積み重ねられる
$@を作るべきだ。Makefile全体としては、ルールが別のルールに依存できるので、任意の数の出力物を作れるここから、phonyターゲットに関するきれいな経験則が導ける。
$@にだけ触れないレシピなら、$@を phony としてマークすべきだ。phonyターゲットを一覧で管理するとずっと簡単になるphonies :=phonies += somethingsomething:./do-somethingphonies += something-elsesomething-else: something./do-something-else# touches $@ and thus does not need to be phonycreate-file:./generate-some-output > $@.PHONY: $(phonies)Makefileはひどい技術だ。ただ、私たちが作ってきた他のたいていのビルドシステムより少しだけマシなので、自虐的な意味で有用になっている
ビルドシステムはたいてい、あまりに基本的すぎておもちゃ以上のものをビルドすると混乱するか、あまりに複雑すぎて事前知識・官僚主義・同期・定型文が過剰になるか、標準ライブラリがないせいで同じビルドパターンが1万通りの非互換な実装に散らばるか、制約が強すぎて要求が増えると移行が必要になるか、魔法が多すぎるか、構文が難解で一貫性がない
私が使うソフトウェアの半分以上は、普通のMakefileとたまにconfigureスクリプトだけを使っている。autotoolsなしで必要に応じて
./configureを実行し、make、make installをすればそのまま動く。これはまったくおもちゃのような構成ではなく、1つの仕事をうまくこなす小さなプログラム群でできているMakeとその動作方式は、少なくとも私にとっては理解しやすい。人々が経験した悪い体験は、Makeそのものというより、複雑なプロジェクトのまずいビルド設定や必要な知識不足によることが多いと思う
標準ライブラリがないのは ツール非依存性 を意図したもので、だからこそ多様なツール・言語・用途に普遍的に適用できる。欠陥ではなく機能だと見ている。単純な層は依存関係追跡であり、ターゲットと依存関係も明確なので、魔法的だとは言いにくい。構文はたしかにもっと良くできる
複数言語をまたぐ汎用ビルドシステムは、
makeか、CMakeのようにmakeを対象にするシステムくらいしかない。これがいまひとつなのは、makeがいまひとつだからだ。makeは正しく使うのが本当に難しく、再帰makeと非再帰makeのような問題があり、Unix/POSIX make、BSD make、GNU make、Windowsnmakeのような非互換な亜種も多く、かなり不格好だそれでも、正しく書かれた
makeはかなり良い。最小公倍数としてmakeがあるのは本当にありがたいMakeがサブディレクトリすらきれいに扱えないというのは、かなりひどい話だ。Makeより良いものがあるのか気になる。Ninjaはあるが、人が直接書くようには設計されていない
どの程度だと思う? 20%、40%、70%くらいは当たっていないか
Makeは本当に大好きだ。この20年間で手を入れたあらゆる作業やプロジェクトで、少なくとも1つは書いてきた
賢いことは何もなく、変数がいくつかある寄せ集めにすぎない。
make run、make test、make lintみたいな感じだmake recentは、lintの後に最後に変更されたスクリプトを実行する。Bashや他のシェルでもできるが、そうすると開発者の世界に吸い込まれて複雑さが増すだけで、付加価値はない。Makeは「こういうファイルをこういうファイルにし、そのときこのコマンドを1つか2つ実行する」という ドメイン固有言語 にすぎない。だからこそとても強力だ何十年も前、Javaでテストフレームワークを作ったことがあるが、makeに似た構文でテストと依存クラスを指定できるようにしていた。「基準となるテスト群」を定義する一連のテストクラスを置き、その上に依存する別のテストクラス階層は、前段が成功した場合にのみ実行されるようにしていた
最近はなぜ何でもこんなに複雑にするのかわからない。私はいつも、標準的なUnixツールとそのやり方、つまり時間によって実証されたやり方を守れと助言してきた。そうすることがどうしても不可能な状況でない限りは。時間は有限なのだから、問題解決の核心であるシステム/プログラム設計、モデリング、構造、パターンなどに使いたいのであって、残りは副次的なものだ
スタック、言語、リポジトリに関係なく、
make testを実行すれば動くとわかっているのは大きな救いだ前のプロジェクトのMakefileには、
build、test、frontend、deployという4つの主要コマンドがあった。buildとtestはmavenを呼び出し、frontendはnpmを、deployはdockerとawsを呼び出していたこれらのツールはどれも独自の状態追跡、キャッシュ、増分処理を持っているが、何をしたかを報告しない。そのため、「ビルドが更新された場合のみデプロイする」といった原子的なルールを使えない。maven/cargo/dotnet/npm/goがその情報を公開していないからだ
筆者は mtime ベースの依存関係追跡すら使っていない。ターゲットも
.PHONYであるべきなのに、その指定がない。$1を読んでマッチングし、やることを決めるシェルスクリプトで置き換えることもできたはずhttps://just.systems/
.PHONY: $(MAKECMDGOALS)./build.sh devはmake devより明快さに欠ける。phony でない段階がまったくなくても Make を使うもう一つの理由は、後で必要になればそうした段階を追加できることだ。もちろん、筆者が{dev,build,deploy}を phony として明示すべきだという点には同意する自分の Makefile は技術的に改善できるかもしれないが、より重要なのはプロジェクトに入って「始める」体験が一貫していることだ
人々はこれにスクリプト言語、依存関係トラッカーなど多くの別の役割を期待し、肥大化したツールを持ち込もうとする。新しいからといって必ずしも優れているわけではない。Autoconf と automake は make ではない
技術的には、これらの make ターゲットはすべてターゲット名と同じ名前のファイルを探す。各ターゲットは実際には
.PHONYとして定義されるべきだ以前は私もいつもこういう Makefile を書いていたが、ここ数年は just と justfile に移った。こうした振る舞いがデフォルトで、全体的にもっと使いやすい。パラメータのようなものもより簡単だ
https://github.com/casey/just
:で始めるようにして、名前を見るだけで区別できるべきだったもちろん、今となってはもう遅い
just や ripgrep のような新しくて便利なツールをまとめて、どこでも使いたい環境に入る新しい coreutils ディストリビューションのようなものが出てくる時期かもしれない
make がこれほど激しい議論を呼ぶのは面白い。昔の半ば宗教戦争のようだった vi 対 emacs 論争に似ている
元記事には全面的に同意するし、特に、より高度なビルドツールを使うとしても最上位の Makefile で包むやり方は賢いと思う。利点は 標準化だ。何かを覚える必要はなく、自分が書いたプロジェクトならただ
makeを打てば動くと分かるたとえば C を使う人が Rust プロジェクトをコンパイルしようとするとき、cargo の使い方を調べる必要はなく、
makeかgmakeを打つだけでよい。GNU 固有機能は使わず POSIX 準拠を目指しているが、実際には make のほぼ 100% が gmake であるのも事実だ時代を超えた
makeを一種の最上位ビルドシステム起動役として使おうという提案は、おそらく250年後でも通用するだろうcargo buildを使うからだMake 自体は素晴らしいが、C プロジェクトで働いていたときの大半は cmake/autotools とグローバルなパッケージインストールの組み合わせで、そういうものはしばしば調べ直す必要があった
ここの感覚には同意するが、多くのものを Justfile に書き直しているところだ
https://github.com/casey/just
Makefile 特有の奇妙さをかなり避けられる
その通り、Make は素晴らしい。本当にいろいろなことに使っている。作業自動化には良いやり方だ
たとえば私の個人ウェブサイトでは、Makefile が bash スクリプトを呼び出して更新されたウェブページを再生成し、サーバーへ git push した後、サーバー側の git hook が Make を呼び出してデプロイする。ただし、講義資料の PDF のようによく変わるバイナリの塊は Git リポジトリに入れたくない。それでも問題ない。Makefile に
uploadsターゲットがあり、変更された PDF だけをサーバーに上げ、このターゲットは git push を行うdeployターゲットの依存関係なので、特に気にする必要がないまた、講義資料用の更新済み PDF は別の Makefile がウェブサイトのソースツリーに自動で入れてくれる。その Makefile で LaTeX ソースから自分が使う PDF を作ったり、同じソースから学生向けの別バージョン、たとえば演習問題の解答がない版を作ったりして、ローカルのウェブサイト版に自動公開し、望むときに更新済みウェブサイトへ上げられる
いわば Makefile が延々と連なる構造だ。Makefile は良い
こういう場合、筆者に本当に必要なのは make ではなく just だ
https://just.systems/man/en/
タスクランナーとしての make は悪くないが、最近はほかのコメントにもあるように
justのような、より優れた代替手段がある。ビルドシステムとしての make も、欠点にぶつかるまでは悪くない。make/Makefile は標準化されていないため automake が存在する。つまり Makefile を書くのではなく、テンプレートを書いて実際の makefile を生成することになる。ツールチェーン全体を自分で所有しているなら問題ないが、ほとんどの場合はそうではないので、移植性のある Makefile を保証するためにこうしている。
make はあらゆる種類の依存関係解決もできず、必要なものがその場にあると仮定する。だから
configureスクリプトが生まれ、configureスクリプトも標準ではないため、make ターゲットを実行する前にautoconf/autoreconfでconfigureスクリプトを生成することになる。make と、automake/autoconf/autoreconf のような周辺ツールは、入力が古いかどうかを判断するのに mtime を使う。
autoconf/autoreconf/automake/configureを実行しても、入力が永久に古いままになって何もビルドできない状態に陥ることがある。ちなみに、多くのビルドシステムは適切な依存関係追跡さえできていれば、mtime を使っても問題ない。全体として、make の根本的な設計上の欠陥は Unix の哲学、つまり「一つのことをうまくやる」に合わせて作られている点にある。その一つのこととは「入力が古ければターゲットを再ビルドする」ことで、これは非常に限定的な道具なので、現代のビルドシステムでは、単なる基本的なタスクランナー以上に役立つものにするため、その上に多くの作業を積み上げなければならない。
makeが依存関係解決をできないって?dependency:...target: dependency...make configureだ。これがmake [build]に必要なものを設定する。だから touchfile を使うことになる。汚いやり方ではあるが、それでも他の多くのものよりはまだうまく動く。特に
docker buildのキャッシュを見てそう思う。