Gradleでマルチプロジェクトをやるならsubprojects{}じゃなくてプラグインシステムを使おう

Gradleでマルチプロジェクトってどうやるんだろう?って公式ドキュメントを眺めて遊んだのでメモ。タイトルの通りの話。

Gradleって変化が速い印象ある。ので、しばらくするとこのやり方も非推奨になるのかもしれない。2021年2月時点のGradle 6.8.2のお話。

❯ gradle -v

------------------------------------------------------------
Gradle 6.8.2
------------------------------------------------------------

Build time:   2021-02-05 12:53:00 UTC
Revision:     b9bd4a5c6026ac52f690eaf2829ee26563cad426

Kotlin:       1.4.20
Groovy:       2.5.12
Ant:          Apache Ant(TM) version 1.10.9 compiled on September 27 2020
JVM:          15.0.2 (AdoptOpenJDK 15.0.2+7)
OS:           Linux 5.4.0-59-generic amd64

マルチプロジェクトをやってみたい

マルチプロジェクトをやってみたいなぁと思って、じゃあGradleでやってみようーって気持ちになった。そもそもGradle自体をあんまりちゃんと理解してないんだけど、ちょうど良い機会だし面白いかなと。

ということで、Gradleでマルチプロジェクトのやり方について、適当に検索してブログとか日本語ドキュメントとかを読んでみたけどいまいちよくわかんなくて、あと情報が古そうだったので、結局公式ドキュメントを読むことにした。

https://docs.gradle.org/6.8.2/userguide/multi_project_builds.html

最初から公式ドキュメント読めよって自分にいいつつ。だって、英語だと気合入れないと、すぅーって単語が頭を通り過ぎていくんだもん日本語で先にハードルさげたいじゃんって自分にいいつつ。読んだ。

プロジェクトの構造

なるほど?とか言いながら、ぼーっと読みながら手を動かしてみた。実際にはその次のページに書いてあるこっちの構造を手元で作った。

https://docs.gradle.org/6.8.2/userguide/declaring_dependencies_between_subprojects.html

先に言っておくと、↑のページに書いてある情報だけだとビルドが通らないので、↓を参考にして buildSrc/build.gradle を書いたらビルドできた。

https://docs.gradle.org/6.8.2/samples/sample_convention_plugins.html

共通の設定をどこに書くか?

マルチプロジェクトだと共通の処理をどこかに定義すると思うんだけど、その定義には「プロジェクト横断設定を書くんじゃなくて、プラグインシステムを使おう」って感じのことが書いてあって「へー」って思った。

https://docs.gradle.org/6.8.2/userguide/sharing_build_logic_between_subprojects.html

プロジェクト横断設定?

結構これを紹介してるサイト多かったんだけど subprojects {}allprojects {} を使うやり方。これはこれでシンプルで分かりやすいと思う。「Gradle マルチプロジェクト」とかで検索したらたくさん出てくるからそちらを参考にしてください。

プラグインシステム?

プラグインシステムと言っても、プラグインを別でビルドしておいて使うんじゃなくて、サブプロジェクトの横に置いてしまう。んで、サブプロジェクトでそれを使う。

buildSrc というフォルダの中に共通設定を書いとくと自動的にプラグインとしてビルドされて使えるようになるみたいね。↓こんな風に書いておくと myproject.java-conventions というIDでプラグインとして利用できる。

❯ cat buildSrc/build.gradle 
plugins {
    id 'groovy-gradle-plugin'
}

❯ cat buildSrc/src/main/groovy/myproject.java-conventions.gradle 
plugins {
    id 'java'
}

group = 'me.bufferings'
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    testImplementation "junit:junit:4.13"
}

利用する側はこう。

❯ cat api/build.gradle 
plugins {
    id 'myproject.java-conventions'
}

dependencies {
    implementation project(':shared')
}

なるほどー。まぁ、こっちの方がキレイかな。

もっと複雑な場合

ここをざっと読んだ。

https://docs.gradle.org/6.8.2/userguide/structuring_software_products.html

ソースはここのをダウンロードして遊んだ。

https://docs.gradle.org/6.8.2/samples/sample_structuring_software_projects.html

大きな違い

大きな違いは、プラグインを複数定義できるようにしていること、と、プラットフォームを定義していること。かな。

プラグインを複数定義

プラグインを複数定義しているので、buildSrc は使わずに build-logic ってフォルダ名にしてある。

なので使う側も明示的にそのフォルダを includeBuild してから使ってる。

❯ cat server-application/settings.gradle 
// == Define locations for build logic ==
pluginManagement {
    repositories {
        gradlePluginPortal()
    }
}
includeBuild('../platforms')
includeBuild('../build-logic')

// == Define locations for components ==
dependencyResolutionManagement {
    repositories {
        mavenCentral()
    }
}
includeBuild('../user-feature')
includeBuild('../admin-feature')

// == Define the inner structure of this component ==
rootProject.name = 'server-application' // the component name
include('app')

プラットフォームを定義

Gradleの持つplatformという仕組みを使ってバージョンを先に決めておくことで、利用する側ではバージョンを書かなくてすむ。

プラットフォームの定義はこんな感じ

❯ cat platforms/product-platform/build.gradle 
plugins {
    id('java-platform')
}

group = 'com.example.platform'

// allow the definition of dependencies to other platforms like the Spring Boot BOM
javaPlatform.allowDependencies()

dependencies {
    api(platform('org.springframework.boot:spring-boot-dependencies:2.4.0'))

    constraints {
        api('org.apache.juneau:juneau-marshall:8.2.0')
    }
}

この例ではSpring Bootのplatformに加えて、juneau-marshall のバージョンを指定してる:

使う側はこう

❯ cat build-logic/commons/src/main/groovy/com.example.commons.gradle 
plugins {
    id('java')
    id('com.example.jacoco')
}

group = 'com.example.myproduct'

java {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}

dependencies {
    implementation(platform('com.example.platform:product-platform'))

    testImplementation(platform('com.example.platform:test-platform'))
    testImplementation('org.junit.jupiter:junit-jupiter-api')
    testRuntimeOnly('org.junit.jupiter:junit-jupiter-engine')
}

tasks.named("test") {
    useJUnitPlatform()
}

この中の↓の部分。

implementation(platform('com.example.platform:product-platform'))

面白かった

大きなマルチプロジェクトの場合に、プラグインをどんな風に構成するかは頭を悩ませるだろうけど、公式ドキュメントのおすすめが分かって良かったや。