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.
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 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.
Starting in sbt 0.13.5, all the default settings are now provided through three auto plugins:
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:
In the past, including a plugin into any build was a two part process:
project/plugins.sbt
file.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.
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.
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:
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.
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.
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!