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

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. 最近は減っているかもしれない