kmizuの日記

プログラミングや形式言語に関係のあることを書いたり書かなかったり。

MiMa(Migration Manager)でScalaプロジェクトのバイナリ互換性をチェックする

MiMaは主にScalaライブラリ(Scala本体を含む)のバイナリ非互換な変更をチェックしてくれるライブラリです(おそらくバイトコードレベルの検査なので、Javaプロジェクトでもチェックできると思うのですが試していない)。

元々の経緯としては、Scala 2.9以降、マイナーバージョンアップでのバイナリ下位互換性は保証するよー、という話があり(それ以前は全く保証がなかった)、そのことを保証するために開発されたツールで、それがオープンソース化されたのがMiMaとなります。

詳しい経緯については

を参照してください。

さて、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にそれを検出させてみましょう。

github.com

意味の無い変更をコミットするのはさすがにむなしいので(別ブランチ作れという話はあるけど)、名前変更を行ってみました。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をしたい場合等、特定のエラーは無視したいというケースはあると思います。そのような場合、

github.com

に書かれているように、部分的にチェックを無効にすることができます。というわけで、MiMaの概要について紹介してみました。それでは快適なScalaライフを!(ひょっとしたらJavaライフも)