1 ポイント 投稿者 GN⁺ 2024-09-22 | 2件のコメント | WhatsAppで共有
  • 新しいビルドツールが次々に登場しても、Makefile はプロジェクトの反復作業をまとめる軽量な基本ツールとして依然として有用
  • makemake buildmake installmake dev のような慣習的なコマンドのおかげで、初めて見るプロジェクトでもビルド・インストール・開発サーバーの起動をすばやく試せる
  • Jekyll、Hugo、11ty、Python の Web フレームワーク、Docker、gulp のようにスタックが異なっても、Makefile は共通の実行インターフェースとして機能する
  • 個人プロジェクトでは、条件分岐やフラグよりも devbuilddeploy のような単純なターゲットがいくつかあれば十分なことが多い
  • GNU Make は広く普及しており、たとえなくても Makefile 内のシェルコマンドを直接実行できるため、制約の多い環境でも負担が小さい

プロジェクトごとに異なるコマンドをひとつの入口にまとめる

  • Makefile は、ビルド・実行・デプロイ方法が異なるプロジェクトをなじみのあるコマンド群で包む用途に使われる
  • 初めて見るプロジェクトに Makefile があれば、たいていまず次のコマンドから試せる
    • make
    • make build
    • make 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 encryptnetlify deploy --prod を実行
    • encrypt: npx staticrypt_site フォルダの HTML ファイルを暗号化

知っておきたい制約と利点

  • 例では phony targets を省略しているが、devbuildwatchdeployencrypt といった名前のファイルがあると、Makefile が想定どおりに動作しない可能性がある
  • GNU Make は makefile を実行するソフトウェアであり、Linux にはすでにインストールされている可能性が高く、MacBook でも別途インストールした記憶がないほど一般的
  • Make は多くの追加依存関係を必要としないため、セキュリティ上の理由でパッケージのインストールが難しい、または不可能な制約の多い環境で役立つことがある
  • Make がなくても、Makefile 内のコマンドをシェルに直接コピーして実行できる
    • 逆に、サーバーに gulp がなければ JavaScript コードをそのままターミナルに貼り付けて実行するのは難しい、という違いがある
  • より優れていて高速な新しいビルドツールを見つけたとしても、Make は異なるツールや設定を管理する共通コマンド層として引き続き活用できる

2件のコメント

 
kayws426 2024-09-22

