sbt Beyond the Defaults: Part III - Scopes
Welcome to another post in sbt Beyond the Defaults series! Today I am going to cover another key concept of sbt: Scopes. In previous chapters of this series we learned the different types of keys and their usage in a very straightforward way. Also we have seen the lifecycle of each one of the keys, how to define our own and how to use them.
Now I am going to show you the different scopes that covers each key and how to understand each one of them.
Facts about Keys
I will share some aspects about Keys.
- Each key can have more than one value. That is, for every key there is an associated value in a context.
- A key could have dependencies on other keys.
- Based on the context hierarchy, a key could have a fallback value.
Note that I have mentioned the word context twice. That context is called a scope.
Scopes
A scope is the context in which a specific key will apply. This is a way of separate different behaviors that a key would have. For instance: when you want to apply a different compilation process to the sources of your project and the tests. That example could be extended if you want to implement a static code analysis tool like scalastyle or scapegoat in your project and you want to throw a compilation error if some of your configured rules were violated during the process. But, you don’t want to apply the same behaviour in the same phase (compilation) of your tests.
In that case, the compile
taskKey is the same (syntactically) for compilation and testing, but its value is different for each of the scopes.
We are going to see more on this later.
Scope Axes
sbt
has three scope types (called axes) in where each key can be defined with its own value:
- Project Axis.
- Configuration Axis.
- Task Axis.
But before to take a deep look to each of them we are going to see a way to fully understand the hierarchy of a key.
Inspect
me!
sbt
has a built in task called inspect
that let us see all the details of a specific key, thus you can watch its description, dependencies and definition.
I am going to take the same basic sbt example that I’ve previously used for demonstration purposes:
name := """my_app_name"""
version := "1.0"
scalaVersion := "2.12.1"
// Change this to another test framework if you prefer
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
Remember that this is the very basic definition of a build.sbt
file, provided by Lightbend Activator. If you remember the types of keys that we’ve seen in the previous chapter you’ll notice that this file only has SettingKeys
(name
, version
, scalaVersion
and libraryDependencies
), so let’s try inspecting one of them:
$ sbt
> inspect name
[info] Setting: java.lang.String = my_app_name
[info] Description:
[info] Project name.
[info] Provided by:
[info] {file:/Users/AlejandroE/Documents/my_app_name/} my_app_name/*:name
[info] Defined at:
[info] /Users/AlejandroE/Documents/my_app_name/build.sbt:1
[info] Reverse dependencies:
[info] *:description
[info] *:onLoadMessage
[info] *:projectInfo
[info] *:normalizedName
[info] Delegates:
[info] *:name
[info] {.}/*:name
[info] */*:name
You’ll see this output in your console. What does that means?
- The Setting describes the type of the key and its value. In this case it’s a Setting, of type
java.lang.String
with valuemy_app_name
. - The Description shows the description value of the key. Remember: when you are defining a key you can provide a description (we’ve seen this in the previous chapter). In this case, this value is taken from the defaults.
- The Provided by shows which key defines its value. We’re going to see that the asterisk in
*:name
has an important meaning. For now, I can say that this key is provided by the global defaults. - The Defined at shows where the key is defined at. In this case it’s in our
build.sbt
at line 1. - The Reverse dependencies shows what other keys uses this specific key. In this case
name
is used bydescription
,onLoadMessage
,projectInfo
andnormalizedName
. - The Delegates shows you in where
sbt
will look in case of not finding the value of this key in thebuild.sbt
file.
As you can see, every key defined in sbt has its properties and its defined scope and dependencies. In the previous example we have looked at a Setting Key, but what happens when you look at a Task Key or even an Input Key? Let’s see.
$ sbt
> inspect compile
[info] Task: sbt.inc.Analysis
[info] Description:
[info] Compiles sources.
[info] Provided by:
[info] {file:/Users/AlejandroE/Documents/my_app_name/} my_app_name/compile:compile
[info] Defined at:
[info] (sbt.Defaults) Defaults.scala:280
[info] Dependencies:
[info] compile:manipulateBytecode
[info] compile:incCompileSetup
[info] Reverse dependencies:
[info] compile:discoveredSbtPlugins
[info] compile:discoveredMainClasses
[info] compile:products
[info] compile:printWarnings
[info] Delegates:
[info] compile:compile
[info] *:compile
[info] {.}/compile:compile
[info] {.}/*:compile
[info] */compile:compile
[info] */*:compile
[info] Related:
[info] test:compile
You can see that we’ve inspected the compile
task and, in fact, sbt are telling you that it’s a Task
of type sbt.inc.Analysis
. Also note that the structure of the inspection is almost the same as the previous inspection with name
. Now we are going to see the different scopes and how relate to this structure.
Detail of Scope Axes
Project Axis
With sbt, you can have a multi-project structure. That is, as its name says, subprojects inside a build definition. In our case, we are working with a single project definition, so the keys that we define are by default scoped to our project (unless we specify the scope).
If we choose to have a multi-project definition we can scope our keys by a single subproject or even to the entire build and that counts as a project scope. The latter example requires to understand the multi-project structure, so I won’t be covering any more details on this at the moment for the sake of clarity.
Retaking our example, if I do this:
$ sbt
> my_app_name/name
I’ll get:
> [info] my_app_name
Note that I’ve prefixed the name
key with the project name and a forward slash. With that syntax I’m telling sbt to show me the value of name
in the project axis. Remember that we have the value my_app_name
at that scope because we defined it in our build.sbt.
But, what would happen if I remove the name
key from our build.sbt and then I try to get the value associated to the key in the project scope? Let’s see
//name := """my_app_name""" - I commented this line
version := "1.0"
scalaVersion := "2.12.1"
// Change this to another test framework if you prefer
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.1" % "test"
$ sbt
> my_app_name/name
> [info] my_app_name
>
> name
> [info] my_app_name
Please note that I’ve asked sbt for the key name
in two ways: prefixing the project name (project scope) and with the global scope. Both of them returned the name of my project, but why? I have commented the key value, haven’t I?
Yes, but remember
- Your project has a name, even if you don’t specify it in the
build.sbt
. Having said that,sbt
will look that value, if you don’t tell it one associated to that key, finding the folder name. - We have seen with the
inspect name
command that the key has Delegates, that means, if you don’t specify the value the tool will look at*:name
,{.}/*:name
and*/*:name
scopes. Remember that the*
prefix means global (the entire build).
Configuration Axis
The configuration axis defines the possible settings that a task need for its execution. For instance, take a look at compile
: it is a configuration axis that has a task with the same name as follows.
$ sbt
> compile:compile
[success] Total time: 0 s, completed 5/02/2017 11:54:50 AM
When you type compile
(without the configuration axis) you are actually typing an alias of compile:compile
. What does compile
, as a configuration scope has? Let’s see.
$ sbt
> inspect compile:sources
[info] Task: scala.collection.Seq[java.io.File]
[info] Description:
[info] All sources, both managed and unmanaged.
[info] Provided by:
[info] {file:/Users/AlejandroE/Documents/my_app_name/} my_app_name/compile:sources
[info] Defined at:
[info] (sbt.Defaults) Defaults.scala:200
[info] Dependencies:
[info] compile:unmanagedSources
[info] compile:managedSources
[info] Delegates:
[info] compile:sources
[info] *:sources
[info] {.}/compile:sources
[info] {.}/*:sources
[info] */compile:sources
[info] */*:sources
[info] Related:
[info] test:sources
In this case we’ve inspected the task sources
of the configuration axis of compile
. That means that when you are going to compile the project’s code sbt
will perform a series of task executions that are dependencies for the whole compilation process. In this case, sources
returns the sequence of files to be compiled.
The concept of configuration, as noted by the official sbt documentation comes from the same term from Ivy and the Dependecy Scopes of Maven.
Task Axis
The last axis is related to task. Imagine a task chaining operation in where one task depends on the outcome of another one. It doesn’t necessarily depends on its result, but on the execution of the latter.
Taking the example of the official documentation, the predefined task packageSrc
has a setting called packageOptions
that modifies the outcome of the task. That is, if you want to package your source code (packageSrc
), the binaries of your application (packageBin
) or the documentation (packageDoc
) of the application. These last ones belongs to the set of options for that task. Let’s inspect it!
$ sbt
> inspect packageSrc:packageOptions
[info] Task: scala.collection.Seq[sbt.PackageOption]
[info] Description:
[info] Options for packaging.
[info] Provided by:
[info] */*:packageOptions
[info] Defined at:
[info] (sbt.Defaults) Defaults.scala:624
[info] Delegates:
[info] {.}/packageSrc:packageOptions
[info] {.}/*:packageOptions
[info] */packageSrc:packageOptions
[info] */*:packageOptions
[info] Related:
[info] compile:packageSrc::packageOptions
[info] test:packageBin::packageOptions
[info] test:packageSrc::packageOptions
[info] */*:packageOptions
[info] compile:packageBin::packageOptions
As you can see, packageOptions
is a task that returns the sequence (enumeration) of the possible packaging options that you have. This is like a modifier for changing the behavior of the task.
Structure of the Axes
We’ve seen different syntax for illustrate the different scopes, but it’s good to know that sbt
has a standard one for parsing the right value according to the scope accessed as follows:
{<build-uri>}<project-id>/config:intask::key
Where
{<build-uri>}<project-id>
is the project axis. Remember that I have used this syntax previously for showing thename
key of the project.config
is the configuration axis.intask
is the task axis.key
is the key accesed through the scope that we’re specifying.
Remember that, if some of these parts are not specified to sbt it will assume the default scope for that axes. For instance:
$ sbt
> show my_app_name/compile:compile::sources
[info] * /Users/AlejandroE/Documents/my_app_name/src/main/scala/Domain.scala
[success] Total time: 0 s, completed 5/02/2017 01:44:55 PM
In this case I told sbt to show me the settingKey sources
that is scoped by my project in the compile task of the compile configuration scope. The outcome is the only .scala
file that I have in my project (Domain.scala
).
Summary
We’ve seen the three scopes provided by sbt: configuration, project and task. Also we’ve learned how to identify its hierarchy through the inspect
command.
I brought an example of each of them and also I described the information provided by the inspection of the keys.
This chapter is fundamental for the next series. We’ve learned the very basics of sbt
and I think that we are ready for more practical posts. So, please stay tuned for the next one!
Finally, a very quick recap of this topic was done in an excellent answer by Jacek Laskowsky in StackOverflow.
Again, as usual, thank you for reading! See you in the next one :)