🚴

[Gradle][Kotlin] ライブラリの管理をVersion Catalogに移行する

2023/11/26に公開

概要

  • Gradleの dependencies ブロックに書いているライブラリの定義をVersion Catalogに移行する
  • Gradle Kotlin DSLを併用するとIDEの補完も効いてラク
  • Dependabotとも相性が良い

みたいな話。

Version Catalogとは

とりあえず公式のドキュメントはこちら。
https://docs.gradle.org/current/userguide/platforms.html

settings.gradle.ktslibs.version.toml に定義することで、使用するライブラリのバージョンを1箇所に集中させるというやり方。
1箇所で集中管理することで特にマルチプロジェクト構成のGradleプロジェクトの際に効力を発揮する。

本題

事前準備

まずはマルチプロジェクト構成のGradleプロジェクトを作りましょう。
先にも書きましたがシングルプロジェクトだとVersion Catalogを使う意味は正直ありません。

Version Catalogを導入する前のマルチプロジェクト構成だと皆さん大体こうしてますよね。

  • sandbox-app
    • gradle
      • wrapper
        • gradle-wrapper.jar
        • gradle-wrapper.properties
    • sandbox-api
      • build.gradle.kts
    • sandbox-database
      • build.gradle.kts
    • gradlew
    • gradlew.bat
    • build.gradle.kts
    • settings.gradle.kts

これをVersion Catalogを導入するということで以下のようにしましょう。

  • sandbox-app
    • buildSrc
      • src/main/kotlin
        • sandbox-app.java-common-dependencies-conventions.gradle.kts
        • sandbox-app.java-conventions.gradle.kts
      • build.gradle.kts
      • settings.gradle.kts
    • gradle
      • wrapper
        • gradle-wrapper.jar
        • gradle-wrapper.properties
      • libs.versions.toml
    • sandbox-api
      • build.gradle.kts
    • sandbox-database
      • build.gradle.kts
    • gradlew
    • gradlew.bat
    • build.gradle.kts
    • settings.gradle.kts

ポイントとしては buildSrc ディレクトリと libs.versions.toml ファイルの2つになります。

libs.versions.toml

書き方はGradleの公式ドキュメントに書いてあるので読んでください。

https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format

[versions] にはバージョンだけ書き出して、 version.ref で参照するようにすると良いことがあるので共通化しておきましょう。
[bundles] ブロックも上手いこと使うと依存関係を綺麗にまとめられます。

gradle/libs.versions.toml
[versions]
lombok = "1.18.30"
ulid = "8.3.0"
spring-boot = "3.1.5"
spring-dependency-management = "1.1.4"
guava = "32.1.3-jre"
commons-lang = "3.13.0"
commons-io = "2.15.0"
mysql = "8.2.0"

[libraries]
lombok = { module = "org.projectlombok:lombok", version.ref = "lombok" }
ulid = { module = "de.huxhorn.sulky:de.huxhorn.sulky.ulid", version.ref = "ulid" }
guava = { module = "com.google.guava:guava", version.ref = "guava" }
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commons-lang" }
commons-io = { module = "commons-io:commons-io", version.ref = "commons-io" }
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }
spring-boot-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "spring-boot" }
spring-boot-starter-data-jpa = { module = "org.springframework.boot:spring-boot-starter-data-jpa", version.ref = "spring-boot" }
mysql = { module = "com.mysql:mysql-connector-j", version.ref = "mysql" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }
spring-dependency-management = { id = "io.spring.dependency-management", version.ref = "spring-dependency-management" }

[bundles]
apache-commons = [
    "commons-lang3",
    "commons-io"
]

buildSrc ディレクトリの中身

Gradleにおいて buildSrc ディレクトリは特殊な意味を持つディレクトリです。
ここにルートプロジェクト配下の全プロジェクトに適用する定義を落とし込んでいきます。

https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:build_sources

buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}

Version Catalogとしてライブラリのバージョンを管理する libs.versions.toml を読み込んでおきましょう。

buildSrc/settings.gradle.kts
rootProject.name = "buildSrc"

dependencyResolutionManagement {
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}

