sbt/src/sphinx/Detailed-Topics/Tasks.rst

660 lines
21 KiB
ReStructuredText

=====
Tasks
=====
Tasks and settings are introduced in the :doc:`getting started guide </Getting-Started/Basic-Def>`,
which you may wish to read first.
This page has additional details and background and is intended more as a reference.
Introduction
============
Both settings and tasks produce values, but there are two major
differences between them:
1. Settings are evaluated at project load time. Tasks are executed on
demand, often in response to a command from the user.
2. At the beginning of project loading, settings and their dependencies
are fixed. Tasks can introduce new tasks during execution, however.
Features
========
There are several features of the task system:
1. By integrating with the settings system, tasks can be added, removed,
and modified as easily and flexibly as settings.
2. :doc:`Input Tasks </Extending/Input-Tasks>` use :doc:`parser combinators <Parsing-Input>` to define the syntax for their arguments.
This allows flexible syntax and tab-completions in the same way as :doc:`/Extending/Commands`.
3. Tasks produce values. Other tasks can access a task's value by calling `value` on it within a task definition.
4. Dynamically changing the structure of the task graph is possible.
Tasks can be injected into the execution graph based on the result of another task.
5. There are ways to handle task failure, similar to `try/catch/finally`.
6. Each task has access to its own Logger that by default persists the
logging for that task at a more verbose level than is initially
printed to the screen.
These features are discussed in detail in the following sections.
Defining a Task
===============
Hello World example (sbt)
-------------------------
build.sbt
::
lazy val hello = taskKey[Unit]("Prints 'Hello World'")
hello := println("hello world!")
Run "sbt hello" from command line to invoke the task. Run "sbt tasks" to
see this task listed.
Define the key
--------------
To declare a new task, define a lazy val of type `TaskKey`:
::
lazy val sampleTask = taskKey[Int]("A sample task.")
The name of the `val` is used when referring to the task in Scala code and at the command line.
The string passed to the `taskKey` method is a description of the task.
The type parameter passed to `taskKey` (here, `Int`) is the type of value produced by the task.
We'll define a couple of other keys for the examples:
::
lazy val intTask = taskKey[Int]("An int task")
lazy val stringTask = taskKey[String]("A string task")
The examples themselves are valid entries in a `build.sbt` or can be
provided as part of a sequence to `Project.settings` (see
:doc:`Full Configuration </Getting-Started/Full-Def>`).
Implement the task
------------------
There are three main parts to implementing a task once its key is
defined:
1. Determine the settings and other tasks needed by the task. They are
the task's inputs.
2. Define the code that implements the task in terms of these inputs.
3. Determine the scope the task will go in.
These parts are then combined just like the parts of a setting are combined.
Defining a basic task
~~~~~~~~~~~~~~~~~~~~~
A task is defined using `:=`
::
intTask := 1 + 2
stringTask := System.getProperty("user.name")
sampleTask := {
val sum = 1 + 2
println("sum: " + sum)
sum
}
As mentioned in the introduction, a task is evaluated on demand.
Each time `sampleTask` is invoked, for example, it will print the sum.
If the username changes between runs, `stringTask` will take different values in those separate runs.
(Within a run, each task is evaluated at most once.)
In contrast, settings are evaluated once on project load and are fixed until the next reload.
Tasks with inputs
~~~~~~~~~~~~~~~~~
Tasks with other tasks or settings as inputs are also defined using `:=`.
The values of the inputs are referenced by the `value` method. This method
is special syntax and can only be called when defining a task, such as in the
argument to `:=`. The following defines a task that adds one to the value
produced by `intTask` and returns the result.
::
sampleTask := intTask.value + 1
Multiple settings are handled similarly:
::
stringTask := "Sample: " + sampleTask.value + ", int: " + intTask.value
Task Scope
~~~~~~~~~~
As with settings, tasks can be defined in a specific scope. For example,
there are separate :key:`compile` tasks for the `compile` and `test`
scopes. The scope of a task is defined the same as for a setting. In the
following example, `test:sampleTask` uses the result of
`compile:intTask`.
::
sampleTask in Test := (intTask in Compile).value * 3
On precedence
~~~~~~~~~~~~~
As a reminder, infix method precedence is by the name of the method and postfix methods have lower precedence than infix methods.
1. Assignment methods have the lowest precedence. These are methods with
names ending in `=`, except for `!=`, `<=`, `>=`, and names
that start with `=`.
2. Methods starting with a letter have the next highest precedence.
3. Methods with names that start with a symbol and aren't included in 1.
have the highest precedence. (This category is divided further
according to the specific character it starts with. See the Scala
specification for details.)
Therefore, the the previous example is equivalent to the following:
::
(sampleTask in Test).:=( (intTask in Compile).value * 3 )
Additionally, the braces in the following are necessary:
::
helloTask := { "echo Hello" ! }
Without them, Scala interprets the line as `( helloTask.:=("echo Hello") ).!`
instead of the desired `helloTask.:=( "echo Hello".! )`.
Separating implementations
--------------------------
The implementation of a task can be separated from the binding.
For example, a basic separate definition looks like:
::
// Define a new, standalone task implemention
lazy val intTaskImpl: Initialize[Task[Int]] =
Def.task { sampleTask.value - 3 }
// Bind the implementation to a specific key
intTask := intTaskImpl.value
Note that whenever `.value` is used, it must be within a task definition, such as
within `Def.task` above or as an argument to `:=`.
Modifying an Existing Task
--------------------------
In the general case, modify a task by declaring the previous task as an
input.
::
// initial definition
intTask := 3
// overriding definition that references the previous definition
intTask := intTask.value + 1
Completely override a task by not declaring the previous task as an
input. Each of the definitions in the following example completely
overrides the previous one. That is, when `intTask` is run, it will
only print `#3`.
::
intTask := {
println("#1")
3
}
intTask := {
println("#2")
5
}
intTask := {
println("#3")
sampleTask.value - 3
}
.. _multiple-scopes:
Getting values from multiple scopes
===================================
Introduction
------------
The general form of an expression that gets values from multiple scopes is:
::
<setting-or-task>.all(<scope-filter>).value
The `all` method is implicitly added to tasks and settings.
It accepts a `ScopeFilter` that will select the `Scopes`.
The result has type `Seq[T]`, where `T` is the key's underlying type.
Example
-------
A common scenario is getting the sources for all subprojects for processing all at once, such as passing them to scaladoc.
The task that we want to obtain values for is :key:`sources` and we want to get the values in all non-root projects and in the `Compile` configuration.
This looks like:
::
lazy val core = project
lazy val util = project
lazy val root = project.settings(
sources := {
val filter = ScopeFilter( inProjects(core, util), inConfigurations(Compile) )
// each sources definition is of type Seq[File],
// giving us a Seq[Seq[File]] that we then flatten to Seq[File]
val allSources: Seq[Seq[File]] = sources.all(filter).value
allSources.flatten
}
)
The next section describes various ways to construct a ScopeFilter.
ScopeFilter
-----------
A basic `ScopeFilter` is constructed by the `ScopeFilter.apply` method.
This method makes a `ScopeFilter` from filters on the parts of a `Scope`: a `ProjectFilter`, `ConfigurationFilter`, and `TaskFilter`.
The simplest case is explicitly specifying the values for the parts:
::
val filter: ScopeFilter =
ScopeFilter(
inProjects( core, util ),
inConfigurations( Compile, Test )
)
Unspecified filters
~~~~~~~~~~~~~~~~~~~
If the task filter is not specified, as in the example above, the default is to select scopes without a specific task (global).
Similarly, an unspecified configuration filter will select scopes in the global configuration.
The project filter should usually be explicit, but if left unspecified, the current project context will be used.
More on filter construction
~~~~~~~~~~~~~~~~~~~~~~~~~~~
The example showed the basic methods `inProjects` and `inConfigurations`.
This section describes all methods for constructing a `ProjectFilter`, `ConfigurationFilter`, or `TaskFilter`.
These methods can be organized into four groups:
* Explicit member list (`inProjects`, `inConfigurations`, `inTasks`)
* Global value (`inGlobalProject`, `inGlobalConfiguration`, `inGlobalTask`)
* Default filter (`inAnyProject`, `inAnyConfiguration`, `inAnyTask`)
* Project relationships (`inAggregates`, `inDependencies`)
See the `API documentation <../../api/sbt/ScopeFilter$$Make.html>`_ for details.
Combining ScopeFilters
~~~~~~~~~~~~~~~~~~~~~~
`ScopeFilters` may be combined with the `&&`, `||`, `--`, and `-` methods:
a && b
Selects scopes that match both `a` and `b`
a || b
Selects scopes that match either `a` or `b`
a -- b
Selects scopes that match `a` but not `b`
\-b
Selects scopes that do not match `b`
For example, the following selects the scope for the `Compile` and `Test` configurations of the `core` project
and the global configuration of the `util` project:
::
val filter: ScopeFilter =
ScopeFilter( inProjects(core), inConfigurations(Compile, Test)) ||
ScopeFilter( inProjects(util), inGlobalConfiguration )
More operations
---------------
The `all` method applies to both settings (values of type `Initialize[T]`)
and tasks (values of type `Initialize[Task[T]]`).
It returns a setting or task that provides a `Seq[T]`, as shown in this table:
==================== =========================
Target Result
==================== =========================
Initialize[T] Initialize[Seq[T]]
Initialize[Task[T]] Initialize[Task[Seq[T]]]
==================== =========================
This means that the `all` method can be combined with methods that construct tasks and settings.
Missing values
~~~~~~~~~~~~~~
Some scopes might not define a setting or task.
The `?` and `??` methods can help in this case.
They are both defined on settings and tasks and indicate what to do when a key is undefined.
`?`
On a setting or task with underlying type `T`, this accepts no arguments and returns a setting or task (respectively) of type `Option[T]`.
The result is `None` if the setting/task is undefined and `Some[T]` with the value if it is.
`??`
On a setting or task with underlying type `T`, this accepts an argument of type `T` and uses this argument if the setting/task is undefined.
The following contrived example sets the maximum errors to be the maximum of all aggregates of the current project.
::
maxErrors := {
// select the transitive aggregates for this project, but not the project itself
val filter: ScopeFilter =
ScopeFilter( inAggregates(ThisProject, includeRoot=false) )
// get the configured maximum errors in each selected scope,
// using 0 if not defined in a scope
val allVersions: Seq[Int] =
(maxErrors ?? 0).all(filter).value
allVersions.max
}
Multiple values from multiple scopes
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The target of `all` is any task or setting, including anonymous ones.
This means it is possible to get multiple values at once without defining a new task or setting in each scope.
A common use case is to pair each value obtained with the project, configuration, or full scope it came from.
:key:`resolvedScoped`
Provides the full enclosing `ScopedKey` (which is a `Scope` + `AttributeKey[_]`)
:key:`thisProject`
Provides the `Project` associated with this scope (undefined at the global and build levels)
:key:`thisProjectRef`
Provides the `ProjectRef` for the context (undefined at the global and build levels)
:key:`configuration`
Provides the `Configuration` for the context (undefined for the global configuration)
For example, the following defines a task that prints non-Compile configurations that define
sbt plugins. This might be used to identify an incorrectly configured build (or not, since this is
a fairly contrived example):
::
// Select all configurations in the current project except for Compile
lazy val filter: ScopeFilter = ScopeFilter(
inProjects(ThisProject),
inAnyConfiguration -- inConfigurations(Compile)
)
// Define a task that provides the name of the current configuration
// and the set of sbt plugins defined in the configuration
lazy val pluginsWithConfig: Initialize[Task[ (String, Set[String]) ]] =
Def.task {
( configuration.value.name, definedSbtPlugins.value )
}
checkPluginsTask := {
val oddPlugins: Seq[(String, Set[String])] =
pluginsWithConfig.all(filter).value
// Print each configuration that defines sbt plugins
for( (config, plugins) <- oddPlugins if plugins.nonEmpty )
println(s"$config defines sbt plugins: ${plugins.mkString(", ")}")
}
Advanced Task Operations
========================
The examples in this section use the task keys defined in the previous section.
Streams: Per-task logging
-------------------------
Per-task loggers are part of a more general system for task-specific data called Streams.
This allows controlling the verbosity of stack traces and logging individually for tasks as well
as recalling the last logging for a task.
Tasks also have access to their own persisted binary or text data.
To use Streams, get the value of the :key:`streams` task. This is a
special task that provides an instance of
`TaskStreams <../../api/sbt/std/TaskStreams.html>`_
for the defining task. This type provides access to named binary and
text streams, named loggers, and a default logger. The default
`Logger <../../api/sbt/Logger.html>`_,
which is the most commonly used aspect, is obtained by the `log`
method:
::
myTask := {
val s: TaskStreams = streams.value
s.log.debug("Saying hi...")
s.log.info("Hello!")
}
You can scope logging settings by the specific task's scope:
::
logLevel in myTask := Level.Debug
traceLevel in myTask := 5
To obtain the last logging output from a task, use the `last` command:
.. code-block:: console
$ last myTask
[debug] Saying hi...
[info] Hello!
The verbosity with which logging is persisted is controlled using the
:key:`persistLogLevel` and :key:`persistTraceLevel` settings. The `last`
command displays what was logged according to these levels. The levels
do not affect already logged information.
Dynamic Computations with `Def.taskDyn`
---------------------------------------
It can be useful to use the result of a task to determine the
next tasks to evaluate. This is done using `Def.taskDyn`. The
result of `taskDyn` is called a dynamic task because it introduces
dependencies at runtime. The `taskDyn` method supports the same
syntax as `Def.task` and `:=` except that you return a task instead
of a plain value.
For example, ::
val dynamic = Def.taskDyn {
// decide what to evaluate based on the value of `stringTask`
if(stringTask.value == "dev")
// create the dev-mode task: this is only evaluated if the
// value of stringTask is "dev"
Def.task {
3
}
else
// create the production task: only evaluated if the value
// of the stringTask is not "dev"
Def.task {
intTask.value + 5
}
}
myTask := {
val num = dynamic.value
println(s"Number selected was $num")
}
The only static dependency of `myTask` is `stringTask`.
The dependency on `intTask` is only introduced in non-dev mode.
.. note::
A dynamic task cannot refer to itself or a circular dependency will result.
In the example above, there would be a circular dependency if the code passed to `taskDyn` referenced `myTask`.
Handling Failure
----------------
This section discusses the `failure`, `result`, and `andFinally`
methods, which are used to handle failure of other tasks.
`failure`
~~~~~~~~~~~
The `failure` method creates a new task that returns the `Incomplete` value
when the original task fails to complete normally. If the original task succeeds,
the new task fails.
`Incomplete <../../api/sbt/Incomplete.html>`_
is an exception with information about any tasks that caused the failure
and any underlying exceptions thrown during task execution.
For example:
::
intTask := error("Failed.")
intTask := {
println("Ignoring failure: " + intTask.failure.value)
3
}
This overrides the `intTask` so that the original exception is printed and the constant `3` is returned.
`failure` does not prevent other tasks that depend on the target
from failing. Consider the following example:
::
intTask := if(shouldSucceed) 5 else error("Failed.")
// Return 3 if intTask fails. If intTask succeeds, this task will fail.
aTask := intTask.failure.value - 2
// A new task that increments the result of intTask.
bTask := intTask.value + 1
cTask := aTask.value + bTask.value
The following table lists the results of each task depending on the initially invoked task:
============== =============== ============= ============== ============== ==============
invoked task intTask result aTask result bTask result cTask result overall result
============== =============== ============= ============== ============== ==============
intTask failure not run not run not run failure
aTask failure success not run not run success
bTask failure not run failure not run failure
cTask failure success failure failure failure
intTask success not run not run not run success
aTask success failure not run not run failure
bTask success not run success not run success
cTask success failure success failure failure
============== =============== ============= ============== ============== ==============
The overall result is always the same as the root task (the directly
invoked task). A `failure` turns a success into a failure, and a failure into an `Incomplete`.
A normal task definition fails when any of its inputs fail and computes its value otherwise.
`result`
~~~~~~~~~~
The `result` method creates a new task that returns the full `Result[T]` value for the original task.
`Result <../../api/sbt/Result.html>`_
has the same structure as `Either[Incomplete, T]` for a task result of
type `T`. That is, it has two subtypes:
- `Inc`, which wraps `Incomplete` in case of failure
- `Value`, which wraps a task's result in case of success.
Thus, the task created by `result` executes whether or not the original task succeeds or fails.
For example:
::
intTask := error("Failed.")
intTask := intTask.result.value match {
case Inc(inc: Incomplete) =>
println("Ignoring failure: " + inc)
3
case Value(v) =>
println("Using successful result: " + v)
v
}
This overrides the original `intTask` definition so that if the original task fails, the exception is printed and the constant `3` is returned. If it succeeds, the value is printed and returned.
andFinally
~~~~~~~~~~
The `andFinally` method defines a new task that runs the original task
and evaluates a side effect regardless of whether the original task
succeeded. The result of the task is the result of the original task.
For example:
::
intTask := error("I didn't succeed.")
lazy val intTaskImpl = intTask andFinally { println("andFinally") }
intTask := intTaskImpl.value
This modifies the original `intTask` to always print "andFinally" even
if the task fails.
Note that `andFinally` constructs a new task. This means that the new
task has to be invoked in order for the extra block to run. This is
important when calling andFinally on another task instead of overriding
a task like in the previous example. For example, consider this code:
::
intTask := error("I didn't succeed.")
lazy val intTaskImpl = intTask andFinally { println("andFinally") }
otherIntTask := intTaskImpl.value
If `intTask` is run directly, `otherIntTask` is never involved in
execution. This case is similar to the following plain Scala code:
::
def intTask(): Int =
error("I didn't succeed.")
def otherIntTask(): Int =
try { intTask() }
finally { println("finally") }
intTask()
It is obvious here that calling intTask() will never result in "finally"
being printed.