Gradle: Composite Build でマルチモジュールプロジェクトの build.gradle.kts 設定を共通化する


Gradle Composite Build を用いて build.gradle.kts の設定を共通化する方法を解説します。

https://docs.gradle.org/current/userguide/composite_builds.html#settings_defined_composite

> 2023/02/22 Version Catalog をなるべく使うような構成となるように記事を更新しました
> 2025/02/07 Gradle 8.11 以降でビルドできるようにするために version = "unspecified" 指定を削除しました

# buildSrc よりも Composite Build がおすすめ

build.gradle.kts における設定の共通化はこれまでは buildSrc の利用が一般的でしたが、最近は buildSrc よりも Composite Build が推奨されることが多くなっています。

buildSrc は Gradle の構成全体に影響を与えるため、buildSrc の小さな変更がプロジェクト全体のリビルドに繋がる点などが問題であると言われています。

https://developer.squareup.com/blog/herding-elephants/

# Gradle Plugin について

Composite Build で Gradle Plugin を実装することで設定の共通化を行う方針とします。

Gradle Plugin の作り方も参考となります。Gradle Plugin については以下の公式ドキュメントを参照してください。

https://docs.gradle.org/current/userguide/custom_plugins.html#custom_plugins

# 前提

サンプルの解説にあたって以下の環境を想定します。いずれも必須というわけではありません。

* Gradle 8.11
    * Composite Build はかなり古く、 Gradle 3.1 から対応しているようです
* build.gradle.kts (Kotlin DSL) 環境であること
* Version Catalog を使用していること

# Composite Build の導入

## 全体構成

全体構成は以下のようになります。`build-logic` モジュールが Composite Build です。

![全体構成](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/9f429ae1-1262-34a9-4e98-ced69dc0741a.png)

## Version Catalog の設定

例として、以下のように Version Catalog を設定します。

`[plugins]` の `buildlogic-...` に Plugin Id を定義しています。Composite Build の Plugin はバージョンを持たないため version 指定は省略します。

`gradle/libs.versions.toml`

```toml
[versions]
gradle-android = "7.4.1"
gradle-android-compile-sdk = "33"
gradle-android-target-sdk = "33"
gradle-android-min-sdk = "16"
...
[libraries]
gradle-android = { module = "com.android.tools.build:gradle", version.ref = "gradle-android" }
...
[plugins]
android-library = { id = "com.android.library", version.ref = "gradle-android" }
// Plugin Id の定義
buildlogic-android-application = { id = "build-logic.android.application" }
buildlogic-android-library = { id = "build-logic.android.library" }
buildlogic-dependencygraph = { id = "build-logic.dependency-graph" }
```

## build-logic モジュールの追加

`build-logic` モジュールを追加し、settings.gradle.kts を以下のように設定します。

ここではモジュール名は `build-logic` としていますが任意の名前で構いません。複数の Composite Build モジュールを配置することもできます。

Version Catalog を共通化するため、明示的に親となるプロジェクトのファイルを読み込ませています。

`build-logic/settings.gradle.kts`

```kotlin
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
    versionCatalogs {
        create("libs") {
            from(files("../gradle/libs.versions.toml"))
        }
    }
}
rootProject.name = "build-logic"
```

`build-logic` モジュールに Plugin の設定を追加します。

`build-logic/build.gradle.kts`

```kotlin
plugins {
    `kotlin-dsl`
}

dependencies {
    implementation(libs.gradle.android)
}

gradlePlugin {
    plugins {
        // 以下のプラグインの定義はサンプルです
        // id は Version Catalog の定義から取り出して設定しています
        register("android.application") {
            id = libs.plugins.buildlogic.android.application.get().pluginId
            // Version Catalog を使わない場合は以下のように id をそのまま設定します
            //id = "build-logic.android.application"
            implementationClass = "net.irgaly.buildlogic.AndroidApplicationPlugin"
        }
        register("android.library") {
            id = libs.plugins.buildlogic.android.library.get().pluginId
            implementationClass = "net.irgaly.buildlogic.AndroidLibraryPlugin"
        }
        register("dependency-graph") {
            id = libs.plugins.buildlogic.dependencygraph.get().pluginId
            implementationClass = "net.irgaly.buildlogic.ProjectDependencyGraphPlugin"
        }
    }
}
```

