MiMa(Migration Manager)でScalaプロジェクトのバイナリ互換性をチェックする
MiMaは主にScalaライブラリ(Scala本体を含む)のバイナリ非互換な変更をチェックしてくれるライブラリです(おそらくバイトコードレベルの検査なので、Javaプロジェクトでもチェックできると思うのですが試していない)。
元々の経緯としては、Scala 2.9以降、マイナーバージョンアップでのバイナリ下位互換性は保証するよー、という話があり(それ以前は全く保証がなかった)、そのことを保証するために開発されたツールで、それがオープンソース化されたのがMiMaとなります。
詳しい経緯については
@xuwei_k @ikeike443 言葉不足でした。それはそうだろうと思います。Scala Days 2011の時点でMIMAが既にあったわけですから、それ以前からチェックはやり始めていただろういうのはおっしゃる通り。公式的な互換性の保証という意味で言っていました。
— 水島 宏太(Klassic作成中) (@kmizu) 2012年12月24日
を参照してください。
さて、MiMaは単体で動作するコマンドラインツールとしてと、sbtプラグインとしてと両方が提供されています。しかし、Scalaでライブラリ作ってる人はまあ大抵sbt使ってると思いますので、そっちについて軽く説明します。詳しくはMiMaのWikiをどうぞ。
とここまで前置きです。導入は非常に簡単で、project/plugins.sbt
に以下の一行の記述を加え(バージョンは適宜その時点での最新のものに置き換えてください。2016/03/16時点では、0.1.9が最新版となっています)
addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.9")
互換性を検査する基点となるartifactを以下のようにbuild.sbt
に追加するだけです。
mimaPreviousArtifacts := Set("com.github.kmizu" % "macro_peg_2.11" % "0.0.7")
現在、Macro PEGの最新リリース(Maven Centralに同期されているもの)は0.0.7なのでこのようになっていますが、もちろん、スナップショットリリースなどを指定することもできます。
さて、このようにした上で、コマンドラインから
> sbt mimaReportBinaryIssues
と叩きます。何も問題がなければ、
[info] macro_peg: found 0 potential binary incompatibilities while checking again
のように表示されます。まあ、これで終わっては意味がないので、バイナリ互換性を壊す変更を入れてみましょう。と、その前に、バイナリ互換性についておおざっぱに考えてみます。たとえば、hoge 0.0.1では、class Hoge
が次のように定義されていたとします。全く無意味なライブラリですが、例のためです。
class Hoge { def hoge(): Unit = { println("hoge") } }
hoge 0.0.1のユーザは、class Hoge
を継承して、class HogeHoge extends Hoge
としてhoge
メソッドをオーバーライドしても良いですし、(new Hoge).hoge()
と呼び出してもかまいません。さて、hoge 0.0.2では、メソッドhogehoge(): Unit
を追加し、その中ではhoge()
を2回呼び出したいとします。hoge()
メソッドが一回だけhoge
を出力するのを保証したいので、hoge()
メソッドはfinal
にしましょう。
class Hoge { final def hoge(): Unit = { println("hoge") } def hogehoge(): Unit = { hoge() hoge() } }
さて、こうするとhoge 0.0.1でHoge
クラスを継承して、hoge()
メソッドをオーバーライドしていたユーザがhoge 0.0.2にアップデートするとどうなるでしょうか。答えは、(早ければ)コンパイルエラーか(遅くとも)クラスのリンク時にエラーになる、です(若干正確ではありません)。これは、hoge 0.0.2のバイナリにおいて、Hoge
のクラスファイルで、hoge()
がfinal
と指定されたことによります。このような変更をバイナリ非互換な変更と呼びましょう。他にも、hoge()
メソッドを削除した場合とか、バイナリ非互換になる理由は色々あります。
そして、これは誤解されがちな点ですが、別にScalaがメジャーバージョンアップでバイナリ非互換になるからという理由だけで、Scalaライブラリがバージョンアップによってバイナリ非互換になる、というわけではないということです。
Javaのプロジェクトであっても、既存のメソッドを削除したり、既存のメソッドをfinalにしたりすれば同様の問題は起こります。ただし、ScalaライブラリはScalaのメジャーバージョンに依存しているので、Scalaのメジャーバージョンアップにともなって、Scalaライブラリもバイナリ非互換になるという点はScala特有の事情です(よく使われているScalaライブラリでは、この問題を解決するために、複数のScalaメジャーバージョン向けに同一ソースから複数種類のバイナリをクロスビルドしていることが多いです)。
前置きが長くなりましたが、実際にバイナリ非互換な変更を行って、MiMaにそれを検出させてみましょう。
意味の無い変更をコミットするのはさすがにむなしいので(別ブランチ作れという話はあるけど)、名前変更を行ってみました。pfun
というよくわからない名前より、delayedParser
の方が実態をよく表していると思います(ちなみに、classのval/varパラメータはby nameになることができません)。
変更によってmacro_peg
がコンパイルエラーになったりはしていません。ですが、pfun
はcase classのパラメータである以上、この名前で外部にアクセスできるように公開しているということになります。この名前を変更するというのはまさにバイナリ非互換な変更です。そこでMiMaの出番です。上記のコミットに対して、
> sbt mimaReportBinaryIssues
を走らせます。すると…
[error] * method pfun()scala.Function0 in class com.github.kmizu.macro_peg.comb not have a correspondent in current version [error] filter with: ProblemFilters.exclude[DirectMissingMethodProblem]("com. arsers#ReferenceParser.pfun")
現在のバージョンにはpfun
というメソッドがないよーと文句を言ってきました。ここでは既にあったメソッドがなくなったという簡単な例でしたが、他にも、
といったバイナリ非互換の要因になる様々なものに対してチェックをしてくれます。ただ、意図してbreaking changeをしたい場合等、特定のエラーは無視したいというケースはあると思います。そのような場合、
に書かれているように、部分的にチェックを無効にすることができます。というわけで、MiMaの概要について紹介してみました。それでは快適なScalaライフを!(ひょっとしたらJavaライフも)