Porting sbt plugins
Many sbt plugins are genuinely useful and you don't want to lose them when you move to Bleep. The good news is that most of them can be ported straightforwardly, and the result is usually simpler and easier to follow than the original.
This page is for the case where you need functionality that already exists as an sbt plugin and you want to use it from a Bleep script or sourcegen script. If you're writing new functionality from scratch, just write a regular script. No plugin mechanism needed.
The translation
sbt plugins hide most of their structure behind keys, scopes, and auto-imports. When ported to plain Scala, the structure becomes visible:
| sbt | Bleep |
|---|---|
class FooPlugin extends AutoPlugin | class FooPlugin(...) (regular class) |
lazy val fooSetting = ... | Constructor parameter |
lazy val fooTask = taskKey[...] | def foo(...): ... (method) |
override def requires = ... | Pass dependencies into the constructor |
sbt.Logger | ryddig.Logger |
Keys.baseDirectory.value | Parameter or started.buildPaths.buildDir |
No AutoPlugin, no autoImport, no Keys, no implicit task graph. Just a class you instantiate and methods you call.
Real ported plugins
Bleep ships with several sbt plugins already ported. They live in the liberated/ directory as git submodules. Each tracks the upstream project so we can merge fixes over time.
Currently ported:
- sbt-ci-release: CI-driven Sonatype releases
- sbt-sonatype: Maven Central publishing (including the new Central Portal)
- sbt-dynver: Git-derived version strings
- sbt-pgp: Artifact signing
- sbt-native-image: GraalVM native image generation
- mdoc: Documentation generation
- sbt-scalafix: Code linting and rewriting
- sbt-jni: JNI compilation
Example: DynVerPlugin
Here is the ported sbt-dynver plugin in full. Look how easy it is to follow end-to-end:
package bleep.plugin.dynver
import java.io.File
import java.util.Date
class DynVerPlugin(
val baseDirectory: File,
/* The prefix to use when matching the version tag */
val dynverTagPrefix: Option[String] = None,
/* The separator to use between tag and distance, and the hash and dirty timestamp */
val dynverSeparator: String = DynVer.separator,
/* The current date, for dynver purposes */
val dynverCurrentDate: Date = new Date,
/* Whether to append -SNAPSHOT to snapshot versions */
val dynverSonatypeSnapshots: Boolean = false
) {
lazy val tagPrefix = {
val vTagPrefix = dynverVTagPrefix
val tagPrefix = dynverTagPrefix.getOrElse(if (vTagPrefix) "v" else "")
assert(vTagPrefix ^ tagPrefix != "v", s"Incoherence: dynverTagPrefix=$tagPrefix vs dynverVTagPrefix=$vTagPrefix")
tagPrefix
}
/* The dynver instance for this build */
lazy val dynverInstance: DynVer =
DynVer(Some(baseDirectory), dynverSeparator, tagPrefix)
/* Whether or not tags have a 'v' prefix */
lazy val dynverVTagPrefix: Boolean =
dynverTagPrefix.getOrElse(DynVer.tagPrefix) == "v"
/* The output from git describe */
lazy val dynverGitDescribeOutput: Option[GitDescribeOutput] =
dynverInstance.getGitDescribeOutput(dynverCurrentDate)
/* The last stable tag */
lazy val dynverGitPreviousStableVersion: Option[GitDescribeOutput] =
dynverInstance.getGitPreviousStableTag
lazy val isSnapshot: Boolean =
dynverGitDescribeOutput.isSnapshot
/* The version string identifies a specific point in version control, so artifacts built from this version can be safely cached */
lazy val isVersionStable: Boolean =
dynverGitDescribeOutput.isVersionStable
/* The last stable version as seen from the current commit (does not include the current commit's version/tag) */
lazy val previousStableVersion: Option[String] =
dynverGitPreviousStableVersion.previousVersion
def getVersion(date: Date, out: Option[GitDescribeOutput]): String =
out.getVersion(date, dynverSeparator, dynverSonatypeSnapshots)
// The version of your project, from git
lazy val dynver: String =
getVersion(new Date, dynverInstance.getGitDescribeOutput(new Date))
lazy val version: String =
getVersion(dynverCurrentDate, dynverGitDescribeOutput)
// Asserts if the version derives from git tags
def dynverAssertTagVersion(): Unit =
dynverGitDescribeOutput.assertTagVersion(version)
// Checks if version and dynver match
def dynverCheckVersion: Boolean =
dynver == version
// Asserts if version and dynver match
def dynverAssertVersion(): Unit = {
val v = version
val dv = dynver
if (!dynverCheckVersion)
sys.error(s"Version and dynver mismatch - version: $v, dynver: $dv")
}
}
Porting workflow
- Copy the source into
liberated/your-plugin/as a git submodule, with bothorigin(your fork) andorigin-original(upstream) remotes. - Strip sbt imports: remove
import sbt._,import Keys._,import sbt.plugins._. - Unwrap
AutoPlugin: replace with a regularclasstaking constructor parameters. - Convert keys to parameters: each
SettingKeybecomes a constructor parameter, eachTaskKeybecomes adef. - Replace
sbt.Loggerwithryddig.Logger(bleep's structured logger). - Add it to
bleep.yamlas a project withsources:pointing at the liberated directory, then use it from a script. - Maintain: when upstream releases fixes, merge them with
git merge origin-original/mainand resolve conflicts in favour of the bleep adaptations.
The CLAUDE.md at the root of the bleep repo has the full maintenance process documented.
Limitations
Scopes
Bleep does not have scopes. Compile and Test don't exist as scopes; the test scope is a separate project. If a plugin leans heavily on scopes (e.g. Test / fullClasspath), you'll need to express that by passing the relevant project explicitly.
Global mutable state
Some sbt plugins communicate via shared global mutable state (sbt.Keys.state, task attribute maps). In Bleep these values must be passed as explicit parameters. This is usually an improvement (the data flow becomes visible), but it can require more invasive porting for plugins that were designed around ambient state.
Multi-project semantics
sbt plugins sometimes react to project structure via thisProject.value. Bleep scripts instead iterate over started.build.explodedProjects and handle each project explicitly.
Distribution
After porting, the plugin is just a normal Scala library. You can cross-publish it to Maven Central and have other Bleep builds depend on it by coordinate, exactly like any other dependency. Bleep itself does this with its ported plugins; they're published under build.bleep::bleep-plugin-*.