An "sbt-github-actions"-like way to configure your Scala tooling dotfiles: declare your config as typed sbt keys and have the build generate the checked-in file. Four plugins live here, layered from specific to fully general — install whichever fits:
sbt-scala-dotfiles-scalafix— generate.scalafix.conffor Scalafix, including a separate file per configuration soTestcan run a more relaxed rule set thanCompile.sbt-scala-dotfiles-scalafmt— generate.scalafmt.conffor scalafmt.sbt-scala-dotfiles-hocon— generate any HOCON file(s) from typed value trees. The two tool plugins above are specializations of this one.sbt-scala-dotfiles-files— the most general: maintain any files at all, astarget -> exact content. The engine the others are built on.
They all share a small HOCON-rendering core and the same generate/check engine. The two tool
plugins additionally wire the generated file into the upstream plugin (sbt-scalafix /
sbt-scalafmt), so the IDE and the CLI still see a real checked-in config.
Status: playground. This is an experiment that sits on top of
sbt-scalafix/sbt-scalafmtand drives them through their public keys.
- Scalafix: sbt-scala-dotfiles-scalafix
- scalafmt: sbt-scala-dotfiles-scalafmt
- Any HOCON file: sbt-scala-dotfiles-hocon
- Any file: sbt-scala-dotfiles-files
- Limitations
Scalafix is normally configured through a hand-written, checked-in .scalafix.conf (HOCON).
That's a separate file to learn, with no connection to the rest of your build. This plugin lets
you express the same configuration as typed sbt keys and generate the file, so:
- there's one place to configure things — your
build.sbt; - you can have different rules per configuration (the main motivation: a stricter
Compile, a relaxedTest), which.scalafix.confalone can't express; - the generated file is checked in, so the IDE and the Scalafix CLI still see a real
.scalafix.conf.
In project/plugins.sbt (add sbt-scalafix too — this plugin builds on it):
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.6")
addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-scalafix" % version)This plugin builds only for sbt 1.x / Scala 2.12, because sbt-scalafix does not publish
an sbt 2.0 artifact.
Enable the plugin explicitly (it is not auto-applied) and declare your rules:
lazy val myProject = project
.enablePlugins(ScalafixConfigPlugin)
.settings(
Compile / scalafixConfiguredRules := Seq("DisableSyntax", "OrganizeImports"),
Compile / scalafixConfiguredSettings := Map(
"DisableSyntax" -> Map("noFinalize" -> true),
"OrganizeImports" -> Map(
"targetDialect" -> "Scala3",
"groups" -> Seq("re:javax?\\.", "scala", "*"),
),
),
)scalafixConfiguredGenerate (or scalafixConfiguredGenerateAll for every configuration) turns
that into a .scalafix.conf next to your build:
# This file was automatically generated by the sbt-scala-dotfiles-scalafix plugin.
# Do not edit by hand — change your build's scalafixConfigured* keys instead
# and re-run scalafixConfiguredGenerate.
rules=[
DisableSyntax,
OrganizeImports
]
DisableSyntax {
noFinalize=true
}
OrganizeImports {
groups=[
"re:javax?\\.",
scala,
"*"
]
targetDialect=Scala3
}Then run Scalafix as usual — sbt-scalafix picks up the generated file automatically:
sbt scalafixConfiguredGenerateAll # writes .scalafix.conf and .scalafix.test.conf
sbt 'scalafix --check'
sbt 'Test / scalafix --check'scalafixConfiguredSettings values may be any HOCON-representable shape: String, Boolean,
numbers, null, Seq[Any] (lists), and nested Map[String, Any] (objects) — e.g.
Map("Rule" -> Map("nested" -> Seq(1, 2, 3))) renders to a Rule { nested = [1, 2, 3] }
block. They are rendered to HOCON by the
Typesafe config library.
The scalafixConfigured* keys are independent per configuration — Test does not
automatically inherit Compile. Since they're plain sbt settings, the idiomatic way to say
"Test is Compile, minus a rule" is to derive one from the other. The common case —
disabling a rule in Test while keeping everyone else's settings — is one line:
// Test = Compile, but drop OrganizeImports (its settings block comes along for the ride
// via the inherited scalafixConfiguredSettings, then we strip the now-unused key).
Test / scalafixConfiguredRules :=
(Compile / scalafixConfiguredRules).value.filterNot(_ == "OrganizeImports"),
Test / scalafixConfiguredSettings :=
(Compile / scalafixConfiguredSettings).value - "OrganizeImports",This generates a separate .scalafix.test.conf, and Test / scalafix reads it (while
Compile / scalafix keeps reading .scalafix.conf). Of course you can also just write the
Test rules out from scratch if the two configs have little in common.
All scalafixConfigured* keys are scoped per configuration (Compile, Test). The *All
tasks fan out over both.
| Key | Scope | Description |
|---|---|---|
scalafixConfiguredRules |
Compile, Test |
The rules to run, rendered as the rules = [...] array. |
scalafixConfiguredSettings |
Compile, Test |
Per-rule settings, rendered as HOCON blocks. |
scalafixConfiguredFile |
Compile, Test |
Output path. Defaults to .scalafix.conf (Compile) / .scalafix.test.conf. |
scalafixConfiguredGenerate |
Compile, Test |
Render and write the .scalafix.conf for this configuration. |
scalafixConfiguredCheck |
Compile, Test |
Fail if the on-disk file is out of date with the keys. |
scalafixConfiguredGenerateAll |
project | Run scalafixConfiguredGenerate for both Compile and Test. |
scalafixConfiguredCheckAll |
project | Run scalafixConfiguredCheck for both Compile and Test. |
Scalafix has no API to accept configuration content in memory —
ScalafixArguments.withConfig only takes a file path. So this plugin renders your keys to a
real .scalafix.conf on disk (with a banner header pointing back here) and wires
sbt-scalafix's own scalafixConfig setting to it, per configuration:
Compile / scalafixConfig := Some((Compile / scalafixConfiguredFile).value) // when rules are set
Test / scalafixConfig := Some((Test / scalafixConfiguredFile).value)Because sbt-scalafix reads scalafixConfig per configuration, Compile / scalafix and
Test / scalafix each pick up their own generated file. The wiring only kicks in when a
configuration actually declares rules; otherwise scalafixConfig is left untouched.
The generated files are meant to be committed. To make sure they don't drift from the keys, run the check in CI:
sbt scalafixConfiguredCheckAllIt re-renders from the keys and fails (with a pointer to scalafixConfiguredGenerate) if the
on-disk file differs.
sbt-scala-dotfiles-scalafmt does the same thing for
scalafmt: declare your scalafmt config as sbt keys and
generate a .scalafmt.conf, wiring it into sbt-scalafmt.
It's independent of the Scalafix plugin — install whichever you need. Unlike Scalafix, scalafmt is
configured by a single .scalafmt.conf (there's no Compile/Test split), so these keys are
project-scoped rather than per-configuration.
In project/plugins.sbt (add sbt-scalafmt too — this plugin builds on it):
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6")
addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-scalafmt" % version)Enable the plugin explicitly (it is not auto-applied) and declare your config:
lazy val myProject = project
.enablePlugins(ScalafmtConfigPlugin)
.settings(
scalafmtConfiguredVersion := "3.11.1",
scalafmtConfiguredSettings := Map(
"maxColumn" -> 100,
"runner" -> Map("dialect" -> "scala213"),
"rewrite" -> Map("rules" -> Seq("RedundantBraces", "RedundantParens")),
),
)scalafmtConfiguredGenerate turns that into a .scalafmt.conf next to your build:
# This file was automatically generated by the sbt-scala-dotfiles-scalafmt plugin.
# Do not edit by hand — change your build's scalafmtConfigured* keys instead
# and re-run scalafmtConfiguredGenerate.
version="3.11.1"
maxColumn=100
rewrite {
rules=[
RedundantBraces,
RedundantParens
]
}
runner {
dialect=scala213
}scalafmt requires a version, so scalafmtConfiguredVersion is a typed key (everything else goes
into the free-form scalafmtConfiguredSettings map). It defaults to the scalafmt version bundled
with the sbt-scalafmt this plugin builds against (3.10.0); override it to pin your own.
| Key | Description |
|---|---|
scalafmtConfiguredVersion |
The scalafmt version. Defaults to the bundled 3.10.0. |
scalafmtConfiguredSettings |
Everything else, rendered as HOCON. Same value shapes as Scalafix's map. |
scalafmtConfiguredFile |
Output path. Defaults to .scalafmt.conf. |
scalafmtConfiguredGenerate |
Render and write the .scalafmt.conf. |
scalafmtConfiguredCheck |
Fail if the on-disk file is out of date with the keys (run this in CI). |
Enabling ScalafmtConfigPlugin means "I manage .scalafmt.conf from the build here": it always
points sbt-scalafmt's scalafmtConfig at the generated file, so sbt scalafmtAll /
scalafmtCheckAll and your editor pick it up.
The scalafix and scalafmt plugins are specializations of a general HOCON generator. If you have
other HOCON files to keep in sync with your build, use sbt-scala-dotfiles-hocon directly:
declare target file -> value tree and the build renders each tree to HOCON and maintains it.
It has no upstream tool to wire into (and adds no banner — the rendered HOCON is the whole file),
so unlike the tool plugins, you drive the generic managedFiles* tasks
to generate and check.
In project/plugins.sbt:
addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-hocon" % version)lazy val myProject = project
.enablePlugins(HoconFilesPlugin)
.settings(
hoconFiles := Map(
baseDirectory.value / "app.conf" -> Map(
"maxColumn" -> 100,
"runner" -> Map("dialect" -> "scala213"),
"rules" -> Seq("A", "B"),
)
),
)sbt managedFilesGenerate # writes every file in hoconFiles (+ any raw managedFiles entries)
sbt managedFilesCheck # fails if any is out of date (run this in CI)hoconFiles value trees take the same shapes as the tool plugins' settings maps (String,
Boolean, numbers, null, Seq[Any], nested Map[String, Any]). Entries are added to
managedFiles, so you can mix HOCON files and raw files (see below) in one project.
The most general plugin: maintain any files, as target -> exact content (written verbatim —
no banner, no transformation). This is the engine every other plugin is built on; reach for it when
the file isn't HOCON.
In project/plugins.sbt:
addSbtPlugin("org.polyvariant" % "sbt-scala-dotfiles-files" % version)lazy val myProject = project
.enablePlugins(ManagedFilesPlugin)
.settings(
managedFiles := Map(
baseDirectory.value / ".gitignore" -> "target/\n.bsp/\n",
baseDirectory.value / "sub" / "VERSION" -> version.value,
),
)| Key | Description |
|---|---|
managedFiles |
Files to maintain, as target path -> exact content (verbatim, no banner). |
managedFilesGenerate |
Write every managed file. |
managedFilesCheck |
Fail if any managed file is out of date — a missing file counts (run in CI). |
- sbt 1.x / Scala 2.12 only (follows
sbt-scalafix/sbt-scalafmt). - Custom-rule resolution is unchanged — declare custom rules with
sbt-scalafix'sscalafixDependenciesas usual. rules/versionare always rendered first; the settings blocks that follow are emitted in sorted order (a property of the underlying config library), not source order. This is harmless — Scalafix and scalafmt parse the file either way — and keeps the output deterministic for the up-to-date check.scalafmtConfiguredVersioncan't be derived fromsbt-scalafmt: scalafmt reads its version from.scalafmt.conf, andsbt-scalafmtexposes no key for it. The plugin's default tracks the version bundled with thesbt-scalafmtit builds against, so it can drift — pin your own.