Gradleなんとなくわかってきた(その2)

ant.importBuildだけじゃ何のヒネリも無いので,antの時の構成そのままにビルドスクリプトだけgradleに変えてみた。「antの構成そのまま」ってのがポイントで,

ってのを確認したかったのが目的。今回からは「Gradle User Guide」を片時も手放せませんでした。翻訳して頂いた皆さんには頭が上がりません。多謝多謝。
あと細かい話になると,これも欠かせないです。⇒ Gradle DSL Version 5.3.1


できあがった build.gradle 全部を載せるには長いので,完全版はこちらを参照のこと。あとは,いろいろ個別に解説を進めるよ。
ant-sample-project/build.gradle at 6d009fccf9c7c3afd8cbd850d66858f4880a0933 · masanobuimai/ant-sample-project · GitHub


プロジェクトのレイアウトについては,先のチェンジセットの build.gradle から辿ってもらうか,この辺参照してちょうだい(カバレッジ測定がJaCoCoになってるだけで,それ以外はビルド職人本のサンプルと同じ。
2012-04-07 - marsのメモ

プラグインの指定

antからの移行だからといって,いちいちタスクを書いてちゃ堪らんので,そこは gradle のプラグインを利用させて貰った。ひとつひとつ apply plugin: 'ほにゃらら' とするのがうざいなぁと思ったら,下記のような短縮記法もOKとのこと。

apply {
  plugin 'java'
  plugin 'war'
  plugin 'jetty'
}

// checkstyleとfindbugsの設定
apply {
  plugin 'checkstyle'
  plugin 'findbugs'
}

あと上記のように複数に分けても記述できるので,近しいプラグインに分類して記述できるのが便利。

Javaコンパイルやwarファイル作成まわり

まずはソースパスをgradle標準(src/main/java や src/test/java)を用いないので,その定義を行う。ただ,これ厳密には「以下のソースパスも追加する」って意味だから,ソースパスの参照するときにちょっと面倒な思いをする(手ぇ抜いたけど。

// Gralde標準外のソースパスを追加
sourceSets {
  main {
    java { srcDir 'src/java' }
  }
  test {
    java { srcDir 'src/test' }
  }
}

// Webリソースの場所(標準だと src/main/webapp)
webAppDirName = 'web'

それと依存ライブラリの指定。これは "Gradle User Guide" にも載っていたので,すぐ解決。

// 依存ライブラリはローカルにあるJarファイルを直接参照
dependencies {
  providedRuntime fileTree(dir: 'lib/provided', include: '*.jar')
  compile fileTree(dir: 'lib/compile', include: '*.jar')
  testCompile fileTree(dir: 'lib/test', include: '*.jar')
}

fileTree()って関数は gradle が提供しているモノらしいのだけど,ディレクトリツリーを再帰しないで,指定したディレクトリ直下だけ対象にしたいとかどうすんだろ?と思った。よく考えたら,

  compile fileTree(dir: 'lib/compile', include: '*.jar')

(↑)この指定で目的を達してるんじゃないかな?再帰的に見たい場合は,(↓)こうするんじゃないかと(未確認。

  compile fileTree(dir: 'lib/compile', include: '**/*.jar')

あとオプションの指定がわかんなくて悩んだけど,ググって解決(申し訳ないこどに,参照先は失念してしまいました。

// javacのオプションに encoding=UTF-8 を追加する
[compileJava, compileTestJava]*.options*.encoding = 'UTF-8'

この書き方に分かるまで大分悩まされたけど,わかると便利(といっても,ちゃんと理解できてない。:-P


この辺のオプション指定の大らかさというか分かりづらさは,ant転校組の頭を悩ませるかも知んない(いや,gradleそのものの分かりづらさかも...)。といっても慣れりゃいい話なので,gradleダメ言うつもりは無いですよ。
プラグインの設定やらなにやらでハマったら,まずは「Gradle User Guide」の該当プラグインの説明を確認。それでもわかんなかったら「Gradle DSL Reference」を確認。でダメならググって見つからなかったら諦めるw


ちなみにJavaコンパイルまわりだと参照すべきページは,このあたり。

CheckstyleFindbugs

gradle標準では他にもpmdとかjdependsとかのプラグインが用意されているのだけど,元にしてるプロジェクトがcheckstylefindbugsしか準備してないので,この2つだけ。
checkstylefindbugsもプロジェクト内にダウンロード済みなので,mavenリポジトリから取ってこないで,それを使うようにして欲しい。その設定,どうやるのかと思ったら,意外と簡単で次のように dependencies を定義すりゃいいだけだった。

// mavenリポジトリからツールを取得せず,すでにあるツールを利用する
dependencies {
  checkstyle fileTree(dir: 'tool/checkstyle-5.1/', include: '*.jar')
  findbugs fileTree(dir: 'tool/findbugs-1.3.9/lib/', include: '*.jar')
}

checkstyleだけルールファイルの指定が必要で,デフォルトは 'config/checkstyle/checkstyle.xml' を見に行くらしい。これの変更にずいぶん悩んだが,できてみると大したことない。

// checkstyleの設定ファイルや実行時のオプションをそれぞれ設定する
checkstyleMain {
  configFile = file('tool/checkstyle-5.1/sun_checks.xml')
  ignoreFailures = true
}

「Gradle User Guide」や「Gradle DSL」には「設定ファイルは configFile に指定する」ってあるから,ずーっと「configFile」というプロパティに代入してだけど,その前にタスク名を付けてあげなきゃダメだった(や,わからんて)。
ignoreFailuresは「検査がコケても他のチェックを継続させるため」に設定しといた。デフォルトは false なので,どれかチェックでコケるとその場で終わってしまう(例:checkstylefindbugsの順にチェックするけど,checkstyleでコケるとか。


んで,タスクってのがcheckstylefindbugsそれぞれでプロダクトコードとテストコードの2つあるので,全部で4つ。それぞれにオプション指定するのもシンドイので,先のJavaオプション同様,次のような記述もできる。

// オプションを一括指定
[checkstyleMain, checkstyleTest]*.configFile = file('tool/checkstyle-5.1/sun_checks.xml')
[checkstyleMain, checkstyleTest, findbugsMain, findbugsTest, pmdMain, pmdTest]*.ignoreFailures = true

そういえば,この手の検査系はプロダクトコードにだけ施せば十分なので,テストコードには実施しないようにもしたいけど,それはまだやってない。なんとなく,checkタスクを上書きすりゃ出来んじゃないか?なんて安易に考えているけど,なんか浅はかな予感もしている。:-)

カバレッジ測定を組み込む

カバレッジ測定にはJaCoCoを使ってます。「なぜにJaCoCo?」という問には「すでにJaCoCoを使ってたから」が答え。つまり深い意味は無い。
結論から言うと,gradleでJaCoCoは失敗してCoberturaに戻したんだけど,Coberturaの使い方は,また別のエントリで話すのであとでね。


で,JaCoCo。こいつの特徴は,カバレッジ測定準備(instrumentation)なしで,on-the-flyでカバレッジ計測ができることなんだけど,ビルド手順を事前定義しているgradleみたいなツールとは相性悪かった(上手く説明できないから察してw。
それでもググってみたら,使い方は見つかるんだから,ほんとつくづくネットは広大だわ
Gradle & JaCoCo · GitHub


このgistの感心な点は,gradleのjunit実行部分にJavaオプションとして,JaCoCoのエージェントを登録してること。あーなるほど,これならJaCoCoのantタスク使わなくていいんだなと惚れ惚れ。でも実際に動かしてみたところ,ちゃんと証跡も取れてるけどカバレッジレポートは網羅率0%のままだった。なんでだろ?


ちょっと悔しかったんで,JaCocOのantタスクを build.gradle に展開してみた。結果は変わらずだったけど,build.gradle にantタスクを記述するややこしさも分かったので,そっちのために載せておく。サンプル見る前にポイントを言うけど,気にすることは,この2つくらい。

  • gradle標準のantタスクでは junitタスクが無い(別途,ant-junit.jarとjunitタスクの宣言が必要。
  • antタスク内で gradleが提供するファイル操作関数はたぶん使えない(未確認。

そんでもって,JaCoCoのサンプル(JaCoCoの特性上,gradleのtestタスクを「上書き」してます。

// カバレッジ測定(jacoco)の設定するために,testタスクを上書きする
task test(overwrite: true) << {
  // なんか納得いかないけど,ここでディレクトリ掘らないと怒られる
  sourceSets.test.runtimeClasspath.each {
    if (!it.exists()) ant.mkdir dir: it.absolutePath
  }
  def jacoco_home = 'tool/jacoco-0.5.6'
  ant {  // -> ここからantのターン
    delete file: 'jacoco.exec'  // -> これ gradle の delete じゃなくて ant の delete
    mkdir dir: testResultsDir
    // junitタスクを宣言する(ant-junit.jarも追加しとく)
    taskdef(name: 'junit', classname: 'org.apache.tools.ant.taskdefs.optional.junit.JUnitTask') {
      classpath path: 'tool/ant-junit.jar'
    }
    taskdef(resource: 'org/jacoco/ant/antlib.xml') {
      classpath path: "${jacoco_home}/lib/jacocoant.jar"
    }
    coverage {
      junit(fork: 'on', printsummary: 'on', maxmemory: '256m') {
        sourceSets.test.runtimeClasspath.each {
          classpath location: it.absolutePath
        }
        formatter type: 'xml'
        batchtest(todir: testResultsDir) {
          fileset(dir: sourceSets.test.output.classesDir, includes: '**/*Test.class')
        }
      }
    }

    // カバレッジレポートの作成までやっちゃう
    mkdir dir: "${buildDir}/reports/coverage"
    report {
      executiondata { file: "jacoco.exec" }
      structure(name: archivesBaseName) {
        // classfiles { fileset(dir: sourceSets.main.output.classesDir) } 書くのが行儀よさげだけど、
        classfiles { fileset(dir: "${buildDir}/classes/main") }
        // ...で十分かと。

        // sourcefiles(encoding: 'UTF-8') { fileset(dir: sourceSets.main.java.srcDirs.toArray()[0]) } と書きたいところだけど、
        sourcefiles(encoding: 'UTF-8') { fileset(dir: 'src/java') }
        // ...で妥協する
      }
      xml destfile: "${buildDir}/reports/coverage/jacoco.xml"
      html destdir: "${buildDir}/reports/coverage"
    }
  }
}

どうゆう理屈なのかわからないけど,gradle test とすると,antタスク内で参照している sourceSet のディレクトリが存在しないといってエラーになるため,testタスクの最初に必要そうなディレクトリを作成するようにしといた。でも,こいつらって testタスクが依存している compileタスクとかで作成するんだけどな...。よくわかりません。><


既存のタスクを上書きするので「task test(overwrite: true) << {」としてる。実は gradle のタスクの宣言方法をちゃんと理解してない。:-)
何種類か記述の仕方があるんだけど、まだなんとなーくしかわかってなくて、とりあえずタスクの依存関係やら説明文やら記述するのは、別途次のような定義が必要だった。

// testタスクの依存関係とかを定義する(何でか task test 〜 << {' ではできなかった。
test {
  dependsOn classes, testClasses
  description = 'Runs the unit tests.'
  group = 'Verification'
}

まとめ

gradleのルールに従わない独自ルールにも融通が利くあたりがmavenより好印象。あと,既存のタスクに対して doFirstやらdoLast を使ってプロジェクト個別のルールや手順を差し込めるってのが良い。
ただ暗黙的に定義されるプロパティやらタスクやらが多いので,慣れるまではリファレンスをみながらじゃ無いと何していいかさっぱりわかんない。あと,表記の自由度もわけわかんなさを加速させてるけど,どれも慣れの問題なのよね。


次は gradle の機能フル活用したケースを書く予定。