理想のビルドツールについて

Posted on December 8, 2014

概要

最近、新しいビルドツールを作ろうと真剣に考えている。いまさら新しいビル ドツールなんて、と思われるかもしれない。しかし私の知る限り、現状のビル ドツールはすべて理想から程遠い。私個人が理想とするビルドツールとは一体 どのようなものか、長い時間をかけて考えてきたが、大体の構想が決まったの で一度まとめてみようと思う。

ビルドツールの本質

私がビルドツールと聞いて真先に思い付くのは Make である。 Make は古典的 なビルドツールで、時にはその古さゆえに馬鹿にされることもあるが、実はビ ルドツールの本質を最も捉えている存在なのである。

ビルドツールの本質とは、ある成果を要求すると最小の手間でそれを達成する ことである。 Make の場合、「成果」とは実行可能ファイルやオブジェクトファ イルなどのファイルである。「最小の手間」は 宣言的に記述された Makefile から依存関係を抽出し「成果」を生成するまでの最小手順を解析することで保 証される。

「最小の手間」はビルドツールの本質ではない、と思われるかもしれない。確 かに、すべての「成果」を逐次的に出力するようビルドスクリプトを記述すれ ばよいかもしれない。しかし、プロジェクトが大きくなるにつれビルド時間が だんだんボトルネックになってくる。さらに深刻なのは、人間が依存関係を直 列化する必要がある、ということである。複雑な依存関係を正確に直列化でき る人間なんておそらく存在しないし、それは機械の仕事だ。

余談だが、 JavaScript 界隈は Make の歴史をもう一度やり直そうとしている ように見える。うまく対処しないと上記した問題に直面することになるだろう。

目指す理想

では Make で十分なのかと言うとそうではない。 Make の問題点は後で詳しく 述べるとして、先に目指す理想を明らかにしておく。基本的には Make を一般 化する、という戦略を取るつもりだ。そのため Make の持つ以下のような望ま しい性質は基本的に保存される。

  1. 要求された成果を最小の手間で達成する
  2. 依存関係をある程度自由に設定できる
  3. プロジェクト固有の設定を簡単に追加できる
  4. 最終的に実行されるコマンドを観察できる

これらはすべて現実的な要請に応えるための重要な性質だ。

(1)これは開発効率の悪化を防ぐために必須である。また、後述する開発ツー ルとの連携にも必要である。なぜなら開発者は開発に必要な何らかの操作に対 して高速なフィードバックを要求するからだ。 1

(2)「設定より規約」という言葉があるが、依存関係を規約に収めるのはナ ンセンスだ、という思想がここにある。例えば、依存関係スキャナ 2では捉 えきれない依存関係は手動で設定せざるを得ない。現実的にプロジェクトを進 めるために回避策が残されているのは重要なのである。

(3)これは(2)と似ている。例えばディレクトリ構成を規約によって定める ことはナンセンスである。もし規約から外れてしまったらどうするのか。私は これまで規約から外れたが為に非本質的でアドホックなビルド定義をいくつも 見てきた。重要なのは規約ではなく、自由度や拡張性である。規約はよくでき たデフォルトに過ぎない。

(4)これはさらに現実的である。もしビルドツールがブラックボックスと化 していたら、どのようにビルド定義をデバッグするのだろうか。長時間を費や してビルド定義をデバッグし問題を解決しても現実の仕事は 1 ミリも進まな いのは我々にとって不幸な真実である。

以上の性質に加え、以下のような性質を追加するつもりだ。

  1. プロジェクト規模に対するスケーラビリティ
  2. プロジェクトをまたいだ再利用可能性
  3. 粒度の細かい「成果」
  4. エディタや静的解析ツールなどの開発ツールとの親和性

これらについては後で詳しく説明する。

色々と述べてみたが、根本にある動機は、開発者がビルドにまつわる問題に邪 魔されることなく快適に開発できる環境を実現することである。

背景

ここで私が新しいビルドツールを必要とする背景を述べてみよう。

感情に関する背景

最も根源的な部分はおそらく感情だろう。今まで触れてきたビルドツールを精 神的ダメージの大きい順に並べると大体次のようになる。

Maven はインクリメンタルビルドおよび一貫性のあるビルドをサポートできて いない時点で外れ値とみなしてもよかったがあえて載せてある。 Maven 愛好 家のために補足すると、 Maven をビルドツールではなく外部ライブラリの依 存関係を解決するツールだと考えれば特に問題ない。しかしビルドツールとし て使われている現場は実際に存在するのだ。

OCamlbuild と OASIS はその名の通り OCaml 界隈でよく使われる 3ビルドツールで、作者の方には大変申し訳 ないが、これがとんでもなく使えないツールなのである。使えないというのは、 文字通りドキュメントがなくて使えないという意味もあるが、なんとなくコン セプトを理解してもなお使えないという意味でもある。これはおそらくコンセ プトが本質的な単純さを欠いており必要以上に複雑になっているからだと思わ れる。

