SBT plugin – Introduction
To create a SBT plugin, I am using SBT 1.2.6 and Scala 2.12.x. My consumer project is using SBT 1.2.6 and Scala 2.11.12.
When you build a plugin, the requirements are:
SBTversion of the plugin needs to be the same as theSBTversion of your project.SBTversion of the plugin needs to match theScalaversion of the plugin.SBT 1.2.6was built withScala 2.12.xso that’s what I need to use for my plugin.SBT 0.13.17is built withScala 2.10.x.
I have not seen many differences between SBT 0.13.x and SBT 1.2.x . I added a part with the differences I noticed.
If you want to know more, visit the documentation for SBT.
Keywords to know and that I am going to use
Definitions
- Module: This is a library. In
build.sbt, you create it by doing:"org" % "name" % "revision".ModuleIDis the type in the plugin source code.
- Task: This is something you can call by doing
sbt myTask. - Setting: This is something you can set a value to in
build.sbt. (mySetting := ???) - Step: For
taskandsetting, I am using the wordstepto describe both. In the SBT documentation it is referred asKey, but I think it is confusing. - Scope: For
SBT 0.13.17, this isglobal,projectorbuild. When you use it insbtconsole:- Build:
{.} - Global:
* - Project: None
- Build:
- Configuration: This is when you use
inin yourbuild.sbt, likeTestorCompile. When you use it insbtconsole:- You will have
Scope/Configuration:Step. - In the code, they are starting by an uppercase letter.
- You will have
- For
1.2.6:- You have
Global,ThisBuild,Compile,Test, etc… You do not have weird symbol like*anymore. - And also it seems that
/and:are not as distinct anymore in this version
- You have
Example for SBT 0.13.17
Below, you will find examples of the conversion of steps from the sbt console to the plugin source code terminology.
myStep:- Scope: Project
- Configuration: Root
- Step:
myStep
*/*:myStep:- Scope: Project
- Configuration: Root
- Step: myStep
{.}/Compile:compile:- Scope: Build
- Configuration: Compile
- Step: compile
*/Test:update:- Scope: Global
- Configuration: Test
- Step: update
Example for SBT 1.2.6
The structure is the same but the * does not exist anymore. I’ll copy paste what I said above:
For 1.2.6:
- You have
Global,ThisBuild,Compile,Test, etc… You do not have weird symbol like*anymore. - And also it seems that
/and:are not as distinct anymore in this version.
Difference between SBT 0.13.x and 1.2.x
dependencyOverrides
In 0.13.x it is a Set but in 1.2.x it is a Seq.
The method configuration() take a string instead of a ConfReg
This change is minor but it caused me issues.
In 0.13.x , the signature is:
def configuration(s: String): Option[ConfigurationReport]
But in 1.2.x , the signature is:
def configuration(s: ConfigRef): Option[ConfigurationReport]
But do not panic, ConfigRef is a class with this definition:
class ConfigRef(name: String)
So you can just use the configuration.name instead of configuration.
Useful commands
The following commands will help you debug and understand what is going on during each task process.
I would advise you to look at some commands like update, compile, sbtVersion, scalaVersion, etc…
And in different Configuration like Compile, Test, etc…
To inspect steps in SBT
Open the SBT console by typing sbt in your terminal.
Then, you can use the inspect actual command.
For instance, you can do:
> inspect actual */*:sbtVersion [info] Setting: java.lang.String = 0.13.17 [info] Description: [info] Provides the version of sbt. This setting should be not be modified. [info] Provided by: [info] */*:sbtVersion [info] Defined at: [info] (sbt.Defaults) Defaults.scala:136 [info] Dependencies: [info] */*:appConfiguration [info] Reverse dependencies: [info] */*:sbtResolver [info] */*:sbtDependency [info] */*:pluginCrossBuild::sbtVersion [info] */*:sbtBinaryVersion [info] *:sbtVersion [info] Delegates: [info] */*:sbtVersion [info] Related: [info] */*:pluginCrossBuild::sbtVersion [info] *:sbtVersion
It is very useful to know which steps depend on which steps.
For instance, here we see that sbtVersion depends on */*:appConfiguration. There are 4 steps which need it, you can see that in the Reverse dependencies.
With 1.2.6, you do not have the * anymore, instead there are new keywords for all the scopes and configurations. It also seems that : is replaced by /.
Get the full dependencies of a step
You can use inspect tree [step].
> inspect tree *:libraryDependencies [info] *:libraryDependencies = List(org.scala-lang:scala-library:2.11.12) [info] +-*/*:autoScalaLibrary = true [info] +-*/*:managedScalaInstance = true [info] +-*/*:sbtPlugin = false [info] +-*/*:scalaHome = None [info] +-*/*:scalaOrganization = org.scala-lang [info] +-*:scalaVersion = 2.11.12 [info]
Feel free to try with compile or update, the log is much bigger.
With 1.2.6, you do not have the * anymore, instead there are new keywords for all the scopes and configurations. It also seems that : is replaced by /.
Example of step dependency tree

By running the previous commands several times, I was able to create this dependency tree. I was curious to know how update get its list of dependencies coming from libraryDependency.
Get the output of a step
To get the output of a step you can use the show command.
For instance:
> show scalaVersion [info] 2.11.12
You will see the output of the step. Feel free to try with update and try with different Configuration like Compile vs *.
Get the detailed execution
Using the command last right after the execution of a task will give you a detailed log of what just happened. You can also do it for each individual step. The SBT documentation will have more detailed information.
Project Structure: How to start your plugin?
Finally!
Here is the structure of my project folder:
$> tree -L 3 -I target .
.
├── README.md
├── build.sbt
├── project.sbt
├── publish.sbt
├── project
│ ├── build.properties
│ └── plugins.sbt
└── src
└── main
└── scala
Here is a nice screenshot of what I get (if the weird ASCII characters does not work above) :

Below, I will go through each file:
README.md
Readme.md is describing your project and this is written in markdown syntax. If you have never seen a Readme before, you can look at examples on Github: Search for README.md on github.
build.sbt
crossSbtVersions := Seq("0.13.17")
This file is pretty much the same thing as the build.sbt for any other SBT project. It contains the libraries you need inside your SBT plugin. One particular thing for an SBT plugin is crossSbtVersions which allow you to compile for several SBT versions. There are plenty of examples of how to do that on the internet and we won’t go over that here. You can begin by taking a look at this project: sbt dependency graph on Github for a good starting example.
project.sbt
This is where you define the version and names of your SBT plugin:
sbtPlugin := true organization := "com.myorg" name := "mySuperPlugin" version := "1.2.3"
sbtPlugin := true is telling SBT that this is an SBT plugin. Important.
organization, name and version is for when you are going to load this SBT plugin inside your consumer project:
"organization" % "name" % "version"
publish.sbt
This file is everything related to how to deploy your SBT plugin:
publishMavenStyle := true
publishTo := Some("name of my artifactory" at "https://artifactory.myorg.com/artifactory/")
publishArtifact in Test := false
pomIncludeRepository := { _ => false }
You can just copy paste the above code and change the destination. I have read that people have issues with organization from the project.sbt file when it comes to publish: if you are only on github you might have to use com.github for organization.
Also, while you are debugging you can simply use sbt publishLocal which will publish the SBT plugin locally, and can be used by other projects.
project/build.properties
This file is setting the SBT version for this SBT plugin.
sbt.version=1.2.6
In my case, I am using 1.2.6 ( which is the latest at the time I am writing this post ) but you can set it to 0.13.17 ( which is the most commonly used stable previous major version ) .
project/plugin.sbt
This file is where you would load SBT plugin for your SBT plugin. Yeah inception style.
src/main/scala/com/myorg/MyMainPluginFile.scala
This is where your main file will be. You can go to the next part of this post to read more about its content.
SBT Plugin components and techniques
Major imports for SBT plugin
Most of your code will use the imports:
import sbt._
sbt is the core of everything.
To get a step which already exists in sbt, you have to use Keys. For instance: Keys.compile or Keys.update.
You can do import sbt.Keys._ , if you do not want to have to type it every time. But your code might be more readable if you do use Keys. every time you are using a pre-defined step.
The main class: AutoPlugin
The main class is the AutoPlugin:
object MyPluginMainClass extends AutoPlugin
This is where most of the architecture of your SBT plugin will go.
Log and Error and Exception
Error / Exception
To throw an error within the compile step you use:
sys.error
This method will throw something like that in the console:
java.lang.RuntimeException: This is an error message at scala.sys.package$.error(package.scala:27) [...more stack trace here...] [error] ([RUNNING TASK NAME]) This is an error message [error] Total time: [TIME] s, completed [DATE]
Logger
To fetch the logger within your step:
myTask := {
val log = sbt.Keys.streams.value.log
log.debug("debug message")
log.info("info message")
log.warn("warning message")
log.error("error message")
}
With the above code, sbt myTask will yield:
[info] info message [error] error message [success] Total time: [TIME] s, completed [DATE]
to see all the messages, you need to do:
$> sbt 'set logLevel := Level.Debug' myTask ... ...some more log... ... [debug] debug message [info] info message [warn] warning message [error] error message [success] Total time: [TIME] s, completed [DATE]
Notes
Printing error message with log.error does not trigger a failed task.
Since streams is a task, you cannot use it within a setting. You will get an error like: A setting cannot depend on a task.
If you look at how to log in settingkey in sbt on Stackoverflow you will have two options:
- Wrap your
settinginside atask. - Use a hard-coded
ConsoleLogger.
If you want to learn more about the task in a setting, there is a part about this below.
Enable your SBT plugin
In you main object, the one extending AutoPlugin, you need to override the trigger method:
override def trigger = allRequirements
SBT plugin Task/Setting
Below are descriptions on how to use tasks and settings.
Settings
Settings are things that are being set to a value in the build.sbt, for example libraryDependencies.
It is evaluated once when sbt console start.
Find an example of a declared setting below:
val mySetting = settingKey[TypeOfMySetting]("The description of my setting")
Then the consumers of your SBT plugin will set the setting, in their build.sbt:
mySettingName := ???
To make sure that your setting is set to a value by the consumer of your SBT plugin, here is what you can do:
mySetting := {
val mySettingValue =mySetting.??(undefinedKeyError(mySetting.key)).value
mySettingValue
},
The key is with this custom method:
private def undefinedKeyError[A](key: AttributeKey[A]): A = {
sys.error(s"Please declare a value for the `${key.label}` key. " +
s"Description: '${key.description.getOrElse("A required key")}'"
)
}
I found this method at Tapad/sbt-tweeter on Github which is also a great plugin to look at for examples. I found this repository through an exercise in complex sbt plugin development.
Tasks
Tasks are actions, like compile or update. They are evaluated once and only once per task call.
They are declared like this:
val myTask = taskKey[TypeOfMyTask]("The description of my task")
Then the consumer of your SBT plugin will use the task in the SBT console:
$> sbt ...starting the sbt console... > myTask ...execute your task... [success] Total time: [TIME] s, completed [DATE]
or all at once with sbt myTask inside your bash terminal.
Declare your steps
Inside your AutoPlugin class, you need to have an object:
object autoImport {
val mySetting = settingKey[MyType]("description")
val myTask = taskKey[MyOtherType]("description")
}
Once you have this sub-object inside your version of AutoPlugin you need to import it:
import autoImport._
This is where all the public steps will go. Those steps (task and setting) will be exposed by your SBT plugin.
If you do not want to expose it, you can leave it in the core object as private.
Usually, SBT plugins have another file name MyPluginKeys which looks like that:
import sbt.{settingKey, taskKey}
object SafetyPluginKeys {
val mySetting = settingKey[MyType]("description")
val myTask = taskKey[MyOtherType]("description")
// more settings and tasks
}
Then your autoImport will become:
object autoImport {
val mySetting = SafetyPluginKeys.mySetting
val myTask = SafetyPluginKeys.myTask
}
import autoImport._
for the exposed one and just
import SafetyPluginKeys._
for the others.
Implement your steps
There are 3 levels of steps in an SBT plugin:
Project
You have to override:
override def projectSettings: Seq[Def.Setting[_]]
To be able to set a step in the project scope:
override def projectSettings: Seq[Def.Setting[_]] = { Seq[Def.Setting[_]](
mySetting := {
/* Create my value here */
}
) }
When you use the command inspect actual , this correspond to looking at mySettingName in the SBT console.
Global
You have to override:
override def globalSettings: Seq[Def.Setting[_]]
To be able to set a step in the global scope:
override def globalSettings: Seq[Def.Setting[_]] = { Seq[Def.Setting[_]](
mySetting := {
/* Create my value here */
}
) }
When you use the command inspect actual , this correspond to looking at */mySettingName in the SBT console for SBT 0.13.17.
Build
You have to override:
override def buildSettings: Seq[Def.Setting[_]]
To be able to set a step in the build scope:
override def buildSettings: Seq[Def.Setting[_]] = { Seq[Def.Setting[_]](
mySetting := {
/* Create my value here */
},
myTask := {
/* ... */
}
) }
When you use the command inspect actual , this correspond to looking at {.}/mySettingName in the SBT console for SBT 0.13.17.
How to use Configuration ?
In any of those three methods: projectSettings, globalSettings, buildSettings.
To add a configuration, you need to use the method in:
mySettingName in Compile := {
/* Create my value here */
}
With this code, you are overriding the Compile:mySettingName which would be Compile/mySettingName in the SBT console when you are testing your plugin.
Also, in can be used with other step as input, not only configuration :
- For instance, you can have
mySetting in update, which mean that this will overridemySettingwhen called byupdate. - You can also nest
mySetting in update in Compile. Which in this case, makes it really confusing and I would advise to use.in(...)instead of the infix method.
This is true for sbt 0.13.17, for sbt 1.2.6, you have to use / instead of in. You can read more at “how to override the right task in sbt plugin” on Stackoverflow.
How to reuse my task code ?
If you have a complex step code and want to reuse it for let’s say Test and Compile, here is how you have to do it.
For Setting
For a setting, you will have to write a method like this:
private def mySettingMethod(): Def.Initialize[MySettingType] = {
Def.settingDyn {
// fetch dependencies
// Remember, you can not use task inside a setting
val dep1 = thisOtherSetting.value
Def.setting {
// the code of my setting
theReturnValueOfMySettingOfTypeMySettingType
}
}
}
As you see, you will have to use Def.settingDyn to start up the context of this setting. And then, you will have to use Def.setting to write the code of this setting.
Now that you have this method, you can use it in several places:
override def projectSettings: Seq[Def.Setting[_]] = {
Seq[Def.Setting[_]](
// other steps ( task and setting )
mySetting := mySettingMethod().value,
mySetting in Compile := mySettingMethod().value,
mySetting in Test := mySettingMethod().value,
// other steps ( task and setting )
)
}
Be careful not to forget to call the .value of the method.
If you call .value at the end of your method, you will get an error:
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
So you have to create the context in your method and then call .value where you want it to be evaluated.
For Task
For a task, you will have to write a method like this:
private def myTaskMethod(): Def.Initialize[Task[MyTaskType]] = {
Def.taskDyn {
// fetch dependencies
val dep1 = thisOtherSetting.value
val dep2 = thisOtherTask.value
Def.task {
// the code of my task
theReturnValueOfMyTaskOfTypeMyTaskType
}
}
}
As you see, you will have to use Def.taskDyn to start up the context of this task. And then, you will have to use Def.task to write the code of this task.
Now that you have this method, you can use it in several places:
override def projectSettings: Seq[Def.Setting[_]] = {
Seq[Def.Setting[_]](
// other steps ( task and setting )
myTask := myTaskMethod().value,
myTask in Compile := myTaskMethod().value,
myTask in Test := myTaskMethod().value,
// other steps ( task and setting )
)
}
Be careful not to forget to call the .value of the method.
If you call .value at the end of your method, you will get an error:
`value` can only be used within a task or setting macro, such as :=, +=, ++=, Def.task, or Def.setting.
So you have to create the context in your method and then call .value where you want it to be returned.
Note – with arguments
You can pass arguments to those methods, for instance you can pass the Configuration:
private def myMethod(configuration: Configuration): Def.Initialize[Task[MyTaskType]] = {
Def.(task|setting)Dyn {
val dep1 = thisOtherSetting.value in configuration
Def.(task|setting) {
theReturnValue
}
}
}
And then you can use it this way:
override def projectSettings: Seq[Def.Setting[_]] = {
Seq[Def.Setting[_]](
// other steps ( task and setting )
myStep in Compile := myMethod(Compile).value,
myStep in Test := myMethod(Test).value,
// other steps ( task and setting )
)
}
How to override a setting ?
Let’s say you want to modify the output of a setting, for instance always add a library to compile:libraryDependencies.
libraryDependencies in Compile := {
val currentLibs = (libraryDependencies inCompile).value
currentLibs ++ Seq("org" % "name" % "revision")
}
You see the method .value which allow you to get the result of a step ( setting or task).
Now you can use one of the commands in sbt that we saw in the first part of this article ; here we would want to use show.
So go to the sbt console in the project which use your SBT plugin and type show Compile/libraryDependencies. Then, you should see the new ModuleID, that you added.
How to run a step before another task ?
For this section and the next one, thanks to “sbt plugin run tasks before after an other task” on StackOverflow.
Let’s say you want to run myTask before the task Compile/update.
You would use dependsOn:
myTask := {
/* Create my value here */
println("test")
},
update in Compile := (compile in Compile).dependsOn(myTask)
Now, when you go to the sbt console in your consumer project, you will see "test" when you execute the step Compile/compile. You can also see when you do inspect actual Compile/compile that myTask show up as dependencies.
How to run a task after an other task ?
For this section, and the previous one, thanks to “sbt plugin run tasks before after an other task” on StackOverflow.
This is a bit more tricky since that is not really build-in.
So to do that in your SBT plugin, you need to override a task, make it run, then do your step and then return the result you previously fetch:
update := Def.taskDyn {
val updateResult = update.value
Def.task {
val _ = myOtherTask.value
updateResult
}
}.value
Def.taskDynandDef.taskare to create the structure of the task..valueis the same we saw before, it allow you to get the result of astep.- Do not forget the
.valueat the end
When you execute update in the sbt console, you will be able to see the logs of myOtherTask after the update one.
Note
You can, before .value have .dependsOn so you can have a task running before and another task running after.
Why can’t I use a task inside a setting ?
You cannot use a task inside setting because:
- A
settingis evaluated once whensbtstart. - A
taskis evaluated once and only once, pertask.
For instance, if you are calling myTask which depends on mySetting and myParentTask:
mySettingwould have been evaluated whensbtconsole have started.myParentTaskwill be evaluated once, even if it is called by several other task. If, for instance, you have:myTaskdepends ontaskAandtaskBand bothtaskAandtaskBdepends onmyParentTask- Then,
myParentTaskwill be evaluated only once and the result reused fortaskAandtaskBandmyTask - If you call
myTaskagain,myParentTaskwill be re-evaluated.
How to use the dependency graph
I was trying to get the dependency graph to alter it. It was really complex but reading “sbt dependency graph” on Github helped me a lot. Particularly, thank you jrudolph for answering my question issues #169 about the step to fetch the full graph on dependencies.
Why can’t I publish to Artifactory?
If you are using sbt 1.2.6, sometime you will encounter :
java.net.ProtocolException: Unexpected status line: 0
When trying to sbt publish to artifactory.
To resolve this problem, you can add:
updateOptions := updateOptions.value.withGigahorse(false)
To your build.sbt.
I found the solution to this problem at this Github issue #3519. I also found a StackOverflow answer stating that it might be from a bug in the Http client.
Conclusion
Once your plugin is ready, you can follow these steps to make your plugin known.
Please leave me a comment if you have any questions, insights or advice.
I will also update this article as I learn more about SBT plugins.
Fantastic post. Btw:
import sbt.{Def, _}
Could and should be simplified to:
import sbt._
Thank you. It is possible to simplify, that’s true.
Excellent post! It nicely summarizes I vet through with my plugins. Recently it and helped me solve my release issue. BTW Do you know what is the root cause of the Artifactory publish issue?
Thank you ! Glad I could help !
Are you talking about the gigahorse issue ? I found the solution through this post: https://github.com/sbt/sbt/issues/3519 , there are not really talking about any root cause. I found https://stackoverflow.com/questions/48767784/sbt-1-x-fails-to-resolve-parent-pom which seem to say that it is coming from a bug in the Http Client.
Thanks Leo, how might you call one plugin from inside another one. For example if you wanted to wrap the functionality of a plugin and only expose specific tasks to your users?
Thanks, L
Hello Luke !
I asked this question on Stackoverflow not too long ago, you can see my bookmark at https://leobenkel.wpcomstaging.com/2019/01/scala-sbt-fp-spark-bookmarks/#sbt , the direct link is https://stackoverflow.com/questions/53401718/how-to-add-a-plugin-as-a-library-to-a-plugin-i-made . Hope that helped ! Let me know if you have any more questions.