Different versions of Scala can be binary incompatible, despite
maintaining source compatibility. This page describes how to use sbt
to build and publish your project against multiple versions of Scala and
how to use libraries that have done the same.
For cross building sbt plugins see also Cross building plugins.
The underlying mechanism used to indicate which version of Scala a
library was compiled against is to append _<scala-binary-version>
to the
library’s name. For example, the artifact name dispatch-core_2.12
is used
when compiled against Scala 2.12.0, 2.12.1 or any 2.12.x version. This fairly simple approach
allows interoperability with users of Maven, Ant and other build tools.
For pre-prelease versions of Scala such as 2.13.0-RC1 and for versions prior to 2.10.x, full version is used as the suffix.
The rest of this page describes how sbt handles this for you as part of cross-building.
To use a library built against multiple versions of Scala, double the
first %
in an inline dependency to be %%
. This tells sbt
that it
should append the current version of Scala being used to build the
library to the dependency’s name. For example:
libraryDependencies += "net.databinder.dispatch" %% "dispatch-core" % "0.13.3"
A nearly equivalent, manual alternative for a fixed version of Scala is:
libraryDependencies += "net.databinder.dispatch" % "dispatch-core_2.12" % "0.13.3"
No plugin is required to enable cross-building in sbt, although consider using sbt-projectmatrix that is capable of cross building across Scala versions and different platforms in parallel.
Define the versions of Scala to build against in the
crossScalaVersions
setting. Versions of Scala 2.10.2 or later are
allowed. For example, in a .sbt
build definition:
lazy val scala212 = "2.12.18"
lazy val scala211 = "2.11.12"
lazy val supportedScalaVersions = List(scala212, scala211)
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := scala212
lazy val root = (project in file("."))
.aggregate(util, core)
.settings(
// crossScalaVersions must be set to Nil on the aggregating project
crossScalaVersions := Nil,
publish / skip := true
)
lazy val core = (project in file("core"))
.settings(
crossScalaVersions := supportedScalaVersions,
// other settings
)
lazy val util = (project in file("util"))
.settings(
crossScalaVersions := supportedScalaVersions,
// other settings
)
Note: crossScalaVersions
must be set to Nil
on the root project to avoid double publishing.
To build against all versions listed in crossScalaVersions
, prefix
the action to run with +
. For example:
> + test
A typical way to use this feature is to do development on a single Scala
version (no +
prefix) and then cross-build (using +
) occasionally
and when releasing.
Here’s how we can change some settings depending on the Scala version.
CrossVersion.partialVersion(scalaVersion.value)
returns Option[(Int, Int)]
containing
the first two segments of the Scala version.
This can be useful for instance if you include a dependency that requires the macro paradise
compiler plugin for Scala 2.12 and the -Ymacro-annotations
compiler option for Scala 2.13.
lazy val core = (project in file("core"))
.settings(
crossScalaVersions := supportedScalaVersions,
libraryDependencies ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n <= 12 =>
List(compilerPlugin("org.scalamacros" % "paradise" % "2.1.1" cross CrossVersion.full))
case _ => Nil
}
},
Compile / scalacOptions ++= {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n <= 12 => Nil
case _ => List("-Ymacro-annotations")
}
},
)
In addition to src/main/scala/
directory, src/main/scala-<scala binary version>/
directory is included as a source directory.
For, example if the current subproject’s scalaVersion
is 2.12.10, then
src/main/scala-2.12
is included as a Scala-version specific source.
By setting crossPaths
to false
, you can opt out of both Scala-version source directory
and the _<scala-binary-version>
publishing convention. This might be useful for non-Scala projects.
Similarly, the build products such as *.class
files are written into
crossTarget
directory, which by default is target/scala-<scala binary version>
.
A special care must be taken when cross building involves pure Java project.
Let’s say in the following example, network
is a Java project, and core
is
a Scala project that depends on network
.
lazy val scala212 = "2.12.18"
lazy val scala211 = "2.11.12"
lazy val supportedScalaVersions = List(scala212, scala211)
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := scala212
lazy val root = (project in file("."))
.aggregate(network, core)
.settings(
// crossScalaVersions must be set to Nil on the aggregating project
crossScalaVersions := Nil,
publish / skip := false
)
// example Java project
lazy val network = (project in file("network"))
.settings(
// set to exactly one Scala version
crossScalaVersions := List(scala212),
crossPaths := false,
autoScalaLibrary := false,
// other settings
)
lazy val core = (project in file("core"))
.dependsOn(network)
.settings(
crossScalaVersions := supportedScalaVersions,
// other settings
)
crossScalaVersions
must be set to Nil
on the aggregating projects such as the root.
crossPaths
to false, which turns off the _<scala-binary-version>
publishing convention and the Scala-version specific source directory.
crossScalaVersions
to avoid double publishing, typically scala212
.
crossScalaVersions
, but must avoid aggregating Java subprojects.
You can use ++ <version> [command]
to temporarily switch the Scala version currently
being used to build the subprojects given that <version>
is listed in their crossScalaVersions
.
For example:
> ++ 2.12.18
[info] Setting version to 2.12.18
> ++ 2.11.12
[info] Setting version to 2.11.12
> compile
<version>
should be either a version for Scala published to a repository or
the path to a Scala home directory, as in ++ /path/to/scala/home
.
See Command Line Reference for details.
When a [command]
is passed in to ++
, it will execute the command
on the subprojects that supports the given <version>
.
For example:
> ++ 2.11.12 -v test
[info] Setting Scala version to 2.11.12 on 1 projects.
[info] Switching Scala version on:
[info] core (2.12.18, 2.11.12)
[info] Excluding projects:
[info] * root ()
[info] network (2.12.18)
[info] Reapplying settings...
[info] Set current project to core (in build file:/Users/xxx/hello/)
Sometimes you might want to force the Scala version switch regardless of the crossScalaVersions
values.
You can use ++ <version>!
with exclamation mark for that.
For example:
> ++ 2.13.0-M5! -v
[info] Forcing Scala version to 2.13.0-M5 on all projects.
[info] Switching Scala version on:
[info] * root ()
[info] core (2.12.18, 2.11.12)
[info] network (2.12.18)
The ultimate purpose of +
is to cross-publish your
project. That is, by doing:
> + publishSigned
you make your project available to users for different versions of Scala. See Publishing for more details on publishing your project.
In order to make this process as quick as possible, different output and managed dependency directories are used for different versions of Scala. For example, when building against Scala 2.12.7,
./target/
becomes ./target/scala_2.12/
./lib_managed/
becomes ./lib_managed/scala_2.12/
Packaged jars, wars, and other artifacts have _<scala-version>
appended to the normal artifact ID as mentioned in the Publishing
Conventions section above.
This means that the outputs of each build against each version of Scala are independent of the others. sbt will resolve your dependencies for each version separately. This way, for example, you get the version of Dispatch compiled against 2.11 for your 2.11.x build, the version compiled against 2.12 for your 2.12.x builds, and so on.
crossVersion
setting can override the publishing convention:
CrossVersion.disabled
(no suffix)
CrossVersion.binary
(_<scala-binary-version>
)
CrossVersion.full
(_<scala-version>
)
The default is either CrossVersion.binary
or CrossVersion.disabled
depending on the value of crossPaths
.
Because (unlike Scala library) Scala compiler is not forward compatible among
the patch releases, compiler plugins should use CrossVersion.full
.
In a Scala 3 project you can use Scala 2.13 libraries:
("a" % "b" % "1.0") cross CrossVersion.for3Use2_13
This is equivalent to using %%
except it resolves the _2.13
variant
of the library when scalaVersion
is 3.x.y.
Conversely we have CrossVersion.for2_13Use3
to use the _3
variant of the
library when scalaVersion
is 2.13.x:
("a" % "b" % "1.0") cross CrossVersion.for2_13Use3
Warning for library authors: It is generally not safe to publish
a Scala 3 library that depends on a Scala 2.13 library or vice-versa.
The reason is to prevent your end users from having two versions x_2.13
and x_3
of the same x library in their classpath.
You can have fine-grained control over the behavior for different Scala versions
by using the cross
method on ModuleID
These are equivalent:
"a" % "b" % "1.0"
("a" % "b" % "1.0").cross(CrossVersion.disabled)
These are equivalent:
"a" %% "b" % "1.0"
("a" % "b" % "1.0").cross(CrossVersion.binary)
This overrides the defaults to always use the full Scala version instead of the binary Scala version:
("a" % "b" % "1.0").cross(CrossVersion.full)
CrossVersion.patch
sits between CrossVersion.binary
and CrossVersion.full
in that it strips off any trailing -bin-...
suffix which is used to
distinguish variant but binary compatible Scala toolchain builds.
("a" % "b" % "1.0").cross(CrossVersion.patch)
CrossVersion.constant
fixes a constant value:
("a" % "b" % "1.0") cross CrossVersion.constant("2.9.1")
It is equivalent to:
"a" % "b_2.9.1" % "1.0"
A constant cross version is mainly used when cross-building and a dependency isn’t available for all Scala versions or it uses a different convention than the default.
("a" % "b" % "1.0") cross CrossVersion.constant {
scalaVersion.value match {
case "2.9.1" => "2.9.0"
case x => x
}
}
sbt-release implemented cross building support by copy-pasting sbt 0.13’s +
implementation,
so at least as of sbt-release 1.0.10, it does not work correctly with sbt 1.x’s cross building,
which was prototyped originally as sbt-doge.
To cross publish using sbt-release with sbt 1.x, use the following workaround:
ThisBuild / organization := "com.example"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / scalaVersion := scala212
import ReleaseTransformations._
lazy val root = (project in file("."))
.aggregate(util, core)
.settings(
// crossScalaVersions must be set to Nil on the aggregating project
crossScalaVersions := Nil,
publish / skip := true,
// don't use sbt-release's cross facility
releaseCrossBuild := false,
releaseProcess := Seq[ReleaseStep](
checkSnapshotDependencies,
inquireVersions,
runClean,
releaseStepCommandAndRemaining("+test"),
setReleaseVersion,
commitReleaseVersion,
tagRelease,
releaseStepCommandAndRemaining("+publishSigned"),
setNextVersion,
commitNextVersion,
pushChanges
)
)
This will then use the real cross (+
) implementation for testing and publishing.
Credit for this technique goes to James Roper at playframework#4520 and later inventing releaseStepCommandAndRemaining
.