src/main/kotlin 配下には全プロジェクトに展開する定義を書いていきましょう。
地味にファイルの命名規則があるので要注意で {rootプロジェクト名}.xxxxx.gradle.kts のように、ファイルの先頭にrootプロジェクトのプロジェクト名をつけるのを忘れないでください。

Javaプロジェクトとしての共通定義をここに落としていきましょう。
Gradleプラグインやリポジトリ、コンパイル周りの定義をしていきます。

src/main/kotlin/sandbox-app.java-conventions.gradle.kts
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.testing.Test
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.kotlin.dsl.eclipse
import org.gradle.kotlin.dsl.idea
import org.gradle.kotlin.dsl.java
import org.gradle.kotlin.dsl.repositories
import org.gradle.kotlin.dsl.withType

plugins {
    java
    idea
    eclipse
}

repositories {
    mavenCentral()
}

tasks.withType<JavaCompile> {
    options.encoding = "UTF-8"
}

tasks.withType<Test> {
    useJUnitPlatform()
    testLogging {
        events(
            "SKIPPED",
            "PASSED",
            "FAILED",
            "STANDARD_ERROR",
        )
        exceptionFormat = TestExceptionFormat.FULL
    }
}

共通のライブラリの依存関係をここに落とし込んでいきましょう。
LombokやGuavaといった「どのプロジェクトでも使うな」みたいなライブラリの依存関係を定義していきます。

Version Catalogのことを調べると libs.xxx みたいな書き方しか出てこないのですが、 src/main/kotlin 配下のファイルだとその書き方使えません。これマジで要注意。
ここで面白いのが findLibrarylibs.versions.toml[libraries] ブロックのキーを引っかけたり、 findBundle[bundles] ブロックのキーを引っかけられるんですが、Kotlinを上手く使うことで書きっぷりを綺麗にまとめることができます。

src/main/kotlin/sandbox-app.java-common-dependencies-conventions.gradle.kts
plugins {
    id("melinite.java-conventions")
}

val catalog = project.extensions.getByType<VersionCatalogsExtension>().named("libs")
dependencies {
    // Lombok
    // => 4行に渡ってartifact:group:versionを書かなくてもよくなる
    catalog.findLibrary("lombok").ifPresent {
        implementation(it)
        annotationProcessor(it)
        testImplementation(it)
        testAnnotationProcessor(it)
    }

    // ULID
    catalog.findLibrary("ulid").ifPresent {
        implementation(it)
    }

    // Guava
    catalog.findLibrary("guava").ifPresent {
        implementation(it)
    }

    // Apache Commons
    // => commons-lang3とcommons-ioをバンドルしているので、これだけで2つのライブラリの依存関係が足される
    catalog.findBundle("apache.commons").ifPresent {
        implementation(it)
    }
}

例えばLombokなんかをVersion Catalogを使わずにベタに定義するとこうなると思うんですが、綺麗にまとめられますね。

dependencies {
  implementation("org.projectlombok:lombok:1.18.30")
  annotationProcessor("org.projectlombok:lombok:1.18.30")
  testImplementation("org.projectlombok:lombok:1.18.30")
  testAnnotationProcessor("org.projectlombok:lombok:1.18.30")
}

各プロジェクトの build.gradle.kts

Version Catalogを使うことで各プロジェクトの build.gradle.kts を書く際には libs から勝手にメソッドが生えてきます。

libs.versions.toml[libraries] ブロックに書いたものは libs.xxx のように参照できますし、 [plugins] ブロックに書いたものは libs.plugins.xxx のように参照できます。
注意点としてはtomlのキーはケバブケースで書くのですが、 build.gradle.kts ではドット区切りで参照してくことになります。

sandbox-api/build.gradle.kts
import org.gradle.kotlin.dsl.application

plugins {
    application
    alias(libs.plugins.spring.boot)
    alias(libs.plugins.spring.dependency.management)
    id("melinite.java-common-dependencies-conventions")
}

dependencies {
    implementation(libs.spring.boot.starter.web)
    implementation(libs.spring.boot.starter.actuator)
    implementation(project(":sandbox-database"))

    testImplementation(libs.spring.boot.starter.test)
}

