sbt-nosbt is an sbt plugin to make your complex, multi-module build definition more maintainable
by moving build definition from .sbt files to plain Scala files and providing a nice convention for
hierarchical organization of subprojects.
Table of Contents generated with DocToc
- Overview
- Usage example
- Complex, multi-level hierarchies
- Cross project support (for Scala.js & Scala Native)
- Caveats
sbt can be intimidating. This is mostly due to various layers of abstraction and "magic" that it uses. However, deep
down,
sbt build definition is ultimately just plain Scala code. This plugin aims to bring that plain Scala to the surface,
removing at least some of the sbt's magic.
sbt requires your build to be defined in .sbt files, which are Scala-like files preprocessed in a special way.
Most importantly, that preprocessing includes:
- automatic import of keys and other definitions from
sbtcore and plugins - extracting all project definitions by looking for all
lazy vals (andvals) typed asProject
.sbt files may also refer to definitions in project/*.scala files, which are regular Scala files without any special
treatment. While this allows you to move a lot of utility functions out of .sbt files, you are still forced to
enumerate all your projects in .sbt files. Typically, this is a single build.sbt file.
The nosbt plugin allows you to move all your project definitions into plain Scala files. This removes all the
special .sbt preprocessing and allows you to organize your build definition like regular Scala code by splitting
it into multiple files that explicitly refer to each other. sbt-nosbt also establishes a convention for project
(and directory) hierarchy that makes it easier to define complex, multi-project builds.
The full example is available in an example project repository
Add the nosbt plugin to your project/plugins.sbt:
addSbtPlugin("com.github.ghik" % "sbt-nosbt" % "<version>")Now create a project/MyProj.scala file with definition of a ProjectGroup:
import com.github.ghik.sbt.nosbt.ProjectGroup
import sbt.Keys._
import sbt._
object MyProj extends ProjectGroup("myproj") {
// the root project of your build; its ID is `myproj`
// and its base directory is the root directory of the build
lazy val root: Project = mkRootProject
/* Subprojects of your build */
// ID of this subroject is `myproj-api`, its base directory is `api/`
lazy val api: Project = mkSubProject
.settings(/* ... */)
// ID of this subroject is `myproj-impl`, its base directory is `impl/`
lazy val impl: Project = mkSubProject
.dependsOn(api)
.settings(/* ... */)
// settings applied to all projects (optional)
override def commonSettings: Seq[Def.Setting[_]] = Seq(
scalaVersion := "3.2.2",
)
// settings in ThisBuild scope (optional)
override def buildSettings: Seq[Def.Setting[_]] =
Seq(/* settings that you wish to be in ThisBuild scope */)
// settings in Global scope (optional)
override def globalSettings: Seq[Def.Setting[_]] =
Seq(/* settings that you wish to be Global scope */)
}The above file is a complete definition of an sbt multi-project build, in plain Scala:
- The root project must be defined as
lazy val rootand implemented withmkSubProject. ID of this project will be the same as name of theProjectGroup, i.e.myproj. Base directory of this project is the build root directory. - All subprojects in the project group must be defined as
lazy vals, just like you would do in an.sbtfile. However, usage ofmkSubProjectmakes sure that subprojects follow hierarchical naming and directory convention. For examplelazy val api: Project = mkSubProjectwill define a subproject with IDmyproj-apiand base directoryapi/. Note how this is different from the defaultsbtbehaviour which would place the project in a directory corresponding directly to its ID (i.e.myproj-api/). - Settings shared by all the projects in your build can be defined by overriding
commonSettings. Note how this is not the same as defining settings inGlobalorThisBuildscopes -commonSettingsare applied directly on each and every project which is more reliable thanGlobal/ThisBuildand generally more recommended. There are also variations ofcommonSettings, e.g.subprojectSettings,leafSubprojectSettings, etc. which allow you to refine the exact set of projects that you want to apply settings on. Refer toProjectGroups API for details. - Settings in
Globalscope can be set by overridingglobalSettings - Settings in
ThisBuildscope can be set by overridingbuildSettings.
Because MyProj.scala is a regular Scala file, its contents may be split and reorganized as you wish, e.g. be
extracting traits, subclasses, etc. into separate files. It becomes maitainable like plain Scala code.
We also need to tell sbt that MyProj.scala is the entry point of the entire build definition. In order to do that,
we need to create a minimal, "bootstrapping" build.sbt file:
lazy val root = MyProj.rootet voila!
Let's say your build is more complex. It is split into several "services", each one consisting of multiple subprojects. Let's say you want to achieve a project structure like this:
myproj
myproj-commons
myproj-commons-db
myproj-commons-api
myproj-fooservice
myproj-fooservice-api
myproj-fooservice-impl
myproj-barservice
myproj-barservice-api
myproj-barservice-impl
which corresponds to the following directory structure:
myproj/
commons/
db/
api/
fooservice/
api/
impl/
barservice/
api/
impl/
You can achieve this with the following set of definitions:
import com.github.ghik.sbt.nosbt.ProjectGroup
import sbt.Keys._
import sbt._
object MyProj extends ProjectGroup("myproj") {
// setting shared by all projects in this group and all its child groups
override def commonSettings: Seq[Def.Setting[_]] = Seq(
scalaVersion := "3.2.2",
)
lazy val root: Project = mkRootProject
lazy val commons: Project = Commons.root
lazy val fooservice: Project = FooService.root
lazy val barservice: Project = BarService.root
}
object Commons extends ProjectGroup("commons", MyProj) {
lazy val root: Project = mkRootProject
lazy val db: Project = mkSubProject
lazy val api: Project = mkSubProject
}
object FooService extends ProjectGroup("fooservice", MyProj) {
lazy val root: Project = mkRootProject
lazy val api: Project = mkSubProject.dependsOn(Commons.api)
lazy val impl: Project = mkSubProject.dependsOn(api, Commons.db)
}
object BarService extends ProjectGroup("barservice", MyProj) {
lazy val root: Project = mkRootProject
lazy val api: Project = mkSubProject.dependsOn(Commons.api)
lazy val impl: Project = mkSubProject.dependsOn(api, Commons.db, FooService.api)
}Note how Commons, FooService and BarService declare MyProj as their parent project group. The MyProj
must also explicitly declare lazy vals referring to subgroups' root projects in order for sbt to see them.
Finally, the boostrapping build.sbt file:
lazy val root = MyProj.rootIf you want to use sbt-crossproject for defining
projects cross compiled to Scala.js and/or Scala Native, use sbt-nosbt-crossproject:
addSbtPlugin("com.github.ghik" % "sbt-nosbt-crossproject" % "<version>")Then, use CrossProjectGroup instead of ProjectGroup and use mkCrossSubProject in place of
crossProject macro from the sbt-crossproject plugin:
import com.github.ghik.sbt.nosbt.crossproject.CrossProjectGroup
import sbt.Keys._
import sbt._
import sbtcrossproject.{CrossProject, JVMPlatform}
import scalajscrossproject.JSPlatform
object MyProj extends CrossProjectGroup("myproj") {
lazy val root: Project = mkRootProject
lazy val foo: Project = mkSubProject // not cross compiled
lazy val utils: CrossProject = mkCrossSubProject(JVMPlatform, JSPlatform) // cross compiled to JVM & JS
}-
Settings defined in
GlobalandThisBuildscopes by overridingglobalSettingsandbuildSettingshave lower priority than if they would be defined directly in the.sbtfile. This means they may get overwritten by settings from othersbtplugins in your build. If this is a problem, you can lift their priority back by referring to them explicitly in thebuild.sbtbootstrapping file:inScope(Global)(MyProj.globalSettings) inThisBuild(MyProj.buildSettings) lazy val root = MyProj.root
In order to avoid these problems altogether, prefer overriding
ProjectGroup.commonSettingsrather than usingThisBuild.