This page describes the core settings engine a bit. This may be useful for using it outside of sbt. It may also be useful for understanding how sbt works internally.
The documentation is comprised of two parts. The first part shows an example settings system built on top of the settings engine. The second part comments on how sbt’s settings system is built on top of the settings engine. This may help illuminate what exactly the core settings engine provides and what is needed to build something like the sbt settings system.
To run this example, first create a new project with the following build.sbt file:
libraryDependencies += "org.scala-sbt" %% "collections" % sbtVersion.value
resolvers += sbtResolver.value
Then, put the following examples in source files SettingsExample.scala
and SettingsUsage.scala
. Finally, run sbt and enter the REPL using
console
. To see the output described below, enter SettingsUsage
.
The first part of the example defines the custom settings system. There are three main parts:
Scope
type.
Scope
(plus an AttributeKey
) to
a String
.
Scope
s in
which to look up a value.
There is also a fourth, but its usage is likely to be specific to sbt at this time. The example uses a trivial implementation for this part.
SettingsExample.scala
:
import sbt._
/** Define our settings system */
// A basic scope indexed by an integer.
final case class Scope(index: Int)
// Extend the Init trait.
// (It is done this way because the Scope type parameter is used everywhere in Init.
// Lots of type constructors would become binary, which as you may know requires lots of type lambdas
// when you want a type function with only one parameter.
// That would be a general pain.)
object SettingsExample extends Init[Scope]
{
// Provides a way of showing a Scope+AttributeKey[_]
val showFullKey: Show[ScopedKey[_]] = new Show[ScopedKey[_]] {
def apply(key: ScopedKey[_]) = key.scope.index + "/" + key.key.label
}
// A sample delegation function that delegates to a Scope with a lower index.
val delegates: Scope => Seq[Scope] = { case s @ Scope(index) =>
s +: (if(index <= 0) Nil else delegates(Scope(index-1)) )
}
// Not using this feature in this example.
val scopeLocal: ScopeLocal = _ => Nil
// These three functions + a scope (here, Scope) are sufficient for defining our settings system.
}
This part shows how to use the system we just defined. The end result is
a Settings[Scope]
value. This type is basically a mapping
Scope -> AttributeKey[T] -> Option[T]
. See the
Settings API documentation for
details.
SettingsUsage.scala
:
/** Usage Example **/
import sbt._
import SettingsExample._
import Types._
object SettingsUsage {
// Define some keys
val a = AttributeKey[Int]("a")
val b = AttributeKey[Int]("b")
// Scope these keys
val a3 = ScopedKey(Scope(3), a)
val a4 = ScopedKey(Scope(4), a)
val a5 = ScopedKey(Scope(5), a)
val b4 = ScopedKey(Scope(4), b)
// Define some settings
val mySettings: Seq[Setting[_]] = Seq(
setting( a3, value( 3 ) ),
setting( b4, map(a4)(_ * 3)),
update(a5)(_ + 1)
)
// "compiles" and applies the settings.
// This can be split into multiple steps to access intermediate results if desired.
// The 'inspect' command operates on the output of 'compile', for example.
val applied: Settings[Scope] = make(mySettings)(delegates, scopeLocal, showFullKey)
// Show results.
for(i <- 0 to 5; k <- Seq(a, b)) {
println( k.label + i + " = " + applied.get( Scope(i), k) )
}
}
This produces the following output when run:
a0 = None
b0 = None
a1 = None
b1 = None
a2 = None
b2 = None
a3 = Some(3)
b3 = None
a4 = Some(3)
b4 = Some(9)
a5 = Some(4)
b5 = Some(9)
None
results, we never defined the value and there was no
value to delegate to.
a3
, we explicitly defined it to be 3.
a4
wasn’t defined, so it delegates to a3
according to our delegates
function.
b4
gets the value for a4
(which delegates to a3
, so it is 3) and
multiplies by 3
a5
is defined as the previous value of a5
+ 1 and since no previous
value of a5
was defined, it delegates to a4
, resulting in 3+1=4.
b5
isn’t defined explicitly, so it delegates to b4
and is therefore
equal to 9 as well
sbt defines a more complicated scope than the one shown here for the
standard usage of settings in a build. This scope has four components:
the project axis, the configuration axis, the task axis, and the extra
axis. Each component may be
Zero (no specific value),
This
(current context), or
Select (containing a specific value). sbt
resolves This_
to either
Zero or
Select
depending on the context.
For example, in a project, a
This project axis becomes a
Select referring to the defining project. All other axes that are
This are
translated to
Zero. Functions like inConfig
and inTask
transform
This
into a
Select for a specific value. For example,
inConfig(Compile)(someSettings)
translates the configuration axis for
all settings in someSettings to be Select(Compile)
if the axis value
is
This.
So, from the example and from sbt’s scopes, you can see that the core
settings engine does not impose much on the structure of a scope. All it
requires is a delegates
function Scope => Seq[Scope]
and a display
function. You can choose a scope type that makes sense for your
situation.
The app
, value
, update
, and related methods are the core methods
for constructing settings. This example obviously looks rather different
from sbt’s interface because these methods are not typically used
directly, but are wrapped in a higher-level abstraction.
With the core settings engine, you work with HList
s to access other
settings. In sbt’s higher-level system, there are wrappers around HList
for TupleN
and FunctionN
for N = 1-9 (except Tuple1
isn’t actually
used). When working with arbitrary arity, it is useful to make these
wrappers at the highest level possible. This is because once wrappers
are defined, code must be duplicated for every N. By making the wrappers
at the top-level, this requires only one level of duplication.
Additionally, sbt uniformly integrates its task engine into the settings
system. The underlying settings engine has no notion of tasks. This is
why sbt uses a SettingKey
type and a TaskKey
type. Methods on an
underlying TaskKey[T]
are basically translated to operating on an
underlying SettingKey[Task[T]]
(and they both wrap an underlying
AttributeKey
).
For example, a := 3
for a SettingKey a will very roughly translate
to setting(a, value(3))
. For a TaskKey a, it will roughly translate
to setting(a, value( task { 3 } ) )
. See
main/Structure.scala for details.
sbt also provides a way to define these settings in a file (build.sbt
and Build.scala
). This is done for build.sbt
using basic parsing and
then passing the resulting chunks of code to compile/Eval.scala
. For
all definitions, sbt manages the classpaths and recompilation process to
obtain the settings. It also provides a way for users to define project,
task, and configuration delegation, which ends up being used by the
delegates
function.