search term:

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

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",
    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

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

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"
  )

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.