Monday 4 July 2016

SBT New Version: 1.0 Features

Introduction

In this post, we are going to discuss SBT New version 1.0 features. 

Most of the real-time projects are using SBT version 0.13.8. Lightbend is going to release SBT new version 1.0 with lot of new features and enhancements soon.

SBT New Version: 1.0 Features

SBT 1.0 is coming with lot of new features and massive enhancements to existing features

The two main new features are:

  1. Auto Plug-ins
  2. SBT as a build server

Java version

sbt 0.13 is on JDK 6. sbt 1.0 will be based on JDK 8.

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 sbthelloimport 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.

No comments:

Post a Comment