Kengo's blog

Technical articles about original projects, JVM, Static Analysis and TypeScript.

エコシステムにビルドツールがたくさんあるのは悪いことではない

JavaやNodeJSには多数のビルドツールがあります。ものによってはビルドツールではなくタスクランナーとかワークフローとか名前が付いてるかもしれませんが些細なことです、ここでは以下のようなツールのことをまとめてビルドツールと呼びます:

一方で言語公式のビルドツールを用意している言語もあります。これによってプロジェクトごとに異なる技術を学ぶ必要性が減りますし、一貫性のある開発体験を得ることができます。javac javadoc のような単純なコマンドしか提供しないJavaとは異なる方針を言語として持っていることは明らかでしょう。

では言語のエコシステムにビルドツールがたくさんあることはモダンではなく不便なのでしょうか?そんなことはないだろうというのが自分の考えです。もちろん欠点がないとは言いませんが、以下に私見を述べます:

プロジェクトによってビルドツールに求められる役割は異なるため、きめ細かな選択肢を選べる

例えばプログラマが若干名のプロジェクトでは、コンパイルやテストが一箇所にまとまっていてフットワーク良く改善を回せていけることが望ましいでしょう。複数リポジトリやサブプロジェクトを作る必要性もまだ薄いでしょうし、そこまで統制について考えることもありません。自分なら開発が活発でパフォーマンスも良いGradleを選択することになると思います。

一方で何百人ものプログラマが関与するプロジェクトでは、ビルドツールやワークフローについても統制を考えるケースが出てきます。 mvn test を実行したらテスト実行結果が必ずJUnitのXML形式で target/surefire-reports/TEST-*.xml に吐き出されなければならないとか、Reproducible Buildsに準拠するとか、developブランチにマージしたらSonarQubeを実行せよとか、ビルドするにはJava 8を使わなければならないとか、そういったベースとなる要求をすべてのプロジェクトに守らせることでリポジトリ横断的な品質改善に役立てたりするわけです。

今だとこういった要求もGradleで満たせそうですが、7年くらい前?に自分が似た状況にあったときは、Mavenのparent projectによる制約の中央管理とバージョン管理が非常にマッチしました。DSLがないので自由度が低く、統制側としては考慮すべきことが減るというのもあります。中央管理する以上は各リポジトリの困りごとをきちんと拾い上げる姿勢は必要になりますが、その工数を考慮してもMavenに軍配が上がることはあるでしょう。

ビルドツールの思想に種類があることを学べる

そもそもこうした違いはどこから生じるのでしょうか。モダンな技術を使って開発された新しいビルドツールは常にレガシーなものよりも優れているべきではないのでしょうか?実はそうではなく、むしろ最もレガシーなApache Antと最もモダンなGradleはかなり近い特徴があります。

Apache AntとGradleはタスクを繋いで有向非巡回グラフ(DAG)を作るという発想で作られています。テストはテストケースのコンパイルに依存し、テストケースのコンパイルは実装のコンパイルに依存し、実装のコンパイルはアノテーションを使ったコード生成に依存する……といったタスクの間の依存関係を明示することで、タスクを並列実行したり不要なタスクの準備を省いたりして高速化ができるのです:

graph LR;
  annotation-processing --> compile --> testCompile --> test --> check --> build;
  compile --> jar --> assemble --> build;

特定のタスクだけ実行する・特定のタスクだけ除外するといった操作も簡単に行なえます。プロジェクト固有のタスクや概念を導入することも容易ですが、一方でタスク実行時に必要な入力がすでに生成されているかどうかを管理するため、タスクの入出力を宣言したり、タスクが依存するタスクを明記する必要があります。DAGをメンテナンスする責任をユーザが負い、それを前提にタスクの内部実装を気にせずに済むようになっています。

Mavenはビルドライフサイクルという概念があり、すべてのプロジェクトはこのライフサイクルに従うことを期待されています。ビルドライフサイクルをゼロから作ることも可能ですが、かなり重い作業です。

