sbt-sidedish を使ったアプリのダウンロードと実行

sbt プラグインから JAR をダウンロードしてそれを実行したいという要望が出てきてる。
最近だと Brooklyn での nescala で Shane Delmore (@shanedelmore) さんに聞かれた。

アンカンファレンスのセッションでデモっぽいものをやっつけで作ったけども、家に帰ってからも色々いじったのでここに報告する。

sbt-sidedish

sbt-sidedish はアプリをサイドメニュー的に落としてきて実行するためのプラグイン作者のためのツールキットだ。それそのものは特にプラグインを定義しない。

rewritedemo、コマンドラインアプリ

サイドで走らせたいアプリを作る。これは Scala 2.11 や 2.12 を使ってもいい。
Scalafix を使って import 文を追加するデモアプリを書いた。Scalafix は Scala コードの書き換えツールとライブラリで scala.meta を使っている。詳細は Scalafix のドキュメンテーションとソースを参照。

sbt-rewritedemo、sbt プラグイン

次に、rewritedemo アプリをあるサブプロジェクト相手に実行して別のサブプロジェクトを導出したいとする。
sbt-sidedish を使って以下のようなプラグインが書ける。

package sbtrewritedemo
 
import sbt._
import Keys._
import sbtsidedish.Sidedish
 
object RewriteDemoPlugin extends AutoPlugin {
  override def requires = sbt.plugins.JvmPlugin
 
  object autoImport extends RewriteDemoKeys
  import autoImport._
 
  val sidedish = Sidedish("sbtrewritedemo-metatool",
    file("sbtrewritedemo-metatool"),
    // scalaVersion
    "2.12.1",
    // ModuleID of your app
    List("com.eed3si9n" %% "rewritedemo" % "0.1.2"),
    // main class
    "sbtrewritedemo.RewriteApp")
 
  override def extraProjects: Seq[Project] =
    List(sidedish.project
      // extra settings
      .settings(
        // Resolve the app from sbt community repo.
        resolvers += Resolver.bintrayIvyRepo("sbt", "sbt-plugin-releases")
      ))
 
  override def projectSettings = Seq(
    rewritedemoOrigin := "example",
    sourceGenerators in Compile +=
      Def.sequential(
        Def.taskDyn {
          val example = LocalProject(rewritedemoOrigin.value)
          val workingDir = baseDirectory.value
          val out = (sourceManaged in Compile).value / "rewritedemo"
          Def.taskDyn {
            val srcDirs = (sourceDirectories in (example, Compile)).value
            val srcs = (sources in (example, Compile)).value
            val cp = (fullClasspath in (example, Compile)).value
            val jvmOptions = List("-Dscalameta.sourcepath=" + "\"" + srcDirs.mkString(java.io.File.pathSeparator) + "\"",
              "-Dscalameta.classpath=" + "\"" + cp.mkString(java.io.File.pathSeparator)+ "\"",
              "-Drewrite.out=" + out)
            Def.task {
              sidedish.forkRunTask(workingDir, jvmOptions = jvmOptions, args = Nil).value
            }
          }
        },
        Def.task {
          val out = (sourceManaged in Compile).value / "rewritedemo"
          (out ** "*.scala").get
        }
      ).taskValue
  )
}
 
trait RewriteDemoKeys {
  val rewritedemoOrigin = settingKey[String]("")
}
 
object RewriteDemoKeys extends RewriteDemoKeys

sbt 0.13.13 で入ったシンセティック・サブプロジェクトという機能を使っている。

ユーザのビルドからアプリに引数を渡すのが結構面倒なことになっている。ダイナミック・タスクを入れ子にして元プロジェクトのソース・ディレクトリを JVM オプションとして渡している。

sbt-rewritedemo がどう使われるか

build.properties:

sbt.version=0.13.13

plugins.sbt:

addSbtPlugin("com.eed3si9n" % "sbt-rewritedemo" % "0.1.2")

build.sbt:

lazy val example = (project in file("example"))
  .settings(
    name := "example",
    scalaVersion := "2.12.1"
  )
 
lazy val derived1 = (project in file("derived1"))
  .enablePlugins(RewriteDemoPlugin)
  .settings(
    name := "derived1",
    rewritedemoOrigin := "example",
    scalaVersion := "2.12.1"
  )

ここで example/src/main/scala/Example.scala 以下に Example.scala というファイルがあるとする:

package foo
 
object Example extends App {
  println(Seq(1, 2, 3))
}

sbt シェルから derived1/compile を実行すると、Scala 2.12 で書かれた書き換えアプリを使って以下のファイルが managed source directory に生成される:

package foo
 
import scala.collection.immutable.Seq
object Example extends App {
  println(Seq(1, 2, 3))
}

言い換えると、sbt-sidedish を使って 2.12 アプリを sbt プラグインから実行できたことになる。