laserdisc-io / sbt-laserdisc-defaults   0.2.0

MIT License GitHub

An sbt plugin to autoconfigure a project to some common defaults

Scala versions: 2.12
sbt plugins: 1.x

sbt-laserdisc-defaults

GitHub Actions Workflow Status GitHub Release

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!

Direct Usage

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.

What it Does

When SBT loads your project, the LaserDiscDefaultsPlugin will automatically perform the following:

Adds Additional Plugins

Categories

Apply Core Settings

  • Apply a bunch of default values:

    • scalaVersion to a recent version
    • organization to io.laserdisc
    • organizationName to LaserDisc
  • 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 correctly
    • sbt build - shortcut for checkFormat, clean, then test
    • sbt release - shortcut for build (above) then publish.

Apply Compiler Settings

  • Sets the compiler to build for scala 3. This can be changed using CompileTarget:
    • 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
      Remember that you need to use + in front of compilation-triggering tasks to trigger cross-compilation. The build 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 your build.sbt (don't check this in!)
        • passing -DlaserdiscFailOnWarn=false as a SBT option (useful for local dev)
    • For Scala 2, this includes the better-monadic-for and kind-projector compiler plugins, which aren't necessary for Scala 3.

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 your build.sbt
    • Pass -DlaserdiscGitConfigGenOn=false as a SBT option

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 your build.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 the sbt.version value
  • The templating source is this plugin's project/build.properties file.
  • If the sbt.version in the consuming project is newer that what is in project/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!
  • Set ThisBuild / laserdiscSBTVersionGenOn:=false in the top level of your build.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.

Extending Your Own

This SBT build comprises two modules:

  • plugin

    • 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)
  • plugin-shared

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

To build your own sbt plugin:

  1. 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 ...            
  2. Then, create your implementation of LaserDiscDefaultsPluginBase. The sbt-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!

Releasing This Plugin

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.

Developing Tips

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 of test)
    • 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
  • As you start to develop new defaults, understand the distinction between buildSettings and projectSettings

    • 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.
  • 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 of value:

        Keys.sLog.value.info("foo") // must be inside a task/setting macro!

Help

This plugin was developed by @barryoneill

In addition to creating issues on this repo, please use the #laserdisc slack for discussion about this plugin!