やんくさんに頼まれたので、誰かが書くだろうと思って待ってたけど誰も書かない Gradle の kotlin-dsl
プラグインについて書きます。
@mike_neck
— やんく (@yy_yank) 2020年12月12日
mikeさんアドベントカレンダー書くネタあったりしませんか?👉👈https://t.co/ZDwmyBpbC0
まず、先日(2020/12/11)開催された JJUG ナイトセミナーにて、時間がないのでほぼ飛ばしてしまった、こちらの話題について解説します。
build.gradle
(build.gradle
) の中で if
文(Kotlin だと式ですね)とか for
文(こっちは式だったっけ?)を書くと、宣言的なコード(例えば依存ライブラリーの宣言など)と命令的なコードが混在してしまいます。そのため、ビルドスクリプトを読むために頭の切り替えが必要になるのでメンテナンス性が下がるというのが理由です。したがって、命令的なコードを書いている部分はすべてプラグインにして、 buildSrc
プロジェクトに移動するのがよいと公式のドキュメントに書いてあります。
しかし、これは素直におすすめできないところがあって、プラグインを書くためには少しだけプラグインの仕組みの学習コストがかかります(まあ、大したコストではありませんが…) 。そこで今持っている build.gradle
(build.gradle.kts
) の 知識でプラグインを無理なく書ける仕組みを使うのがよいでしょう。
プリコンパイルド・スクリプト・プラグイン(Precompiled script plugins) というのがその仕組です。
具体的には以前このブログで紹介した groovy-gradle-plugin
が Groovy の場合のプラグインです。ここでは詳しく解説しないので、以下のエントリーを読んでください。
これの Kotlin 版が kotlin-dsl
プラグインです。(この言い方は語弊があって、先に kotlin-dsl
があって、その後に Groovy 版ができたらしい)
なお、ググラビリティが悪いので、このプラグインの名前はどうにかしてほしい
kotlin-dsl
の書き方
buildSrc/build.gradle.kts
ファイルに以下のように書きます。 repositories {}
ブロックを忘れると kotlin-dsl
をビルドするための Kotlin をダウンロードできなくなってエラーが発生します
plugins { `kotlin-dsl` } repositories { jcenter() }
buildSrc/src/main/kotlin
の下に任意の名前で .gradle.kts
ファイルを作成します。ここではサンプルのため、 example.gradle.kts
というファイル名にします。なお、内容的には先の
Java toolchains のエントリーの build.gradle
と同じ様な内容にしています
plugins { `java` } repositories { mavenCentral() } dependencies { compileOnly("org.jetbrains:annotations:19.0.0") implementation("org.slf4j:slf4j-api:1.7.30") testImplementation("org.junit.jupiter:junit-jupiter:5.7.0") testImplementation("org.assertj:assertj-core:3.18.1") } internal val ss = sourceSets internal val testSourceSets = ss.getByName("test") @Suppress("UnstableApiUsage") internal val javaToolChains = extensions.getByType<JavaToolchainService>() @Suppress("UnstableApiUsage") internal val javaPluginExt = extensions.getByType<JavaPluginExtension>() javaPluginExt.toolchain { @Suppress("UnstableApiUsage") languageVersion.convention(JavaLanguageVersion.of(8)) @Suppress("UnstableApiUsage") vendor.convention(JvmVendorSpec.AZUL) @Suppress("UnstableApiUsage") implementation.convention(JvmImplementation.VENDOR_SPECIFIC) } open class ProjectTestConfig { @Suppress("UnstableApiUsage") // 指定されたバージョンの Java でテストを実行するタスクを登録します fun testOnJavaVersion(vararg javaVersions: Int): Map<Int, TaskProvider<Test>> = javaVersions.map { javaVer -> javaVer to tasks.register<Test>("testJava${javaVer}") { group = "verification" description = "runs test on Java $javaVer" classpath = testSourceSets.runtimeClasspath testClassesDirs = testSourceSets.output.classesDirs useJUnitPlatform() javaLauncher.convention(javaToolChains.launcherFor { languageVersion.convention(JavaLanguageVersion.of(javaVer)) }) } }.onEach { pair -> tasks.named("check").configure { dependsOn(pair.second) } }.fold(mapOf()) { acc, pair -> acc + pair } } extensions.add<ProjectTestConfig>("testConfig", ProjectTestConfig())
なお、このスクリプトですが、書いている途中でコンパイルエラーが発生すると補完が効かなくなるので、気合で乗り越えていく必要があります。 これが僕が Kotlin DSL をあまり好まない理由の一つです。
最後に、ルートディレクトリーつまりメインプロジェクトの build.gradle.kts
です。ここで、先程の .gradle.kts
ファイルの拡張子を抜いた部分(ここまでの例だと example.gradle.kts
→ example
) をプラグインとして指定します。
plugins { `example` } // テストは Java 11 と 15 でも実行する testConfig.testOnJavaVersion(11, 15)
かなりシンプル(というより、なにもない)になりました。
では、 Java 8 以外で実行すると落ちるようなテストを書いて実行してみます
class Version { @Test void show() { String javaHome = System.getProperty("java.home"); assertThat(javaHome).contains("jre"); } }
というわけで、 kotlin-dsl
を使うとビルドスクリプトがシンプルになったと思います。また、 groovy での記事でも書いたように複数のサブプロジェクトに対してプロジェクト横断的に同一な記述が必要になるケース(例えば、同じ Kotlin のバージョンと Linter と フォーマッターを使いたい)で、 kotlin-dsl
で作ったプラグインをサブプロジェクトで利用できます。
kotlin-dsl
のおさらい
- プロジェクト(プロダクト)用の
build.gradle.kts
から命令的なコードを取り除いて、buildSrc
プロジェクトに命令的なコードを記述する buildSrc
にプラグインを書く場合にkotlin-dsl
を使うと.gradle.kts
ファイルを作成する
以上のような形でビルドスクリプトをシンプルにできます。