Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/sbt/sbt/llms.txt

Use this file to discover all available pages before exploring further.

Overview

In sbt, there are two fundamental types of keys: Settings and Tasks. Understanding the difference is crucial for writing effective build definitions.

Settings

Settings are evaluated once when the project is loaded. They represent fixed values that don’t change during the build session.

Defining Settings

From Keys.scala:56-58:
val logLevel = settingKey[Level.Value](
  "The amount of logging to display."
).withRank(ASetting)
Settings use the := operator for assignment:
name := "my-project"
version := "1.0.0"
scalaVersion := "3.3.1"

Common Settings from Keys.scala

// Project metadata (Keys.scala:147-154)
val baseDirectory = settingKey[File](
  "The base directory for the build, project, configuration, or task."
)

val sourceDirectory = settingKey[File](
  "Default directory containing sources."
)

val scalaVersion = settingKey[String](
  "The version of Scala used for building."
)

// Compilation settings (Keys.scala:227-235)
val scalaOrganization = settingKey[String](
  "Organization/group ID of the Scala used in the project."
)

val scalaBinaryVersion = settingKey[String](
  "The Scala version substring describing binary compatibility."
)

val crossScalaVersions = settingKey[Seq[String]](
  "The versions of Scala used when cross-building."
)

val classpathOptions = settingKey[ClasspathOptions](
  "Configures handling of Scala classpaths."
)

Tasks

Tasks are evaluated on demand when explicitly invoked or when needed by other tasks. They can have side effects and represent computational work.

Defining Tasks

From Keys.scala:258-262:
val clean = taskKey[Unit](
  "Deletes files produced by the build, such as generated sources, compiled classes, and task caches."
).withRank(APlusTask)

val console = taskKey[Unit](
  "Starts the Scala interpreter with the project classes on the classpath."
).withRank(APlusTask)

val compile = taskKey[CompileAnalysis](
  "Compiles sources."
).withRank(APlusTask)

Common Tasks

// Compilation tasks (Keys.scala:262-274)
val compile = taskKey[CompileAnalysis]("Compiles sources.")
val manipulateBytecode = taskKey[CompileResult]("Manipulates generated bytecode")
val compileIncremental = taskKey[(Boolean, VirtualFileRef, HashedVirtualFileRef)](
  "Actually runs the incremental compilation"
)

// Package tasks (Keys.scala:314-317)
val packageBin = taskKey[HashedVirtualFileRef](
  "Produces a main artifact, such as a binary jar."
)
val packageDoc = taskKey[HashedVirtualFileRef](
  "Produces a documentation artifact."
)
val packageSrc = taskKey[HashedVirtualFileRef](
  "Produces a source artifact."
)

// Resource tasks (Keys.scala:178-184)
val unmanagedResources = taskKey[Seq[File]](
  "Unmanaged resources, which are manually created."
)
val managedResources = taskKey[Seq[File]](
  "Resources generated by the build."
)
val resources = taskKey[Seq[File]](
  "All resource files, both managed and unmanaged."
)

Key Operators

Assignment (:=)

Replaces the previous value entirely:
scalacOptions := Seq("-deprecation", "-feature")

Append (+=)

Adds a single element:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test

Append Sequence (++=)

Adds multiple elements:
scalacOptions ++= Seq(
  "-encoding", "UTF-8",
  "-deprecation",
  "-feature",
  "-unchecked"
)
From the sbt build.sbt:26-41:
ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8")
ThisBuild / Compile / doc / javacOptions := Nil

ThisBuild / developers := List(
  Developer("harrah", "Mark Harrah", "@harrah", url("https://github.com/harrah")),
  Developer("eed3si9n", "Eugene Yokota", "@eed3si9n", url("https://github.com/eed3si9n")),
  Developer("jsuereth", "Josh Suereth", "@jsuereth", url("https://github.com/jsuereth"))
)

Dependency on Other Settings/Tasks

Use .value to reference other settings or tasks:
sourceDirectories := Seq(
  baseDirectory.value / "src",
  baseDirectory.value / "extra-src"
)

