Gradleでマルチプロジェクトビルドする

マルチプロジェクトは個人的には大変なので余りやりたくないのだが、規模が大きくなると様々な理由で必要になってくる。Gradleにももちろんマルチプロジェクトをサポートする機能がある。今回はそれを紹介。

そもそもマルチプロジェクト化する理由って?

プロジェクトによって色んな理由があると思うが、うちだと以下のような理由がメインかな。

  • ビルドのルールを標準化したい
    • ビルド時のソースエンコードとかビルドターゲットのバージョンとかバラバラにならないように統一したい。
  • 依存ライブラリのバージョン定義を集約したい
    • 各プロジェクトで使っているOSSライブラリのバージョンがバラバラ...なんてことを避けたい。
  • ビルドスクリプトを簡略化したい
    • 同じような処理を各プロジェクトで記述するのは無駄だし、メンテナンスも大変。どこかに共通化して定義して、各プロジェクトのビルドスクリプトはすっきりさせたい。
  • サブプロジェクトも含めた統合ビルドを簡単に行いたい
    • 各プロジェクトを統合して1つのパッケージにするような時に、まとめて一度にビルドできるようにしたい。

マルチプロジェクトの構造

マルチプロジェクトの構造には大きく階層構造とフラット構造がある。

  • 階層構造は、親のプロジェクトがルートにあり、その下にサブディレクトリで各プロジェクトがあるようなケース。
  • フラット構造は、親が子プロジェクトをネストするのではなくて親も子も並列にあるようなケース。

Mavenもそうだが、Gradleも両方に対応している。

階層構造のプロジェクトの場合

今回は例として以下のような階層構造のプロジェクトの場合を紹介。良くありそうなJavaのWebアプリを想定して、

  • ルートプロジェクト(hierarchical-sample)
  • JARを作成するservice、shareプロジェクト
    • serviceはshareに依存している
  • WARを作成するwebプロジェクト
    • webはservice、shareに依存しており、WARにパッケージングするときもWEB-INF/libに格納する

という、ルートプロジェクト以下に3つのサブプロジェクトがあるケースで紹介する。ディレクトリ構成は以下のようになる。

