ある日 Android project の Gradle を 7.6 から 8.0 (この記事を書いている時点での最新は 8.0.1) に更新したところ以下のようなエラーと共にビルドに失敗した:
'compile{Variant}JavaWithJavac' task (current target is 1.8) and 'kaptGenerateStubs{Variant}Kotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.
Consider using JVM toolchain:https://kotl.in/gradle/jvm/toolchain
この時の target JVM まわりの設定は以下のようになっていた:
android { // ... compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" } }
つまり target JVM を 1.8 に設定しているのに KaptGenerateStubs task は 17 を target として実行されてしまっているという状況のようだ。このビルドを実行した時の JDK のバージョンは 17 だったので build.gradle
の設定を無視して実行環境上の JVM のバージョンを参照しているように思われる。ついでに JVM toolchain なるものを使いなよという提案もされている。
これをどうにかしないことには Gradle を 8.0.x には上げられないので対処したのだが、その過程で JVM toolchain (Gradle の docs では "Java toolchain" と呼ばれていたので以後 Java toolchain とする) を設定することになったのでそのまわりについて調べたことをざっと記録していこうと思う。
kapt が jvmTarget
を参照しなくなった
Gradle 8.0 の release note と Kotlin 1.8 の what's new を読んだ感じ以下のような状況らしい:
- Gradle 8.0 から Kotlin DSL の API level が 1.8 になった
- Kotlin 1.8 から Java と Kotlin の compile task で target JVM が一致していない場合の反応をエラーとするようにした
- kapt が
jvmTarget
の設定を見なくなったので、これと合わせるように設定していたcompileJavaWithJavac
task の target compatibility とずれてビルドエラーが発生した
kapt の挙動変更については Kotlin の what's new に記載がなかったのでこのエラーについて何か情報がないか検索したところ、 YouTrack にそれそのものな issue があった:
- Unable to set kapt jvm target version: https://youtrack.jetbrains.com/issue/KT-55947
現時点のやりとりにおける JetBrains の中の人の見解は以下のような感じ:
- Kotlin 1.8.0 から kapt が
jvmTarget
, というかkotlinOptions
やcompilerOptions
を参照しなくなったのは意図的な変更で、バグという認識をしていない jvmTarget
による指定をやめて Java toolchain を使ってほしそう
報告者も言っているように意図的な、しかも後方互換性を破壊するような挙動の変更ならちゃんと what's new で言及してよと思うが、document を読んだ感じ Gradle としても target JVM まわりの設定は Java toolchains に寄せてほしそうだし、そういう気持ちなら Java toolchain を設定する方向でいいか、となった。
JavaVersion sourceCompatibility
Java version compatibility to use when compiling Java source. Default value: language version of the toolchain from this extension.
Note that using a toolchain is preferred to using a compatibility setting for most cases.
ところで報告者は Android Studio に同梱された JDK を使ってビルドしたいが JVM target は固定したい (≒ ビルドに使う JDK のバージョンと異なる target を指定したい)と言っており、この場合 Java toolchains による暗黙的な設定に置き換えると指定した toolchain のバージョンに target JVM が揃えられてしまうので要求を満たせない。このような要求に対する現状の workaround は報告者のコメントなどを見るに KotlinCompile
系の task に対して configureEach
で target JVM の指定を適用するという方法くらいのようだ。ただし報告者によるとパフォーマンスの問題があったり build script の構成によっては上手くいかないこともあるらしい (未確認):
tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile>().configureEach {
kotlinOptions {
jvmTarget = "<指定したい jvm version>"
}
}
KSP も同様
KSP だとどうだろうと思って、以下の repository がちょうどいい構成だったのでこれで Gradle を 8.0.1 にして確認したところ同様のエラーが発生した:
'compileDebugJavaWithJavac' task (current target is 1.8) and 'kspDebugKotlin' task (current target is 17) jvm target compatibility should be set to the same Java version.
Consider using JVM toolchain: https://kotl.in/gradle/jvm/toolchain
ということで ksp しか使っていない project でも Java toolchain の設定をするか上記の workaround を入れるかして対処する必要がある。
Java toolchain の設定をする
Java toolchain とは
Java toolchain とは大雑把に言うと JVM 系の project のビルドや実行に使われるツール群 (javac
とか java
とか) のことで、 Gradle 6.7 から Gradle を実行しているものとは別の toolchain を project や task の単位で指定できるようになったとのこと。異なるマシン上でも実行環境を揃えたり、 project 内で異なる Java version での task 実行を行ったり、 Gradle がサポートしていないバージョンの Java で project / task の実行をしたりといったことを目的としているようだ。
Document はこれ:
6.7 時点の話なので 8.0 での挙動等と異なる点はあるけどこの記事も読むといいかも:
設定の記述やルール
今回は基本的には project level での設定をすれば十分なのでその方法のみ紹介する。 Task level での切り替え設定をしたい場合は document を読んでほしい。
Java toolchain の設定は java
extension block で以下のように記述する:
java { toolchain { languageVersion.set(JavaLanguageVersion.of(17)) vendor.set(JvmVendorSpec.ADOPTIUM) } }
設定できる項目は以下の3つ:
- バージョン (
languageVersion
): toolchain に指定する Java のバージョン - 配布元 (
vendor
): 使用する JDK の配布元 - VM の実装 (
implementation
): 主に OpenJ9 を使用したい時に設定する- Android project だとまあまあ関係ない話かも
これらの設定には有効性の概念があり、下記の2パターンの内のどちらかに該当するものが有効とされる。そうでない設定は無効と判定されビルドエラーとなる:
- 何も指定がない
languageVersion
の指定が含まれている
Android project では基本的に languageVersion
の設定だけで十分だと思っていて、もし distribution にこだわりがある or そこまで実行環境を揃えておきたいという場合には vendor
も書くという風になるだろう。
また、toolchain の設定は Kotlin Gradle plugin からも行うことができ、 distribution の指定が必要ない場合はより短い記述で設定可能になっている:
kotlin { jvmToolchain { // この block の中の this は java.toolchain と同じ JavaToolchainSpec なので書くものは同じ languageVersion.set(JavaLanguageVersion.of(17)) vendor.set(JvmVendorSpec.ADOPTIUM) } // バージョン指定だけで良い場合は以下のように書ける jvmToolchain(17) }
このようにして toolchain (のバージョン) の指定を行うと、 Java における sourceCompatibility
や targetCompatibility
, Kotlin における jvmTarget
を設定しなかった場合に指定された toolchain のバージョンが target JVM として使用されるようになる。
Toolchain に使用する JDK/JRE の選択と自動ダウンロード
Java toolchain の指定がなされた project で build 等を実行すると、ローカルにインストールされている toolchain たちの中から指定に該当するものを自動で選択し、それを用いて各 task を実行する。ローカルにインストールされている toolchain の検知対象については document に以下のように書かれている:
- Operation-system specific locations: Linux, macOS, Windows
- Package Managers: Asdf-vm, Jabba, SDKMAN!
- Maven Toolchain specifications
- IntelliJ IDEA installations
Package manager でインストールしたものが使えるので、普段 JDK の管理でそれらを使用している人ならそのまま使いまわせて便利。
ところで、自動検知の結果ローカルに該当する toolchain が見つからなかった場合、これまで紹介した設定だけだと該当なしということでエラーになってしまう。実は元々は該当なしだった場合 AdoptOpenJDK (現: Adoptium) が自動でダウンロードされるようになっていたのだが、 8.0 でその機能が削除されてしまっている。
ただし、「ローカルに指定に該当する toolchain が無かった場合にはダウンロードして取ってくる」という機能自体は 8.0 以降も存在しており、 download repository の設定を記述することで有効になる。 Download repository の設定は基本的には以下のような感じで settings.gradle
に resolver について書くことになる。 Toolchain resolver は plugin として自分で作ることもできるが、既に Foojay Toolchains Plugin というものが公開されており、一般的な JDK distribution を使うならこの plugin を使用すれば事足りる。
pluginManagement { // ... } plugins { id("org.gradle.toolchains.foojay-resolver") version("0.4.0") } toolchainManagement { jvm { javaRepositories { repository("foojay") { resolverClass.set(org.gradle.toolchains.foojay.FoojayToolchainResolver::class.java) } } } }
Download repository に foojay しか使わない場合は foojay-resolver-convention
plugin を使うことで toolchainManagement
block の記述を省略できる:
// foojay しか使わないならこれを追加するだけで OK plugin { id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0") }
自動ダウンロードでの toolchain の取得を当てにする場合、あらゆる種類の JDK を取得できるわけではないので注意。例えば foojay は JavaVendorSpec
で定義されているすべての vendor をサポートしているわけではないし、それぞれの vendor でダウンロード可能な JDK のバージョンも違いがある。欲しい toolchain が取得可能かは使用する resolver plugin の対応状況や、 asdf など package manager がそれぞれの vendor からどのバージョンを取得可能かのリストなどを確認するとよい。とはいえ最新の Java に追従しているわけではない Android では基本的には LTS のバージョンを使うことになるだろうし、 LTS のものなら入手できないことはまずないので、気にするとしたら使いたい vendor で JDK 8 が配布されてるかどうかくらいか。
参考 (asdf-java が取得可能な JDK の一覧):
Android project 向けの workaround
Kotlin の document の Configure a Gradle project のページを見たらわかるが、 toolchains support の冒頭に Android project に関する注意が書かれていて、 Java toolchains のサポートは AGP 7.4.0 以上だということの他に toolchain を設定しても Java の compile task の target JVM に反映されないという現状の不具合について issuetracker へのリンクと対処法が示されている。
- Setting JVM toolchain does not affect JavaCompile targetCompatibility value: https://issuetracker.google.com/issues/260059413
こういう状況なので現時点では toolchain の設定で指定した JVM のバージョンに対応する JavaVersion
の値を compileOptions
で指定する必要がある:
kotlin { // 例えば Java toolchain の設定が 11 なら jvmToolchain(11) } android { // ... compileOptions { // これらの compatibility options も 11 に合わせる sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 } }
Robolectric を使っている場合について
Robolectric は Android の target SDK 29 以上で実行する場合実行環境に Java 9 以上を要求する:
このため Java toolchain の設定に乗り換える際に toolchain のバージョンを単純に 8 に指定するだけだと Robolectric を使用したテストの実行でエラーになってしまう。この問題を対処するには
- toolchain のバージョン指定を 9 以上にする (= target JVM を 9 以上に上げる)
robolectric.properties
で実行時の target SDK を 28 以下にする
の2通りの方法が考えられる。
Target JVM のバージョンを上げる方法に関して、 application project の場合はあまり問題にならないと思うので軽率に上げてしまって良さそうだが、 library project の場合は利用者の target JVM より高くなると使えなくなってしまうので、移行期間を設けたり test の toolchain だけ 9 以上になるように設定したりするなど上手くやる必要がある。個人的には普通の Android アプリ開発で target JVM 1.8 にこだわる理由は特にないのではと思っているのでみんな軽率に 11 に上げてしまっていいんじゃないかと思っている。17 は R8 がまだ対応しきれていないっぽいのでまだいいかなという感触。
sdk
property で対処する方法は簡単だし外への影響がないのだが、 minimum SDK が 29 以下の間しか使えないので一時凌ぎとして捉えた方が良い。
CI について
(2023-07-19 内容を変更、元の記述は間違った内容を含むので削除)
CI 環境のセットアップについては自動ダウンロードを有効にしている場合でもそうでない場合でもまず Gradle を動かすための JDK を task 実行前に用意しておく必要がある。自動ダウンロードを無効にしている場合はさらに project で指定した version の Java tool chain をダウンロードするステップが追加されるが、これに関しては CI が複数の JDK を配置させられるような機能を持っていれば*1それを使うなり package manager をインストールしてそれ経由でダウンロードするなりで実現できるだろう。 Gradle による Java toolchain の自動検知対象となる場所以外に配置しても、下記の property に配置場所の path を指す環境変数を指定して gradle.properties
に追記することで Gradle に認識させることができる:
org.gradle.java.installations.fromEnv=<JDK の path を指す環境変数 (,区切り)>
できる限り手元の開発環境でも CI 環境でも使う toolchain を揃えたいという場合は自動ダウンロードに任せてしまった方が設定の手間もなくポータブルだが、そこまで厳密な運用を必要としないなら好みの範囲かと思う。
おわりに
kapt / ksp の挙動変更については疑問が残るが、 Java toolchain の設定自体は便利機能だし、当初思っていたより設定の手間がずっと少なかったので、ビルド環境に関する制約がなければ設定の移行をしてしまって良いと思う。今は Java toolchain の設定を導入したくないとしても、これまで通りの target JVM の指定方法を維持するための workaround も一応存在するので、 workaround を入れてとりあえず Gradle 8.0 に上げてから最終的にどうするか考えるという手段を取ることもできそう。なんにせよ Android Gradle Plugin 8.0 から要求される Gradle の最低バージョンが 8.0 になるので、今のうちから Gradle のバージョンを 8.0 以上に上げられるようにしておきたい。
それで Java toolchain の設定をするのに結局どういう変更をしたらいいのかというのが散らかってしまっていると思うので、私が行った build script の変更の抜粋を以下に置いておく。今回対象となった project は target JVM を 11 にしても問題なかったのでそのように修正しているが、移行したいが少なくとも build の target JVM は 1.8 でないと問題がある + Robolectric を使っているという場合は task level での toolchain の設定で乗り切れるはずなので調べて試してみてほしい。
build.gradle
+ kotlin { + jvmToolchain(11) + } android { ... compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + // https://issuetracker.google.com/issues/260059413 が修正されたら削除 + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } - kotlinOptions { - jvmTarget = "1.8" - } ... }
settings.gradle
... + plugin { + id("org.gradle.toolchains.foojay-resolver-convention") version("0.4.0") + } ...
調査メモ:
*1:例えば GitHub Actions の setup-java では複数の JDK のインストールを指定できる: https://github.com/actions/setup-java/blob/4fb397523b853fa75ed64fd1d10a967e1eb3148a/README.md#install-multiple-jdks