Android Gradle Plugin 9.0.0 (以降AGP 9)からKotlin の組み込みサポートが導入され、デフォルトで有効になります。オプトアウトの手段はあるものの、Kotlin Gradle Plugin(以降KGP)の記述を削除し、buitInKotlin利用への移行が推奨されるようになっています。
Kotlinでライブラリを開発している場合、ほぼ必須とも言える binary-compatibility-validator (以降BCV) ですが、AGP 9の buitInKotlin の環境では使えなくなってしまっています。
さらにタイミングが悪いことに、BCVはメンテナンスモードとなっており、
KGPのABIバリデーション機能への移行が案内されている状態です。
BCVにAGP 9対応のための改修が入る可能性は低いでしょう。
KGPのABIバリデーション機能はというと、以下のissueにある通り同様にAGP 9環境では使えない問題が発生しています。
AGPとKGP両者でお見合い状態、今のところ進展する様子はなく、解決するとしても長い時間が掛かりそうな状況です。
しばらくはAGP 9対応を見送る、もしくはオプトアウトしてKGPを使い続けることもできます。しかし、Android向けの機能を開発している以上、遠くない未来に移行が必要となってしまいます。
では、BCVの利用を諦めるか、というとそういうわけにもいきません。ライブラリを開発している以上、バイナリ互換性を破壊するような変更を行ってしまうと、多くの利用ユーザーに迷惑をかけることになってしまいます。BCVもしくはそれと同等の機能によるチェックは必須といえます。
なぜ使えなくなっているのか?
なんとか抜け道はないかということで、ソースコードを調べてみます。BCVはそれほど大規模な機能ではないのでなんとかなるかもしれません。
なるほど、Android環境の場合はkotlin-androidプラグインをフックしてtaskを作っていますね。builtInKotlin環境ではこのプラグインが存在しないため、BCVのタスクが追加されず、使えなくなっていたってことですね。
最低限使えるようにする
kotlin-androidプラグインをフックしているとはいえ、本質的にそれがないとどうにもならないわけではなく、出力先などの情報を取得しているだけです。自分の環境をハードコードするなど割り切った構成で「最低限使えるようにする」だけならそこまで難しくなさそうです。
ということでやってみます。
かなり割り切ったワークアラウンドです
- BCVのapplyは行わず、クラスパスに追加した状態にしてください
- Product Flavorがある場合
compileReleaseKotlinはcompile<Flavor>ReleaseKotlinに置き換えるなど環境に応じて読み替えてください - intermediatePathは内部パスなので将来変更される可能性があります。また、ビルドバリアントもパス内に含まれているため環境に応じて読み替えてください
internal fun Project.configureBinaryCompatibilityValidator() {
val bcvRuntimeClasspath = createBcvRuntimeClasspath()
val projectName = name
val extension = extensions.create("apiValidation", ApiValidationExtension::class.java)
val enabled = projectName !in extension.ignoredProjects && !extension.validationDisabled
val apiFileName = "$projectName.api"
val apiDir = layout.projectDirectory.dir(extension.apiDumpDirectory)
val apiFile = apiDir.file(apiFileName)
// 現在のbuiltInKotlinのclassファイル出力先のパスをハードコードしています。
val intermediatePaths = listOf(
"intermediates/built_in_kotlinc/release/compileReleaseKotlin/classes",
"intermediates/javac/release/compileReleaseJavaWithJavac/classes",
)
val apiBuild = tasks.register<KotlinApiBuildTask>("apiBuild") {
dependsOn("compileReleaseJavaWithJavac")
isEnabled = enabled
group = LifecycleBasePlugin.VERIFICATION_GROUP
inputClassesDirs.from(
*intermediatePaths.map { layout.buildDirectory.dir(it) }.toTypedArray()
)
outputApiFile.set(layout.buildDirectory.dir("api").map { it.file(apiFileName) })
runtimeClasspath.from(bcvRuntimeClasspath)
}
val apiCheck = tasks.register<KotlinApiCompareTask>("apiCheck") {
dependsOn(apiBuild)
isEnabled = enabled
group = LifecycleBasePlugin.VERIFICATION_GROUP
projectApiFile.set(apiFile)
generatedApiFile.set(apiBuild.flatMap { it.outputApiFile })
}
tasks.register<SyncFile>("apiDump") {
isEnabled = enabled
group = LifecycleBasePlugin.VERIFICATION_GROUP
from.set(apiBuild.flatMap { it.outputApiFile })
to.set(apiFile)
}
tasks.named("check").configure { dependsOn(apiCheck) }
}
private fun Project.createBcvRuntimeClasspath(): Configuration {
val configurationName = "bcvRuntimeClasspath"
val runtimeClasspath = configurations.create(configurationName) {
isCanBeResolved = true
isCanBeConsumed = false
}
dependencies.add(configurationName, "org.ow2.asm:asm:9.9.1")
dependencies.add(configurationName, "org.ow2.asm:asm-tree:9.9.1")
// BCVではプロジェクトで利用しているKotlinバージョンと合わせるように記載されています。
// gradle version catalogを参照するなど環境に合わせて読み替えてください
dependencies.add(configurationName, "org.jetbrains.kotlin:kotlin-metadata-jvm:2.3.0")
return runtimeClasspath
}
@DisableCachingByDefault(because = "No computations, only copying files")
internal abstract class SyncFile : DefaultTask() {
@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
abstract val from: RegularFileProperty
@get:OutputFile
abstract val to: RegularFileProperty
@Suppress("NewApi")
@TaskAction
fun copy() {
val fromFile = from.asFile.get()
val toFile = to.asFile.get()
if (fromFile.exists()) {
fromFile.copyTo(toFile, overwrite = true)
} else {
Files.deleteIfExists(toFile.toPath())
}
}
}
BCV本来はapiCheckのみverificationグループですが、探しやすいようにapiBuild/apiDumpもverificationに入れています。
あとは、ライブラリモジュールのbuild.gradleなどで configureBinaryCompatibilityValidator() を呼び出せばOKです。
「最低限使えるようにするだけ」なのでもっときれいな対応方法もあるかと思いますが「私はこれでなんとかなりました」という紹介でした。
以上です。