依存関係を定義していない makefile は、justfile に置き換えるとより良い使い勝手が得られます。

 
GN⁺ 2024-09-22
Hacker Newsのコメント
  • このスレッドで「makeを間違って使っている」と言われても落ち込む必要はない。Make が優れたツールである理由のひとつは、見た目以上にシンプルに始められることだ
    .PHONY を使わないと問題が起きることはあるが、小さなプロジェクトなら1年後にようやく踏む落とし穴かもしれないし、その時でも1時間ほど頭をかけば済む可能性が高い。99%の場合、「定石」を気にせずすぐに始められるし、Makeが要求する複雑さも破綻しない程度に収まっている

    • Makeの最悪な点のひとつは、見た目には単純そうに見えることだ
      Makeは入力ファイルと依存関係を受け取り、正確に1つの出力ファイルを生成する。installallclean のように出力を作らないルールや、記事内のすべてのターゲットは .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やその一部を生成するスクリプトやプログラムを持つようになって、場当たり的なメタビルドシステムができてしまうことだ
      さらに大きな問題は、それを使うこと自体ではなく、それを「みんなに」解決策として勧めることだ。そしてプロジェクトルートに builddev のようなディレクトリやファイルができた瞬間、そのMakefileは動かなくなる
    • 新しいTypeScriptプロジェクトを始めるときに使う、もう少し複雑なMakefileの例はここにある: https://github.com/borisovg/node-ts-template/blob/main/Makef...
      最近はpnpmに切り替えてはいるが、それでもそこまで複雑だとは思わない。文法をある程度知っている必要はあるが、他のツールを最初から正しく動かすよりは難しくない
    • 20年以上にわたって多くのプロジェクトと複数の言語で make をうまく使ってきたが、人々がそれほど気にする .PHONY 周りで問題を経験したことはない
      シンプルで、読みやすく、修正しやすく、組み合わせやすく、どこにでもすでに入っている。表面に書かれていることをやり、それ以上のことはほとんどしない
      参考までに、他のプロジェクトで流行りのビルドシステムや悪夢のようなビルドシステムを扱わなければならないときも、makeで包んで使っている
    • 完全に同意する。作業に合ったツールを使うほうがよく、専門家でなくても、完璧主義的な批判を避けようとして間違ったツールを選ぶよりずっとましだ
      誰でもどこかから始めなければならないし、いったん動き始めれば段階的な改善は簡単に積み重ねられる
    • すべてのmakefileレシピは、正確に1つの出力物である $@ を作るべきだ。Makefile全体としては、ルールが別のルールに依存できるので、任意の数の出力物を作れる
      ここから、phonyターゲットに関するきれいな経験則が導ける。$@ にだけ触れないレシピなら、$@ を phony としてマークすべきだ。phonyターゲットを一覧で管理するとずっと簡単になる
      phonies :=
      phonies += something
      something:
      ./do-something
      phonies += something-else
      something-else: something
      ./do-something-else
      # touches $@ and thus does not need to be phony
      create-file:
      ./generate-some-output > $@
      .PHONY: $(phonies)
  • Makefileはひどい技術だ。ただ、私たちが作ってきた他のたいていのビルドシステムより少しだけマシなので、自虐的な意味で有用になっている
    ビルドシステムはたいてい、あまりに基本的すぎておもちゃ以上のものをビルドすると混乱するか、あまりに複雑すぎて事前知識・官僚主義・同期・定型文が過剰になるか、標準ライブラリがないせいで同じビルドパターンが1万通りの非互換な実装に散らばるか、制約が強すぎて要求が増えると移行が必要になるか、魔法が多すぎるか、構文が難解で一貫性がない

    • 私はMakefileは優れた技術だと思うし、多くの人はきちんと学んでいないか、意図されたやり方で使っていないだけだと思う
      私が使うソフトウェアの半分以上は、普通のMakefileとたまにconfigureスクリプトだけを使っている。autotoolsなしで必要に応じて ./configure を実行し、makemake install をすればそのまま動く。これはまったくおもちゃのような構成ではなく、1つの仕事をうまくこなす小さなプログラム群でできている
      Makeとその動作方式は、少なくとも私にとっては理解しやすい。人々が経験した悪い体験は、Makeそのものというより、複雑なプロジェクトのまずいビルド設定や必要な知識不足によることが多いと思う
      標準ライブラリがないのは ツール非依存性 を意図したもので、だからこそ多様なツール・言語・用途に普遍的に適用できる。欠陥ではなく機能だと見ている。単純な層は依存関係追跡であり、ターゲットと依存関係も明確なので、魔法的だとは言いにくい。構文はたしかにもっと良くできる
    • 最悪のビルドシステムは、特定のプログラミング言語を中心に作られたものだ。言語が N>>1 個あるなら、ビルドシステムも N>>1 個になり、認知負荷が大きくなりすぎてスケールしない
      複数言語をまたぐ汎用ビルドシステムは、make か、CMakeのように make を対象にするシステムくらいしかない。これがいまひとつなのは、make がいまひとつだからだ。make は正しく使うのが本当に難しく、再帰makeと非再帰makeのような問題があり、Unix/POSIX make、BSD make、GNU make、Windows nmake のような非互換な亜種も多く、かなり不格好だ
      それでも、正しく書かれた make はかなり良い。最小公倍数として make があるのは本当にありがたい
    • 同感だ。代替手段のかなりの部分も、Makeのような汎用の「何かを実行する」ツールではなく、特定の言語に特化している
      Makeがサブディレクトリすらきれいに扱えないというのは、かなりひどい話だ。Makeより良いものがあるのか気になる。Ninjaはあるが、人が直接書くようには設計されていない
    • その混乱は、「非常に仮定的に言えば」コンパイラやモジュールのない言語のせいではまったくない、と言い切れるだろうか
      どの程度だと思う? 20%、40%、70%くらいは当たっていないか
    • 「1つ以上」ならかなり幅がある。私の見立てはこうだ。Makefileは基本的すぎるわけではない。複雑になりすぎることはあるが、それはどう作るか次第だ。標準ライブラリについては組み込み関数がある。制約が強すぎると感じたことはないので違うと思う。ターゲットと依存関係が明確だから、魔法が多いわけでもない。ただし 構文 はたしかにもっと良くできるし、普通のJSONファイルであるだけでも今よりましだと思う
  • Makeは本当に大好きだ。この20年間で手を入れたあらゆる作業やプロジェクトで、少なくとも1つは書いてきた
    賢いことは何もなく、変数がいくつかある寄せ集めにすぎない。make runmake testmake lint みたいな感じだ
    make recent は、lintの後に最後に変更されたスクリプトを実行する。Bashや他のシェルでもできるが、そうすると開発者の世界に吸い込まれて複雑さが増すだけで、付加価値はない。Makeは「こういうファイルをこういうファイルにし、そのときこのコマンドを1つか2つ実行する」という ドメイン固有言語 にすぎない。だからこそとても強力だ

    • 「こういうファイルをこういうファイルにし、そのときこのコマンドを1つか2つ実行する」という表現がいい
      何十年も前、Javaでテストフレームワークを作ったことがあるが、makeに似た構文でテストと依存クラスを指定できるようにしていた。「基準となるテスト群」を定義する一連のテストクラスを置き、その上に依存する別のテストクラス階層は、前段が成功した場合にのみ実行されるようにしていた
      最近はなぜ何でもこんなに複雑にするのかわからない。私はいつも、標準的なUnixツールとそのやり方、つまり時間によって実証されたやり方を守れと助言してきた。そうすることがどうしても不可能な状況でない限りは。時間は有限なのだから、問題解決の核心であるシステム/プログラム設計、モデリング、構造、パターンなどに使いたいのであって、残りは副次的なものだ
    • 実際に出会ったすべてのMakefileは、「それだけ」と言うにはずっと多くのことをしていた
    • 私も似たようなものだ。昔はあまり使わなかったが、今の職場ではほぼすべてのリポジトリにMakefileがある
      スタック、言語、リポジトリに関係なく、make test を実行すれば動くとわかっているのは大きな救いだ
    • Makeの問題はMakeではなく、Makeが呼び出すものがもはやそのモデルどおりに動かないことにある
      前のプロジェクトのMakefileには、buildtestfrontenddeploy という4つの主要コマンドがあった。buildとtestはmavenを呼び出し、frontendはnpmを、deployはdockerとawsを呼び出していた
      これらのツールはどれも独自の状態追跡、キャッシュ、増分処理を持っているが、何をしたかを報告しない。そのため、「ビルドが更新された場合のみデプロイする」といった原子的なルールを使えない。maven/cargo/dotnet/npm/goがその情報を公開していないからだ
  • 筆者は mtime ベースの依存関係追跡すら使っていない。ターゲットも .PHONY であるべきなのに、その指定がない。$1 を読んでマッチングし、やることを決めるシェルスクリプトで置き換えることもできたはず

    • あるいは just のような単純なコマンドランナーに置き換えることもできる
      https://just.systems/
    • 実際に取り組んでいるプロジェクトから生まれた一行ホラーがある: .PHONY: $(MAKECMDGOALS)
      ./build.sh devmake dev より明快さに欠ける。phony でない段階がまったくなくても Make を使うもう一つの理由は、後で必要になればそうした段階を追加できることだ。もちろん、筆者が {dev,build,deploy} を phony として明示すべきだという点には同意する
    • 技術的にはその通りだが、この記事から得た核心は、自分のプロジェクトだけでなくサードパーティのプロジェクトでも 使い方の一貫性を得られるという点だ
      自分の Makefile は技術的に改善できるかもしれないが、より重要なのはプロジェクトに入って「始める」体験が一貫していることだ
    • 私たちも皆、かつては初心者だった。ツールを学ぶには、完璧でない初心者的な使い方であっても実際に使ってみることが助けになる。学びながら徐々に改善していけばよい
    • make とシェルの美しさは、シンプルで一つのことをうまくやるという Unix の原則に従っている点にある
      人々はこれにスクリプト言語、依存関係トラッカーなど多くの別の役割を期待し、肥大化したツールを持ち込もうとする。新しいからといって必ずしも優れているわけではない。Autoconf と automake は make ではない
  • 技術的には、これらの make ターゲットはすべてターゲット名と同じ名前のファイルを探す。各ターゲットは実際には .PHONY として定義されるべきだ
    以前は私もいつもこういう Makefile を書いていたが、ここ数年は just と justfile に移った。こうした振る舞いがデフォルトで、全体的にもっと使いやすい。パラメータのようなものもより簡単だ
    https://github.com/casey/just

    • この種の make 風システムはかなり良いが、一つ問題がある。Make はすべての Linux と Mac に最初からあり、Windows でもかなり簡単に手に入る。Git Bash に入っていないのは本当に残念だ。最大公約数を使えること自体が Make の大きな利点だと思う
    • Make の大きな失敗は、phony ターゲットとファイルターゲットを同じ名前空間に混在させたことだ。たとえば phony ターゲットは : で始めるようにして、名前を見るだけで区別できるべきだった
      もちろん、今となってはもう遅い
    • just は本当に良いが、一般的にはインストールされていないので少し面倒だ
      just や ripgrep のような新しくて便利なツールをまとめて、どこでも使いたい環境に入る新しい coreutils ディストリビューションのようなものが出てくる時期かもしれない
  • make がこれほど激しい議論を呼ぶのは面白い。昔の半ば宗教戦争のようだった vi 対 emacs 論争に似ている
    元記事には全面的に同意するし、特に、より高度なビルドツールを使うとしても最上位の Makefile で包むやり方は賢いと思う。利点は 標準化だ。何かを覚える必要はなく、自分が書いたプロジェクトならただ make を打てば動くと分かる
    たとえば C を使う人が Rust プロジェクトをコンパイルしようとするとき、cargo の使い方を調べる必要はなく、makegmake を打つだけでよい。GNU 固有機能は使わず POSIX 準拠を目指しているが、実際には make のほぼ 100% が gmake であるのも事実だ
    時代を超えた make を一種の最上位ビルドシステム起動役として使おうという提案は、おそらく250年後でも通用するだろう

    • こんなに単純なタイトルが炎上級の議論を引き起こしたのが面白い。記事自体は make の狂気じみるほど単純なユースケースで、2024年に gulp を使うという内容なのに、明らかに誰も読んでいないようだ
    • cargo は悪い例だ。一般には cargo build を使うからだ
      Make 自体は素晴らしいが、C プロジェクトで働いていたときの大半は cmake/autotools とグローバルなパッケージインストールの組み合わせで、そういうものはしばしば調べ直す必要があった
    • その前に bazel/blaze が覇権を取れば、そうならないかもしれない。疑うなら Chrome 対 Firefox、あるいは Kubernetes 対 docker-compose を見ればよい
  • ここの感覚には同意するが、多くのものを Justfile に書き直しているところだ
    https://github.com/casey/just
    Makefile 特有の奇妙さをかなり避けられる

    • GPT が Makefile 作成を手伝うことで、Makefile に第二の生命を与え、その死を遅らせたのは確かだ
    • 私も同じだ。同じファイルが Windows でも動くのもおまけとしてうれしい
  • その通り、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/

    • just は単なるもう一つの依存関係にすぎない。make はどこにでもある
  • タスクランナーとしての make は悪くないが、最近はほかのコメントにもあるように just のような、より優れた代替手段がある。
    ビルドシステムとしての make も、欠点にぶつかるまでは悪くない。make/Makefile は標準化されていないため automake が存在する。つまり Makefile を書くのではなく、テンプレートを書いて実際の makefile を生成することになる。ツールチェーン全体を自分で所有しているなら問題ないが、ほとんどの場合はそうではないので、移植性のある Makefile を保証するためにこうしている。
    make はあらゆる種類の依存関係解決もできず、必要なものがその場にあると仮定する。だから configure スクリプトが生まれ、configure スクリプトも標準ではないため、make ターゲットを実行する前に autoconf/autoreconfconfigure スクリプトを生成することになる。
    make と、automake/autoconf/autoreconf のような周辺ツールは、入力が古いかどうかを判断するのに mtime を使う。autoconf/autoreconf/automake/configure を実行しても、入力が永久に古いままになって何もビルドできない状態に陥ることがある。ちなみに、多くのビルドシステムは適切な依存関係追跡さえできていれば、mtime を使っても問題ない。
    全体として、make の根本的な設計上の欠陥は Unix の哲学、つまり「一つのことをうまくやる」に合わせて作られている点にある。その一つのこととは「入力が古ければターゲットを再ビルドする」ことで、これは非常に限定的な道具なので、現代のビルドシステムでは、単なる基本的なタスクランナー以上に役立つものにするため、その上に多くの作業を積み上げなければならない。

    • make が依存関係解決をできないって?
      dependency:
      ...
      target: dependency
      ...
    • 的外れな質問かもしれないが、依存関係解決は make の中核ではないのか? ここで言っているのは正確には何なんだ?
    • そちらの古いやり方では make configure だ。これが make [build] に必要なものを設定する。
    • make で本当に惜しいのは、mtime 以外の基準で mtime を解釈できる能力だけだ。
      だから touchfile を使うことになる。汚いやり方ではあるが、それでも他の多くのものよりはまだうまく動く。特に docker build のキャッシュを見てそう思う。