GeekFactory

int128.hatenablog.com

Gradleのマルチプロジェクト構成を運用してみた

Gradleでマルチプロジェクト構成を運用してみて、気付いた点を書いてみます。

以下の環境で確認しています。

マルチプロジェクト構成については下記が参考になります。

必要性

マルチプロジェクト構成は複数のコンポーネントや成果物を扱う必要がある場合に使います。いくつものEclipseプロジェクトをいじったりビルドスクリプトを整備するのはかなり面倒なので、可能な限りシングルプロジェクト構成をおすすめします。Gradleにはソースセットという概念があるので、うまく使い分けるとよいと思います。

コンポーネントをマルチプロジェクトで分離するかソースセットで分離するかは論点になると思います。今回は出荷の単位がコンポーネントなので、マルチプロジェクト構成を選択しています。もし出荷の単位を単一のWARファイルにする等であれば、ソースセットで分離した方が管理しやすいです。

レイアウトの選択

Gradleでは階層レイアウトとフラットレイアウトを選択できます*1。階層レイアウトは親プロジェクトの下に子プロジェクトを配置するのに対して、フラットレイアウトは両者を同じ階層に配置します。

階層レイアウト

  • /trunk
    • /buildroot (親プロジェクト)
      • /feature1
      • /feature2
      • /common

フラットレイアウト

  • /trunk
    • /buildroot (親プロジェクト)
    • /feature1
    • /feature2
    • /common

今回はフラットレイアウトを選択しました。Eclipse Subversiveを使う場合、階層レイアウトでは複数のプロジェクトを一括インポートできません。フラットレイアウトでは、チェックアウト元に /trunk を指定することで複数のプロジェクトを一括インポートできます。

Jenkinsでビルドを実行する場合は、階層レイアウトでもフラットレイアウトでも差はないと思います。

Gradle IDE

Gradle IDEはマルチプロジェクトに対応しており、親子関係を認識してくれます。以下がうまく動くことを確認しています:

  • 親プロジェクトの subprojects に書いた設定が子プロジェクトに引き継がれること。
  • プロジェクト間の依存関係がEclipseのクラスパスに反映されること。
  • プロジェクト間の依存関係がWTPのDeployment Assemblyに反映されること。

子プロジェクトでRefresh Dependenciesを実行すると、親プロジェクトを指す設定ファイル*2が作成されて親子関係を覚えるようです。また、親プロジェクトを閉じていて子プロジェクトだけ開いている場合は挙動が怪しいようです。

Eclipse設定の生成

Gradleのeclipseプラグインを使用し、eclipseタスクを実行することで .classpath や .project を自動生成できます。これはかなり便利で、プロジェクト数が多い場合には設定の手間を大きく削減できます。

クラスパス

ただし、そのままeclipseタスクを実行すると .classpath にJARファイルのパスが直接書き込まれてしまいます。Gradle IDEを使う場合はGradle IDEがクラスパスを管理してくれるのでJARファイルの参照は不要です。これを回避するために、build.gradleに以下を追加しています:

subprojects {
  apply plugin: 'eclipse'
  eclipse {
    classpath {
      containers = [
        'org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7',
        'org.springsource.ide.eclipse.gradle.classpathcontainer'  // Gradle IDEのクラスパスコンテナ
      ]
      file {
        // .classpathファイルの出力時にクラスパスからJARエントリを除外する
        whenMerged { classpath ->
          classpath.configure classpath.entries.grep { entry ->
            !(entry instanceof org.gradle.plugins.ide.eclipse.model.Library)
          }
        }
      }
    }
  }
}
JDT設定

eclipseプラグインが対応している設定ファイルは .project や .classpath の他に .settings/org.eclipse.jdt.core.prefs があります。このファイルにはコードフォーマットなどの設定が入っています。親プロジェクトの設定ファイルを子プロジェクトにも適用するようにすると、複数のプロジェクトでコードフォーマットなどを統一できて便利です。

こんな感じ:

subprojects {
  eclipse {
    jdt {
      file {
        withProperties { properties ->
          def rootProperties = new Properties()
          new File(rootProject.projectDir, '.settings/org.eclipse.jdt.core.prefs').withReader { rootProperties.load it }
          properties.putAll rootProperties.findAll { k, v -> k.startsWith 'org.eclipse.jdt.core.formatter' }
        }
      }
    }
  }
}

もし他の設定ファイルも子プロジェクトに適用したい場合はコピーしてあげるとよいでしょう。エンコーディングや改行コードは標準でサポートしてくれてもいいのになぁと思います。

こんな感じ:

subprojects {
  def eclipsePreferenceFiles = [
    'org.eclipse.jdt.ui.prefs',
    'org.eclipse.core.resources.prefs',
    'org.eclipse.core.runtime.prefs'
  ]

  task eclipsePreferences(type: Copy) {
    description 'copy Eclipse preferences from the root project'
    from eclipsePreferenceFiles.collect { new File(rootProject.projectDir, '.settings/' + it) }
    into new File(projectDir, '.settings')
  }
  tasks.eclipse.dependsOn eclipsePreferences

  task cleanEclipsePreferences(type: Delete) {
    description 'clean up Eclipse preferences'
    delete eclipsePreferenceFiles.collect { new File(projectDir, '.settings/' + it) }
  }
  tasks.cleanEclipse.dependsOn cleanEclipsePreferences
}

複数プロジェクトの集約

親プロジェクトから子プロジェクトのタスクに依存関係を張っておくと、Jenkinsから一括実行できたりして便利です。こういう感じで色んなタスクに適用できます:

task buildSubprojects {
  description 'build sub-projects'
  dependsOn subprojects*.tasks.build
}
JavaDoc

プロジェクト全体のJavaDocを生成するには、子プロジェクトのソースコードに対してJavadocタスクを実行します:

task javadocSubprojects(type: Javadoc) {
  description 'generates JavaDoc of sub-projects'
  source subprojects*.sourceSets.main.allJava
  destinationDir = new File(buildDir, 'javadoc')
  classpath = files(subprojects*.sourceSets.main.compileClasspath)
  options.encoding = 'UTF-8'
}
テスト

プロジェクト全体のテスト結果を生成するのは簡単にはいきませんでした。Gradleの内部APIを探してみるとそれらしいものを見つけたので、以下のようにしています。内部APIなので今後のバージョンでは動かないかもしれません。

import org.gradle.api.internal.tasks.testing.junit.report.DefaultTestReport
task testSubprojects {
  description 'generates test report of sub-projects'
  dependsOn subprojects*.tasks.test
  doFirst {
    copy {
      from { subprojects.collect { project -> new File(project.buildDir, 'test-results') } }
      into { new File(buildDir, 'test-results') }
    }
    new DefaultTestReport(
      testReportDir: new File(buildDir, 'reports/tests'),
      testResultsDir: new File(buildDir, 'test-results')).generateReport()
  }
}