1
/
5

buildSrcを使う際にKotlinバージョンが不一致になる罠

Photo by Iswanto Arif on Unsplash

モバイルエンジニアの久保出です。

Visit Androidプロジェクトで、Kotlinをアップグレードする際に罠にハマってしまったので、記録として残しておきます。
GradleとKotlinの依存性について深掘っていますが、buildSrcを使っている場合の限定的な話であることに留意してください。

TL;DR

buildSrc/build.gradle.ktsdependenciesimplementation(kotlin("gradle-plugin")) のようにバージョン指定せずにKotlinへの依存を書いていると、Gradleに埋め込まれたKotlinのバージョンが使われてしまいます。

罠にハマった経緯

Kotlinをアップグレードしようとした際に罠にハマったという話なのですが、なぜそうなったかを話すためにプロジェクトの構成の話からしていきます。

buildSrc

Visit Androidはマルチモジュール構成になっています。
30以上のモジュールそれぞれで build.gradle.kts の設定をするのはとても大変であるため、 buildSrc の中でカスタムプラグインを作り、共通の設定をそのプラグインによって管理するという手法をとっています。

この話はdroidconで同僚のMalvinが話していたので、興味があれば御覧ください。

droidcon APAC after report (En/Ja) | Wantedly, Inc.
Hi everyone, I'm Malvin from the Android team of Wantedly. Last week, I had a chance of giving a talk at droidcon APAC 2020 and now I would like to talk about my overall experience of the event. Droidcon is a series of developer conferences focusing on th
https://www.wantedly.com/companies/wantedly/post_articles/300770

buildSrc/build.gradle.kts はかなり省略してますが次のようになっています。

// buildSrc/build.gradle.kts
plugins {
    `java-gradle-plugin`
    `kotlin-dsl`
}

gradlePlugin {
    plugins {
        register("visit-module-plugin") {
            id = "visit-module"
            implementationClass = "com.wantedly.android.visit.gradle.plugins.VisitModulePlugin"
        }
    }
}

dependencies {
    compileOnly(gradleApi())
    implementation(kotlin("gradle-plugin"))
}

カスタムプラグインの中でKotlin Gradle Pluginへの依存があるため implementation(kotlin("gradle-plugin") として依存を追加しています。そしてこれが今回の肝になってきます。

これで、各サブモジュールで plugins { id("visit-module") } と書けばプラグインを適用できて、共通の設定がされるようになっていたわけです。

Kotlinのバージョン指定

使用するKotlinのバージョンは、 buildSrc の中の Dependencies.kt に記述しています。

// buildSrc/src/main/kotlin/Dependencies.kt
private object Versions {
    const val KOTLIN = "1.4.0"
}

object Dependencies {
    const val KOTLIN_GRADLE_PLUGIN = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}"
}

そしてプロジェクトのルートにある build.gradle.ktsclasspath を指定しています。

// ./build.gradle.kts
buildscript {
    dependencies {
        classpath(Dependencies.KOTLIN_GRADLE_PLUGIN)
        // ...
    }
}

これにより、各サブモジュールの build.gradle.ktsplugins { kotlin("android") } と書けば、 Versions で指定しているKotlinのバージョンが適用される、と思っていました。

Kotlinのアップグレード

Kotlin 1.4.21もリリースされたので、アップグレードしようとしました。

Kotlin 1.4.20からは、Android ExtensionがDeprecatedになっています。

https://kotlinlang.org/docs/reference/whatsnew1420.html#kotlin-android-extensions

For now, they will keep working with a deprecation warning.

Visit Androidでも Parcelize のためにAndroid Extensionを利用していました。本来ならアップグレード後には警告が表示されるはずでしたが、警告は表示されませんでした。

おかしいと思いつつ、Android Extensionを移行先の kotlin-parcelize プラグインに置換しようとします。

// app/build.gradle.kts
plugins {
    kotlin("android")
-    kotlin("android-extensions")
+    kotlin("parcelize")
}

