もはや時代遅れな bsd.lib.mk をどうにかしたい[前編]
make(1) の誕生
make(1) は V6 UNIX ベースの商用開発環境 PWB(Programmer Work Bench) 続いて V7 UNIX で導入されたビルド自動化ツールである。
それ以前のビルドは run という名前のシェルスクリプトをソースツリーに配置していた。
1 chdir /usr/sys; pwd; time sh run
...
1 chdir ken
2 cc -c -O *.c
3 ar r ../lib1
4 rm *.o
5
6 chdir ../dmr
7 cc -c -O *.c
8 ar r ../lib2
9 rm *.o
...
しかしこれだと変更の無いファイルに対しても常に再コンパイルが実行されてしまう。 当時のプアな計算機性能にとって無駄が大き過ぎるのである。
また PWB の Mashey Shell そしてV7 UNIX で Thompson Shell を置換えた Bourne Shell も高機能ではない(関数もまだ無い)ため、スクリプトも冗長になり保守が大変。
そこで make(1) を導入し run スクリプトを Makefile で置き換えれば
- ソースファイル(依存関係含む)のタイムスタンプが更新されていない限り再コンパイルは実行されない
- 定型処理はビルトイン変数およびルールとして提供される
- マクロ機能によりビルド処理の記述が格段に楽になる
と上記の問題からオサラバできるってわけ。
あまりにも便利過ぎて、ビルドの自動化にとどまらずありとあらゆるバッチ処理をシェルスクリプトではなく Makefile で書くという Makefile 原理主義も生まれるわけである。
ただし make(1) 開発の動機はちょっと違う、作者の Stuart Feldman 氏によると
- ある日(ある日)森の中(森の中)バグに(バグに)出会った(出会った)
- どーせタイムスタンプが更新されてるオブジェクトが原因だろうと見当をつけてデバッグ開始
- よくよく調べたらソースコードに変更は無かった
- 全く見当違いの無駄な作業で半日溶かしてた事にようやく気づく
- ララララーラーラーラー
- 嫌だなぁ同僚がやらかした話ですよアハハハ
という事故があってカッとなって作った、タイムスタンプ変わるのが許せなかったなどと供述している。
うーん、そもそもこの手の事故を防ぎたかったらタイムスタンプなんて頼りないものには頼らず、存在しない過去記事 sys/cdefs.hとは何ですか? (その10) で解説した
を使ってデバッグ対象を特定すべき問題であり make(1) 作ったところで解決になってねえ!
PWB には sccs(1) も 含まれてた けど使ってなかった(まだ OS/360 から移植されてなかった?)悲劇なんやな。
まぁいいやこのような経緯によって make(1) は誕生したのである。
PMake の誕生
一般的に BSD make などと呼ばれる make(1) 実装は Adam de Boor 氏による PMake(Parallel Make) がベースとなっている。
元々は Sprite という分散 OS 向けに書かれたものである。 同 OS が起源のものには他に Tcl/Tk なんかがありますな、あと Log-structured File System も Sprite が最初に実装したものなので 4.4BSD LFS への影響もある。
特徴は名前の通り Parallel つまり分散環境上で並列ビルドが実行できること、コンパイル限定なら今では分散 OS でなくとも distcc 使って似たようなことできるけど、当時としては画期的なことだったのだ。
V7 make と PMake との違い
V7 make におけるビルトインの変数及びルールは C の文字列配列によるテーブル管理である、そこそこ長いので cc(1) と as(1) 関連だけ引用すると
6 char *builtin[] =
7 {
8 ".SUFFIXES : .out .o .c .f .e .r .y .yr .ye .l .s",
...
15 "CC=cc",
16 #ifdef vax
17 "AS=as".
18 #else
19 "AS=as -",
20 #endif
21 "CFLAGS=",
...
29 ".c.o :",
30 "\t$(CC) $(CFLAGS) -c $<",
...
35 ".s.o :",
36 "\t$(AS) -o $@ $<",
...
78 ".s.out .c.out .o.out :",
79 "\t$(CC) $(CFLAGS) $< $(LOADLIBES) -o $@",
...
95 0 };
という感じなので、変更が必要な場合は Makefile で上書きするか files.c を修正して再コンパイルが必要となる。
一方 PMake ではこれらのビルトイン変数およびルールは -m
オプションで指定されたディレクトリ(デフォルトは /lib/pmake
)以下にある sys.mk
という名前の Makefile に記述することが可能となっている。
...
15 .SUFFIXES : .out .a .ln .o .c .cc .F .f .e .r .y .l .s .cl .p .h \
16 .c,v .cc,v .y,v .l,v .s,v .h,v
...
29 CC = cc
...
32 #if defined(vax) || defined(sun)
33 AS = as
34 #else
35 AS = as -
36 #endif
...
39 CFLAGS =
40 AFLAGS =
...
58 .c.o :
59 $(CC) $(CFLAGS) -c $(.IMPSRC)
...
70 .s.o :
71 $(AS) $(AFLAGS) -o $(.TARGET) $(.IMPSRC)
...
93 .s.out .c.out .o.out :
94 $(CC) $(CFLAGS) $(.IMPSRC) $(LOADLIBES) -o $(.TARGET)
...
なのでこのファイルを編集するなり -m
スイッチで別のディレクトリにある sys.mk
を読ませることで make(1) の呼ぶコマンドなどを簡単に切り替えることができるわけ。
これはクロスコンパイルに非常に有利といえる、よし PMake の P には Parallel だけでなく Portable も含めていいぞ!
また
- プログラミング言語 C プリプロセッサの
#include
のように他の Makefile を読み込む機能 - 同様に
#if
~#else
~#endif
による条件分岐機能 - 変数の展開中の文字列置換機能
が利用できるのも V7 make には無い強力な開発支援である。
例えばごく単純化した例ではあるけど以下のような Makefile があった場合
- foo/Makefile
SRCS=main.c foo.c ...
OBJS=main.o foo.o ...
#if !empty(BUZ)
SRCS+=buz.c
OBJS+=buz.o
#endif
all: foo
foo: ${OBJS}
${CC} -o foo ${OBJS}
- bar/Makefile
SRCS=main.c bar.c ...
OBJS=main.o bar.o ...
#if !empty(BUZ)
SRCS+=buz.c
OBJS+=buz.o
#endif
all: bar
bar: ${OBJS}
${CC} -o bar ${OBJS}
これを以下のように外部の Makefile に共通処理として置くよう書換えることができる。
- foo/Makefile
PROG=foo
SRCS=main.c foo.c ...
#include "../buz.mk"
- bar/Makefile
PROG=bar
SRCS=main.c bar.c ...
#include "../buz.mk"
- buz.mk
#if !empty(BUZ)
SRCS+=buz.c
#endif
OBJS=${SRCS:.c=.o}
all: ${PROG}
${PROG}: ${OBJS}
${CC} -o ${PROG} ${OBJS}
ちなみに Sprite では /lib/pmake 以下にカーネルやライブラリそしてプログラム向けの様々な共通 Makefile が用意されている (内容についてはいまさら解説する意味が無いので割愛)。
プログラマは Makefile が必要な場合これらをインクルードしていくつかの変数を定義するだけで終わり、make(1) の文法なんぞ一切覚える必要を感じないほどである。
いや実際自分も PMakeの子孫である BSD make で用意された共通 Makefile に頼り切りの人生でまともに make(1) の文法知らねえ!
BSD make の誕生
4.3BSD-Reno において V7 make は PMake の BSD 移植版に置換えられることとなる。 これはまだ USL vs BSDi の訴訟前なので知財どうこうではなく PMake の方がシンプルに優れていると判断されてのことだと思われる。
なお BSD 移植版では #include
や #if
~ #else
~ #endif
はそれぞれ .include
および .if
~ .else
~ .endif
に改められている。
これは Makefile におけるコメント開始が #
なのに cpp(1) 風の文法を採用するってバカなの?って話で、正気に返っただけの話。
また BSD は分散 OS ではないのでマシンをまたいでの分散ビルド機能は実装できない。 ただし同一ホスト上に限れば fork(2) による複数プロセスでの並列ビルドは可能なので Parallel であることに偽りは無いので引き続き PMake と呼ばれていたはずである。
しかし現在では BSD make と呼ばれることの方が一般的である、これは上記の歴史を一切無視して V7 make の再実装たる GNU make との文法の違いだけに注目し GNU vs BSD の対立構造 みたいな色眼鏡によって生まれたもので要はゲハ蔑称みたいなもんなんやな。
まぁいいや、BSD 移植版では以下の共通 Makefile が用意された。
- bsd.doc.mk … TeX ドキュメント向けの変数やルール
- bsd.lib.mk … ライブラリ向けの変数やルール
- bsd.man.mk … マニュアル向けの変数やルール
- bsd.prog.mk … プログラム向けの変数やルール
- bsd.subdir.mk … サブディレクトリ処理のためのルール
これら bsd.*.mk に定義される変数やルールについては bsd.README に解説があるのでそっちを読んでくれ。
ここでようやくタイトル回収、そう bsd.lib.mk には今となっては非常に根の深い問題があるのである。
次回
本題に入る前に力尽きたが、次回は bsd.lib.mk の変遷となぜ表題の通りもはや時代遅れなのかを書きたいと思う。