ここではサンプルのプラグインを記載していますが、任意のプラグインをいくつでも追加できます。用途に合わせて好きなだけプラグインを追加しましょう。

* `register("")` の名前は他と被らなければなんでも構いません。他の箇所から使うこともない名前です。
* `id` はプラグインを使用する module から指定するプラグイン ID 名を付けます。他のプラグイン ID と被らなければなんでも構いません。
* `implementationClass` はプラグインの実装クラスを指定します

## Plugin 実装の追加

`build-logic/build.gradle.kts` に定義したクラスを追加します。

たとえば「Android ライブラリモジュールに共通で適用したいプラグイン」 として `AndroidLibraryPlugin` を作るとすると以下のようなものになります。

`build-logic/src/main/kotlin/net/irgaly/buildlogic/AndroidLibraryPlugin.kt`

```kotlin
package net.irgaly.buildlogic

import org.gradle.api.Plugin
import org.gradle.api.Project

class AndroidLibraryPlugin: Plugin<Project> {
    override fun apply(target: Project) {

　　　　　　　　　　　　　　　　// ここで自由にプロジェクトの設定を適用する

        // たとえば以下のように書く
        with(target) {
            with(pluginManager) {
                // 共通のプラグインの適用
                apply("com.android.library")
            }
            // Android モジュール向けの共通設定を適用 (内容は Extensions.kt に定義)
            configureAndroid()
            // Android ライブラリモジュール向けの共通設定を適用 (内容は Extensions.kt に定義)
            configureAndroidLibrary()
        }
    }
}
```

サンプルとして共通設定のロジックを Extensions.kt に切り出しています。Extensions.kt は以下の通りです。

`build-logic/src/main/kotlin/net/irgaly/buildlogic/Extensions.kt`

```kotlin
package net.irgaly.buildlogic

import com.android.build.gradle.BaseExtension
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Project
import org.gradle.api.artifacts.VersionCatalog
import org.gradle.api.artifacts.VersionCatalogsExtension
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType

/**
 * android build 共通設定を適用する
 */
fun Project.configureAndroid() {
    extensions.configure<BaseExtension> {
        // 任意の共通設定を実装
        // サンプルとして　VersionCatalog からバージョン指定を取り出す実装をしています
        compileSdkVersion(libs.version("gradle-android-compile-sdk").toInt())
        defaultConfig {
            minSdk = libs.version("gradle-android-min-sdk").toInt()
            targetSdk = libs.version("gradle-android-target-sdk").toInt()
        }
    }
}

/**
 * android library 共通設定を適用する
 */
fun Project.configureAndroidLibrary() {
    extensions.configure<LibraryExtension> {
        // 任意の共通設定を実装
        buildFeatures {
            buildConfig = false
        }
    }
}

/**
 * VersionCatalog の取得
 */
val Project.libs: VersionCatalog get() {
    return extensions.getByType<VersionCatalogsExtension>().named("libs")
}

/**
 * VersionCatalog version の取得
 */
fun VersionCatalog.version(name: String): String {
    return findVersion(name).get().requiredVersion
}
```

## build-logic モジュールを includeBuild() で取り込む

Composite Build モジュールをプラグインとして使えるようにします。

プロジェクト全体の settings.gradle.kts へ `includeBuild()` 設定を追加します。

`settings.gradle.kts`

```kotlin
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "gradle-composite-build"
include(":sample:android")
includeBuild("build-logic")  // includeBuild で Composite Build を読み込む
```

## rootProject でプラグインのバージョンを指定する

マルチモジュールでは rootProject の plugins ブロックでバージョンを指定します。`apply false` で読み込むことで、Plugin を rootProject へ適用せずにバージョンを指定することができます。

