A plugin to reduce the boilerplate in many of the simpler laserdisc projects (but can be used in any sbt project).
It auto-configures things like sbt & scala versioning, cross compiling, scalafmt & git configuration and more.
The plugin can be used as-is, or extended into your own custom plugin, picking and choosing the defaults you like!
Add the following to project/plugins.sbt
:
addSbtPlugin("io.laserdisc" % "sbt-laserdisc-defaults" % LATEST-VERSION-HERE)
// note: this plugin brings in sbt-scalafmt, sbt-git and sbt-native-packager automatically!
Then, in your build.sbt
enable the plugin (either on the single project it contains, or in the case of a multi-module build, the root project)
lazy val root = (project in file("."))
// other project configuration
.enablePlugins(LaserDiscDefaultsPlugin)
Note Any settings in your
build.sbt
override those set by this plugin.
When SBT loads your project, the LaserDiscDefaultsPlugin will automatically perform the following:
Adds Additional Plugins
- sbt-scalafmt - for applying formatting standards automatically to source code
- sbt-git - automatic versioning based on the current commit hash/tag
- sbt-native-packager -
Apply Core Settings
-
Apply a bunch of default values:
scalaVersion
to a recent versionorganization
toio.laserdisc
organizationName
toLaserDisc
-
Add some common command aliases:
sbt format
- formats all Scala and SBT sources (According to.scalafmt.conf
, see below)sbt checkFormat
- ensures all Scala and SBT sources are formatted correctlysbt build
- shortcut forcheckFormat
,clean
, thentest
sbt release
- shortcut forbuild
(above) thenpublish
.
Apply Compiler Settings
- Sets the compiler to build for scala 3. This can be changed using
CompileTarget
:-
Remember that you need to use
import laserdisc.sbt.CompileTarget ThisBuild / laserdiscCompileTarget := CompileTarget.Scala2Only // builds only for scala 2 ThisBuild / laserdiscCompileTarget := CompileTarget.Scala3Only // builds only for scala 3 (default) ThisBuild / laserdiscCompileTarget := CompileTarget.Scala2And3 // cross compile both
+
in front of compilation-triggering tasks to trigger cross-compilation. Thebuild
alias added by this plugin automatically invokes+test
.
-
- Apply our standard set of
scalacOptions
compiler and linting configurations (for each scala version).- This includes
-Xfatal-warnings
which fails the build by default if warnings are present.- This can be disabled via two mechanisms:
set ThisBuild / laserdiscFailOnWarn := false
in the top level of yourbuild.sbt
(don't check this in!)- passing
-DlaserdiscFailOnWarn=false
as a SBT option (useful for local dev)
- This can be disabled via two mechanisms:
- For Scala 2, this includes the better-monadic-for and kind-projector compiler plugins, which aren't necessary for Scala 3.
- This includes
Generate .gitignore
- This file should be checked in (for instant IDE support when opening the project before sbt has initialized)
- The templating source is the
.gitignore
that configures this project. - It is possible to disable this functionality (temporarily, please!) by doing the following
- Set
ThisBuild / laserdiscGitConfigGenOn:=false
in the top level of yourbuild.sbt
- Pass
-DlaserdiscGitConfigGenOn=false
as a SBT option
- Set
Generate .scalafmt.conf
- This file should be checked in (for instant IDE support when opening the project before sbt has initialized)
- The templating source is the
.scalafmt.conf
that configures this project. - It can be useful to disable this generation when trialing new scalafmt configurations locally.
- However, please commit updated configurations to this project to maintain consistency!
- Set
ThisBuild / laserdiscScalaFmtGenOn:=false
in the top level of yourbuild.sbt
- Pass
-DlaserdiscScalaFmtGenOn=false
as a SBT option
Set/Upgrade the sbt version in project/build.properties
- This file should be checked in.
- You should reload
sbt
if the plugin changes thesbt.version
value
- You should reload
- The templating source is this plugin's
project/build.properties
file. - If the
sbt.version
in the consuming project is newer that what is inproject/build.properties
, the file will not be templated.- However, be a good citizen in that case, and upgrade
sbt
in this project so others get the upgrade!
- However, be a good citizen in that case, and upgrade
- Set
ThisBuild / laserdiscSBTVersionGenOn:=false
in the top level of yourbuild.sbt
to disable this functionality (only if it causes issues).
Validate compliance with LaserDisc Standards Settings
- Fails the
dist
task if CODEOWNERS file is missing or empty.
This SBT build comprises two modules:
-
- This module builds and publishes the
sbt-laserdisc-defaults
SBT plugin. - There is minimal code in this module, just the concrete implementation of the shared code (next)
- This module builds and publishes the
-
- All the logic of the plugin is here, rolling up under an extendable
LaserDiscDefaultsPluginBase
abstract implementation. - This library is published as a dependency JAR
io.laserdisc:sbt-laserdisc-defaults-shared
for custom implementations to extend.
- All the logic of the plugin is here, rolling up under an extendable
To build your own sbt plugin:
- Define an SBT plugin project, with a dependency on the shared library, and enable all the relevant plugins:
lazy val root = project .in(file(".")) .settings( sbtPlugin := true, organization := "com.modaoperandi", name := "sbt-moda-defaults", addSbtPlugin("io.laserdisc" % "sbt-laserdisc-defaults-shared" % "<version>") .... etc ...
- Then, create your implementation of
LaserDiscDefaultsPluginBase
. Thesbt-laserdisc-defaults
plugin does this, so look at the source for the actual code, but at a high level, it looks like this:object LaserDiscDefaultsPlugin extends LaserDiscDefaultsPluginBase { // define the settings keys with your desired naming strategy object autoImport { lazy val laserdiscFailOnWarn = settingKey[Boolean](Compiler.FailOnWarnKeyDesc) lazy val laserdiscCompileTarget = settingKey[CompileTarget](Compiler.CompileTargetKeyDesc) // .. etc .. } // define the publishing settings everyone who uses this plugin should have private val laserdiscDefaults = new GithubPublishDefaults { override def githubOrg: String = "springfield-nuclear" override def orgName: String = "Springfield Nuclear Power Plant" override def groupId: String = "com.simpsonsarchive.nuclear" override def licenseCheck: LicenseCheck = LicenseRequired } // for context if logging/errors is necessary (use https://github.com/sbt/sbt-buildinfo) override implicit val pluginCtx: PluginContext = PluginContext( pluginName = PluginBuildInfo.name, pluginVersion = PluginBuildInfo.version, pluginHomepage = "https://github.com/springfield-nuclear/springfield-nuclear-plant" ) // select all the category implementation you want (or create your own), passing in the key defs from above override val categories: Seq[DefaultsCategory] = Seq( Publishing(laserdiscDefaults, laserdiscPublishDefaults, laserdiscRepoName), Compiler(laserdiscFailOnWarn, laserdiscCompileTarget), Standards(), Core() // keep last, so the warning message about defaults being used shows first ) }
And that's it!
Draft a new release, ensuring the format of the release follows the v1.2.3
format (note the v
prefix), and the appropriate Github Action will publish version 1.2.3
(without the v) to sonatype.
An SBT plugin is developed as an SBT project just like a regular scala app, but some notes for anyone wanting to contribute:
-
It helps to have some familiarity with using existing scala plugins
-
The statement
sbtPlugin := true
causes SBT to configuring this project to build as a plugin.- One of the implications of this is that you are limited to Scala 2.12.x usage (sbt itself is currently built against 2.12)
-
Testing is a little different than what standard projects use:
- The scripted test framework provides a
scripted
command, that runs the plugin tests (instead oftest
) - Each plugin test is an actual sbt project configured in a particular way, with a set of assertions
- Change
scriptedBufferLog := false
in build.sbt to show full test output when troubleshooting tests
- The scripted test framework provides a
-
As you start to develop new defaults, understand the distinction between
buildSettings
andprojectSettings
- This plugin primarily defines the more global
buildSettings
- I have attempted to break down the functionality by area to keep things tidier, so check out the
DefaultsCategory
trait and its implementations to see how the current configuration is applied.
- This plugin primarily defines the more global
-
One common gotcha is trying to access
someSetting.value
outside of a task or setting macro-
those values are only available when SBT computes its task graph
val optionKey = settingKey[Boolean]("Enable secret option") // fails with "`value` can only be used within a task or setting macro.." val allowSecretOption = optionKey.value scalacOptions ++= { if (allowSecretOption) Seq("-Xallow-secret-feature") else Seq() } // access the value _inside_ the definition scalacOptions ++= { val allowSecretOption = optionKey.value if (allowSecretOption) Seq("-Xallow-secret-feature") else Seq() }
-
Even accessing the logger (
Keys.sLog
) requires use ofvalue
:Keys.sLog.value.info("foo") // must be inside a task/setting macro!
-
This plugin was developed by @barryoneill
In addition to creating issues on this repo, please use the #laserdisc slack for discussion about this plugin!