Gradle 6.7 で リリースされる Toolchain support for JVM projects を試してみる

Gradle 6.7 (現在 6.7 rc-3がリリースされています) から導入される Toolchain support for JVM projects という機能を試してみます。

この機能で何が出来るのかというと Gradle を動かすJVMコンパイルやテストで使うJVMを簡単に分離することが出来るようになります。 また、コンパイルやテストの間でも簡単にJVMのバージョン制御を行うことが出来ます。

追記: 今までは複雑なステップが必要でした。 https://docs.gradle.org/6.6.1/userguide/building_java_projects.html#compiling_and_testing_java_67

デモのgifを見ると分かりやすいので見てみましょう。

ホストに入っている AdoptOpenJDK 11 を使って Gradleを動かして ビルドに14を使うデモです。

デモを見てみると分かるのですが 自動でビルドに必要なJDKをダウンロードしています

これはワクワクしますね。

前前職でJavaのアップデートしてた時に欲しかった・・・

というわけで、今回は、この Toolchain support for JVM Projects を使ってみます。 以下の目次でブログを書いていきます。

  • Gradle 6.7 の Toolchain support for JVM projects を試してみる
  • gradle で Javaリポジトリを作る
  • Toolchain support の設定を追加する
  • ビルドに使われたJDKが設定で指定したものになっているか確認する
  • 実際に動かしてみる
  • どのようにして使うJVMが選ばれるのか
  • Test/Compile だけ違うバージョンのJVMで実行も出来る
  • まとめ
  • おわりに

今回は、gradle で Javaリポジトリを作るところからやっていきましょう。

gradle で Javaリポジトリを作る

gradle init で interactive な形で リポジトリを生成しました。 今回は Java アプリケーションの実行を Java 15 で動かすとして ライブラリを Java 8 でビルドするような形のプロジェクトの構成で試してみます。

ホストに入っている Java は AdoptOpenJDK の 11 です。

$ gradle init 

Welcome to Gradle 6.7-rc-3!

Here are the highlights of this release:
 - File system watching is ready for production use
 - Declare the version of Java your build requires
 - Java 15 support

For more details see https://docs.gradle.org/6.7-rc-3/release-notes.html

Starting a Gradle Daemon (subsequent builds will be faster)

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 2 

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Project name (default: gradle-toolchain-examples): 
Source package (default: gradle.toolchain.examples): 

> Task :init
Get more help with your project: https://docs.gradle.org/6.7-rc-3/samples/sample_building_java_applications_multi_project.html

BUILD SUCCESSFUL in 1m 0s
2 actionable tasks: 2 executed

生成してみたところこんな感じのソースが生成されました

$ tree
locales-launch: Data of ja_JP locale not found, generating, please wait...
.
├── README.md
├── app
│   ├── build.gradle
│   └── src
├── buildSrc
│   ├── build.gradle
│   └── src
│       └── main
│           └── groovy
│               ├── gradle.toolchain.examples.java-application-conventions.gradle
│               ├── gradle.toolchain.examples.java-common-conventions.gradle
│               └── gradle.toolchain.examples.java-library-conventions.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── list
│   ├── build.gradle
│   └── src
├── settings.gradle
└── utilities
    ├── build.gradle
    └── src

49 directories, 21 files

久しぶりに生成したせいなのか、めっちゃファイルが生成されて驚きましたが無視していきましょう。

今回の記事で試すのに必要なのは設定を書くだけです。

Toolchain support の設定を追加する

サンプルプロジェクトの構成上、buildSrc配下のgroovyというかgradleファイルに書けば良さそうですね。 実際に変更したのはこの部分です。

gradle が Java 11で動いていることを確認しておきます。

./gradlew -v

------------------------------------------------------------
Gradle 6.7-rc-3
------------------------------------------------------------

Build time:   2020-09-30 19:16:51 UTC
Revision:     836e96a80625c9c48b612e662e3b13bd2e2f4c3b