SBT と Cabal はほとんど使ってないから順位に妥当性はないが、もっと使っ ていたらさらに上位に位置している可能性は十分にある。 SBT に関してはコ ンセプトすら理解ができない。つまり、ビルドを純粋関数的に書けて何がうれ しいのかさっぱり理解できないのである。個人的にはぜひとも論理的に書きた い。

Ant は Make とほとんど同じであるが、 Java 的にポータブルであるという点 と、「成果」重視というよりタスク重視という点で異なる。前者について、ポー タブルであるのは良いと思うが、 UNIX ツールのノウハウをほとんど再利用で きない上、 XML での記述が極めて冗長なため書くのも読むのも辛い代物であっ た。後者について、これは本質的な違いだと思うが、タスクというのは命令書 であって最終目的である「成果」を指示するものではない。したがって、成果 の依存関係に依る Make より粒度が粗いのだ。しかしこれは Make の欠点でも ある。 Make では成果をファイルでしか観測できないのだ。つまり、何かを行 なってそれが完了したか否かのような行為そのものを成果とみなすことができ ない。タスクという概念が登場した背景にはそのような問題があったのだろう と考えている。

このリストの上位を見ていると、精神的ダメージが大きいビルドツールに共通 して「設定より規約」を重視しているように思えてくる。「設定より規約」の 考え方はおそらくこうであろう。つまり、大抵の設定は 80% ぐらいは共通す るのだからそれを規約にすれば、設定に費やす時間を丸々節約できる、と。そ れは場合によっては正しいと思う。しかし不思議でならないのは、これがビル ドについても成り立つと勘違いしてしまうことだ。単純なプロジェクトならと もかく、ビルドは本質的に難しいのである。そこを考慮せず安易にビルドツー ルを設計すると、数多ある「ダメ」なビルドツールに仲間入りしてしまうので ある。

つまるところ、これ以上の精神的ダメージを受けないために、また人生の貴重 な時間をビルド定義のデバッグという無駄な作業に費やさないために、「ダメ」 なビルドツールが登場しては消える今の悪い流れを断ち切る必要があるのだ。

合理性に関する背景

開発ツール全体に関する統一性のなさはそのまま経済的な合理性を欠く結果に なっていると思う。ここで言う開発ツールとは、ツールチェインのようなもの で、エディタやコンパイラ(言語)、静的解析ツール、依存解決ツール(主に 外部ライブラリ)など、開発に欠かせないものをすべて含む。

例をあげて説明してみよう。あるエディタで、ある言語のためのコード補完ツー ルを作りたいとする。この時、エディタはそのプロジェクトで利用されている ビルドツールを知らなければならない。というのも、そのコードがどのような オプションでコンパイルあるいは実行されるか知らなければ正確なコード補完 が実現できないからである。

例えば、ある C のコードが次のようにコンパイルされるとしよう。

$ gcc -I foobar -c a.c

これは foobar をインクルードパスに追加しない限り a.c はコンパイル できないことを意味する。ということはつまり、コード補完を実現するために は、エディタは是が非でもそのことを知らないといけないわけだ。その結果、 エディタはビルドツールを迂回してその情報を知る必要がある、ということに なる。

実際、未踏ユースプロジェクトで C++ の正確なコード補完を Emacs に実装し たときは、 GCC を拡張した上で、 GCC のラッパーコマンドを作り、コンパイ ル時のコマンドラインを記憶しておくことで、エディタからコード補完ができ るようにした。これはどのようなプロジェクト構成でも対応しうる点において 完全であったが、本質的な問題を解決したとは到底言えなかった。

特に開発ツールの移り変わりが激しい昨今において、この非合理性は個人的に 深刻な問題だと思う。例えば、エディタが N 個、ビルドツールが M 個あると する。このとき、エディタがそれぞれのビルドツールを知らない限り、正確な リアルタイムコードチェックやコード補完を実現できないとすれば、 N×M 個 の組み合わせをすべて実装しなければならなくなる。しかも、統一されたイン ターフェースを持たないために、すべてがアドホックな実装になり完全性はな かなか保証されない。実際、 Emacs や Vim での状況はこれと大差なく、例え ば色々な言語でリアルタイムコードチェックを実現してくれる flycheck とい う Emacs 拡張は、対応するすべての言語についてそれぞれアドホックで不完 全な実装を持っている。

ではどうればいいかと言うと、単にコード補完や静的解析用のインターフェー スをビルドツールに用意してやればよい。そうすればエディタはそのインター フェースだけを知っていればよく、ビルドツールを迂回して個別のプロジェク ト設定を知る必要はない。具体的には以下のようなコマンドが提供されていれ ばよい。

$ ideal-build-tool code-complete --file a.c --line 10 --column 3

これは a.c の 10 行目、 3 カラム目でコード補完を実行せよ、という意味 だ。ビルドツールは a.c をコンパイルするのに必要なオプションを正確に 知っているため、コンパイル時とコード補完時とで齟齬があることは決してな い。

さらに結果をそれぞれ Human Readable 、 S 式、 JSON などで出力できれば 十分だろう。 Emacs 拡張にとっては、上記のコマンドを実行し、その結果を パースした上で、コード補完の UI に渡せばよいだけになる。とてもシンプル ではないだろうか。

