sbt のプラグイン作成 - 単純な Groovy スクリプトをコンパイル

sbt 0.11 プラグインの作成方法を簡単にご紹介します。
題材として、単純な Groovy スクリプトをコンパイルするプラグインを作成してみます。

ソースは http://github.com/fits/try_samples/tree/master/blog/20120304/

はじめに

Groovy スクリプトをコンパイルする必要最小限の処理を Java で実装すると以下のようになります。

GroovySimpleCompiler.java
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.tools.Compiler;

class GroovySimpleCompiler {
    public static void main(String[] args) {
        CompilerConfiguration conf = new CompilerConfiguration();
        //出力先ディレクトリを設定
        conf.setTargetDirectory("dest");

        Compiler compiler = new Compiler(conf);

        //実行時引数で指定した Groovy スクリプトファイルをコンパイル
        compiler.compile(args);
    }
}

今回はこの処理を sbt プラグイン化してみます。

プラグイン作成

基本的な sbt プラグインの作成手順は以下の通りです。

  1. ビルド定義ファイルで sbtPlugin に true を設定
  2. Plugin トレイトを extends した object を用意

sbt 0.11 には以下の 2通りのビルド定義ファイルが使えますが、今回は .sbt の方を使用します。

  • .sbt (DSL で定義)
  • .scalaScala で定義)

それでは、ビルド定義ファイル build.sbt を作成します。

sbtPlugin に true を設定して、ライブラリの依存設定に groovy-all を追加します。(Groovy は 1.8.6 を使っています)

値の設定は := を、ライブラリの依存設定追加には += を使います。

ビルド定義ファイル sbt_groovy_sample_plugin/build.sbt
sbtPlugin := true

name := "sbt-groovy-sample-plugin"

organization := "fits"

libraryDependencies += "org.codehaus.groovy" % "groovy-all" % "1.8.6"

次にプラグインの本体を実装します。

Plugin トレイトを extends した object を作成し、設定値に SettingKey をタスクに TaskKey を使い、Seq() で設定内容をまとめます。(下記サンプルの groovySettings)

設定値は静的な値を格納するためのものなので、Groovy スクリプトファイルの検索結果等を格納するには TaskKey を使います。(下記サンプルの groovySources)

SettingKey や TaskKey の第1引数にタスクの「ラベル」を、第2引数に「説明」を指定できます。(「ラベル」は sbt のコンソールで呼び出す際の名称となります)


また、今回の Groovy コンパイル処理のように複数のキー(SettingKey や TaskKey)を参照するようなタスクは以下のようにして生成できます。

(キー1, キー2, ・・・) map {
    (変数1, 変数2, ・・・) => {
        処理内容
    }
}

この map は暗黙変換(t3ToTable3 等)によってタプルから変換された RichTaskables クラス *1 のメソッドです。


なお、下記の groovyCompileTask ではログ出力のために streams キーを使っています。

プラグインの実装 sbt_groovy_sample_plugin/GroovySamplePlugin.scala
package fits.sample

import sbt._
import Keys._

import org.codehaus.groovy.control.CompilerConfiguration
import org.codehaus.groovy.tools.Compiler

object GroovySamplePlugin extends Plugin {

    val groovySourceDirectory = SettingKey[File]("groovy-source-directory")
    val groovyOutputDirectory = SettingKey[File]("groovy-output-directory")
    val groovySources = TaskKey[Seq[File]]("groovy-sources")
    val groovyCompile = TaskKey[Unit]("groovy-compile", "Run Groovy compiler")

    val groovySettings = Seq(
        groovySourceDirectory <<= sourceDirectory(_ / "main" / "groovy"),
        groovyOutputDirectory <<= crossTarget / "groovy",
        groovySources <<= groovySourceDirectory map { dir =>
            //Groovy スクリプトファイルのパスを取得
            (dir ** "*.groovy").get
        },
        groovyCompile <<= groovyCompileTask
    )

    def groovyCompileTask = (groovySources, groovyOutputDirectory, streams) map {
        (src, destDir, s) => {

            val conf = new CompilerConfiguration()
            //出力先ディレクトリの設定
            conf.setTargetDirectory(destDir)

            val compiler = new Compiler(conf)

            //ログ出力
            s.log.info("src : " + src)

            //コンパイル実行
            compiler.compile(src.toArray)
        }
    }
}

