8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

gradle.kts ベースで Gradle Convention Plugin を作る

Last updated at Posted at 2024-11-09

こんにちは、こんばんは、kitakkunです。

Gradleビルドスクリプトの共通化・外部化する際、Composite Build という仕組みを活用して Convention Plugin を実装することが一般的になってきました。

しかし、現在広く使われている org.gradle.api.Plugin を実装する手法には、いくつか欠点があります。

  • ボイラープレートが増える。
  • settings.gradle.kts の共通化が難しい。

本記事では、ktsファイル を使ってより効率的かつ柔軟に Convention Plugin を構成する手法を紹介します。

従来の方法

説明上、簡単なものの方が好ましいと思うので、適用時に "Hello, convention plugin!" と出すだけのものを作ります。

gradle-conventions モジュールの作成

まず、Convention Pluginの実装を含むモジュール(gradle-conventions)を作ります。

gradle-conventions
 ├─ src/main/kotlin/MyConventionPlugin.kt
 ├─ build.gradle.kts
 └─ settings.gradle.kts
gradle-conventions/src/main/kotlin/MyConventionPlugin.kt
class MyConventionPlugin : Plugin<Project> {
    override fun apply(target: Project) {
        println("Hello, convention plugin!")
    }
}
gradle-conventions/build.gradle.kts
plugins {
    `kotlin-dsl`
}

gradlePlugin {
    plugins {
        register("my-convention") {
             id = "my-convention"
             implementationClass = "MyConventionPlugin"
        }
    }
}
gradle-conventions/settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}

まあよくある構成ですね。

プラグインを includeBuild する

次に、ルートプロジェクトの settings.gradle.kts 内の pluginManagementincludeBuild します。

settings.gradle.kts
pluginManagement {
    includeBuild("gradle-conventions")
}

すると、以下のようにカスタムプラグインが適用可能になります。

build.gradle.kts
plugins {
    id("my-convention")
}

Gradle Sync して以下のように出ればOKです。

> Configure project :
Hello, convention plugin!

このように、一般的には org.gradle.api.Plugin クラスを実装する方法が使われます。
しかし、実は src の中に直接 gradle.kts を配置する方法もあります。

gradle.kts を src 配下に置く手法

src の配下に println だけ書いた my-convention.gradle.kts を作成します。

gradle-conventions/src/main/kotlin/my-convention.gradle.kts
println("Hello, convention plugin! (from kts)")

gradlePlugin の定義は不要なので消してしまいます。

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

- gradlePlugin {
-     plugins {
-         register("my-convention") {
-             id = "my-convention"
-             implementationClass = "MyConventionPlugin"
-        }
-     }
- }

gradle-conventions/src/main/kotlin/MyConventionPlugin.kt も削除します。

gradle-conventions/src/main/kotlin/MyConventionPlugin.kt
- class MyConventionPlugin : Plugin<Project> {
-     override fun apply(target: Project) {
-         println("Hello, convention plugin!")
-     }
- }

もう一度 Gradle Sync してみましょう。同じ結果になるはずです。

> Configure project :
Hello, convention plugin! (from kts)

随分と短く簡潔に作れましたね。ボイラープレートも減って見やすくなりました。

何が起きているのか?

src 内に配置された gradle.kts は、Pluginクラスにプリコンパイルされます。
ですから、my-convention.gradle.kts からは MyConventionPlugin が生成されます。

gradle-conventions/build/generated-sources/kotlin-dsl-plugins/kotlin/MyConventionPlugin.kt
/**
 * Precompiled [my-convention.gradle.kts][My_convention_gradle] script plugin.
 *
 * @see My_convention_gradle
 */
public
class MyConventionPlugin : org.gradle.api.Plugin<org.gradle.api.Project> {
    override fun apply(target: org.gradle.api.Project) {
        try {
            Class
                .forName("My_convention_gradle")
                .getDeclaredConstructor(org.gradle.api.Project::class.java, org.gradle.api.Project::class.java)
                .newInstance(target, target)
        } catch (e: java.lang.reflect.InvocationTargetException) {
            throw e.targetException
        }
    }
}