実は flymake という Emacs 拡張はこのような設計に基いている。しかし、残 念なことに Make に足を引きずられて廃れてしまった。実際、アドホックな手 段に変わりはなかったから仕方ないと思う。

以上、現状の経済的な非合理性は、私がずいぶん昔から常々感じていたことで、 是非とも解決したい気持ちがある。

Make の問題点

さて Make の問題点を詳しく述べてみようと思う。先述したビルドツールに追 加すべき性質を再掲する。

  1. プロジェクト規模に対するスケーラビリティ
  2. プロジェクトをまたいだ再利用可能性
  3. 粒度の細かい「成果」
  4. エディタや静的解析ツールなどの開発ツールとの親和性

(4)については前節で詳述した。

(1)(2)は Make の代表的な問題で以下の論文がこの問題に言及している。

(1)はプロジェクトの規模が大きくなるにつれ、ビルド定義が複雑に膨れ上 がって管理不能になってしまう問題で、(2)はビルド定義を部分的に再利用 することができない問題である。同じ問題とも言えるが、(1)はプロジェク ト内でも再利用可能性、(2)はプロジェクト間の再利用可能性を重視してい る点が異なる。ビルド定義はパッケージリポジトリのようなものを経由して広 く再利用されるべきなのである。

Make はこの二つの問題に対して非常に脆弱である。数多あるビルドツールの 登場の間接的な原因は、この Make の弱さにあると言っても過言ではない。再 利用性がないために、ほとんど自明なビルドルールをいちいち書かなくてはな らないのだ。それならもっと優れたビルドツールを作れるはずだ、と思ってし まっても仕方がないだろう。

先の論文の主題である OMake は両方の問題を解決したと主張しているが、こ れは少し怪しい。例えば、あるソースファイルのコンパイルオプションを、あ る特別な場合にのみ変更したいとする。 OMake ではコンパイルオプション用 の変数をあるスコープに限定して変更することでこれを達成する。これはすご く手続き的な発想だ。結局、アドホックな条件分岐が積み重なって本質を見失 うことになる。

(3)は Make が扱う対象をファイルから事実まで一般化するアイデアである。 Make でよく困るのは、ファイルの依存関係とタイムスタンプに依る以外にルー ルを書く方法がない点である。例えば、ファイルのチェックサムで変更を検出 する、といったことができない。これは Make が前回の状態を保持しないこと に根本的な原因がある。また「あるディレクトリ以下のすべてのファイル」の ような依存を書くのも難しい。さらに「ディレクトリ A とディレクトリ B が 同期している」のような依存を書くのも難しい。無理に書けば、そしてこれが ほとんど唯一の解なのであるが、 PHONY ルールを使って以下のように書く しかないだろう。

.PHONY: a-b-synced
a-b-synced:
	rsync -a a/ b/

.PHONY: do-something
do-something: a-b-synced
	...

ディスクキャッシュにのれば、その都度 rsync を実行してもさほど気にな らないだろうが、もしこれがもっと時間がかかる処理ならどうだろうか。なお、 言うまでもないかもしれないが、 Make のファイルは「一貫したファイルが存 在する」事実に他ならない。

以上から、「一貫したファイルが存在する」や「ディレクトリ A とディレク トリ B が同期している」などの事実を依存関係の対象にすること、さらにビ ルド終了時に最終状態を保存し、次回のビルドに利用することが是非とも必要 になる。

このように考えると、ビルドとは証明の探索に他ならない。探索中に確認され た事実はそのまま証拠となる。このようにしてビルドの一貫性を保つことは極 めて合理的・現実的である。

さらに、事実の集合は知識ベースに他ならないと考えることもできる。したがっ て、 Emacs や Vim などのエディタからプロジェクト固有の設定を参照するこ とは、何も不都合がなく完全である。

Prolog との関係

実は Make と Prolog はかなり近い関係にある。どちらもルールの集合とクエ リを与えてトップダウンに評価するという点において基本的に違いがないのだ。 実際、先述したファイルから事実への一般化は Prolog などの論理プログラミ ング言語に着想を得ている。

まだまだアイデア段階だが、もし完成したら、それは一つの論理プログラミン グ言語と呼んでも差し支えないものになるだろう。いくつか大きな課題が残っ ているが、個人的にはかなりうまくいくと考えている。

課題

今のところ重大な課題が二つある。

一つはどのように再利用可能性を保証するかである。一案としてモジュール機 構を搭載することが考えられるが、論理プログラミングにおけるモジュールと は何か、という問題をまだ解決できていない。一説によればモジュールは含意 (Implication)らしいが、本当にそれで良いのか分からない。

もう一つはある条件を満たす場合にのみ、あるソースファイルのコンパイルオ プションを変更するといった特殊ケースにどのように対応するかである。もし かしたら優先度のような概念を表現できる必要があるかもしれない。しかし何 が一番自然で一貫性を保てるか何も分かっていない。

実現にはもう少し時間がかかりそうだが、着実にゴールに近づいている実感が ある。


  1. 例えばテストや静的解析の実行

  2. gcc -M などのファイル間の依存関係を自動でスキャンするツール

  3. 最近は減っているかもしれない