ビルドライフサイクルにはフェーズが定義されており、このフェーズにプラグインのゴールを紐付けることで、どのようなプロジェクトでも同じビルドライフサイクルで臨んだ結果を得られるようにしています:

graph LR;
  subgraph compile
    compiler:compile
  end
  subgraph test-compile
    compiler:testCompile
  end
  subgraph test
    surefire:test
  end
  subgraph package
    jar:jar
  end
  compile --> test-compile --> test --> package

そのフェーズに入った時点で以前のフェーズはすべて完了していると信じられるため、ゴールの入力がすでに生成されているかを気にする必要は比較的薄いでしょう。フェーズ内で実行する処理に依存関係がある場合、 compiler:compile ゴールがアノテーションプロセッシングとコンパイルの両方を行うように、ひとつのゴールにまとめてしまうことで単純化します。

一方でやはり柔軟性には欠けます。ライフサイクルの一部だけ実行したい場合、例えばテストを再実行してレポートを生成する場合など、必要なプラグインのゴールを特定してそれを直接実行しなければなりません。逆にテスト以外のすべてを実行する場合も、プラグインの実装を理解して -DskipTests オプションを指定するといったことも必要です。依存先のゴールを自動的に推定・実行することもないためゴールの入力が不正になることも多く、昔は「とにかく mvn clean してやりなおす」ということもよくやっていました。おそらく多くのMavenプロジェクトでは、開発時の混乱を避けるためにREADME.mdCONTRIBUTING.mdにこういうときはこうするというコマンド一覧が載っていると思います。

長くなりましたが、すべての状況にマッチするツールが存在しないのは、ツールの根底にある思想によって適した現場がそれぞれ異なるからだと考えられます。これらの思想そのものは20年以上変化していない時代の荒波に揉まれたものですので、一長一短はあれど使い所が合えば価値の高いものだと言えます。ビルドツールの多様性は、それすなわち言語の活用幅の広さだということなのでしょう。

単に歴史が長いのでビルドツールが数多く生まれてきた

特にJavaは言語としての歴史が長いので、多くのビルドツールが作成され検討されてきたという側面はあると思います。例えばApache Antを使っているfb-contribは2005年からあります。当時からJavaのビルドツールが成長せず、Antだけでここまで来れたかというと、ちょっと考えられませんね。最近(と言っても8年前ですが)Java Moduleにネイティブで対応するビルドツールも提案されていたりして、今でも新しい形が模索されています。

それで言うと今はひとつしかビルドツールを備えていない言語も、もしかしたら今後はビルドツールが2つ3つと増えてくるかもしれませんね。どんなニーズにもひとつのツールチェインで応えようとすると収集つかないこともありそうなので。

とはいえ新しいツールに乗り換えたほうがいいこともある

古いビルドツールをずっと使い続けると技術革新の恩恵を得にくいみたいなところはありますので、ビルドツールをモダンなものに変えていく努力はしたほうが良いことはあります。これからも活発に変更を入れていくプロジェクトであれば特に、更新が活発なビルドツールに移行したほうが良いでしょう、私もFindBugsをSpotBugsにforkするときはAntとMavenを使っていたプロジェクトをGradleで書き換えるという経験をしました

異なる思想を持つツールに移行する場合は両方のツールに詳しくないと思わぬところで失敗するなんてこともあるので、一時的に有識者に手伝ってもらうことも検討しましょう。私もビルドツール移行の副業を受け付けております(唐突な宣伝):

youtrust.jp

まとめ

エコシステムにビルドツールがたくさんあることは悪いことではありません。キャッチアップが大変とか、コミュニティの知見が分散してしまうとかはもちろんあるのですが、コミュニティの抱えるプロジェクトの多様性を担保し、歴史あるプロジェクトと新鋭気鋭なプロジェクトとが同居する上でとても重要な貢献をしています。

キャッチアップコストが気になる場合はREADMEを整備するとか情報の多い新しめのツールに乗り換えるとか、自衛策を取ることもできます。最初からビルドツールがひとつだったら払わなくて済むコストでは確かにあるのですが、言語の歴史と実績に思いを馳せていただければと思います。