あなたの知らない Gradle の kotlin-dsl プラグイン
やんくさんに頼まれたので、誰かが書くだろうと思って待ってたけど誰も書かない 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ファイルを作成する
以上のような形でビルドスクリプトをシンプルにできます。