Gradle で Single-Project から Multi-Project への変換

適当に single project で始めた後に事業拡張等で複数のアプリケーションが必要になったり共通化したりしたくなった場合にどのような作業を行うといいかというの検討します。

バージョン情報

  • Gradle 6.5

Gradle で Single-Project から Multi-Project への変換

Gradle での Multi Project とは?

gradle がそのプロジェクトが single か multi かを判別するのは ビルドライフサイクルでの3つあるフェーズのうちの initialize phase のときです1

そしてこれは settings.gradle(.kts) ファイルを見つけ出し、 include で自身以外のプロジェクトが追加されているかどうかで決まります。 Multi Project に含まれるプロジェクトは Root Project や Sub Project と呼ばれます2

Single Project というのはつまり Multi Project における root project をそのまま利用しているという状況なので、これを include で指定した場所にそのまま移動させて sub project として追加できると良さそうです。

Single-Project の開発

例として以下のようなプロジェクトを作成します。build script を Kotlin DSL で記述してありますがここで行う作業では Groovy DSL でも大きくは変わりません。

❯ gradle init --type basic --dsl kotlin --project-name single-to-multi-project

> Task :init
Get more help with your project: https://guides.gradle.org/creating-new-gradle-builds

BUILD SUCCESSFUL in 517ms
2 actionable tasks: 2 executed

以下のコマンドでプロジェクトが root project のみであることが確認できます。

❯ ./gradlew projects

> Task :projects

------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'single-to-multi-project'
No sub-projects

To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :tasks

BUILD SUCCESSFUL in 458ms
1 actionable task: 1 executed

ある程度開発したとして build.grad.kts を以下のようなものになったとします。

plugins {
  java
  id("org.springframework.boot")
  id("io.spring.dependency-management")
}

repositories {
    mavenCentral()
}

dependencies {
  implementation("org.springframework.boot:spring-boot-starter-web")
}

settings.gradle.kts

pluginManagement {
    repositories {
        gradlePluginPortal()
    }

    plugins {
        id("org.springframework.boot") version "2.3.0.RELEASE"
        id("io.spring.dependency-management") version "1.0.9.RELEASE"
    }
}

rootProject.name = "single-to-multi-project"

Single-Project から Multi-Project へ

以下のようにして sub project を作ってみます。 このとき、実体となる build script はこの時点では必要ありません。

❯ echo 'include("single-to-multi-project")' >> settings.gradle.kts 
❯ ./gradlew projects                                    

> Task :projects

------------------------------------------------------------
Root project
------------------------------------------------------------

Root project 'single-to-multi-project'
\--- Project ':single-to-multi-project'

To see a list of the tasks of a project, run gradlew <project-path>:tasks
For example, try running gradlew :single-to-multi-project:tasks

BUILD SUCCESSFUL in 1s
1 actionable task: 1 executed

ちなみにこの時点での構成は下記であるとします。

❯ tree
.
├── build.gradle.kts
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
    └── main
        └── java
            └── com
                └── example
                    └── ExampleApplication.java

このとき、gradle build を実行すると build/libs 以下に single-to-multi-project.jar という jar が生成されます。

❯ ./gradlew build

BUILD SUCCESSFUL in 769ms
2 actionable tasks: 2 executed

❯ tree build
 build
 ├── classes
 │   └── java
 │       └── main
 │           └── com
 │               └── example
 │                   └── ExampleApplication.class
 ├── generated
 │   └── sources
 │       ├── annotationProcessor
 │       │   └── java
 │       │       └── main
 │       └── headers
 │           └── java
 │               └── main
 ├── libs
 │   └── single-to-multi-project.jar
 └── tmp
     ├── bootJar
     │   └── MANIFEST.MF
     └── compileJava
         └── source-classes-mapping.txt

さてここで本題となる、sub project に中身も移動して multi project を完成させましょう。

❯ mkdir single-to-multi-project
❯ mv build.gradle.kts single-to-multi-project
❯ tree
.
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── single-to-multi-project
    ├── build.gradle.kts
    └── src
        └── main
            └── java
                └── com
                    └── example
                        └── ExampleApplication.java

8 directories, 7 files

以下のコマンドでビルドしてみます。

いずれの場合であっても subproject 側の build ディレクトリに jar ファイルが生成されていると思います。

❯ ./gradlew build
or
❯ ./gradlew :single-to-multi-project:build

❯ tree single-to-multi-project/build 
single-to-multi-project/build
├── classes
│   └── java
│       └── main
│           └── com
│               └── example
│                   └── ExampleApplication.class
├── generated
│   └── sources
│       ├── annotationProcessor
│       │   └── java
│       │       └── main
│       └── headers
│           └── java
│               └── main
├── libs
│   └── single-to-multi-project.jar
└── tmp
    ├── bootJar
    │   └── MANIFEST.MF
    └── compileJava
        └── source-classes-mapping.txt

ここまでで一旦 multi project に変換するまでを行いました。multi project の場合の共通処理の書き方やビルドパフォーマンスを上げる方法などいくつか話題はありますが次回以降の話題とします。

Single-Project から Multi-Project へ(別解)

ある project と他の project を関連させながら開発を行う方法として gradle の Composite Build3 という仕組みがあります。 これは厳密には Multi Project ではないため、Gradleドキュメント上で Multi Project と記載されているものと混同しないように注意してください。 この方法のメリットとしては、元のプロジェクトに全く手を加えることなく他のプロジェクトを作成し、依存として利用することができます。

さて、「Single-Project の開発」からの続きであるとして、その次のステップとして root project に相当する project を作成します。これは元のプロジェクトとは別のプロジェクトとして作成します。 注意点としては、このプロジェクトは 現在の gradle バージョンにおいては composite build の制限により同名のプロジェクト名を利用できません

❯ gradle init --type basic --dsl kotlin --project-name multi-project 

> Task :init
Get more help with your project: https://guides.gradle.org/creating-new-gradle-builds

BUILD SUCCESSFUL in 492ms
2 actionable tasks: 2 executed

他のプロジェクトと連携したい project を multi-project に追加します。

❯ echo 'includeBuild("single-to-multi-project")' >> settings.gradle.kts
❯ mv path/to/single-to-multi-project .

build.gradle.kts に以下のような記述を追加します(現在の gradle の制限により(まだ)直接コマンドラインからは起動できないため4 )。

tasks.register("build") {
    dependsOn(gradle.includedBuild("single-to-multi-project").task(":build"))
}
❯ ./gradlew build                                           

BUILD SUCCESSFUL in 1s

対象プロジェクトの jar ができていることが確認できます。

❯ tree single-to-multi-project/build/
single-to-multi-project/build/
├── classes
│   └── java
│       └── main
│           └── com
│               └── example
│                   └── ExampleApplication.class
├── generated
│   └── sources
│       ├── annotationProcessor
│       │   └── java
│       │       └── main
│       └── headers
│           └── java
│               └── main
├── libs
│   └── single-to-multi-project.jar
└── tmp
    ├── bootJar
    │   └── MANIFEST.MF
    └── compileJava
        └── source-classes-mapping.txt

ここまでで一旦 multi project に変換、 というより他のプロジェクトからビルドするまでを行いました。

このまま他のプロジェクトを追加する際の注意点などはいろいろありますが、構成や制限などがいろいろありややこしいため別途 composite build として話題とするか ドキュメント5 を参照してください。

以上です。