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 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