モバイルエンジニアの久保出です。
Visit Androidプロジェクトで、Kotlinをアップグレードする際に罠にハマってしまったので、記録として残しておきます。 GradleとKotlinの依存性について深掘っていますが、buildSrcを使っている場合の限定的な話であることに留意してください。
TL;DR buildSrc/build.gradle.kts
の dependencies
で implementation(kotlin("gradle-plugin"))
のようにバージョン指定せずにKotlinへの依存を書いていると、Gradleに埋め込まれたKotlinのバージョンが使われてしまいます。
罠にハマった経緯 Kotlinをアップグレードしようとした際に罠にハマったという話なのですが、なぜそうなったかを話すためにプロジェクトの構成の話からしていきます。
buildSrc Visit Androidはマルチモジュール構成になっています。 30以上のモジュールそれぞれで build.gradle.kts
の設定をするのはとても大変であるため、 buildSrc
の中でカスタムプラグインを作り、共通の設定をそのプラグインによって管理するという手法をとっています。
この話はdroidconで同僚のMalvinが話していたので、興味があれば御覧ください。
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.kts
で classpath を
指定しています。
// ./build.gradle.kts
buildscript {
dependencies {
classpath(Dependencies.KOTLIN_GRADLE_PLUGIN)
// ...
}
}
これにより、各サブモジュールの build.gradle.kts
で plugins { 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
のクラスパスに含まれるようになる、つまり buildSrc
の dependencies
の依存性もまた各 build.gradle.kts
のクラスパスに含まれるようになります。
原因はなんとなくですが、 buildSrc/build.gradle.kts
の dependencies
で implementation(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のバージョンになっています。
これで原因がはっきりしました。
まとめると、
バージョンを指定せず`kotlin-dsl`プラグインをbuildSrcで適用しているため、Gradleに埋め込まれたバージョン1.3.72が適用される。 buildSrcのdependenciesでのkotlin("gradle-plugin")もバージョンを指定していないため、resolutionStrategyによって1.3.72がconstraint付きで依存性として解決される。 buildSrcは各build.gradle.ktsにクラスパスとして適用されるため、1.3.72がbuild.gradle.ktsのクラスパスに追加される。1.3.72にconstraintがあるため、ルートのbuild.gradle.ktsのclasspath指定は無視されてしまう。 各build.gradle.ktsでもバージョンを指定していないため、クラスパスの1.3.72が最終的に適用される。 ということでした。
我々は新しいKotlinを使っていると思ったら、Gradleに埋め込まれた古いバージョンのKotlinを使っていたわけです・・・
解決策 buildSrc/build.gradle.kts
の dependencies
で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にはまだできていません。 そちらもいずれ記事にできたらなと思っています。