しかしこれはプラグインがクラスパスに見つからないというエラーになりました。Kotlin 1.4.20以降がクラスパスに含まれていれば、そんなエラーにはならないはずです。
つまり、クラスパスのKotlin Gradle Pluginのバージョンが古いということです。

原因を探る

なぜそうなるか、原因を順に探っていきます。

Gradle scan

Gradleには --scan というオプションがあります。これはビルド時間からパフォーマンスや依存性の解決などのビルドの詳細なデータを取得し、グラフィカルに分析することができるオプションです。

scanを使ってプラグインの依存性を確認したところ、次のようになっていました。

Kotlin Gradle Pluginのバージョンが1.3.72になっています。

ルートのクラスパスには1.4.21を指定しているのになぜか?それを更に探っていきます。

buildSrcの仕様

Gradleの buildSrc についてのドキュメントを探ると次のように書かれています。

https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:build_sources

The directory buildSrc is treated as an included build. Upon discovery of the directory, Gradle automatically compiles and tests this code and puts it in the classpath of your build script.

buildSrc は各 build.gradle.kts のクラスパスに含まれるようになる、つまり buildSrcdependencies の依存性もまた各 build.gradle.kts のクラスパスに含まれるようになります。

原因はなんとなくですが、 buildSrc/build.gradle.ktsdependenciesimplementation(kotlin("gradle-plugin")) と書いたことだとわかってきました。

kotlin()の仕様

buildSrc/build.gradle.kts では kotlin("gradle-plugin") と記述していますが、この kotlin() について調べていきます。

kotlin() はGradleがコード生成している拡張関数になっています。

https://github.com/gradle/gradle/blob/4817230783dcff0d8f2f3f266794c2f454f96959/build-logic/kotlin-dsl/src/main/kotlin/gradlebuild/kotlindsl/generator/tasks/GenerateKotlinDependencyExtensions.kt#L83-L90

単に長い名前をラップしているだけですが、versionを指定しないと "org.jetbrains.kotlin:kotlin-gradle-plugin" とバージョン指定しない記述になります。

基本的にはバージョンを指定しないと依存性は解決できません。しかし、GradleにはBill of Materials(BOM)の仕組みがあり、BOMが読み込まれていればバージョンを指定しなくても良いようになっています。
ですが、BOMの読み込みはしていません。バージョンがどこで解決されているかを見ていくとKotlin Gradle Pluginに行き着きます。

https://github.com/JetBrains/kotlin/blob/29b23e79f32791e456a5b4a453277f0f0b3e984d/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/internal/KotlinDependenciesManagement.kt#L47-L57

private fun configureDefaultVersionsResolutionStrategy(project: Project) {
    project.configurations.all { configuration ->
        // Use the API introduced in Gradle 4.4 to modify the dependencies directly before they are resolved:
        configuration.withDependencies { dependencySet ->
            val coreLibrariesVersion = project.kotlinExtension.coreLibrariesVersion
            dependencySet.filterIsInstance<ExternalDependency>()
                .filter { it.group == KOTLIN_MODULE_GROUP && it.version.isNullOrEmpty() }
                .forEach { it.version { constraint -> constraint.require(coreLibrariesVersion) } }
        }
    }
}

Kotlin Gradle Pluginの中でバージョン解決のデフォルトの挙動が書かれています。
coreLibrariesVersion がデフォルトで使用されることがわかります。