rootProject でバージョンを指定しないと、他のモジュールで Plugin のバージョン不整合のエラーとなります。

`build.gradle.kts`

```kotlin
plugins {
    // Plugin バージョンを指定するが、apply false により rootProject ではプラグインは実行しない
    alias(libs.plugins.buildlogic.android.application) apply false
    alias(libs.plugins.buildlogic.android.library) apply false
    // rootProject に適用するには apply false なしで読み込み
    alias(libs.plugins.buildlogic.dependencygraph)
}
...
```

## モジュールにプラグインを適用する

このサンプルでは　AndroidLibraryPlugin　を `build-logic.android.library` として利用できるようにしました。

プラグインを適用するには、利用したいモジュールの build.gradle.kts で以下のように設定します。

`sample/android/build.gradle.kts`

```kotlin
plugins {
    alias(libs.plugins.buildlogic.android.library)
    // Version Catalog を使用しない場合は以下のように id 指定で適用します
    //id("build-logic.android.library")
}

...
```

上記のようにプラグインを適用すると、このモジュールに対して `AndroidLibraryPlugin.apply()` が実行されます。

# Tips

## プラグインの動作を変更できるようにしたい

Gradle Plugin は `.apply()` に対してパラメーターを渡したりすることはできません。つまり、プラグインの適用 (`.apply()`) そのものの動作を変更することはできません。

`id("...")` でプラグインを読み込むとプラグインに含まれるクラスを利用できるようになります。パラメーターを渡して設定したいものがあれば、build.gradle.kts から必要なクラスを直接利用します。

`id("...") apply false` とすると、プラグインの `.apply()` を実行せずに Composite Build モジュール内のクラスを読み込むことも可能です。

```kotlin
import net.irgaly.buildlogic.configureAndroid // Composite Build モジュール内のクラスを参照できる

plugins {
    // build-logic モジュールのクラスを読み込み、 .apply() も実行する
    id("build-logic.android.library")
    // build-logic モジュールのクラスを読み込むが、 .apply() は実行しない
    //id("build-logic.android.library") apply false
}

configureAndroid(moduleName = "feature1") // パラメーターを渡して任意の機能の呼び出し
...
```


プラグインの適用 (`.apply()`) の動作を変更することはできませんが、Gradle Plugin で追加した Task の設定をするためであれば Extension Objects を利用できます。詳細は以下のドキュメントの `Making the plugin configurable` 章を参照してください。

https://docs.gradle.org/7.5/userguide/custom_plugins.html#sec:getting_input_from_the_build

## Gradle Plugin の実行順について

plugins ブロックでプラグインを適用することで対象プラグインのクラスなどを import などで参照することができるようになります。これは build.gradle.kts を実行するときに plugins ブロックを先行評価することで実現しています。

build.gradle.kts とプラグインの実行順は以下のとおりです。

1. build.gradle.kts の　plugins ブロックが実行される
2. 読み込まれたプラグインの .apply() を実行する
3. 1.~2. によってプラグインが読み込まれた状態で、build.gradle.kts の plugins ブロック以外を実行する

すべてのプラグインの .apply() が完了してから build.graldle.kts が評価されるため、プラグインからさらに他のプラグインを読み込ませることが可能となっています。

たとえば、以下のような build.gradle.kts と AndroidApplicationPlugin (`build-logic.android.application`) を用意して Gradle プロジェクトを読み込ませると、「① → ② → ③-1 → ③-2」 の順に実行されます。

![プラグインの実行順](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/17931/bd46e46f-5a9a-27f1-fed1-20e85b0a8bd8.png)

この build.gradle.kts を読み込ませたときのログは以下のようになります。

```
...
> Configure project :sample:android
---- B
---- C
---- H
---- I
---- J
---- A
---- D
---- E
---- F
---- G
...
```

# 参考

Composite Build を用いて設定を共通化する実装は Now in Android が参考となります。

* https://github.com/android/nowinandroid/blob/fe4e8ec7c1b4438cda8d3cd7962ebc7b407f2263/build-logic/