sbt has scoping axis (subproject, config, and in-task). When I started hacking on sbt, I remember calling scalaVersion key “the axis of evil” because cross building on Scala versions is one of few places where the entire setting values are overturned using ++2.12.6 or +compile command. The situation gets more hairy when there are dependencies between the subproject, or if the test frameworks are missing. In 2014, I experimented with sbt-doge partially to address by inverting the aggregation, but it’s more of a bandaid.

In 2018, there are additional cross building axes platform (consisting of JVM, JS, and Native), sbtVersion, and JDK implementations.

Steps

I have module fooCore , fooApp , and fooPlugin . Let’s say both fooApp and fooPlugin depend on fooCore ; fooCore supports 2.10 ~ 2.13.0-M4, JVM and JS; fooApp supports 2.12 JVM; fooPlugin supports 2.10 JVM and 2.12 JVM. I want to build and test them.

Spacial representation

Instead of mutating the build state with ++2.12.6 , the direction we should explore is spreading them out spatially as subprojects. This idea was pioneered by Tobias Schlatter’s addition of CrossProject mechanism to Scala.js plugin. This was later expanded to sbt-crossproject.

lazy val fooCore = crossProject(JSPlatform, JVMPlatform) .settings( testFrameworks ++= Seq( TestFramework("sbttest.framework.DummyFramework"), TestFramework("inexistent.Foo", "another.strange.Bar") ) ) lazy val fooCoreJS = fooCore.js lazy val fooCoreJVM = fooCore.jvm

However, this only addresses the platform cross building, but not any other parameters, like Scala version.

Enter sbt-cross written by Paul Draper in 2015.

lazy val fooApp = (project in file("app")) .dependsOn(fooCore_2_11) .settings(scalaVersion := "2.11.8") lazy val fooPlugin = (project in file("plugin")) .dependsOn(fooCore_2_12) .settings(scalaVersion := "2.12.1") lazy val fooCore = project.cross lazy val fooCore_2_11 = fooCore("2.11.8") lazy val fooCore_2_12 = fooCore("2.12.1")

I have not used sbt-cross personally, but I think it has a great potential to simplify the cross building. It apparently provides support for cross building on library axis as well.

This notion of build matrix is something Li Haoyi implements in Mill’s Cross Builds as well.

Likely what we want is a combination of sbt-crossproject and sbt-cross, but more on that later.

Source directory layout

sbt already implements Scala version specific source directories (for example src/main/scala-2.12/ ) in addition to the normal src/main/scala directory.

) in addition to the normal directory. The sbt-crossproject CrossType.Full convention of using shared/ , jvm/ , js/ directories are the defacto standard for Scala.js and Native cross building.

Since it’s more common to do Scala version cross building only on JVM, it likely make sense to keep the src/main/scala/ , src/main/scala-2.12/ convention by default. Combining CrossType.Full convention and scala-2.12/ looks like this:

core/ +- shared/ | +- src/ | +- main/ | +- scala/ | +- scala-2.12/ +- jvm/ | +- src/ | +- main/ | +- scala/ | +- scala-2.12/ +- js/ +- src/ +- main/ +- scala/ +- scala-2.12/

Instead, the following might be more consistent:

core/ +- src/ +- main/ +- scala/ +- scala-2.12/ +- scala-jvm/ +- scala-jvm-2.12/ +- scala-js/ +- scala-js-2.12/

Composite project

sbt 1.2.0 adds a notion called composite project originally proposed by Sébastien Doeraene in #3042 and implemented by BennyHill in #4056. It provides a hook to tell sbt about subprojects without explicitly creating lazy val fooCoreJVM .

projectMatrix proposal

Building off of the composite project, I am thinking about projectMatrix DSL:

lazy val fooCore = (projectMatrix in file("core")) .settings( name := "foo-core" ) .scalaVersions("2.12.6", "2.11.12") .jvmPlatform(scalacOptions := ...)

The above will generate fooCoreJVM2_12 and fooCoreJVM2_11 automatically.

I think we mostly need idSuffix and directorySuffix for generalization:

lazy val fooCore = (projectMatrix in file("core")) .settings( name := "foo-core" ) .scalaVersions("2.12.6", "2.11.12") .custom( idSuffix = "Dispatch010", directorySuffix = "-dispatch0.10", scalaVersions = Seq("2.11.12"), settings = Seq(libraryDependencies += ...) )

Using custom(...) hopefully the plugin ecosystem could extend this to support Scala.JS and Native:

lazy val fooCore = (projectMatrix in file("core")) .settings( name := "foo-core" ) .scalaVersions("2.12.6", "2.11.12") .jsPlatform(scalacOptions := ...) // provided by plugins .nativePlatform(scalaVersions = Seq("2.11.12"), scalacOptions := ...)

Let us know what you think. Also, let us know if you want to help implement this thing.