Creating SBT plugins

Summary

How to create a minimal working SBT plugin. There is an example repository on Github here.

Create a new Scala project

$ sbt new scala/scala-seed.g8

Pick a name that starts with sbt-, following the plugin author best practices.

Modify build.sbt

Caution

Remove the scalaVersion from build.sbt. In this issue of SBT the authors explain that there is no version of SBT for Scala 2.13. However, since that is the current stable version of Scala, the scala-seed project template selects it by default, leading to a compilation error.

The only other change to build.sbt is to add a line enablePlugins to the main project. Enabling the SbtPlugin is what marks this project as an SBT plugin at all.

lazy val root = (project in file("."))
  .enablePlugins(SbtPlugin)
  .settings(
    name := "sbt-hello"
  )

Extend AutoPlugin

I am not sure whether there is another interface to create SBT plugins but this one is convenient: create an object that extends AutoPlugin.

Inside that object, the most important things that you should define are a Task for your plugin, and whatever Settings it takes to get configured. There is some discussion in the documentation about the differences between a Task and a Command, but the quick explanation is that the Task is whatever command the user of your plugin is going to execute on the SBT shell, while a Setting is something to be customized in the user’s build.sbt.

There are functions called taskKey[T] and settingKey[T] that allow you to produce the types TaskKey and SettingKey (note the different capitalization).

A simple example taken from the reference documentation is this plugin:

object SbtHelloPlugin extends AutoPlugin {
  object autoImport {
    val greeting = settingKey[String]("greeting")
    val hello = taskKey[Unit]("say hello")
  }
  import autoImport._
  override def trigger = allRequirements
  override lazy val buildSettings =
    Seq(greeting := "Hi!", hello := helloTask.value)
  lazy val helloTask =
    Def.task {
      println(greeting.value)
    }
}

where greeting is a value that can be overridden by users in their build.sbt, and hello is the command name they can use in the SBT shell to perform what the plugin has to offer. The return type of that task is Unit because it just prints a string to the console, and the type of the setting is a String because that is what gets printed.

Usually, you would create another object, in the same package or even in the same file, that implements an apply method that is really doing the work you intend to do. That is what you would have to call in the Def.task part of the Plugin object.

object Hello {
  def apply(): Unit = println("Hi!")
}
object SbtHelloPlugin {
  // ...
  lazy val helloTask =
    Def.task {
      Hello()
    }
}