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, every setting and task key exists within a scope. Scopes allow you to have different values for the same key in different contexts, such as different projects, configurations, or tasks.
The Scope Axes
From the source code (main-settings/src/main/scala/sbt/Scope.scala:21-49):
final case class Scope private (
project: ScopeAxis[Reference],
config: ScopeAxis[ConfigKey],
task: ScopeAxis[AttributeKey[?]],
extra: ScopeAxis[AttributeMap]
)
object Scope:
val ThisScope: Scope = new Scope(This, This, This, This)
val Global: Scope = new Scope(Zero, Zero, Zero, Zero)
val ThisBuildScope: Scope = Scope(Select(ThisBuild), This, This, This)
val GlobalScope: Scope = Global
A scope has four axes:
- Project axis - Which project (in multi-project builds)
- Configuration axis - Which configuration (Compile, Test, Runtime, etc.)
- Task axis - Which task the setting applies to
- Extra axis - Custom scope dimensions (rarely used)
Scope Syntax
sbt uses the slash (/) operator to specify scopes:
// Syntax: project / config / task / key
Test / sourceDirectory // Test configuration
Compile / compile / scalacOptions // compile task in Compile config
ThisBuild / version // Applies to all projects
The modern slash syntax (/) replaced the older in syntax. The slash syntax is more concise and recommended.
Configuration Scope
Configurations are the most commonly used scope axis. sbt has several built-in configurations:
Built-in Configurations
// From librarymanagement library
Compile // Main sources and compilation
Test // Test sources and compilation
Runtime // Runtime dependencies
Provided // Compile-time only dependencies
Configuration Examples
From Keys.scala and practical usage:
// Different source directories per configuration
Compile / sourceDirectory := baseDirectory.value / "src" / "main"
Test / sourceDirectory := baseDirectory.value / "src" / "test"
// Different Scala options per configuration
Compile / scalacOptions ++= Seq("-deprecation", "-feature")
Test / scalacOptions ++= Seq("-deprecation")
// Different dependencies per configuration
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test
From the sbt build.sbt:155-159:
Compile / generateContrabands / sourceManaged :=
baseDirectory.value / "src" / "main" / "contraband-scala"
Compile / managedSourceDirectories +=
baseDirectory.value / "src" / "main" / "contraband-scala"
Compile / generateContrabands / contrabandScala3enum := false
Task Scope
Settings can be scoped to specific tasks:
// Scala options specifically for the console task
console / scalacOptions := {
(Compile / scalacOptions).value.filterNot(_ == "-Xfatal-warnings")
}
// Fork options for the run task
run / fork := true
run / javaOptions += "-Xmx2G"
// Output strategy for test
Test / test / outputStrategy := Some(StdoutOutput)
From Keys.scala:192-196:
val run / traceLevel := 0
val runMain / traceLevel := 0
val bgRun / traceLevel := 0
val fgRun / traceLevel := 0
val console / traceLevel := Int.MaxValue
Project Scope
In multi-project builds, you can scope settings to specific projects:
lazy val core = project
.settings(
name := "core",
version := "1.0.0"
)
lazy val app = project
.dependsOn(core)
.settings(
name := "app",
// Reference core project's setting
version := (core / version).value
)
ThisBuild Scope
Settings scoped to ThisBuild apply to all projects in the build:
From build.sbt:13-25:
ThisBuild / version := "2.0.0-RC9-bin-SNAPSHOT"
ThisBuild / organization := "org.scala-sbt"
ThisBuild / description := "sbt is an interactive build tool"
ThisBuild / licenses := List(
"Apache-2.0" -> url("https://github.com/sbt/sbt/blob/develop/LICENSE")
)
ThisBuild / javacOptions ++= Seq("-source", "1.8", "-target", "1.8")
ThisBuild / Compile / doc / javacOptions := Nil
Use ThisBuild for settings that should be consistent across all subprojects, like organization, version scheme, or compiler flags.
Global Scope
Global scope settings apply everywhere:
// Global settings from Defaults.scala:151-169
Global / excludeFilter := HiddenFileFilter
Global / onChangedBuildSource := SysProp.onChangedBuildSource
Global / concurrentRestrictions += Tags.limitAll(1)
Use global scope sparingly - most settings should be scoped more specifically to avoid unexpected behavior.
Scope Delegation
When a key is not defined in a specific scope, sbt looks for it in more general scopes through scope delegation.
Delegation Order
From Scope.scala and the delegation algorithm:
- Current scope exactly
- Project axis fallback: Current project →
ThisBuild → Global
- Configuration axis fallback: Current config → Parent configs →
Global
- Task axis fallback: Current task →
Global
Example
When you reference Test / scalacOptions:
- Look for
Test / scalacOptions
- If not found, try
scalacOptions (Zero config)
- If not found, try
Global / scalacOptions
// Defined in Compile
Compile / scalacOptions := Seq("-deprecation")
// Test inherits from Compile through delegation
// Unless explicitly overridden:
Test / scalacOptions := (Compile / scalacOptions).value ++ Seq("-Xfatal-warnings")
Inspecting Scopes
Use sbt’s inspect command to see scope information:
sbt:project> inspect Test / scalacOptions
Output shows:
- Current value
- Scope delegation path
- Dependencies
- Reverse dependencies
Practical Examples
Configuration-Specific Settings
// Different source directories
Compile / scalaSource := baseDirectory.value / "src" / "main" / "scala"
Test / scalaSource := baseDirectory.value / "src" / "test" / "scala"
// Different resource directories
Compile / resourceDirectory := baseDirectory.value / "src" / "main" / "resources"
Test / resourceDirectory := baseDirectory.value / "src" / "test" / "resources"
// Different unmanagedSourceDirectories
Compile / unmanagedSourceDirectories := Seq(
(Compile / scalaSource).value,
(Compile / javaSource).value
)
Task-Specific Settings
From Keys.scala and Defaults.scala:
// Specific options for the doc task
Compile / doc / scalacOptions ++= Seq(
"-doc-title", name.value,
"-doc-version", version.value
)
// Forking configuration for run
run / fork := true
run / connectInput := true
run / javaOptions ++= Seq(
"-Xmx4G",
"-XX:+UseG1GC"
)
Multi-Configuration Example
From sbt’s build (build.sbt:101-110):
Compile / javafmtOnCompile := Def
.taskDyn(if ((scalafmtOnCompile).value) Compile / javafmt else Def.task(()))
.value
Test / javafmtOnCompile := Def
.taskDyn(if ((Test / scalafmtOnCompile).value) Test / javafmt else Def.task(()))
.value
Compile / unmanagedSources / inputFileStamps :=
(Compile / unmanagedSources / inputFileStamps).dependsOn(Compile / javafmtOnCompile).value
Custom Configurations
You can define custom configurations:
val IntegrationTest = config("it") extend(Test)
lazy val root = project
.configs(IntegrationTest)
.settings(
inConfig(IntegrationTest)(Defaults.testSettings),
IntegrationTest / scalaSource := baseDirectory.value / "src" / "it" / "scala"
)
From ProjectExtra.scala:158-159:
def inConfig(conf: Configuration)(ss: Seq[Setting[?]]): Seq[Setting[?]] =
Project.inScope(ThisScope.copy(config = Select(conf)))((Keys.configuration :== conf) +: ss)
Scope Best Practices
Most specific scope wins: When defining settings, use the most specific scope that makes sense. This makes your build definition clearer and more maintainable.
Use ThisBuild for cross-cutting concerns: Version, organization, and common compiler flags are good candidates for ThisBuild scope.
Avoid over-scoping: Don’t scope settings unnecessarily. If a setting applies to all configurations, define it without a configuration scope.
// Good - applies to all configurations
scalacOptions += "-encoding" -> "UTF-8"
// Unnecessary - don't do this
Compile / scalacOptions += "-encoding" -> "UTF-8"
Test / scalacOptions += "-encoding" -> "UTF-8"
Debugging Scopes
Commands to understand scoping:
# Show all tasks and settings
sbt> settings -V
# Show value and provenance
sbt> show Test / scalacOptions
# Detailed inspection
sbt> inspect Test / scalacOptions
sbt> inspect tree Test / compile
References