自動生成された MyConventionPlugin は、ファイル名の my-convention の部分をidとして適用できます。

settings.gradle.kts を共通化する

kts化するもう一つのメリットは、settings.gradle.kts が共通化しやすいことです。

settings.gradle.kts の共通化が必要なケースはそう多くないと思いますが、動的に生成する versionCatalog をプロジェクト全体で一貫して使用したい場合などに役立ちます。
例: Kotlinバージョンを動的に切り替えてコンパイラプラグインをビルドしたい時

repositories を設定する Convention Plugin を作成する例

簡単な例として、プロジェクトの依存アーティファクトの参照先となる repositories を設定するConvention Plugin を作ってみましょう。

現在、settings.gradle.kts には repositories の定義が欠けていますので、

settings.gradle.kts
pluginManagement {
    includeBuild("gradle-conventions")
}

build.gradle.kts で Kotlin JVMプラグインを適用しようとすると・・

build.gradle.kts
plugins {
    id("my-convention")
    id("org.jetbrains.kotlin.jvm") version "2.0.0"
}

以下のように「プラグインが解決できないよ」と怒られます。

conventions:main: Cannot resolve external dependency org.jetbrains.kotlin:kotlin-stdlib:2.0.0 because no repositories are defined.
Required by:
    root project :

Possible solution:
 - Declare repository providing the artifact, see the documentation at https://docs.gradle.org/current/userguide/declaring_repositories.html

ゴールは、このエラーを消すことです。具体的な手順は2ステップ。

  1. settings-conventions.settings.gradle.ktsrepositories 設定を書く。
  2. ルートのsettings.gradle.kts内でプラグインを適用する。

まず、settings-conventions.settings.gradle.kts は以下のように書きます。

gradle-conventions/src/main/kotlin/settings-conventions.settings.gradle.kts
pluginManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

dependencyResolutionManagement {
    repositories {
        mavenCentral()
        gradlePluginPortal()
    }
}

これをルートプロジェクトのsettings.gradle.ktsで適用してあげます。

settings.gradle.kts
 pluginManagement {
     includeBuild("gradle-conventions")
 }

+ plugins {
+     id("settings-conventions")
+ }

すると、Gradle Sync が通るようになるはずです。

ついでに gradle-conventions にも冗長な repositories 定義があるので、このプラグインで代替します。

gradle-conventions/settings.gradle.kts
- pluginManagement {
-     repositories {
-         mavenCentral()
-         gradlePluginPortal()
-     }
- }

- dependencyResolutionManagement {
-     repositories {
-         mavenCentral()
-     }
- }

+ apply(from = "src/main/kotlin/settings-conventions.settings.gradle.kts")

こちらも問題なく Gradle Sync 成功するはずです。

まとめ

本記事では、gradle.kts の形式でConvention Pluginを実装することで、ボイラープレートを減らしつつ settings.gradle.kts の共通化も含め柔軟にビルド設定が行えることをご紹介しました。

この手法は、kotlinx.rpc リポジトリをコードリーディングしていた際に発見したものです。

settings.gradle.kts の共通化などはエッジケースかと思いますが、Convention Pluginの実装において面倒な作業を極限まで省けるという恩恵は、どなたでも享受できるものかと思います。

kts convention、やっていきましょう :muscle:

宣伝

現在個人的に開発している Kondition というコンパイラプラグインがあります。

簡単に説明すると、アノテーションをつけることで特定の条件が満たされていることを保証できるプラグインとなっています。

fun myFunction(@NonEmpty value: String) {
    // この中では value が空文字でないことが保証される
    // ※require(value.isNotEmpty()) がIRレベルで生成される
}

Kotlinの複数バージョン対応を行う際に、ktsベースの Convention Plugin がかなり役に立ちました。具体的には、versionCatalogs の共通化など少し高度なことを行っています。もし興味がありましたらご覧ください!

ここまで読んでいただきありがとうございました!
Gradle、何もわからん!!!!

8
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?