hierarchical-sample
|-- build.gradle
|-- settings.gradle
|-- service
|   `-- src
|       `-- main
|           `-- java
|               `-- ...
|-- share
|   `-- src
|       `-- main
|           `-- java
|               `-- ...
`-- web
    `-- src
        `-- main
            |-- java
            |   `-- ...
            `-- webapp
                `-- WEB-INF
                    `-- web.xml
settings.gradleを定義する

Gracleでマルチプロジェクトを行うには、ルートプロジェクトにsettings.gradleを配置する。このファイルで以下のようにサブプロジェクトのディレクトリ名を記述する。これでGradleはサブディレクトリをサブプロジェクトとして認識する。Mavenな人には、親POMでmoduleを定義するような物と思ってもらえればいいです。

include 'web', 'service', 'share'
build.gradleの定義

ルートプロジェクトのbuild.gradleを定義するわけだが、今回はサブプロジェクトには一切ビルドスクリプトを置かないという方法をしてみる(もちろん、各サブプロジェクトにbuild.gradleを置くこともできる)。Mavenだとサブプロジェクトにpom.xmlを必ず置く必要があるが、Gradleだとルートプロジェクトでまとめて定義することもできる。

今回は以下のように定義してみた。

subprojects {
    repositories {
        mavenCentral()
    }
}

project(':service') {
    apply plugin: 'java'
    
    dependencies {
        compile project(':share')
    }
}

project(':share') {
    apply plugin: 'java'
}

project(':web') {
    apply plugin: 'war'
    
    dependencies {
        providedCompile 'javax.servlet:servlet-api:2.5'
        compile project(':service')
        compile project(':share')
    }
}

特徴的なのは以下。

  • subprojects { ... }で、サブプロジェクト全てに対してあらゆる設定ができる。上記ではMavenリポジトリを参照するよう設定している。
  • project(...) { ... }で、ルートプロジェクトのビルドスクリプトから各プロジェクトの個別設定を記述できる。
  • dependenciesでcompile project(':share')のように記述することで、プロジェクト間の依存関係を定義できる。別のプロジェクトで作成されるJARをクラスパスに登録したいとか、WARファイルの中に入れたいとか、そういう時に使う。
ビルドしてみる

ルートプロジェクトの位置からまずはビルドしてみる。タスクにはcompileJavaを指定しコンパイルまで実行してみる。なお、--daemonオプションをつけると次回のGradle起動が高速化されるのでオススメ(エイリアスとして登録すると良い)。

# cd hierarchical-sample
# gradle --daemon compileJava
:share:compileJava
:share:processResources UP-TO-DATE
:share:classes
:share:jar
:service:compileJava
:service:processResources UP-TO-DATE
:service:classes
:service:jar
:web:compileJava

BUILD SUCCESSFUL

Total time: 1.177 secs

ちゃんとプロジェクト間の依存関係を解釈して、share -> service -> webの順にコンパイルしてくれます。
ここまではMavenのマルチプロジェクト機能でも同じだが、次にGradleならではの機能を紹介。

サブプロジェクトからビルドする

サブプロジェクトにcdで移動し、そこから実行してみる。serviceに移動し、またcompileJavaを実行すると...

# cd service
# gradle --daemon compileJava
:share:compileJava UP-TO-DATE
:share:processResources UP-TO-DATE
:share:classes UP-TO-DATE
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.926 secs

このように、serviceはshareに依存しているので、まずはshareをコンパイルしつつserviceをコンパイルしてくれる。Mavenだとこうはいかない。良くも悪くもPOMでの依存関係が徹底されているので、ローカルのMavenリポジトリに依存プロジェクトであるserviceのJARをまずinstallしてからでないと依存関係が解決できないのだ。

ちなみに、UP-TO-DATEというのは、実際は変更がないため何もコンパイルなど処理されていないことを表している。例えば、shareプロジェクトのソースに修正があったとすると、

# cd service
# gradle --daemon compileJava
:share:compileJava
:share:processResources UP-TO-DATE
:share:classes
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.909 secs

のように、ちゃんとshareプロジェクトをコンパイルしてから処理してくれる。

サブプロジェクト一つだけビルドしたい!

shareプロジェクトは修正していないから、いちいち見に行かずにserviceプロジェクトだけコンパイルさせてよ!!という急いでいる人のためにも便利なオプションがある。-aオプションを付けて実行すると...

# cd service
# gradle --daemon -a compileJava
:service:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.848 secs

お望み通りserviceプロジェクトだけコンパイルしてくれる。

各プロジェクトにcdするの面倒なんだけど?

各プロジェクトのディレクトリに移動するのが面倒だ!!って人には以下のようなこともできるよ。:プロジェクト名:タスクで実はどこからでも特定プロジェクトに対してビルドできたりする。もちろん、ルートプロジェクトからもできるので、全く移動せずに特定のサブプロジェクトだけビルドとかできる。

# cd service
# gradle --daemon :web:compileJava
:share:compileJava UP-TO-DATE
:share:processResources UP-TO-DATE
:share:classes UP-TO-DATE
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE
:service:processResources UP-TO-DATE
:service:classes UP-TO-DATE
:service:jar UP-TO-DATE
:web:compileJava UP-TO-DATE

BUILD SUCCESSFUL

Total time: 0.941 secs
WARをビルドする

今回の例だとWebアプリの構成になっているので、最後にWARをビルドしてみる。ルートプロジェクトに戻り、warタスクを実行する。warタスクなんてものはWAR Pluginを利用しているwebプロジェクトでしか使えないのだが、ルートプロジェクトからの実行だとちゃんと認識して、依存関係も解決しつつwebプロジェクトのwarタスクをちゃんと実行してくれる。Gradle賢い!!

# cd hierarchical-sample
# gradle --daemon war
:share:compileJava UP-TO-DATE
:share:processResources UP-TO-DATE
:share:classes UP-TO-DATE
:share:jar UP-TO-DATE
:service:compileJava UP-TO-DATE
:service:processResources UP-TO-DATE
:service:classes UP-TO-DATE
:service:jar UP-TO-DATE
:web:compileJava UP-TO-DATE
:web:processResources UP-TO-DATE
:web:classes UP-TO-DATE
:web:war UP-TO-DATE

BUILD SUCCESSFUL

Total time: 1.009 secs

今回の紹介はこの辺で終了。フラット構造の場合とか、依存関係のまとめ方とかsettings.groovyによる動的なマルチプロジェクトとか書きたいことはあるけどまた今度。
なお、今回作成したサンプルは
https://github.com/wadahiro/gradle-samples/tree/master/multiproject-samples/hierarchical-sample
にあります。