GradleでJDKを自動インストールしてくれるToolchainを触ってみる

## 最近のGradleはビルドの際のJDKを自動でダウンロード＆インストールする機能がついている

みなさんこんにちは！ Gradle 初心者です！

Gradle は 7.6 から [Toolchain](https://docs.gradle.org/current/userguide/toolchains.html) という機能があり、ビルドに使うJDKを自動インストールすることができます。

具体的には以下のように書くと、Javaのビルド時にGradleが事前インストール無しで自動的にJDK 22をダウンロードしてビルドに使ってくれます。[^1]

[^1]: 本記事で利用するGradleのバージョンは 8.7, 実行環境のJavaは 11 以降を利用しています。OSはIntel Macです。この記事ではJDKの自動インストールに触れていますが、Gradleを実行するJVMはそれはそれで別途必要です。

```groovy:settings.gradle
plugins { id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0' }
```

```groovy:build.gradle
java {
    toolchain {
         // Java のビルドは JDK 22 を使ってビルドする
         languageVersion = JavaLanguageVersion.of(22)
    }
}
```

通常、Javaのビルドツール(JDK)のバージョンは [`sdkman`](https://sdkman.io/), [`jenv`](https://github.com/jenv/jenv) などを使って管理していると思いますが、この機能を使えばGradle一本でビルド時のJVMのバージョンを指定することが可能です。

Gradleの実行に使っているJavaバージョンは特に縛らず、ビルドに使うJavaのバージョンはきちんと指定しておくといったことも簡単に実現できます。


## 実際に切り替わるのか？

ためしにJava 22では通り、Java 20では通らないコードを用意してみます。

```java:src/main/java/Main.java
// Java 22 のプレビュー機能を有効にすると使える JEP 463 (暗黙のMainメソッド/クラス) を使ったソースコード
void main (String... args) {
    System.out.println("Hello");
}
```

`settings.gradle`, `build.gradle` は以下を使います。

```gradle:settings.gradle
plugins {
     // 最近のバージョンの Gradle (8.x) だとJDKの自動ダウンロードはプラグインを指定する必要がある
     id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}
```

```gradle:build.gradle
plugins { id 'java' }

java {
    toolchain {
        // ~.of(20) だとビルドが通らないが ~.of(21) や ~.of(22) だとビルドが通る
        languageVersion = JavaLanguageVersion.of(22)
    }
}

// プレビュー機能使わないなら指定は不要
tasks.withType(JavaCompile) {
    options.compilerArgs += "--enable-preview"
}
```

ビルドに利用するJavaのバージョンを切り替えてビルドが通るか試してみます。

```bash
# Gradle 自体は Java11 で動かす予定 (古すぎ?!)
$ java -version
openjdk version "11.0.3" 2019-04-16 LTS

# ~~.of(22) にしてビルド → ビルドは通る
$ sed -i '' 's|of(..)|of(22)|g' build.gradle && ./gradlew build
... Unpacking toolchain archive OpenJDK22U-jdk_x64_mac_hotspot_22_36.tar.gz ...
BUILD SUCCESSFUL in 23s

# ~~.of(20) にしてビルド → ビルドは通らない
$ sed -i '' 's|of(..)|of(20)|g' build.gradle && ./gradlew build
... Unpacking toolchain archive OpenJDK20U-jdk_x64_mac_hotspot_20.0.2_9.tar.gz ...
BUILD FAILED in 27s
```

きちんとバージョンは切り替わっているようですね！

ちなみにビルドだけではなく、Javaを動かす際もバージョン指定は可能です。

```gradle:build.gradle
// ... ソースの続き ...

task run(type: JavaExec) {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(22)
    }

    mainClass = 'Main'
    classpath = sourceSets.main.runtimeClasspath
}

// プレビュー機能使わないなら指定は不要
tasks.withType(JavaExec) {
    jvmArgs += '--enable-preview'
}
```

```bash
$ ./gradlew run
# Java 22 で動いた!
> Task :run
Hello
```

自動でダウンロード＆インストールされたJDKは `~/.gradle/jdks/` で確認できました。

```bash
$ ls -al ~/.gradle/jdks/
drwxr-xr-x   8 ksaitou  staff        256  3 25 16:37 .
drwxr-xr-x  14 ksaitou  staff        448 10  5 10:52 ..
-rw-r--r--   1 ksaitou  staff  197502362  3 25 16:37 OpenJDK20U-jdk_x64_mac_hotspot_20.0.2_9.tar.gz
-rw-r--r--   1 ksaitou  staff         17  3 25 16:37 OpenJDK20U-jdk_x64_mac_hotspot_20.0.2_9.tar.gz.lock
-rw-r--r--   1 ksaitou  staff  192678886  3 25 16:36 OpenJDK22U-jdk_x64_mac_hotspot_22_36.tar.gz
-rw-r--r--   1 ksaitou  staff         17  3 25 16:37 OpenJDK22U-jdk_x64_mac_hotspot_22_36.tar.gz.lock
drwxr-xr-x   3 ksaitou  staff         96  3 25 16:37 eclipse_adoptium-20-x86_64-os_x
drwxr-xr-x   3 ksaitou  staff         96  3 25 16:37 eclipse_adoptium-22-x86_64-os_x
```

**ちなみにこの機能、既にインストールされている Java についてはインストールされません。** 試しに [SDKMan!](https://sdkman.io/) でJava 21をインストールしてから実行してみます。

```bash
$ sdk install java 21.0.2-tem 
# ビルドは通る
$ sed -i '' 's|of(..)|of(21)|g' build.gradle && ./gradlew build
BUILD SUCCESSFUL in 873ms

# ダウンロードされた痕跡はない
$ ls -al ~/.gradle/jdks/ | grep 21
=> 結果なし
```

[既にインストールされているか判定するための Gradle が認識しているJDKのインストール](https://docs.gradle.org/current/userguide/toolchains.html#sec:auto_detection) は `gradle -q javaToolchains` で取得できるようです。ツールチェインの取得ソースとして、OSのJDK並びにSDKManやIntelliJなどメジャーどころは一通りスキャンするらしく便利にできています。

```bash
$ ./gradlew -q javaToolchains | grep -F ' +'
 + AdoptOpenJDK xx.xx
 + Azul Zulu JDK xx.xx
 + Eclipse Temurin JDK xx.xx
 + OpenJDK xx.xx
 + Oracle JDK xx.xx
```


## もう少し深掘りしてみる

これだけでは消化不良なので、もう少し深掘りしていきます。

### どのJDKがダウンロードされるのか？

JDKといってもOracle JDKのほか、色々なJDKが世の中にはあります。

- [OpenJDK](https://openjdk.org/)
    - [Eclipse Temurin](https://adoptium.net/) (旧 AdoptOpenJDK)
    - [Amazon Corretto](https://aws.amazon.com/jp/corretto/)
    - [Micrsoft Build of JDK](https://www.microsoft.com/openjdk)
    - [Zulu JDK](https://www.azul.com/downloads/#zulu)
- [Open J9](https://eclipse.dev/openj9/)

これらベンダーについては [`vendor = JvmVendorSpec.XXXX`](https://docs.gradle.org/current/javadoc/org/gradle/jvm/toolchain/JvmVendorSpec.html) で明示的に指定可能です。

```gradle:build.gradle
java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
        vendor = JvmVendorSpec.ADOPTIUM
    }
}
```

`vendor = ...` を省略した場合などの詳細な動作は `settings.gradle` などに指定した [Toolchain Plugins](https://docs.gradle.org/current/userguide/toolchain_plugins.html#toolchain_plugins) によって決まるようです。 

現段階での99%の人は [Foojay Toolchains Plugin (`org.gradle.toolchains.foojay-resolver-convention`)](https://github.com/gradle/foojay-toolchains) を使うでしょうから[^2]、その場合は [Eclipse Temurin, AOJ(AdoptOpenJDK) が優先して使われる](https://github.com/gradle/foojay-toolchains/blob/15211a0a6958f43ca711a5ffdf3013c449effb4c/foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/distributions.kt#L23)ようです。 選択肢としては無難かつ妥当と思います。

[^2]: 本記事では Foojay Toolchains Plugin を利用することを前提に記述しています。

```kotlin:foojay-resolver/src/main/kotlin/org/gradle/toolchains/foojay/distributions.kt
val distributionOrderOfPreference = listOf("Temurin", "AOJ")
// ...
private fun allDistributionsPrecededByWellKnownOnes(distributions: List<Distribution>): List<Distribution> =
    distributions.sortedBy {
        // Put our preferences first, preserve Foojay order otherwise.
        val indexOf = distributionOrderOfPreference.indexOf(it.name)
        when {
            indexOf < 0 -> distributionOrderOfPreference.size
            else -> indexOf
        }
    }
```

### どこからJDKがダウンロードされるのか？ [foojay Disco API](https://github.com/foojayio/discoapi) とは何か？

ダウンロードすべきベンダーやバージョンが決まったら Foojay Toolchains Plugin は **[foojay Disco API](https://github.com/foojayio/discoapi)** を通じてJDKがダウンロードできるURLを取得するようです。

https://github.com/foojayio/discoapi

(foojay) Disco API (**disco**very api) というのは、いろいろな OpenJDK のビルドを検索するためのAPIのようです。試しに叩いてみます。

```bash
# メジャーバージョンおよびマイナーバージョンの一覧を取得してみる
$ curl https://api.foojay.io/disco/v3.0/major_versions \
  | jq '.result[].major_version'
25
23
... (中略) ...
7
6

# Apple Siliconで動くJava9パッケージを取得してみる
$ curl 'https://api.foojay.io/disco/v3.0/packages?version=9&architecture=aarch64' \
  | jq '.result[].links.pkg_download_redirect'
"https://api.foojay.io/disco/v3.0/ids/XXXXXXXXXXX/redirect"
"https://api.foojay.io/disco/v3.0/ids/YYYYYYYYYYY/redirect"

# 示されたURLからパッケージをダウンロード
$ curl -L -O -J "https://api.foojay.io/disco/v3.0/ids/XXXXXXXXXXX/redirect"

# 内容確認
$ tar -tzf OpenJDK9U-jre_aarch64_linux_hotspot_9_181.tar.gz
./jdk-9+181-jre/
./jdk-9+181-jre/lib/
./jdk-9+181-jre/lib/libj2gss.so
./jdk-9+181-jre/lib/jli/
...
```

JDKの検索とその実行ファイルのダウンロードまで出来ました。実際には、API上ではリダイレクト用のURLがレスポンスされ、その先ではGitHub上のバイナリアーカイブのURLが案内されました。JDKディストリビューションによって最終的に案内されるホストは違うと思われます。


### メジャーバージョンより細かいバージョンは `build.gradle` 上で指定できないのか？

指定できません。 `jenv` や [`nodenv`](https://github.com/nodenv/nodenv) みたいなものと同一視しないほうがいいでしょう。

あくまでやんわりプロジェクトごとにJavaのバージョンを指定しつつ、自動ダウンロードもしてくれる便利機能ぐらいに考え、厳密にバージョンロックする仕組みと考えないほうが吉です。

### Java以外のJVM言語で同じ仕組みは使えるのか？

[マニュアル](https://docs.gradle.org/current/userguide/toolchains.html) によるとScala, Kotlin, Groovy にも設定は継承されるようです。


## まとめ

最近のGradleにてビルドやJava実行に使うJDKを自動選定＆（希望すれば）自動インストールできる機能 ([Toolchain](https://docs.gradle.org/current/userguide/toolchains.html)) があることを学びました。

また、自動インストールの仕組みの中で [foojay Disco API](https://github.com/foojayio/discoapi) というAPIを使ってJDKのダウンロードURLを取得できることが分かりました。

できればGradleの実行に使うJDK自体も自動インストールできると最高ですが、この機能だけでも開発環境セットアップについて色々な面倒は減らせるかもしれません。


### (参考) 今回のソースコードを格納したリポジトリ

https://gitlab.com/ksaitou/2023-06-28_gradleexampletojavatoolchains