https://github.com/JetBrains/kotlin/blob/6c7247cbd35ac80010b2d64c95403477457b0d8b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/KotlinPluginWrapper.kt#L89-L96

        project.createKotlinExtension(projectExtensionClass).apply {
            coreLibrariesVersion = kotlinPluginVersion

coreLibrariesVersion のデフォルト値はKotlin Gradle Pluginのバージョンになっています。

これで原因がはっきりしました。

まとめると、

  1. バージョンを指定せず`kotlin-dsl`プラグインをbuildSrcで適用しているため、Gradleに埋め込まれたバージョン1.3.72が適用される。
  2. buildSrcのdependenciesでのkotlin("gradle-plugin")もバージョンを指定していないため、resolutionStrategyによって1.3.72がconstraint付きで依存性として解決される。
  3. buildSrcは各build.gradle.ktsにクラスパスとして適用されるため、1.3.72がbuild.gradle.ktsのクラスパスに追加される。1.3.72にconstraintがあるため、ルートのbuild.gradle.ktsのclasspath指定は無視されてしまう。
  4. 各build.gradle.ktsでもバージョンを指定していないため、クラスパスの1.3.72が最終的に適用される。

ということでした。

我々は新しいKotlinを使っていると思ったら、Gradleに埋め込まれた古いバージョンのKotlinを使っていたわけです・・・

解決策

buildSrc/build.gradle.ktsdependencies でKotlinのバージョンを指定すれば簡単に解決できます。

buildSrc/build.gradle.kts からは buildSrc/src 以下にある Dependencies.kt を参照できないので、Kotlinのバージョン定義は buildSrc/build.gradle.kts へ移します。

// buildSrc/build.gradle.kts
plugins {
    `java-gradle-plugin`
    `kotlin-dsl`
}

dependencies {
    compileOnly(gradleApi())
    implementation(kotlin("gradle-plugin", "1.4.21"))
}

しかしこれではbuildSrcのコンパイル時に "Runtime JAR files in the classpath should have the same version." という警告が出るようになります。
Gradleに埋め込まれたバージョンとdependenciesで指定しているバージョンが違うためです。
すべて完全に揃えるには、 buildSrc の中の plugins でもバージョンを指定しなければなりません。

最終的に次のようにして解決しました。

2021/04/14追記:後述のAndroidXと同様にconfigurations.classpathの指定が必要でした。

buildscript {
    val kotlinVersion = "1.4.21"
    dependencies {
        classpath(kotlin("gradle-plugin", kotlinVersion))
    }
    configurations.classpath.get().resolutionStrategy.eachDependency {
        if (requested.group == "org.jetbrains.kotlin") {
            useVersion(kotlinVersion)
        }
    }
}

plugins {
    `java-gradle-plugin`
    `kotlin-dsl`
}

dependencies {
    compileOnly(gradleApi())
    implementation(kotlin("gradle-plugin"))
}

参考

AndroidX

AndroidXのプロジェクトの中でも buildSrc とカスタムプラグインは使われています。その記述が解決策として参考になります。

https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:buildSrc/build.gradle;drc=23758df3ade668ae5c369c7ec574d10fb24b7fc1

AndroidXでは、 buildSrc/build.gradle.kts 自体のクラスパス(buildscriptで指定してる)でKotlinのバージョンを指定し、dependenciesでもバージョンを指定しています。

Google Issue Tracker

同様の問題を探したところ、 https://issuetracker.google.com/issues/123491449#comment11 でも buildSrc を使っている場合は、プラグインの依存性は buildSrc 内ですべて記述されている必要があると言及されていました。

まとめ

Kotlin Gradle Pluginの依存性の罠について原因を詳細に探り、解決策を見つけることができました。
原因を探る過程で、Gradleの依存性解決について少し詳しくなれました。

この記事が、同様のプロジェクト構成の人の助けになれば幸いです。

ここまで書いていますが、実のところ別の問題がありKotlin 1.4.21にはまだできていません。
そちらもいずれ記事にできたらなと思っています。

Wantedly, Inc.からお誘い
この話題に共感したら、メンバーと話してみませんか?
Wantedly, Inc.では一緒に働く仲間を募集しています
16 いいね!
16 いいね!

同じタグの記事

今週のランキング

久保出 雅俊さんにいいねを伝えよう
久保出 雅俊さんや会社があなたに興味を持つかも