buildscript ブロックでも同じように libs.xxx で参照できるのがポイントです。
MyBatis GeneratorやDoma CodeGenといったジェネレーター系のGradleプラグインを使おうとすると、 dependencies ブロックだけではなく buildscript ブロックでもDBドライバーをクラスパスに通さないといけないシーンが多々あるので、これはまぁまぁ強力じゃないかなと。

sandbox-database/build.gradle.kts
import org.gradle.kotlin.dsl.`java-library`

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath(libs.mysql)
    }
}

plugins {
    `java-library`
    id("melinite.java-common-dependencies-conventions")
}

dependencies {
    implementation(libs.spring.boot.starter.data.jpa)
    implementation(libs.mysql)

    testImplementation(libs.spring.boot.starter.test)
}

Dependabotとの組み合わせ

Version Catalogが出てきた当初はDependabotが使えなかったんですが、今年の3月にはサポートされるようになっています。

https://github.blog/changelog/2023-03-13-dependabot-version-updates-keeps-gradle-version-catalogs-up-to-date/

そこでVersion Catalogを使用したGradleプロジェクトにDependabotを組み合わせた際に出てくる良い副産物を挙げていきましょう。

Gradleプラグインのバージョンも更新対象になる

Dependabot自体は前々から使っていたんですが、Gradleプロジェクトにおける1番の不満ポイントだった「Gradleプラグインは更新してくれない」という点を解消してくれます。

build.gradle.kts
plugins {
  // こっちはDependabotが反応しない
  id("org.springframework.boot") version "3.1.5"
}

dependencies {
  // こっちはDependabotが反応する
  implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
}

これがVersion Catalogを使うことでどちらもDependabotが更新対象として認知をしてくれるようになります。

libs.versions.toml
[versions]
spring-boot = "3.1.5"

[libraries]
# シンプルなライブラリの依存関係はDependabotが反応する(これまで通り)
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }

[plugins]
# Gradleプラグインの依存関係もDependabotが反応するようになる
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

プルリクエストの数を抑えられる

あとは libs.versions.toml[versions] ブロックでバージョンを共通化しているのでDependabotが作ってくるプルリクの数も抑えられるという利点もあります。
特にマルチプロジェクトな構成で同じライブラリを複数のプロジェクトに依存関係として定義していると、その数だけDependabotはプルリクエストを作ってきます。

つまりこういう定義の仕方をしているとDependabotは問答無用で2つプルリクエストを作ってきます。

projectA/build.gradle.kts
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
}
projectB/build.gradle.kts
dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web:3.1.5")
}

これがVersion Catalogによって依存関係のあるライブラリを1箇所に集中管理することでプルリクエストが1つになります。

libs.versions.toml
[versions]
# この行の変更分しかプルリクエストを作ってこない
spring-boot = "3.1.5"

[libraries]
spring-boot-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "spring-boot" }

[plugins]
spring-boot = { id = "org.springframework.boot", version.ref = "spring-boot" }

Dependabotの定義がシンプルになる

Dependabotを使う上で最終的にはここに落ち着くのかなとは思うのですが、何度も言うようにライブラリの依存関係を1箇所に集中させているので、Dependabotの定義がとにかくシンプルになります。

マルチプロジェクト構成の際は dependencies ブロックのある build.gradle.kts を全部反応させようと思ったら、それらを全てDependabotに教えてあげないといけませんでした。

.github/dependabot.yml
version: 2
updates:
  # build.gradle.ktsの分だけ書かないといけない
  - package-ecosystem: gradle
    directory: projectA
  - package-ecosystem: gradle
    directory: projectB

これが libs.versions.toml の1ファイルに全てまとまっているので、Dependabotには1行分だけでOKです。

.github/dependabot.yml
version: 2
updates:
  # rootディレクトリを指定しておけばlibs.versions.tomlを勝手に探してくれる
  - package-ecosystem: gradle
    directory: /

まとめ

ここ最近の開発においてはOSSのライブラリはなくてはならない存在になっていると思います。
ひと昔前はオレオレフレームワークみたいなのが流行りでしたけど、今となってはただの車輪の再発明でしかないのでただの無駄足にしかなりません。
使えるものは存分に使っていきましょう。

ということで良いVersion Catalogライフを!

Discussion