an unofficial guide to sbt 0.10 v2.0
version 2.0
When the original version was published on 06/19/2011, the motive for writing this guide was to aid the effort of moving people over to sbt 0.10 from 0.7, inspired by Mark’s sbt 0.10 demos that I was able to see live (first at northeast scala, and second at scala days 2011). At the time, the plugins were considered to be a major roadblock to the migration, since build users can’t move to 0.10 without the plugins. So my strategy was to port the plugins myself if they weren’t there, ask questions on the mailing list when I get stuck, and write up the results. I’ve gotten many positive feedbacks, and it’s helped people get on to 0.10. However, as it turns out, my understanding of sbt 0.10 wasn’t always complete, and downright wrong and misleading at times. I take responsibility of my writing. Instead of leaving old contents in, I’ve decided to push it into github, make a new version and move on. The most up-to-date knowledge of writing plugins is compiled in Plugins Best Practices written mostly by Brian and Josh, and a tiny section by me.
don’t panic
If you’ve just landed from 0.7 world, sbt 0.10 is overwhelming. Take your time to understand the concepts, and I assure you that you’ll get it in time, and really love it.
three representations
There are three ways you may interact with sbt 0.10, which could be confusing at first.
- shell, which you get when you start sbt 0.10.
- Quick Configuration DSL, which goes into
build.sbt
or insettings
sequence. - good old Scala code, aka Full Configuration.
Each representation fits into a different kind of usage model. When you’re simply using sbt to build a project, you will mostly spend your time in the shell, issuing commands like publish-local
. When you want to configure basic settings like library dependencies, you then move on to Quick Configuration DSL in build.sbt
. Finally, when you’re defining subprojects or writing a plugin, you still have the full power of Scala using Full Configuration.
the basic concepts (key-value)
At the heart of sbt 0.10 is a key-value table called settings
. For example, the name of the project is stored in a setting called name
and it could be invoked from the shell as name
:
> name
[info] helloworld
What’s interesting, is that settings
not only stores static project settings, but it also stores tasks. An example of such task is publishLocal
and it could be invoked from the shell as publish-local
:
> publish-local
[info] Packaging /Users/eed3si9n/work/helloworld/target/scala-2.8.1.final/helloworld_2.8.1-0.1-sources.jar ...
....
In 0.7 these tasks would be declared as a method that returns Task
object, like publishLocalAction
method together with a lazy value that declares its dependencies. To modify the behavior of a task, you would override the underlying method. To reuse the behavior you could also directly invoke these methods, for example to package a jar file.
In 0.10, both settings and tasks are just entries in settings
sequence.
val name = SettingKey[String]("name", "Name.")
...
val publishLocal = TaskKey[Unit]("publish-local", "Publishes artifacts to the local repository.")
Since both settings and tasks are referred to by their key, it’s essential to familiarize yourself with the key names if you’re writing a plugin. Key.scala defines the predefined keys.
settings vs tasks
Initially, it’s not that important to know the difference between settings and tasks. Settings are static values without side effects that only depend either on constants or other settings. In other words, these are values that can be cached, and will not change until the project is reloaded.
Tasks on the other hand may depend on external sources like file system, and may incur side effects like deleting a directory.
the basic concepts (dependencies)
What makes sbt 0.10 interesting is that each entries in the settings
can declare dependencies (or “deps” for short) to the other keys. (When I say keys, I mean settings and tasks, but you get the idea)
For example, publishLocalConfiguration
’s dependencies are declared as follows:
publishLocalConfiguration <<= (packagedArtifacts, deliverLocal, ivyLoggingLevel) map {
(arts, ivyFile, level) => publishConfig(arts, Some(ivyFile), logging = level )
},
The above is an example of sbt 0.10’s Quick Configuration DSL. It adds dependencies from publishLocalConfiguration
to packagedArtifacts
, deliverLocal
, and ivyLoggingLevel
and calculates the value by calling publishConfig
using the values from dependent keys. What’s more, you can wire any of the keys to an arbitrary value in build.sbt
without any class inheritance.
The dependencies of a setting or a task can be checked from the shell using inspect
command:
> inspect publish-local
[info] Task
[info] Description:
[info] Publishes artifacts to the local repository.
[info] Provided by:
[info] {file:/Users/eed3si9n/work/helloworld/}default/*:publish-local
[info] Dependencies:
[info] {file:/Users/eed3si9n/work/helloworld/}default/*:ivy-module
[info] {file:/Users/eed3si9n/work/helloworld/}default/*:publish-local-configuration
[info] {file:/Users/eed3si9n/work/helloworld/}default/*:streams(for publish-local)
[info] Delegates:
[info] {file:/Users/eed3si9n/work/helloworld/}default/*:publish-local
[info] {file:/Users/eed3si9n/work/helloworld/}/*:publish-local
the basic concepts (configuration)
Another interesting aspect of the settings
is that the entries as well as the declared dependencies can be scoped in a configuration.
libraryDependencies ++= Seq(
"org.specs2" %% "specs2" % "1.6.1" % "test",
"org.specs2" %% "specs2-scalaz-core" % "6.0.1" % "test"
)
The above is an example of scoped dependencies to "test"
, which is short for "test->compile"
. It’s saying the project’s "test"
configuration would depend on spec2’s artifacts published under "compile"
configuration.
So what is a configuration? It’s a concept borrowed from Maven scopes and Ivy configuration, saying that a project can be in a mode of different set of files and dependencies. The default configurations "compile"
, "test"
, and "runtime"
are good examples of a configuration. When you run test
task, for instance, you expect sbt to pick up source code in both src/test/*
and src/main/*
, and grab deps marked "test"
and unmarked ones.
Let’s see how it’s done. running inspect test
reveals that the default test
task is delegated to test:test
.
> inspect test
[info] Task: Unit
[info] Description:
[info] Executes all tests.
[info] Provided by:
[info] {file:/Users/eed3si9n/work/helloworld/}default-b65acd/test:test
...
test:test
is an example of the way shell denotes a configuration-scoped key. In Quick Config DSL, it’s written as test in Test
. Eventually down the dependent keys via executeTests in Test
is compile in Test
, which is what we are after. The interesting thing about compile in Test
is that it’s wired identical to regular compile
, including the dependent settings, except it’s using Test
configuration. For example, let’s compare source code:
> show test:sources
[info] List(/Users/eed3si9n/work/helloworld/src/test/scala/hellospec.scala)
> show compile:sources
[info] List(/Users/eed3si9n/work/helloworld/src/main/scala/hello.scala)
How did test
delegate to test:test
in the first place? When an unconfigured key is passed to the shell, it first looks up in Global
configuration, then it looks up in the order of configurations
of the project, which by default is Seq(Compile, Runtime, Test, Provided, Optional)
.
the basic concepts (project)
When you first start out sbt with just the build.sbt, you are implicitly in the default project. Using full configuration, sbt can manage multiple modules under a single build instance. This allows you to declare dependencies among submodules etc.
object FooBuild extends Build {
lazy val root = Project("root", file("."), settings = buildSettings) aggregate(library, jetty)
lazy val library = Project("library", file("library"))
lazy val jetty = Project("foo-jetty", file("jetty")) dependsOn(library)
}
From the shell, use project
command to switch between the projects:
root> project library
library> compile
...
This demonstrates that the task compile
is scoped by the current project.
the basic concepts (scope)
So far we’ve seen two examples of key scoping, one by configuration, and the other by project. In general scopes provides a context to a key, and encourages reuse of keys and relationship between the keys. For instance compile
depends on sources
, regardless of the project or the configuration.
In sbt, there are total of four axes (plural for “axis”) of scope. They are project, configuration, task, and extra. So far, the extra axis has not been used, so practically we have project, configuration, and task. Yes, tasks can be used for scoping! In the past, I’ve advocated use of configuration as scoping mechanism, but through discussion on the mailing list, I’ve come to better understanding that plugins should strive to be configuration-neutral, and using it for tasks settings was the wrong axis of choice. The recommended approach is to scope settings into the main task of the plugin (See Plugins Best Practices).
Suppose you are defining a task called assembly
that runs the test and creates an executable jar file. Here’s how it could look in a plugin definition:
val assembly = TaskKey[File]("assembly")
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
assembly <<= (test in Test) map { _ =>
// do something
}
)
The user is somewhat stuck with the test in Test
dependencies. We can improve this by scoping test
under assembly
task.
val assembly = TaskKey[File]("assembly")
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
assembly <<= (test in assembly) map { _ =>
// do something
},
test in assembly <<= (test in Test).identity
)
Should the user choose not to run the tests, he or she can rewire test in assembly
without compromising the main test
task.
test in assembly := {}
changing the scopes
As noted, a key can be scoped in four axes. But when I just say for example sources
, which configuration is it actually in? The answer is Scope.ThisScope
, which is defined to be Scope(This, This, This, This)
. The value This
expresses the fact that it is unscoped.
By creating a configuration-neutral chain of settings, similar to compile
, we can reuse it under multiple configurations. I’ve simplified Default.configTasks
for the purpose of demonstration:
lazy val baseCompileSettings = Seq(
compile <<= (compileInputs) map { i => Compiler(i) },
compileInputs <<= (dependencyClasspath, sources) map { (cp, srcs) =>
Compiler.inputs(classpath, srcs)
}
)
The important thing is that none of the keys used in the above is scoped on configuration-axis. sbt provides a powerful utility function called inConfig(conf: Configuration)(ss: Seq[Setting[_]])
. This is a curried function that scopes the settings ss
in to conf
only when it isn’t scoped yet to a configuration. For example,
inConfig(Compile)(baseCompileSettings)
This is equivalent to saying
lazy val compileSettings = Seq(
compile in Compile <<= (compileInputs in Compile) map { i => Compiler(i) },
compileInputs in Compile <<= (dependencyClasspath in Compile, sources in Compile) map { (cp, srcs) =>
Compiler.inputs(classpath, srcs)
}
)
Similarly, we could also wire the settings for Test
:
inConfig(Test)(baseCompileSettings)
notes on scoping keys under tasks (not necessary for sbt 0.12+)
read the documents, source, and other’s source
The the official wiki is full of useful information. It feels a bit scattered, but you can usually find information if you know what you’re looking for. Here are links to some useful pages:
- Migrating from SBT 0.7.x to 0.10.x
- Settings
- Basic Configuration
- Full Configuration
- Library Management
- Plugins
- Task Basics
- Common Tasks
- Mapping Files
- Using the Configuration System
- Plugins Best Practices
When you’re looking for an example, source often is the quickest place to find the answer. Scala X-Ray and scaladocs make it easy to navigate from one source to the other.
In a mailing list thread titled Adrift in a sea of types Mark mentions three sources:
- Default.scala (“All of the built in settings are defined there.”)
- Keys.scala (“The keys are in Keys.”)
- Structure.scala (“Most of these [implicit conversions] are in Scoped, which is in Structure.scala.”)
read other plugins
When you’re starting out on writing a plugin, you can learn many tricks by reading other plugin’s source. Here are the samples that I’ve used:
- sbt/sbt-assembly
- softprops/coffeescripted-sbt
- siasia/xsbt-web-plugin
- Proguard.scala
- ijuma/sbt-idea
- jsuereth/xsbt-ghpages-plugin
learn the small changes from 0.7
What motivated me to write this guide is all the renames and changes that I stumbled while porting codahale/assembly-sbt to sbt 0.10 as eed3si9n/sbt-assembly. We can see both plugins side by side and see what changed.
version number
0.7 (in build.properties):
project.version=0.1.1
0.10 plugins (in build.sbt):
posterousNotesVersion := "0.4"
version <<= (sbtVersion, posterousNotesVersion) { (sv, nv) => "sbt" + sv + "_" + nv }
Unlike 0.7 plugins that were distributed as source package, 0.10 plugins are packaged as binary. This adds dependency to the exact version of sbt. So far there was been 0.10.0 and 0.10.1 already, and plugins compiled against 0.10.0 does not work against 0.10.1. As a workaround, I have adopted the above versioning convention.
0.11 (in build.sbt):
version := "0.4"
So far only RCs are available for 0.11, but it fixes the versioning issue by automatically mangling the artifact name.
super class
before:
package assembly
trait AssemblyBuilder extends BasicScalaProject {
after:
package sbtassembly
object Plugin extends sbt.Plugin {
So the change shows the subtle, but important differences in where the plugin stands in sbt 0.10. In 0.7 plugin was a trait mixed into your project object (“is-a” relationship). In 0.10, it’s a library loaded into the project’s execution environment (“has-a” relationship).
where the settings go
before:
in the trait.
after:
...
lazy val assemblySettings: Seq[sbt.Project.Setting[_]] = baseAssemblySettings // or inConfig(XX)(baseAssemblySettings)
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
...
)
Instead of defining methods to be overridden, for plugins, we create a sequence of sbt.Project.Setting[_]
that the user can load using seq(...)
. This allows the build authors to decide whether to include the plugin settings or not. The only exception is if you’re defining a global command, in which case you would override settings
.
settings that are meant to be overridden
before:
def assemblyJarName = name + "-assembly-" + this.version + ".jar"
after:
object AssemblyKeys {
lazy val jarName = SettingKey[String]("assembly-jar-name")
}
import AssemblyKeys._
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
jarName in assembly <<= (name, version) { (name, version) => name + "-assembly-" + version + ".jar" },
...
)
Define an entry in the baseAssemblySettings
with declared dependencies to other keys (name
and version
). Quick Configuration DSL adds an injected method apply
to the pair, so you can pass in a function value to calculate the value of jarName
key. By putting this in AssemblyKeys
, we can name this jarName
without the prefix. It can be referred to in build.sbt as AssemblyKeys.jarName
, or jarName
if the build user imports AssemblyKeys._
.
static type of Quick Configuration DSL is Initialize[A]
before:
def assemblyTask(...)
lazy val assembly = assemblyTask(...) dependsOn(test) describedAs("Builds an optimized, single-file deployable JAR.")
after:
object AssemblyKeys {
val assembly = TaskKey[File]("assembly", "Builds a single-file deployable jar.")
}
import AssemblyKeys._
private def assemblyTask: Initialize[Task[File]] =
(test in assembly, ...) map { (t, ...) =>
}
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
assembly <<= assemblyTask,
...
)
You can stuff everything in baseAssemblySettings
, but that quickly becomes cluttered, so I started to clean it up inspired by Keith Irwin’s coffeescripted-sbt implementation.
outputPath
is target
, and Path
is sbt.File
before:
def assemblyOutputPath = outputPath / assemblyJarName
after:
object AssemblyKeys {
val outputPath = SettingKey[File]("assembly-output-path")
}
import AssemblyKeys._
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
outputPath in assembly <<= (target in assembly, jarName in assembly) { (t, s) => t / s },
...
)
What used to be called outputPath
is now a key called target: SettingKey[File]
.
sbt.File
is an alias to java.io.File
, which implicitly converts to sbt.RichFile
, which replaces Path
in 0.7. So just say dir: File
, and you can write dir / name
.
runClasspath
is fullClasspath in Runtime
before:
def assemblyClasspath = runClasspath
after:
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
fullClasspath in assembly <<= fullClasspath or (fullClasspath in Runtime).identity,
...
)
reuse existing keys
before:
def assemblyClasspath = runClasspath
after:
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
fullClasspath in assembly <<= fullClasspath or (fullClasspath in Runtime).identity,
...
)
So fullClasspath in assembly
is seeded with the value from current configuration’s fullClasspath
, otherwise fullClasspath in Runtime
, but if the user wants to he or she can rewire it later without defining a hook method. Neat, right?
a classpath is a Classpath
, not Pathfinder
before:
classpath: Pathfinder
after:
classpath: Classpath
functions that are meant to be overridden
before:
def assemblyExclude(base: PathFinder) =
(base / "META-INF" ** "*") ---
(base / "META-INF" / "services" ** "*") ---
(base / "META-INF" / "maven" ** "*")
after:
val excludedFiles = SettingKey[Seq[File] => Seq[File]]("excluded-files")
private def assemblyExcludedFiles(base: Seq[File]): Seq[File] =
((base / "META-INF" ** "*") ---
(base / "META-INF" / "services" ** "*") ---
(base / "META-INF" / "maven" ** "*")).get
lazy val baseAssemblySettings: Seq[sbt.Project.Setting[_]] = Seq(
excludedFiles in assembly := assemblyExcludedFiles _,
...
)
This is slightly complicated. Since sbt 0.10 no longer relies on inheritance for overriding the behaviors, we need to track the method into key-value baseAssemblySettings
. In Scala, you need the method into a function value to assign it to a variable, so we have assemblyExcludedFiles _
. The type of this function value is Seq[File] => Seq[File]
.
prefer Seq[File]
over Pathfinder
before:
base: PathFinder
after:
base: Seq[File]
Seq[File]
can be converted into Pathfinder
implicitly, and 0.10 way is to use normal Scala types like File
and Seq[File]
where it’s exposed for extension.
##
is done via file mapping
before:
val base = (Path.lazyPathFinder(tempDir :: directories) ##)
(descendents(base, "*") --- exclude(base)).get
after:
val base = tempDir +: directories
val descendants = ((base ** (-DirectoryFilter)) --- exclude(base)).get
descendants x relativeTo(base)
See Mapping Files for the details.
Tasks like
package
,packageSrc
, andpackageDoc
accept mappings from an input file to the path to use in the resulting artifact (jar).
Using x
method you can generate mappings.
FileUtilities
is IO
before:
FileUtilities.clean(assemblyConflictingFiles(tempDir), true, log)
after:
IO.delete(conflicting(Seq(tempDir)))
Apparently I didn’t get the memo on this rename, so I asked the mailing list, which is very helpful. Browse API Documentation and look into companion objects to see if they have interesting methods for you.
packageTask
is Package
before:
packageTask(...)
after:
Package(config, cacheDir, s.log)
acquire logger
from streams
before:
log.info("Including %s".format(jarName))
after:
(streams) map { (s) =>
val log = s.log
log.info("Including %s".format(jarName))
}
From Basic Tasks:
New in sbt 0.10 are per-task loggers, which are part of a more general system for task-specific data called Streams. This allows controlling the verbosity of stack traces and logging individually for tasks as well as recalling the last logging for a task.
search the mailing list, ask the mailing list
The simple-build-tool mailing list is full of useful information. There’s a fair chance someone else got stuck on an issue, so try searching the list (and sort the results by date to see new stuff first).
When you get stuck, don’t be shy and ask the mailing list. Someone would usually get back to you with an useful answer.
thx
Thank you for reading all the way. I’m hoping this would save someone’s time. I am going repeat again that I am no sbt 0.10 expert, so I may have gotten half the stuff wrong. Consult the official docs and experts if in doubt.