-
Notifications
You must be signed in to change notification settings - Fork 26
Design Overview
The main execution layers that comprise sbt are the launcher, the command engine, and the build tool, which is subdivided into project configuration and task execution.
Execution starts with the launcher, which is a self-contained jar that pulls down an application and its dependencies according to a configuration file and then runs it.
For sbt, this means getting the requested version of sbt (from project/build.properties
) and the right Scala version and then starting sbt itself.
The launcher also provides several services to launched applications, like reloading a different version of the application or retrieving other Scala versions.
It is possible to use the launcher to launch other applications as well as documented on the launcher page.
Conscript is a lightweight application distribution mechanism built on top of this functionality.
The next level is the command engine, which is where State and Commands come in.
When sbt is launched, it sets up an initial State that registers the default Commands (set, reload, alias, ...
) and sets the initial commands to run.
The initial commands are: add commands from .sbtrc
files, load the project, and enter the interactive prompt if no commands were specified.
Then, sbt starts the engine with MainLoop.runLogged(initialState)
, which runs until all commands are processed and then exits.
You can see this configuration in xMain.run, which is the entry-point to sbt.
State keeps the list of scheduled command strings in remainingCommands. runLogged processes State by:
- taking the next command string from remainingCommands (a
Seq[String]
) - parsing it according to the current
State
's definedCommands (aSeq[Command]
) - producing the command's
State => State
function from this parse - applying the function to the current
State
(running the command) - looping back to 1 if the new State has remainingCommands
There are actually two alternative entry-points to sbt that are defined below the main one, ScriptMain and ConsoleMain, that provide the different front-ends described on the Scripts page. In addition, the command engine can be used to write standalone interactive command line applications.
It is on top of this command engine that the build tool part of sbt is built.
The build tool part is kicked off with the command that loads the build: reload
(called reload
because users usually call it in the context of reloading the build).
The other important build-tool-related command runs tasks on a loaded build.
The reload
command's job is to produce a BuildStructure and put it in State for future commands like task execution to use.
BuildStructure is the data type that represents everything about a build: projects and relationships, evaluated settings, and logging configuration.
It produces a BuildStructure by evaluating build loaders.
The default build loader configures sbt using the standard .sbt
and project/Build.scala
files that you know.
The build loaders page shows an example of how one might write a build loader to read from a pom.xml instead (you couldn't define custom tasks or anything like that without another file, though).
Once the reload
command has the BuildStructure
value, it stores it in State.attributes
, keyed by Keys.stateBuildStructure
.
Now, a diversion back to State
to cover attributes...
State.attributes
is a typesafe map.
Keys are of type AttributeKey[T]
and you can only associate values of type T
with that key.
State
has convenience methods set/get that delegate to the underlying attributes
map.
To pass information between commands, you put data in the attributes
map.
An example of this is release plugin, which sets skipTests
according to the command line options.
The release
command itself schedules other commands to run and those can configure themselves from skipTests
in State.attributes
.
This is one way a command can change the behavior of tasks without needing to reload the project: it sets attributes in State
and the task accesses State
via the state
task.
The Project.extract(state)
call at its core calls state.get(Keys.stateBuildStructure)
to get the BuildStructure
back.
It does some other things as well:
- throws a nicer exception if a project isn't loaded
- loads the session with
state.get(Keys.sessionSettings)
- returns the session and structure in an Extracted value, which provides a better interface to them
The SessionSettings datatype tracks a few pieces of information that are not persisted. The two main pieces are:
- the current project: changed by the
project
command, for example - additional settings: added by the
set
command, for example
SessionSettings only tracks this information; setting these values on a SessionSettings
object does not apply the changes.
In particular, the project has to be reloaded for the additional settings to take effect.
Reloading checks the settings for problems like references to non-existing settings and then the settings are reevaluated.
The release plugin has a reapply method that shows the proper way to add settings to the current project.
Given a sequence of settings (Seq[Setting[_]]
), the Load.transformSettings
method resolves any unspecified scopes in the raw settings sequence.
This usually means associating the settings with the current project.
Then, BuiltinCommands.reapply
actually makes the settings take effect.
It checks, processes, and loads the new settings, updates BuildStructure
, and stuffs everything back into State
.
The task execution command pulls out the current, loaded project from State
(via Project.extract
), looks up the task to run, and runs it.
Commands can get the values produced by tasks, but tasks don't directly transform State
or run commands (there are some rare exceptions).
Tasks can get the current State
via the state
task, which is a special task that gets injected by the task execution command and set to the current State
.