1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AGP 9のbuiltInKotlin環境でもbinary-compatibility-validatorを使えるようにする

1
Last updated at Posted at 2026-02-07

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がある場合compileReleaseKotlincompile<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/apiDumpverificationに入れています。

あとは、ライブラリモジュールのbuild.gradleなどで configureBinaryCompatibilityValidator() を呼び出せばOKです。


「最低限使えるようにするだけ」なのでもっときれいな対応方法もあるかと思いますが「私はこれでなんとかなりました」という紹介でした。
以上です。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?