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.
This guide covers best practices for plugin development, drawn from sbt’s own contribution guidelines and the patterns used in sbt’s core plugins.
Code Organization
Naming Conventions
Use descriptive names for large scopes
// BAD - unclear in larger scope
val x = taskKey[Unit]("")
val s = settingKey[String]("")
// GOOD - clear purpose
val publishArtifacts = taskKey[Unit]("Publishes artifacts")
val semanticdbVersion = settingKey[String]("Version of semanticdb")
Use short names for small scopes
// GOOD - small lambda scope
xs.map { x =>
val y = x - 1
y * y
}
Separate Concerns
Separate keys, settings, and implementation:
// Keys in separate file/trait
trait DependencyTreeKeys {
val dependencyTree = taskKey[Unit]("Shows dependency tree")
val dependencyDot = taskKey[File]("Generates dot file")
val dependencyTreeIncludeScalaLibrary = settingKey[Boolean]("Include Scala lib")
}
// Plugin definition
object DependencyTreePlugin extends AutoPlugin {
object autoImport extends DependencyTreeKeys
import autoImport._
override lazy val projectSettings =
DependencyTreeSettings.coreSettings ++
inConfig(Compile)(DependencyTreeSettings.baseSettings)
}
// Settings in separate object
object DependencyTreeSettings {
def coreSettings: Seq[Setting[_]] = Seq(/* ... */)
def baseSettings: Seq[Setting[_]] = Seq(/* ... */)
}
Functional Programming Practices
Prefer Pure Functions
// BAD - side effects, mutability
var cache: Map[String, String] = Map.empty
def getValue(key: String): String = {
if (cache.contains(key)) {
cache(key)
} else {
val value = expensiveComputation(key)
cache += (key -> value)
value
}
}
// GOOD - pure function
def getValue(key: String, cache: Map[String, String]): (String, Map[String, String]) = {
cache.get(key) match {
case Some(value) => (value, cache)
case None =>
val value = expensiveComputation(key)
(value, cache + (key -> value))
}
}
Use Option and Either Over null and Exception
// BAD
def findConfig(name: String): Config = {
val config = configs.find(_.name == name)
if (config == null) throw new Exception("Not found")
config
}
// GOOD
def findConfig(name: String): Option[Config] = {
configs.find(_.name == name)
}
// GOOD - with error detail
def findConfig(name: String): Either[String, Config] = {
configs.find(_.name == name)
.toRight(s"Configuration $name not found")
}
Scala 3 Style (for new code)
Use Fewer Braces Syntax
// BAD - old brace style
xs.map { x =>
val y = x - 1
y * y
}
// GOOD - Scala 3 style
xs.map: x =>
val y = x - 1
y * y
Use End Markers
// BAD
object MyPlugin {
def foo: Int = 1
}
// GOOD
object MyPlugin:
def foo: Int = 1
end MyPlugin
Example from sbt’s SbtPlugin:
object SbtPlugin extends AutoPlugin:
override def requires = ScriptedPlugin
override lazy val projectSettings: Seq[Setting[?]] = Seq(
sbtPlugin := true,
pluginCrossBuild / sbtVersion := {
scalaBinaryVersion.value match
case "3" => sbtVersion.value
case "2.12" => "1.5.8"
case "2.10" => "0.13.18"
},
)
end SbtPlugin
Avoid Infix Notation
// BAD - infix for non-symbolic methods
a foo 1
list contains item
// GOOD - explicit method calls
a.foo(1)
list.contains(item)
// OK - symbolic methods
a + b
list :+ item
Modular Design
Because sbt has 100+ plugins, careful design prevents breakage:
Maintain Binary Compatibility
Plugins published against sbt 2.0 must work on sbt 2.1. Breaking binary compatibility breaks user builds.
# Always check before releasing
sbt mimaReportBinaryIssues
Rules:
- Never remove public methods
- Never change method signatures
- Add new overloads instead of changing existing methods
- Use
@deprecated to phase out APIs
Hide Implementation Details
// GOOD - public API
package com.example.myplugin
object MyPlugin extends AutoPlugin {
object autoImport {
val myTask = taskKey[Unit]("Public task")
}
}
// GOOD - implementation hidden
package com.example.myplugin.internal
private[myplugin] object MyPluginImpl {
def complexLogic(): Unit = ???
}
Minimize Exposed Types
// BAD - exposes external library type
import org.somelib.ExternalType
val myTask = taskKey[ExternalType]("Returns external type")
// GOOD - use sbt/standard types
val myTask = taskKey[File]("Returns file")
val myOtherTask = taskKey[Seq[String]]("Returns strings")
If you change from org.somelib to org.anotherlib later, users’ builds break.
Settings Best Practices
Set Defaults at Widest Scope
From sbt’s coding guidelines:
object MyPlugin extends AutoPlugin {
// GOOD - global defaults
override lazy val globalSettings = Seq(
myGlobalFlag := false,
myBufferLog := true
)
// Project-specific overrides
override lazy val projectSettings = Seq(
myTask := {
if (myGlobalFlag.value) /* ... */
}
)
}
Example from ScriptedPlugin:
override lazy val globalSettings: Seq[Setting[?]] = Seq(
scriptedBufferLog := true,
scriptedLaunchOpts := Seq(),
scriptedKeepTempDirectory := false,
)
Avoid Capturing Machine-Specific Details
// BAD - captures absolute path
val myTask = taskKey[String]("")
myTask := {
"/Users/john/project/file.txt" // breaks on other machines
}
// GOOD - use sbt keys for paths
myTask := {
(baseDirectory.value / "file.txt").getAbsolutePath
}
Avoid Time-Dependent Behavior
// BAD - task behavior changes over time
val timestamp = System.currentTimeMillis()
// GOOD - deterministic unless explicitly requested
val buildTime = settingKey[Long]("Build timestamp")
buildTime := System.currentTimeMillis() // user controls when this runs
Documentation Standards
Use ScalaDoc for Public APIs
/**
* A plugin representing the ability to build a JVM project.
*
* Core tasks/keys:
* - `run`
* - `test`
* - `compile`
* - `fullClasspath`
* Core configurations
* - `Test`
* - `Compile`
*/
object JvmPlugin extends AutoPlugin {
// ...
}
Document Intent, Not Just Mechanics
// BAD - just repeating the code
/** Returns true if x > 0 */
def isPositive(x: Int): Boolean = x > 0
// GOOD - explains purpose and context
/**
* Checks if a dependency resolution should be retried.
* Returns true when the error is transient (network issues),
* false for permanent failures (artifact not found).
*/
def shouldRetry(error: ResolutionError): Boolean = ???
From sbt guidelines: avoid inline comments that repeat what code already says.
// BAD
val x = 5 // Set x to 5
x + 1 // Add 1 to x
// GOOD - comment explains why, not what
// Use 5 second timeout to accommodate slow CI environments
val timeout = 5
Plugin Activation Patterns
Always-Enabled Core Plugins
object CorePlugin extends AutoPlugin {
// Included by default in all projects
override def trigger = allRequirements
override def requires = empty
override lazy val projectSettings = Defaults.coreDefaultSettings
}
Dependency Chain Plugins
object IvyPlugin extends AutoPlugin {
// Auto-enabled when CorePlugin is present
override def requires = CorePlugin
override def trigger = allRequirements
override lazy val projectSettings =
Classpaths.ivyPublishSettings ++ Classpaths.ivyBaseSettings
}
object JvmPlugin extends AutoPlugin {
// Auto-enabled when IvyPlugin is present
override def requires = IvyPlugin
override def trigger = allRequirements
override lazy val projectSettings =
Defaults.runnerSettings ++
Defaults.paths ++
Classpaths.jvmPublishSettings
}
Opt-In Feature Plugins
object JUnitXmlReportPlugin extends AutoPlugin {
// Auto-enabled if requirements met, but can be disabled
override def requires = JvmPlugin
override def trigger = allRequirements
// Users can disable:
// myProject.disablePlugins(JUnitXmlReportPlugin)
}
Testing Guidelines
Always Add Tests
From CONTRIBUTING.md:
For changes with small scopes prefer unit tests. For changes that require coordination with file changes and tasks, use scripted tests.
Scripted Test Structure
src/sbt-test/
my-plugin/ # Test group
simple/ # Test case
build.sbt
project/plugins.sbt
test # Test script
complex/
build.sbt
project/plugins.sbt
test
Test Script Example
# test file
> myTask
> check
Import Organization
// Put all imports at the top
import sbt._
import sbt.Keys._
import sbt.librarymanagement.Configuration
import java.io.File
object MyPlugin extends AutoPlugin {
// Plugin code
}
Error Handling
Provide Helpful Error Messages
// BAD
if (files.isEmpty) sys.error("Error")
// GOOD
if (files.isEmpty) {
sys.error(
s"""No source files found in ${sourceDirectory.value}.
|Make sure your source files are in the correct directory.
|Expected locations:
| - src/main/scala
| - src/main/java
|""".stripMargin
)
}
Use Proper Logging Levels
myTask := {
val log = streams.value.log
log.debug("Detailed information for debugging")
log.info("Normal operation messages")
log.warn("Potential issues that aren't errors")
log.error("Actual problems that need attention")
}
Use Cached Tasks
Example from SemanticdbPlugin:
private val compileIncAndCacheSemanticdbTargetRootTask = Def.cachedTask {
val prev = compileIncremental.value
val converter = fileConverter.value
val targetRoot = semanticdbTargetRoot.value
val vfTargetRoot = converter.toVirtualFile(targetRoot.toPath)
Def.declareOutputDirectory(vfTargetRoot)
prev
}
Minimize Unnecessary Work
// BAD - always runs expensive computation
myTask := {
val result = veryExpensiveComputation()
if (shouldRun.value) result else ()
}
// GOOD - skip expensive computation when not needed
myTask := Def.taskIf {
if (shouldRun.value) veryExpensiveComputation()
else ()
}.value
Summary Checklist
Before releasing your plugin:
Review sbt’s core plugins like JvmPlugin, SemanticdbPlugin, and DependencyTreePlugin as examples of well-structured plugins.