Kotlin:       1.3.72
Groovy:       2.5.12
Ant:          Apache Ant(TM) version 1.10.8 compiled on May 10 2020
JVM:          11.0.7 (AdoptOpenJDK 11.0.7+10)
OS:           Linux 5.4.0-47-generic amd64

./gradlew compileJava と実行したところ AdoptOpenJDK をダウンロードしてコンパイルが実行されました。

clean してもう1回 ./gradlew -i compileJava で infoログを出しながらビルドしてみます。 その時のログが以下のログです。

$ ./gradlew -i compileJava
Initialized native services in: /home/masaya/.gradle/native
The client will now receive all logging from the daemon (pid: 334377). The daemon log file: /home/masaya/.gradle/daemon/6.7-rc-3/daemon-334377.out.log
Starting 6th build in daemon [uptime: 17 mins 6.354 secs, performance: 100%, non-heap usage: 29% of 268.4 MB]
Using 8 worker leases.
Watching the file system is disabled
Starting Build
Settings evaluated using settings file '/home/masaya/repo/gradle-toolchain-examples/settings.gradle'.
Projects loaded. Root project using build file '/home/masaya/repo/gradle-toolchain-examples/build.gradle'.
Included projects: [root project 'gradle-toolchain-examples', project ':app', project ':list', project ':utilities']

===================== 一部省略 ================================

> Task :list:compileJava
Caching disabled for task ':list:compileJava' because:
  Build cache is disabled
Task ':list:compileJava' is not up-to-date because:
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/list/build/classes/java/main has been removed.
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/list/build/classes/java/main/gradle has been removed.
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/list/build/classes/java/main/gradle/toolchain has been removed.
The input changes require a full rebuild for incremental task ':list:compileJava'.
Full recompilation is required because no incremental change information is available. This is usually caused by clean builds or changing compiler arguments.
#
# 使っているツールチェインの場所が出力されている!!
#
Compiling with toolchain '/home/masaya/.gradle/jdks/jdk8u265-b01'.
Starting process 'Gradle Worker Daemon 3'. Working directory: /home/masaya/.gradle/workers Command: /home/masaya/.gradle/jdks/jdk8u265-b01/bin/java -Djava.security.manager=worker.org.gradle.process.internal.worker.child.BootstrapSecurityManager -Xmx512m -Dfile.encoding=UTF-8 -Duser.country=JP -Duser.language=ja -Duser.variant -cp /home/masaya/.gradle/caches/6.7-rc-3/workerMain/gradle-worker.jar worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Worker Daemon 3'
Successfully started process 'Gradle Worker Daemon 3'
Started Gradle worker daemon (0.338 secs) with fork options DaemonForkOptions{executable=/home/masaya/.gradle/jdks/jdk8u265-b01/bin/java, minHeapSize=null, maxHeapSize=null, jvmArgs=[], keepAliveMode=SESSION}.
Compiling with JDK Java compiler API.
Created classpath snapshot for incremental compilation in 0.0 secs.
:list:compileJava (Thread[Execution worker for ':',5,main]) completed. Took 1.048 secs.
:utilities:compileJava (Thread[Execution worker for ':',5,main]) started.

> Task :utilities:compileJava
Caching disabled for task ':utilities:compileJava' because:
  Build cache is disabled
Task ':utilities:compileJava' is not up-to-date because:
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/utilities/build/classes/java/main has been removed.
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/utilities/build/classes/java/main/gradle has been removed.
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/utilities/build/classes/java/main/gradle/toolchain has been removed.
The input changes require a full rebuild for incremental task ':utilities:compileJava'.
Full recompilation is required because no incremental change information is available. This is usually caused by clean builds or changing compiler arguments.
#
# 使っているツールチェインの場所が出力されている!!
#
Compiling with toolchain '/home/masaya/.gradle/jdks/jdk8u265-b01'.
Compiling with JDK Java compiler API.
Created classpath snapshot for incremental compilation in 0.001 secs.
:utilities:compileJava (Thread[Execution worker for ':',5,main]) completed. Took 0.087 secs.
:app:compileJava (Thread[Execution worker for ':',5,main]) started.
This JVM does not support getting OS memory, so no OS memory status updates will be broadcast

