CustomActions  

Introduction

This page describes how to define your own actions and how to modify the builtin actions. For information on writing the code that implements the action, see ActionContent.

Creating Actions

Create a new action by constructing a Task instance and assigning it to a lazy val. The name of the val is used as the name of the action after transforming camel-case style to be dash separated (for example, testCompile becomes test-compile). A Task is constructed with the task method and represents a unit of work. The argument to task is the code to execute when the task is invoked. Return None if the task executes successfully or return an error message wrapped in Some if it does not. The log method (of type sbt.Logger) can be used for logging information.

Example:

  lazy val print = task { log.info("This is a test."); None }

The val is required so that sbt can discover the task using reflection and so that the task can be used as a dependency of other tasks. Using lazy avoids initialization issues.

You specify the dependencies of a task with the dependsOn method. You provide a description using the describedAs method.

Example:

  lazy val jars = task { ... } dependsOn(compile, doc) describedAs("Package classes and API docs.")
  lazy val compile = ...
  lazy val doc = ...

Note: The dependencies that you refer to must be assigned to a val. The following would be incorrect:

  //Incorrect example:
  lazy val jar = task { ... } dependsOn(compile, test)
  lazy val test = task { ... } dependsOn(compile)
  def compile = task { ... }

In this case, jar depends on a different compile task than test does because the compile method returns a different object each time it is called. So, the compile task would run twice when jar is executed.

For information on writing the code that implements the action, see ActionContent.

Modifying Actions

Most of the builtin actions (such as clean, test, run, doc, and package) are defined using the following pattern, shown here for the package action:

  lazy val `package` = packageAction
  def packageAction = packageTask(packagePaths, jarPath, packageOptions).dependsOn(compile) describedAs PackageDescription
  def packageTask(sources: PathFinder, outputDirectory: Path, jarName: => String, options: => Seq[PackageOption]): Task = task { ... }

This approach is intended to provide flexibility for when you need to change the default behavior. The first statement binds a new action called package to the task returned by packageAction. It is not possible to use super when overriding a lazy val, so a separate method is used (in this case, packageAction). This second method is the method to override in order to change an action's behavior. A third method provides the actual behavior for the task that can be used to create new tasks.

There are two main scenarios that require modifying the default actions. The first is changing the behavior of a default action. For example, you might want to create a different file than a jar file. The second is changing the dependencies of the default tasks. For example, you might want to write a task that preprocesses your sources before compilation. You'd want to make the compile task depend on the preprocessor task. In the first case, you would completely redefine packageAction to be:

  override def packageAction = task { createCustomFile() } dependsOn(compile) describedAs("Creates a war file.")

Again, note that the compile dependency is referenced by the lazy val it is assigned to. That is, do dependsOn(compile) and not dependsOn(compileAction).

In the second case, you just want to change the dependencies, so you do something like:

  lazy val preprocess = task { ... }
  override def compileAction = super.compileAction dependsOn(preprocess)

Conditional Task Execution

Many tasks generate output files (products) from input files (sources). This section describes how to specify that a task should only do its work if its products are out of date.

Products Only

The simplest case is a task that generates a group of files and has no inputs. Declare a task of this kind using the fileTask method with the following signature:

  def fileTask(products: => Iterable[Path])(action: => Option[String]): Task

The first argument is a list of output files and the second argument is the action to perform when the task is invoked if any of the products provided in the first argument do not exist.

Known Products from Sources

The next case is a task that takes sources and generates a known group of products (that is, the files that will be produced are known before the task does its work). The task's work should be done if any of the products do not exist or if any of the products are older than any of the sources. Create a task of this kind in the following manner:

  fileTask(products from sources) { doWork }

where:

  • products is of type Path or Iterable[Path]
  • sources is of type Iterable[Path] or PathFinder (note that Path is a subtype of PathFinder)
  • doWork has type Option[String]

Special Tasks

There are some methods for constructing certain types of tasks:

  • You can use Method Tasks to create the task to be run from arguments given by the user.
  • execTask(ProcessBuilder) Creates a task that, when invoked, runs the provided process and checks its exit value. See Process for details on using the process API.
  • dynamic(=> Task) This creates a special task that, when invoked, evaluates its argument (which is call-by-name) and returns it as the root of a new subgraph of tasks to run. The subgraph must be entirely self-contained. That is, nodes in the subgraph cannot depend on statically defined tasks.