処理内容は、src/main/groovy ディレクトリ内の .groovy ファイル(サブディレクトリも含む)をコンパイルして target/scala-2.9.1/groovy に出力します。

以上でプラグインの実装は完了です。


動作確認のため、ローカルリポジトリへプラグインを publish しておきます。*2

ローカルリポジトリへ publish
sbt_groovy_sample_plugin> sbt
・・・
> publish-local

動作確認

動作確認用に別プロジェクト(sbt-groovy-sample-test)を作成します。

まず、project/plugins.sbt ファイルを作成して addSbtPlugin() を記載します。

プラグイン利用定義ファイル sbt_groovy_sample_test/project/plugins.sbt
addSbtPlugin("fits" % "sbt-groovy-sample-plugin" % "0.1-SNAPSHOT")

次に、ビルド定義ファイルに "seq(プラグインクラス名.Settingsメンバー名: _*)" を追加します。

こうする事でプラグインの設定値やタスクがプロジェクトに追加され、sbt コンソールで groovy-compile 等が実行できるようになります。


以上でプラグインが利用できるようになりますが、このままでは groovy-compile タスクを手動で実行しないと Groovy スクリプトをコンパイルしないので使い勝手があまりよくありません。(JAR ファイルにも Groovy スクリプトのクラスファイルを格納しません)

そのため、もう少し使い勝手をよくするために以下のような設定を追加します。

  • compile を実行した際に groovy-compile も実行されるように groovyCompile タスクを compile の依存設定に追加
  • コンパイル後の Groovy スクリプトを JAR に格納するよう unmanagedResourceDirectories に groovyOutputDirectory を追加
ビルド定義ファイル sbt_groovy_sample_test/build.sbt
import Keys._

name := "sbt-groovy-sample-test"

organization := "fits"

//プラグインの設定値やタスクをプロジェクトに追加
seq(fits.sample.GroovySamplePlugin.groovySettings: _*)

//コンパイル時に Groovy スクリプトファイルをコンパイルするための設定
compile <<= (compile in Compile) dependsOn fits.sample.GroovySamplePlugin.groovyCompile

//コンパイル後の Groovy スクリプトを JAR ファイルに追加するための設定
unmanagedResourceDirectories in Compile <+= fits.sample.GroovySamplePlugin.groovyOutputDirectory

これでプラグインを利用するための準備は整いましたので、以下のような単純な Groovy スクリプトを用意し sbt コンソールで compile してみます。

Groovy スクリプト sbt_groovy_sample_test/src/main/groovy/sample.groovy
package fits.sample

(1..5).each {
    println "count : ${it}"
}
コンパイル
sbt_groovy_sample_test> sbt
・・・
> compile
[info] src : ArrayBuffer(・・・\sbt_groovy_sample_test\src\main\groovy\sample.groovy)
・・・
[success] Total time: 2 s, completed 2012/03/04 10:00:00

target/scala-2.9.1/groovy/fits/sample/ に Groovy スクリプトのクラスファイルが生成され、プラグインが有効に機能している事を確認できました。

次に package でパッケージ化してみます。

パッケージ化
・・・
> package
[info] Packaging ・・・\sbt_groovy_sample_test\target\scala-2.9.1\sbt-groovy-sample-test_2.9.1-0.1-SNAPSHOT.jar ...
[info] Done packaging.
[success] Total time: 0 s, completed 2012/03/04 10:15:00

sbt-groovy-sample-test_2.9.1-0.1-SNAPSHOT.jar ファイルが生成され、sample.class や sample$_run_closure1.class ファイルが格納されています。


最後に、tasks や inspect compile を実行すると以下のようにプラグインの設定が追加されている事を確認できます。

tasks や inspect compile の実行結果例
・・・
> tasks
  groovy-compile              Run Groovy compiler
  groovy-output-directory
  groovy-source-directory
  groovy-sources
・・・

> inspect compile
[info] Task: sbt.inc.Analysis
・・・
[info] Dependencies:
[info]  compile:compile
[info]  *:groovy-compile
・・・

*1:実際はタプルの要素数毎に用意されている RichTaskables のサブクラスに変換されます。例えば RichTaskable3 など

*2:この作業は特に必要ではありません。プラグインの JAR ファイルを project/lib に手動で配置しても可です