compile := {
  val classes = (Compile / compile).value
  // Additional compilation logic
  classes
}

Task Execution Model

Settings Evaluation

Settings are evaluated in dependency order when the build loads:
// These are all evaluated once at load time
val scalaV = scalaVersion.value         // "3.3.1"
val binV = scalaBinaryVersion.value     // "3"
val name = (thisProject / name).value   // "my-project"

Task Execution

Tasks run when invoked and can depend on other tasks:
// Task that depends on compile
packageBin := {
  val analysis = compile.value  // Runs compile first
  val classes = classDirectory.value
  // Package the compiled classes
  // ...
}
Tasks are re-executed every time they’re invoked, unless cached. Settings are evaluated exactly once at project load.

Defining Custom Keys

Custom Setting Key

From the macro implementation (main-settings/src/main/scala/sbt/Def.scala:32-34):
inline def settingKey[A](inline description: String): SettingKey[A] =
  ${ std.KeyMacro.settingKeyImpl[A]('description) }
Usage:
val myCustomSetting = settingKey[String]("A custom configuration value")

myCustomSetting := "custom value"

Custom Task Key

From Def.scala:36-37:
inline def taskKey[A](inline description: String): TaskKey[A] =
  ${ std.KeyMacro.taskKeyImpl[A]('description) }
Usage:
val generateDocs = taskKey[Seq[File]]("Generate custom documentation")

generateDocs := {
  val log = streams.value.log
  val srcDir = (Compile / sourceDirectory).value
  log.info("Generating documentation...")
  // Custom doc generation logic
  Seq.empty[File]
}

Real-World Example from sbt

From Defaults.scala:801-832:
scalaDynVersion := {
  val sv = scalaVersion.value
  val log = streams.value.log
  LibraryManagement.resolveDynamicScalaVersion(sv, log)
}

consoleProject / scalaInstance := {
  val topLoader = classOf[org.jline.terminal.Terminal].getClassLoader
  val scalaProvider = appConfiguration.value.provider.scalaProvider
  val allJars = scalaProvider.jars
  val libraryJars = allJars.filter { jar =>
    jar.getName == "scala-library.jar" || 
    jar.getName.startsWith("scala3-library_3")
  }
  val compilerJar = allJars.filter { jar =>
    jar.getName == "scala-compiler.jar" || 
    jar.getName.startsWith("scala3-compiler_3")
  }
  Compiler.makeScalaInstance(
    scalaProvider.version,
    libraryJars,
    allJars.toSeq,
    Seq.empty,
    state.value,
    topLoader,
  )
}

crossScalaVersions := Seq(scalaVersion.value)

Input Tasks

Input tasks accept command-line arguments: From Keys.scala:337-338:
val run = inputKey[Unit | ClientJobParams](
  "Runs a main class, passing along arguments provided on the command line."
)

val runMain = inputKey[Unit | ClientJobParams](
  "Runs the main class selected by the first argument."
)
Defining custom input tasks:
val greet = inputKey[Unit]("Greet someone")

greet := {
  val args = Def.spaceDelimited("<name>").parsed
  val name = args.headOption.getOrElse("World")
  println(s"Hello, $name!")
}

Task Dependencies

When a task depends on another task using .value, sbt ensures the dependency executes first and passes its result to the dependent task.
val customCompile = taskKey[Unit]("Custom compilation")

customCompile := {
  // These tasks execute in order
  val generated = (Compile / sourceGenerators).value
  val analysis = (Compile / compile).value
  streams.value.log.info("Custom compile completed")
}

Best Practices

Use settings for configuration, tasks for computation: If a value never changes, make it a setting. If it requires computation or I/O, make it a task.
Minimize task execution: Tasks can be expensive. Consider using settings when the value can be computed at load time.
// Good - computed once at load time
val javaVer = settingKey[String]("Java version")
javaVer := sys.props("java.version")

// Overkill - doesn't need to be a task
val javaVerTask = taskKey[String]("Java version")
javaVerTask := {
  sys.props("java.version")
}

References