GNU Make においてサフィックスルールはずっと obsolete だという話

あるいは「お手製 Makefile を書く際に GNU Make 以外の実装について考慮すべきか?」という話。

結論

個人的には積極的に GNU Make(拡張)と心中していきたい……。

概要

Suffix rules are the old-fashioned way of defining implicit rules for make. Suffix rules are obsolete because pattern rules are more general and clearer. They are supported in GNU make for compatibility with old makefiles.

パターンルール(型ルール)の方が一般的だし分かりやすいからサフィックスルールは廃止だー(古い Makefile との互換性の為にサポートはするけど)」(意訳)とのこと。日本語訳版は 1998 年 5 月時点のドキュメントの訳のようだが既にこの記載があり、つまりサフィックスルールは少なくとも 18 年前からずっと obsolete な機能であった。

パターンルールへの書き換え

「C言語のソースファイル hello.c からオブジェクトファイル hello.o を作り、hello.o から実行バイナリ hello を作る」という例をサフィックスルールで書くと以下のようになる1

.SUFFIXES: .c .o

hello: hello.o  
    gcc -o $@ $^

.c.o:
    gcc -c -Wall -O2 -o $@ $<

サフィックスルールの欠点として、「拡張子を .SUFFIXES で定義する必要があって面倒2」「どっちが生成元でどっちが生成先だったかをよく忘れる(私が)」などが挙げられる。

これをパターンルールで書くとこうなる。

hello: hello.o  
    gcc -o $@ $^

%.o: %.c
    gcc -c -Wall -O2 -o $@ $<

確かに「一般的だし分かりやすい」。

パターンルールでできること

依存ファイルの追加

公式マニュアルにもある例。「foo.h を更新した時に hello.o(と hello)も更新したい」というユースケースにおいて、サフィックスルールで、

.c.o: foo.h
    gcc -c -Wall -O2 -o $@ $<

のように書くと、これは「.c.o という変な名前のファイル」を生成するためのルールになってしまう。よって、パターンルールを使って、

%.o: %.c foo.h
    gcc -c -Wall -O2 -o $@ $<

のように書く3

拡張子以外のパターン

拡張子ではない接尾辞や接頭辞についてもパターンルールを使えば以下のように書ける。これはサフィックスルールでは実現できない。

%_256px.png: %.png
    convert -resize 256x256 $< $@

fuga_%.jpg: hoge_%.png  
    convert $< $@

POSIX 規格と BSD Make について

パターンルールは GNU Make の拡張機能であって、POSIX 規格には存在しない。

make の別実装である BSD Make はパターンルールに対応していない。よって、より可搬性の高い Makefile にしたい場合は旧来のサフィックスルールを使うべき、……みたいなことを思っていたが、そもそも GNU Make と BSD Make を両方考慮した Makefile を手書きする(自動生成に頼らずにテキストエディタで書く)のは割と面倒である。

例えば、一番最初に出した例の、

hello: hello.o  
    gcc -o $@ $^

の部分は実は BSD Make では正しく動作しない。これは自動変数 $^ が BSD Make では定義されていないためで、動作させるには $> ($(.ALLSRC)) を使って、

hello: hello.o  
    gcc -o $@ $>

と書く必要がある。が、こう書くと今度は GNU Make で動作しなくなる。つまり自動変数 $^ / $> を使った時点で互換性は既にアウトだった。

POSIX 的に正しい解法はこうだと思う。

OBJS := hello.o

hello: $(OBJS)  
    gcc -o $@ $(OBJS)

まあ個人的には、Makefile を手書きする場合には GNU Make しか考慮しない、というか現実的に GNU Make しか考慮できない、というスタンスで行きたい。GNU Make 拡張があまりにも便利すぎるし、わざわざ POSIX 準拠の Makefile を書いても現状リターンが少なすぎる。何らかの事情で他の make 実装を考慮したい場合には「互換性のある Makefile を生成する何らかのメタなソフトウェアを使う/メタなスクリプトを書く」べきでは、という気がしている。小規模なツールだとそんなメタい機構を使う/作るのも面倒だし、問題が顕在化するまでは GNU Make 限定でやっていかせてくれという気持ち。

BSD に源流を持つ macOS の make はデフォルトで GNU Make だし、おそらく *BSD 使いも devel/gmake を入れている(入れざるを得ない)だろうし、割り切って GNU Make 向けに書いてしまっても大して可搬性は変わらないのではないか、というのは希望的観測か。

とりあえず、GNU Make 向けの Makefile を書く際はファイル名を GNUmakefile とする、多段 make したい場合は make と直に書くのではなく $(MAKE) と書く(同じ make 実装で子も make させる)ようにする、という思いやりは持っておきたい。

おわりに

これまでパターンルールも何も知らないまま雰囲気で make をやっていたので、“Suffix rules are obsolete” という文言を見た時にウワーッとなってしまい、その衝撃からこの記事が生成されてしまった。世間では常識だったらどうしよう(川柳)。個人的には、GNU Make の拡張機能に頼ってしまっていいものかという不安に対して自分の中で折り合い(あるいは踏ん切り)を付けることができてよかった。

GNU Make をスッと使えるようになると捗る場面は多そうだけど、学習難度に微妙な高さがあってよそ見をしていると躓く感じはあるので引き続き注意をしていきたい。

その他資料

  1. 本当は $(CC), $(CFLAGS), $(CPPFLAGS) あたりの変数を使って書くべき気はするが例を単純にしたいのでベタ書きで。

  2. .c や .o などの一部の拡張子については暗黙の定義が行われるので、この例では実際にはわざわざ .SUFFIXES を書く必要はない。

  3. make 側が暗黙に .c → .o のルールを持っているので、そのままだとサフィックスルールを使った例でも中途半端に動いてしまう(foo.h への依存は反映されない)。make -r hello のように -r (--no-builtin-rules) オプションを付けることで暗黙のルール定義を抑止でき、きちんとエラーになる。