> Task :app:compileJava
Caching disabled for task ':app:compileJava' because:
  Build cache is disabled
Task ':app:compileJava' is not up-to-date because:
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/app/build/classes/java/main has been removed.
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/app/build/classes/java/main/gradle has been removed.
  Output property 'destinationDirectory' file /home/masaya/repo/gradle-toolchain-examples/app/build/classes/java/main/gradle/toolchain has been removed.
The input changes require a full rebuild for incremental task ':app:compileJava'.
Full recompilation is required because no incremental change information is available. This is usually caused by clean builds or changing compiler arguments.
#
# 使っているツールチェインの場所が出力されている!!
#
Compiling with toolchain '/home/masaya/.gradle/jdks/jdk-15+36'.
Starting process 'Gradle Worker Daemon 4'. Working directory: /home/masaya/.gradle/workers Command: /home/masaya/.gradle/jdks/jdk-15+36/bin/java @/tmp/gradle-worker-classpath4424283455172721113txt -Xmx512m -Dfile.encoding=UTF-8 -Duser.country=JP -Duser.language=ja -Duser.variant worker.org.gradle.process.internal.worker.GradleWorkerMain 'Gradle Worker Daemon 4'
Successfully started process 'Gradle Worker Daemon 4'
Started Gradle worker daemon (0.304 secs) with fork options DaemonForkOptions{executable=/home/masaya/.gradle/jdks/jdk-15+36/bin/java, minHeapSize=null, maxHeapSize=null, jvmArgs=[], keepAliveMode=SESSION}.
Compiling with JDK Java compiler API.
Created classpath snapshot for incremental compilation in 0.0 secs.
:app:compileJava (Thread[Execution worker for ':',5,main]) completed. Took 1.073 secs.

BUILD SUCCESSFUL in 3s
3 actionable tasks: 3 executed
Stopped 2 worker daemon(s).

ログを見たところ、設定したとおりに順番に、8, 8, 15でビルドされているように見えます。 また、1回目ダウンロードした後、2回目のダウンロードは走りませんでした。 使う toolchain の JDK はキャッシュされています。 CI環境ならtoolchain のキャッシュが必要そうですが、普段使っていても特に困ることはなさそうですね。

ビルドに使われたJDKが設定で指定したものになっているか確認する

確認する方法は、javap を使って確認しましょう。 ところで、バイトコードのバージョンを見るの、-vしかないんだっけ・・・

$ javap -v list/build/classes/java/main/gradle/toolchain/examples/list/LinkedList.class
Classfile /home/masaya/repo/gradle-toolchain-examples/list/build/classes/java/main/gradle/toolchain/examples/list/LinkedList.class
  Last modified 2020/10/01; size 2020 bytes
  MD5 checksum dd80c79fa7cd4fbf29bac6bdc3032e9c
  Compiled from "LinkedList.java"
public class gradle.toolchain.examples.list.LinkedList
  minor version: 0
  major version: 52

〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜

なにはともあれ、major version: 52 と書かれています。 これを Wikipedia にある java class fileを参照すると Java 8 のバイトコードであることが分かります。

utilities 配下に生成されたクラスファイルを見ても同様に Java 8のバイトコードが生成されています。

今度は、Java 15 でビルドしたプロジェクトの方のクラスファイルも見てみましょう。

$ javap -v app/build/classes/java/main/gradle/toolchain/examples/app/App.class 
Classfile /home/masaya/repo/gradle-toolchain-examples/app/build/classes/java/main/gradle/toolchain/examples/app/App.class
  Last modified 2020/10/01; size 947 bytes
  MD5 checksum 2a5ff88a5b8301ebf63b0ff8d0dd5b91
  Compiled from "App.java"
public class gradle.toolchain.examples.app.App
  minor version: 0
  major version: 59

〜〜〜〜〜〜〜〜〜 省略 〜〜〜〜〜〜〜〜〜〜〜

major verison:59 と書かれており、Java15のバイトコードが生成されたことがわかります。

