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:
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)
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
Global (no specific value),
This
(current context), or
Select (containing a specific value). sbt
resolves This_
to either
Global 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
Global. 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 HLists 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.