この記事は、 Scala Advent Calendar 2022 の 15 日目の記事です。
はじめに
こんにちは、 AI事業本部 ミライネージ所属の阿川(@atty303)です。
ミライネージでは、バックエンドをほぼ全て Scala で記述しており、ビルドツールに Mill を採用しています。正確には、最初は普通に sbt でスタートしましたが、半年ほど前に Mill に移行した形となっています。その際、そのままの Mill では自分達のニーズを満たせない部分があったので、Issue をあげたり Pull Request を投げたりして、プロジェクトに採用可能なところまで持っていきました。この記事ではそんな自分のコントリビュートを Mill の紹介を兼ねて振り返ろうと思います。
この1年で自分がコントリビュートした改善
1. BSPプロジェクトの同期時にコンパイルが走らないようになった
最初に着手したのが、BSP プロジェクトの同期の改善です。最初にプロジェクトを開くときや Mill のビルド設定を変更したときは、IDE にプロジェクト構造を同期する必要がありますが、その際に全モジュールのコンパイルが走ってしまうという問題がありました。本来はクラスパスなどプロジェクト設定だけを抽出するだけの軽量なタスクのはずなのに何故コンパイルが走るのか、ソースを追って原因を調べ、考えられる修正方法とともに Issue #1497 を上げました。
結果、メンテナーの Tobias Roeser 氏がより良い形で実装を行ってくださり、 0.10.0-M4 で改善がリリースされました。現在は非常に高速にプロジェクト同期が行われるようになっています。
2. IDEから実行・テストするときに設定されるクラスパスが正しくなった
同一アーティファクト(JAR)の別バージョンがクラスパス上に複数存在するとき、先頭に記述されているアーティファクトが優先してロードされるという挙動がありますが、どちらのバージョンが先頭にくるのかが、IDEとMillで異なっていました。そのため、Mill から実行すると正しく起動するのに、IDE から起動するとバイナリ非互換のエラー(NoSuchMethodErrorなど)が発生する、といった事象が発生しました。
これに対しては #1849 で BSP の JVM 拡張の実装を追加し、IDE が Mill の生成したクラスパスを利用するように修正を加え、 0.10.4 でリリースされました。これにより、IDE と Mill の実行時挙動が一致するようになりました。
このときは IntelliJ IDEA のデバッグ版をビルドしてデバッガで挙動を追いかけたりして、 Scala プラグインの内部をちょっと知ることができました。普段使っている IDE はとても複雑でどのような構造になっているのか皆目見当も付いていなかったですが、デバッガがあれば意外となんとかなるものだと思いました。
3. 子プロジェクトがルートプロジェクトの一部とならないようにした
IDE でプロジェクトを読み込んだとき、 build.sc があるルートディレクトリがソースディレクトリとして認識され、全てのサブディレクトリが Java のパッケージとして認識される問題がありました。サブディレクトリに配置しているサブプロジェクト内の Scala ソースで「パッケージ宣言とファイルシステムの配置場所が違う」という警告が出てしまいます。
これに対しては #1876 で修正を加え、0.10.5 でリリースされました。
4. ファイルシステムの種類に依存したキャッシュの非依存化
Mill はコンパイル結果のキャッシュが有効かどうかをソースファイルのハッシュ値を用いて判定しています。このハッシュ値を計算する際にディレクトリのトラバーサルを行うのですが、トラバース順序によってハッシュ値が変わる実装になっていました。そして、トラバース順序がファイルシステムの種類に依存していた、という因果関係になります。
この挙動は、 ls -f コマンドで確認することができます。Linux で XFS を利用している場合、下記のような結果になります。
$ touch a $ touch b $ ls -f . .. a b $ touch b $ touch a $ ls -f . .. b a
ファイルの作成順によってトラバース順序が変わっているのが確認できます。対して Ext4 で同様の操作を行うと下記の結果となります。
$ touch a $ touch b $ ls -f b . a .. $ touch b $ touch a $ ls -f b . a ..
こちらはファイルの作成順を変えてもトラバース順序が変わりません。そのため、Ext4 で Mill を使っていると、キャッシュが不意に無効化されることがある問題に気付くことが出来ないわけです。自分は CI を XFS で構築していたため、tar コマンドによるキャッシュの退避・復元により本来有効なはずのキャッシュが無効化されることがあることに気づきました。
これに対しては #1992 でトラバース時にファイル名で明示的なソートを行うことで、安定的なハッシュ値を生成するように修正し、0.10.6 でリリースされました。
5. mill-jib プラグインを作った
mill-jib というプラグインを作りました。これは Google が作っている Jib というツールと連携するプラグインで、JVM アプリの実行可能な Docker イメージを Dockerデーモン無しで作成するものとなります。CI は Docker コンテナ内で動いているため、イメージ作成に Docker を使うと Docker in Docker となり複雑になってしまうため、Jib が有効です。
結構有用なプラグインだと思うんですが、最近メンテナンスしていないのとドキュメントを書いていないので、時間を作ってなんとかします…
さいごに
sbt は非常に成熟したツールで情報も多いですが、ある種の複雑さも備えていると思います。簡単なプロジェクトであれば簡単に使えるのですが、複雑なタスクを組もうとすると sbt そのもののモデルの理解を含め、急に難易度が跳ね上がる印象があります。そのあたりは Mill 作者の Li Hayoi 氏が So, what’s wrong with SBT? という記事に問題点をまとめているので、sbt に思うところある方はご一読されることをお勧めします。
逆に Mill はまだ未成熟なツールで情報も少ないですが、概念モデルのシンプルさ、コードの追いかけやすさなど、sbt にはない強みがあります。今回の一連の対応で IntelliJ IDEA から使うには不自由ないレベルになったと思いますので、ぜひ一度触ってみてください。