Blog

March 28, 2014

Preview of upcoming sbt 1.0 features: Read about the new plugins

Times are exciting for sbt. With the current push towards 1.0, it will see massive improvements to promote our core tenets of automation, interaction and integration. The two big-hitter features for 1.0 are auto plugins and "sbt as a build server."

Over the coming months, the sbt team will be releasing previews of these features against the current sbt 0.13 codebase. Currently the latest preview is 0.13.5-M4. We hope to elicit feedback as well as promote the new designs, ideals and features before solidifying the sbt 1.0.

While it brings us much sadness to report the departure of Mark Harrah, who has moved on to do non-build related things, it bring us much joy to present

Eugene Yokota (@eed3si9n). Eugene has been involved with sbt since the sbt 0.7 days (see #38), and he is joining the build tools team at Typesafe with Antonio Cunei and Josh Suereth as one of the technology leads for sbt.  

Below is the description of the new auto plugin feature that we've been working on.

The plugin ecosystem

One of the most powerful aspects of sbt is its plugin ecosystem. Because plugins work exactly the same way as the build definitions, learning sbt translates smoothly into learning how to write plugins. The diversity of the sbt plugins are testament to the robustness of this basic concept. Among them the two that stand out are the Play Framework and Activator.  These are built on top of sbt to provide interactive development experience.

The sbt team is proud to announce the inclusion of auto plugins in sbt 0.13.5, which is a binary compatible technology preview of what's to come in sbt 1.0.

Auto Plugins

Auto plugins work just like the traditional sbt plugins: They can add new tasks, settings, and commands into a project's build definition.  The primary difference is that auto plugins are a bit more opinionated about the way tasks and settings should be loaded into a build than their predecessors, providing the features needed to give users full control over plugins as well as allowing plugin authors to provide new functionality in a seamless way.

Default settings are auto plugins

Starting in sbt 0.13.5, all the default settings are now provided through three auto plugins:

  • CorePlugin (introduces core sbt-isms)
  • IvyPlugin (for dependency management)
  • JvmPlugin (for compiling Scala/Java projects)

These represent the core layers of settings that sbt provides by default.   The new auto plugin feature allows users to directly control which of these layers is enabled.

Let's look more into the features of auto plugins:

projectSettings and buildSettings

In the past, including a plugin into any build was a two part process:

  1. Include the plugin in a project/plugins.sbt file.
  2. Add the settings for the plugin to your build.sbt or project/build.scala file.

With auto plugins, all provided settings (e.g. assemblySettings) are provided by the plugin directly via the projectSettings method. Here’s an example plugin that adds a command named hello to sbt projects:

package sbthello

import sbt._
import Keys._
object HelloPlugin extends AutoPlugin {
  override lazy val projectSettings = Seq(commands += helloCommand)
  lazy val helloCommand =
    Command.command("hello") { (state: State) =>
      println("Hi!")
      state
    }
}

If the plugin needs to append settings at the build-level (that is, in ThisBuild) there's a buildSettings method, and for global-level (in Global), there's globalSettings method.

This level of automation is convenient for plugins to automatically work in sbt, but doesn’t give the user control over how plugins are added.  For that, there’re a few new controls on projects.

enablePlugins

To activate the HelloPlugin, you would still need to declare dependency to sbt-hello in project/plugins.sbt as follows:

addSbtPlugin("com.example" % "sbt-hello" % "0.1.0")

Next, instead of appending the setting sequence in build.sbt, you can now call enablePlugins method on project in build.sbt:

lazy val root = project in file (".")

root.enablePlugins (HelloPlugin)

This will append HelloPlugin.projectSettings into the root project's setting sequence.

Plugin dependencies

When a traditional plugin wanted to reuse some functionality from an existing plugin, it would pull in the plugin as a library dependency, and then it would either (1) add the setting sequence from the dependency as part of its own setting sequence, or (2) tell the build users to include them in the right order. This becomes complicated as the number of plugins increase within an application, and becomes more error prone.

The main goal of auto plugin is to alleviate this setting dependency problem. An auto plugin can depend on other auto plugins and lensure these dependency settings are loaded first.

Suppose we have the SbtLessPlugin and the SbtCoffeeScriptPlugin, which in turn depends on the SbtJsTaskPlugin, SbtWebPlugin, and JvmPlugin. Instead of manually activating all of these plugins, a project can just activate the SbtLessPlugin and SbtCoffeeScriptPlugin like this:

lazy val root = project in file(".")
rootProject.addPlugins(SbtLessPlugin, SbtCoffeeScriptPlugin)

This will pull in the right setting sequence from the plugins in the right order.  The key notion here is you declare the plugins you want, and sbt can fill in the gaps.

The second piece of this is for auto plugins to define their setting dependencies.  Here’s how:

package sbtless

import sbt._
import Keys._

object SbtLessPlugin extends AutoPlugin {
  override def requires = SbtJsTaskPlugin
  override lazy val projectSettings = ...
}

The requires method returns a value of type Plugins, which is a DSL for constructing the dependency list.  The requires method typically contains one of the following values:

  • empty (No plugins, this is the default)
  • other auto plugins
  • && operator (for defining multiple dependencies)

Triggered plugins

Plugin dependencies solve much of the annoyance dealing with multiple inter-related plugins, but the build user still needs to manually include them into each project. Autoplugins also provide a way for plugins to automatically attach themselves to projects if their dependencies are met. This is achieved using the trigger method.

For example, we might want to create a triggered plugin that can append commands automatically to the build. To do this, set the requires method to return empty (this is the default), and override the  trigger method with allRequirements.

package sbthello

import sbt._
import Keys._

object HelloPlugin2 extends AutoPlugin {
  override def trigger = allRequirements
  override lazy val buildSettings = Seq(commands += helloCommand)
  lazy val helloCommand =
    Command.command("hello") { (state: State) =>
      println("Hi!")
      state
    }
}

The build user still needs to include this plugin in project/plugins.sbt, but it is no longer needed to be included in build.sbt. This becomes more interesting when you do specify a plugin with requirements. Let's modify the SbtLessPlugin so that it depends on another plugin:

package sbtless

import sbt._
import Keys._

object SbtLessPlugin extends AutoPlugin {
  override def trigger = allRequirements
  override def requires = SbtJsTaskPlugin
  override lazy val projectSettings = ...
}

As it turns out, PlayScala plugin (in case you didn't know, the Play framework is an sbt plugin) lists SbtJsTaskPlugin as one of it required plugins. So, if we define a build.sbt with:

plazy val root = project in file(".")

root.enablePlugins(PlayScala)

then the setting sequence from SbtLessPlugin will be automatically appended somewhere after the settings from PlayScala.

This allows plugins to silently, and correctly, extend existing plugins with more features.  It also can help remove the burden of ordering from the user, allowing the plugin authors greater freedom and power when providing feature for their users.

Controlling the import with autoImport

In addition to providing settings, another thing the traditional sbt.Plugin provided was a means of automatically adding methods, values and types into the build.sbt DSL.  By default, any member of an sbt.Plugin class was automatically imported.   This lead to possible conflicts and sbt-plugin conventions that promoted putting implementation details outside the sbt.Plugin instance itself.

Auto plugin corrects this by letting you be in charge of what names you want to expose to *.sbt files.  This is done by providing an autoImport member within the AutoPlugin instance.  Let’s see an example:

package sbthello

import sbt._
import Keys._

object HelloPlugin3 extends AutoPlugin {
  object autoImport {
    val greeting = settingKey[String]("greeting")
  }
  import autoImport._

  override def trigger = allRequirements
  override lazy val buildSettings = Seq(
    greeting := "Hi!"
    commands += helloCommand)

  lazy val helloCommand =
    Command.command("hello") { (state: State) =>
      println(greeting.value)
      state
    }
}

This hello plugin provides the greeting key directly in a build.sbt, allowing users to directly reference it without imports.  The build user can still get to your plugin object by typing the full path sbthello.HelloPlugin3.x. But by default, it will only wildcard import names under a field (val, lazy val, or object) named autoImport.

Summary

Auto plugins are a part of the next step in the evolution of sbt: better automation, interaction, and integration, and overall better user experience. These new plugins solve the issues with the previous plugin system and pave the way for better debugging and user control inside builds.

The sbt team hopes that this would further empower the sbt plugin ecosystem and related products like the Play Framework. We would like to know what you think. Please comment questions and ideas to sbt-dev list, and stay tuned!

comments powered by Disqus
Browse Recent Blog Posts