というわけでここまでで toolchain support によって設定した toolchainを使って コンパイルがちゃんとされていることは確認できました。

実際に動かしてみる

今度は、このアプリケーションを実際に動かしてみましょう。 このアプリケーションはすぐに終了してしまうので debug オプションを追加してサスペンドさせて JVMのバージョンを確かめてみます。

$ ./gradlew run --debug-jvm

> Task :app:run
Listening for transport dt_socket at address: 5005
<===========--> 91% EXECUTING [42s]
> :app:run

というわけでサスペンドした状態で起動しました。

VMのバージョンはjcmdで確認しましょう。

$ jps
858300 Jps
847529 GradleWrapperMain
847799 Unknown
334377 GradleDaemon

$ jcmd 847799 VM.version
847799:
OpenJDK 64-Bit Server VM version 15+36
JDK 15.0.0

$ jcmd 847799 VM.system_properties
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
java.home=/home/masaya/.gradle/jdks/jdk-15+36
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

VM.version の結果として、JDK 15.0.0 と表示されているのが分かります。 また、VM.system_properties から Javaのあるディレクトリを確認したところ Gradleのコンパイルで使っていたJDKが使われていることが分かります。

ここからは Toolchain support の 細かい挙動 をドキュメントから調べてみます。

どのようにして使うJVMが選ばれるのか

toolchain supportのドキュメント から挙動が書いてあるところ抜き出してきました。

* Setup all compile, test and javadoc tasks to use the defined toolchain which may be different than the one Gradle itself uses

* Gradle detects locally installed JVMs

* Gradle chooses a JRE/JDK matching the requirements of the build (in this case a JVM supporting Java 14)

* If no matching JVM is found, it will automatically download a matching JDK from AdoptOpenJDK

これによると、インストールされているJVMを自動で検知して、マッチするJVMが無い場合 AdoptOpenJDKから自動でダウンロードしてきてくれます。 また、今回は試しませんでしたが、javadocも指定したランタイムのものを使ってくれます。

インストールされているJVMの自動検知方法ですが、OS固有のインストール先に加えて、以下のパッケージマネージャもサポートされています。 普段SDKMANを使っているので、これは嬉しいですね。 AdoptOpenJDKを使っていない場合でも 自分でSDKMANなどを使ってインストールすれば良いですね。

Test/Compile だけ違うバージョンのJVMで実行も出来る

また、ドキュメントによると複雑なテストやコンパイルにおけるバージョン制御も出来ます。 ドキュメントに以下のサンプルがありました。

tasks.withType(JavaCompile).configureEach {
    javaCompiler = javaToolchains.compilerFor {
        languageVersion = JavaLanguageVersion.of(8)
    }
}
test {
    javaLauncher = javaToolchains.launcherFor {
        languageVersion = JavaLanguageVersion.of(14)
    }
}

これはコンパイルは 8 だけど、14でテストする、みたいなやつでしょうか? これは便利ですね。

まとめ

詳しい挙動を調べてみた結果、めちゃくちゃパワフルな機能であることが分かります。 また、JDKのダウンロード先をミラーにする機能だったり JDKのインストール先を手動で指定できる機能があります。 この辺は困った時にあると workaround として使えて嬉しいですね。

なんでこんないたれりつくせりなんや・・・。 嬉しいですが、めちゃくちゃ作り込まれていてすごいですね・・・。

おわりに

今回、Gradle 6.7 でリリースされる、Toolchain support for JVM Projects を使って コンパイルと実行を共に試してみました。 大体期待通りの挙動になっていることが分かります。

本当にこの機能、前前職でJavaのアップデートしている時に欲しかった・・・

また、細かい挙動をドキュメントから見て調べてみましたが 非常に多くの機能があり、基本的に欲しいなぁと思った機能はすでに実装されています。

Gradle 6.7 では、このブログで紹介した、非常に便利で強力な機能がリリースされます。 rc-3 なのでもうすぐリリースされるんじゃないかなぁと思います。 皆さんGradleのアップデートをしましょう。