downloading and running app on the side with sbt-sidedish


I've been asked by a few people on downloading JARs, and then running them from an sbt plugin.
Most recently, Shane Delmore (@shanedelmore) asked me about this at nescala in Brooklyn.

During an unconference session I hacked together a demo, and I continued some more after I came home.


sbt-sidedish is a toolkit for plugin authors to download and run an app on the side from a plugin.
It on its own does not define any plugins.

rewritedemo, a commandline app

First, create a command line app that you want to run on the side. This could be in Scala 2.11 or 2.12.
Here's a demo app I wrote that uses Scalafix to add an import statement to some code. Scalafix is a Scala code rewriting tool and library that uses scala.meta. See Scalafix docs and sources for the details on that.

sbt-rewritedemo, an sbt plugin

Next, suppose you want to run rewritedemo against some subproject in your build and derive another subproject.
Here's a plugin you can write using 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",
    // scalaVersion
    // ModuleID of your app
    List("com.eed3si9n" %% "rewritedemo" % "0.1.2"),
    // main class
  override def extraProjects: Seq[Project] =
      // extra 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.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( + "\"",
              "-Dscalameta.classpath=" + "\"" + cp.mkString( "\"",
              "-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
trait RewriteDemoKeys {
  val rewritedemoOrigin = settingKey[String]("")
object RewriteDemoKeys extends RewriteDemoKeys

This uses synthetic subproject feature that was added in sbt 0.13.13.

The tricky part is passing the right arguments to the app from the user's build. I had to double wrap dynamic tasks to refer to the source directories from the origin and pass it in as JVM options.

how sbt-rewritedemo gets used



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


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

Now suppose we have Example.scala under example/src/main/scala/Example.scala:

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

When you run derived1/compile from the sbt shell, it runs the rewrite app using Scala 2.12 and generates the following file under the managed source directory:

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

In other words, using sbt-sidedish we could run 2.12 app from an sbt plugin.