TECH-MICCHON.jar

Scalaを中心に技術的な話題を書きます。

sbt plugin の作り方 - 実際の plugin を用いて解説 -

概要

sbt plugin の作り方をこちら

github.com

のコードを使いながらざっくり解説していきます。

sbt plugin を作る

sbt plugin はいくつかの Key に対してその振る舞いを記述していく、というスタイルで実装していきます。 振る舞いとは、例えば

  • Groovy など ScalaJava 以外のソースをコンパイルする
  • プロジェクトを jar に固めてデプロイする

などがあると思います。

sbt plugin はおおまかに分けて

  • Key
  • Task
  • projectSettings

という三つの要素があり、plugin 開発者はそれぞれを実装していく必要があります。

そのテンプレートは次のようになります。

import sbt._
import Keys._

object SimplePlugin extends AutoPlugin {

  object autoImport {
    /** implement your keys */
    ???
  }

  import autoImport._

  override def trigger: PluginTrigger = allRequirements

  override val projectSettings = ???

  lazy val yourTask = Def.task { 
    /** Implement your task(s) */
    ??? 
  }
}

Key

Key には以下の三種類があります。

  • settingKey: 属性、可変の値
  • taskKey: 実際の振る舞い
  • inputKey: taskKey でコマンドライン引数を受け取りたい場合の振る舞い

今回は settingKeytaskKey を使いました。

実際に定義したいのは次の4つです。

  • jflex ファイル *.flex の場所
  • 生成するファイル *.scala の場所
  • 実際に生成する振る舞い
  • その他設定

しがたって、以下のように実装しました。

 object autoImport {
    lazy val jflexSourceDirectory = settingKey[File]("jflex-source-directory")
    lazy val jflexOutputDirectory = settingKey[File]("jflex-output-directory")
    lazy val toolConfiguration = settingKey[JFlexToolConfiguration]("jflex-tool-configuration")
    lazy val pluginConfiguration = settingKey[PluginConfiguration]("jflex-plugin-configuration")
    lazy val jflexGenerateWithCompille = settingKey[Boolean]("jflex-with-compile")
    lazy val jflexSources = taskKey[Seq[File]]("jflex-sources")
    lazy val jflexGenerate = taskKey[Unit]("jflex-generate")
  }

Task

Key を定義したら次は Task を定義します。Task とは「実際の動作」であり、jflex-scala-plugin では

  • jflex API を叩いて *.flex ファイルを生成する

という振る舞いが想定されます。したがって、次のように実装しました。

lazy val jflexGeneratorTask: Def.Initialize[Task[Unit]] = Def.task {
    generateWithJFlex(
      jflexSources.value,
      jflexOutputDirectory.value,
      toolConfiguration.value,
      pluginConfiguration.value,
      streams.value.log
    )
  }

  private[this] def generateWithJFlex(
    srcDir: Seq[File],
    target: File,
    tool: JFlexToolConfiguration,
    options: PluginConfiguration,
    log: Logger
  ): Unit = {
    target.mkdirs()

    log.info(s"JFlex: Using JFlex version ${Main.version} to generate source files.")
    Options.dot = tool.dot
    Options.verbose = tool.verbose
    Options.dump = tool.dump
    Options.setDir(target.getPath)
    Options.emitScala = tool.emitScala

    val grammars = (srcDir ** ("*" + options.grammarSuffix)).get
    log.info(s"JFlex: Generating source files for ${grammars.size} grammars.")

    grammars.foreach { g =>
      Main.generate(g)
      log.info(s"JFlex: Grammar file ${g.getPath} detected.")
    }
  }

projectSettings

ここには、デフォルトの挙動(初期値)を記述していきます。

sbt-jflex-scala の場合、次のような初期値を設定しました。

  • *.flex ファイルはデフォルトで src/main/flex にする
  • 生成される *.scala ファイルはデフォルトで src/main/scala/flex にする
  • コンパイル時の自動生成はデフォルトではオフ

したがって、次のような実装になりました。

  override val projectSettings: Seq[Setting[_]] = Seq(
    jflexSourceDirectory := sourceDirectory.value / "main" / "flex",
    jflexOutputDirectory := sourceDirectory.value / "main" / "scala" / "flex",
    toolConfiguration := JFlexToolConfiguration(),
    pluginConfiguration := PluginConfiguration(),
    jflexSources := (jflexSourceDirectory.value ** "*.flex").get,
    jflexGenerate := jflexGeneratorTask.value,
    unmanagedSourceDirectories in Compile += jflexSourceDirectory.value,
    jflexGenerateWithCompile := false
  ) ++ Seq(
    compile := {
      if (jflexGenerateWithCompile.value)
        (compile in Compile).dependsOn(jflexGenerate).value
      else
        (compile in Compile).value
      }
  )

これで、sbt plugin のパーツが整ったので、あとはテンプレート部分を埋めるだけです。

全体像はこちらを参照してください。

参考