😊

metalavaでKotlinライブラリの破壊的変更を検出する

2021/10/11に公開

metalavaでKotlinライブラリの破壊的変更を検出する

自分のライブラリに破壊的変更が加えられてしまうと、ユーザーが混乱するので、他人に使ってもらう前提で開発するライブラリにおいては、APIに破壊的変更を加えないように注意しておいたほうが良い。仮にAPIに破壊的変更を加える前提で開発中だとしても、リリース間でどのような破壊的変更を加えたのかを把握できるようにしておいたほうが良い。

APIの破壊的変更を検出してくれるパッケージ配布システムもあるが、そうでない言語では破壊的変更が起きないように自衛する必要がある。APIの破壊的変更を検出するには、ビルド時にAPIメタデータとなるテキストを生成しておいて、自分がstable API(あるいは規範的API)として生成しておいたテキストと比較して、内容が異なっていたらエラーとなるようにすれば良い。

筆者は最近だとKotlinでライブラリを開発している(ことが多い)ので、Kotlinでこれを実現できるようにしたい。Android APIなどではAPIメタデータが生成されてAOSPに含まれているので(~/Android/Sdk/platforms/android-31/data/api-versions.xmlなど)、何らかの方法で既に実装されている可能性が高い…と考えて探してみたら、AndroidではmetalavaというツールでAPIメタデータを生成していることが分かった。

ちなみにmetalavaの他にも、バイナリレベルまでAPI差分を検出できるsiom79/japicmpとかKotlin/binary-compatiblity-validatorといったツールもあるようだ

今回は最初に発見したmetalavaをビルドに組み込めるmetalava-gradle-pluginを自分のktmidiライブラリに適用したので、metalavaの使い方をまとめる。といってもAPI差分をチェックしたいモジュールのbuild.gradle(.kts)に、ほんの数行変更を加えるだけだ。

buildscript {
    repositories {
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath "me.tylerbwong.gradle:metalava-gradle:0.1.9"
    }
}

plugins {
    id "me.tylerbwong.gradle.metalava" version "0.1.9"
}

...

metalava {
    filename = "api/$name-api.txt"
    outputKotlinNulls = false
    includeSignatureVersion = false
}

筆者がktmidiライブラリにmetalavaサポートを追加した時のコミットが具体例として参考になると思う(多少余計な変更が入ってはいる)。

生成されるAPIメタデータはこんな感じになる: https://github.com/atsushieno/ktmidi/blob/0ccc209/ktmidi/api/ktmidi-api.txt

この変更を加えてから./gradlew build を実行すると、ビルドが失敗するようになる:

* What went wrong:
A problem was found with the configuration of task ':ktmidi:metalavaCheckCompatibility' (type 'JavaExec').
> File '/media/atsushi/extssd0/sources/ktmidi/ktmidi/ktmidi/api/ktmidi-api.txt' specified for property '$1' does not exist.

これは期待通りの動作で、metalavaがfilename = ...の部分で指定したファイルが発見できなかったためだ。規範的APIのメタデータを用意しないかぎり、ビルドは失敗し続ける。規範的APIメタデータは、metalava-gradle-pluginが比較を実行する前にbuild/metalava/current.txtというファイルに出力されるので、最初はこのファイルをfilename = ... で指定したファイルとしてコピーすれば、ビルドが通るようになるはずだ。

以降は、APIに破壊的変更が加えられたら、ビルドが失敗するようになる。(例):

> Task :ktmidi:metalavaCheckCompatibility FAILED

/media/atsushi/extssd0/sources/ktmidi/ktmidi/ktmidi/build/metalava/current.txt:1194: error: Added class dev.atsushieno.ktmidi.ci.CIFactory [AddedClass]
/media/atsushi/extssd0/sources/ktmidi/ktmidi/ktmidi/api/ktmidi-api.txt:1194: error: Removed class dev.atsushieno.ktmidi.ci.CIFactoryKt [RemovedClass]
Aborting: Found compatibility problems checking the public API (/media/atsushi/extssd0/sources/ktmidi/ktmidi/ktmidi/build/metalava/current.txt) against the API in /media/atsushi/extssd0/sources/ktmidi/ktmidi/ktmidi/api/ktmidi-api.txt

自覚的にAPIの破壊的変更を加えたい場合は先のcurrent.txtからコピーして更新するようにすれば、またビルドが通るようになるだろう。

Discussion