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
+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.
- I have module
fooPlugin. Let’s say both
fooCoresupports 2.10 ~ 2.13.0-M4, JVM and JS;
fooAppsupports 2.12 JVM;
fooPluginsupports 2.10 JVM and 2.12 JVM.
- I want to build and test them.
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
- The sbt-crossproject
CrossType.Fullconvention of using
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-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/
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.
Building off of the composite project, I am thinking about
lazy val fooCore = (projectMatrix in file("core")) .settings( name := "foo-core" ) .scalaVersions("2.12.6", "2.11.12") .jvmPlatform(scalacOptions := ...)
The above will generate
I think we mostly need
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 += ...) )
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.