Previously we pretended that a key like
to one entry in sbt’s map of key-value pairs. This was a simplification.
In truth, each key can have an associated value in more than one context, called a scope.
Some concrete examples:
compilekey may have a different value for your main sources and your test sources, if you want to compile them differently.
packageOptionskey (which contains options for creating jar packages) may have different values when packaging class files (
packageBin) or packaging source code (
There is no single value for a given key
name, because the value may
differ according to scope.
However, there is a single value for a given scoped key.
If you think about sbt processing a list of settings to generate a
key-value map describing the project, as
discussed earlier, the keys in that key-value map are
scoped keys. Each setting defined in the build definition (for example
build.sbt) applies to a scoped key as well.
Often the scope is implied or has a default, but if the defaults are
wrong, you’ll need to mention the desired scope in
A scope axis is a type constructor similar to
that is used to form a component in a scope.
There are three scope axes:
If you’re not familiar with the notion of axis, we can think of the RGB color cube as an example:
In the RGB color model, all colors are represented by a point in the cube whose axes correspond to red, green, and blue components encoded by a number. Similarly, a full scope in sbt is formed by a tuple of a subproject, a configuration, and a task value:
projA / Compile / console / scalacOptions
This is the slash syntax, introduced in sbt 1.1, for:
scalacOptions in ( Select(projA: Reference), Select(Compile: ConfigKey), Select(console.key) )
If you put multiple projects in a single build, each project needs its own settings. That is, keys can be scoped according to the project.
The project axis can also be set to
ThisBuild, which means the “entire build”,
so a setting applies to the entire build rather than a single project.
Build-level settings are often used as a fallback when a project doesn’t define a
project-specific setting. We will discuss more on build-level settings later in this page.
A dependency configuration (or “configuration” for short) defines a graph of library dependencies, potentially with its own classpath, sources, generated packages, etc. The dependency configuration concept comes from Ivy, which sbt used to use for managed dependencies Library Dependencies, and from MavenScopes.
Some configurations you’ll see in sbt:
Compilewhich defines the main build (
Testwhich defines how to build tests (
Runtimewhich defines the classpath for the
By default, all the keys associated with compiling, packaging, and
running are scoped to a configuration and therefore may work differently
in each configuration. The most obvious examples are the task keys
run; but all the keys which affect those keys
fullClasspath) are also
scoped to the configuration.
Another thing to note about a configuration is that it can extend other configurations. The following figure shows the extension relationship among the most common configurations.
Settings can affect how a task works. For example, the
is affected by the
To support this, a task key (such as
packageSrc) can be a scope for
another key (such as
The various tasks that build a package (
packageDoc) can share keys related to packaging, such as
packageOptions. Those keys can have distinct values for each
Each scope axis can be filled in with an instance of the axis type (analogous to
or the axis can be filled in with the special value
So we can think of
Zero is a universal fallback for all scope axes,
but its direct use should be reserved to sbt and plugin authors in most cases.
Global is a scope that sets
Zero to all axes:
Zero / Zero / Zero. In other words,
Global / someKey is a shorthand for
Zero / Zero / Zero / someKey.
If you create a setting in
build.sbt with a bare key, it will be scoped
to (current subproject / configuration
Zero / task
lazy val root = (project in file(".")) .settings( name := "hello" )
Run sbt and
inspect name to see that it’s provided by
ProjectRef(uri("file:/private/tmp/hello/"), "root") / name, that is, the
ProjectRef(uri("file:/Users/xxx/hello/"), "root"), and
neither configuration nor task scope are shown (which means
A bare key on the right hand side is also scoped to
(current subproject / configuration
Zero / task
organization := name.value
The types of any of the scope axes have been method enriched to have a
The argument to
/ can be a key or another scope axis. So for
example, though there’s no good reason to do this, you could have an instance of the
name key scoped to the
Compile / name := "hello"
or you could set the name scoped to the
packageBin task (pointless! just
packageBin / name := "hello"
or you could set the
name with multiple scope axes, for example in the
packageBin task in the
Compile / packageBin / name := "hello"
or you could use
// same as Zero / Zero / Zero / concurrentRestrictions Global / concurrentRestrictions := Seq( Tags.limitAll(1) )
Global / concurrentRestrictions implicitly converts to
Zero / Zero / Zero / concurrentRestrictions, setting
all axes to
Zero scope component; the task and configuration are already
Zero by default, so here the effect is to make the project
that is, define
Zero / Zero / Zero / concurrentRestrictions rather than
ProjectRef(uri("file:/tmp/hello/"), "root") / Zero / Zero / concurrentRestrictions)
On the command line and in the sbt shell, sbt displays (and parses) scoped keys like this:
ref / Config / intask / key
refidentifies the subproject axis. It could be
ProjectRef(uri("file:..."), "id"), or
ThisBuildthat denotes the “entire build” scope.
Configidentifies the configuration axis using the capitalized Scala identifier.
intaskidentifies the task axis.
keyidentifies the key being scoped.
Zero can appear for each axis.
If you omit part of the scoped key, it will be inferred as follows:
For more details, see Interacting with the Configuration System.
fullClasspathspecifies just a key, so the default scopes are used: current project, a key-dependent configuration, and
Test / fullClasspathspecifies the configuration, so this is
Testconfiguration, with defaults for the other two scope axes.
root / fullClasspathspecifies the project
root, where the project is identified with the project id.
root / Zero / fullClasspathspecified the project
root, and specifies
Zerofor the configuration, rather than the default configuration.
doc / fullClasspathspecifies the
fullClasspathkey scoped to the
doctask, with the defaults for the project and configuration axes.
ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspathspecifies a project
ProjectRef(uri("file:/tmp/hello/"), "root"). Also specifies configuration Test, leaves the default task axis.
ThisBuild / versionsets the subproject axis to “entire build” where the build is
ThisBuild, with the default configuration.
Zero / fullClasspathsets the subproject axis to
Zero, with the default configuration.
root / Compile / doc / fullClasspathsets all three scope axes.
In sbt shell, you can use the
inspect command to understand
keys and their scopes. Try
$ sbt sbt:Hello> inspect Test / fullClasspath [info] Task: scala.collection.Seq[sbt.internal.util.Attributed[java.io.File]] [info] Description: [info] The exported classpath, consisting of build products and unmanaged and managed, internal and external dependencies. [info] Provided by: [info] ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath [info] Defined at: [info] (sbt.Classpaths.classpaths) Defaults.scala:1639 [info] Dependencies: [info] Test / dependencyClasspath [info] Test / exportedProducts [info] Test / fullClasspath / streams [info] Reverse dependencies: [info] Test / testLoader [info] Delegates: [info] Test / fullClasspath [info] Runtime / fullClasspath [info] Compile / fullClasspath [info] fullClasspath [info] ThisBuild / Test / fullClasspath [info] ThisBuild / Runtime / fullClasspath [info] ThisBuild / Compile / fullClasspath [info] ThisBuild / fullClasspath [info] Zero / Test / fullClasspath [info] Zero / Runtime / fullClasspath [info] Zero / Compile / fullClasspath [info] Global / fullClasspath [info] Related: [info] Compile / fullClasspath [info] Runtime / fullClasspath
On the first line, you can see this is a task (as opposed to a setting,
as explained in .sbt build definition). The value
resulting from the task will have type
“Provided by” points you to the scoped key that defines the value, in
ProjectRef(uri("file:/tmp/hello/"), "root") / Test / fullClasspath (which
fullClasspath key scoped to the
Test configuration and the
ProjectRef(uri("file:/tmp/hello/"), "root") project).
“Dependencies” was discussed in detail in the previous page.
We’ll discuss “Delegates” later.
inspect fullClasspath (as opposed to the above example,
Test / fullClasspath) to get a sense of the difference. Because
the configuration is omitted, it is autodetected as
inspect Compile / fullClasspath should therefore look the same as
inspect ThisBuild / Zero / fullClasspath for another contrast.
fullClasspath is not
defined in the
Zero configuration scope by default.
Again, for more details, see Interacting with the Configuration System.
You need to specify the scope if the key in question is normally scoped.
For example, the
compile task, by default, is scoped to
configurations, and does not exist outside of those scopes.
To change the value associated with the
compile key, you need to write
Compile / compile or
Test / compile. Using plain
compile would define
a new compile task scoped to the current project, rather than overriding
the standard compile tasks which are scoped to a configuration.
If you get an error like “Reference to undefined setting“, often you’ve failed to specify a scope, or you’ve specified the wrong scope. The key you’re using may be defined in some other scope. sbt will try to suggest what you meant as part of the error message; look for “Did you mean Compile / compile?”
One way to think of it is that a name is only part of a key. In
reality, all keys consist of both a name, and a scope (where the scope
has three axes). The entire expression
Compile / packageBin / packageOptions is a key name, in other words.
packageOptions is also a key name, but a different one (for keys
with no slashes, a scope is implicitly assumed: current project,
An advanced technique for factoring out common settings
across subprojects is to define the settings scoped to
If a key that is scoped to a particular subproject is not found,
sbt will look for it in
ThisBuild as a fallback.
Using the mechanism, we can define a build-level default setting for
frequently used keys such as
ThisBuild / organization := "com.example", ThisBuild / scalaVersion := "2.12.16", ThisBuild / version := "0.1.0-SNAPSHOT" lazy val root = (project in file(".")) .settings( name := "Hello", publish / skip := true ) lazy val core = (project in file("core")) .settings( // other settings ) lazy val util = (project in file("util")) .settings( // other settings )
For convenience, there is
inThisBuild(...) function that will
scope both the key and the body of the setting expression to
Putting setting expressions in there would be equivalent to prepending
ThisBuild / where possible.
Due to the nature of scope delegation that we will cover later,
build-level settings should be set only to a pure value or settings from either
A scoped key may be undefined, if it has no value associated with it in its scope.
For each scope axis, sbt has a fallback search path made up of other scope values.
Typically, if a key has no associated value in a more-specific scope,
sbt will try to get a value from a more general scope, such as the
This feature allows you to set a value once in a more general scope, allowing multiple more-specific scopes to inherit the value. We will discuss scope delegation in detail later.