Cross-compiling Scala in a Gradle project
It is quite common for Scala projects to provide binary artifacts compiled for several versions of the Scala compiler. As a rule, for the purpose of creating several versions of one artifact in a community, it is customary to use SBT, where this feature is right out of the box and is configured in a couple of lines. But what if we want to get confused and create a build for cross compilation without using SBT?
For one of my Java projects, I decided to create a Scala facade. Historically, the entire project is assembled using Gradle, and it was decided to add the facade to the same project as a submodule. Gradle as a whole can compile Scala modules with the caveat that no cross-compiling support has been declared. There is an open ticket in 2017 and a couple of plugins ( 1 , 2 ) that promise to add this feature to your project, but there are problems with them, usually associated with the publication of artifacts. And there is nothing more in general. I decided to check how difficult it is to actually configure the build for cross compilation without special plugins and SMS.
First, we describe the desired result. I would like the same set of sources to be compiled by three versions of the Scala compiler: 2.11, 2.12 and 2.13 (at this moment the most current 2.13.0-RC2). And since Scala 2.13 has a bunch of backward incompatible changes in collections, I would like to be able to add additional source sets for code specific to each of the compilers. Again, in SBT, this is all added to a couple of lines of configuration. Let's see what can be done in Gradle.

The first difficulty that you have to face is that the compiler version is computed from the version of the declared dependency on the scala-library. Plus, all dependencies that have a prefix for the Scala version of the compiler also need to be changed. Those. For each version of the compiler, the dependency list should be different. In addition, the set of flags for different versions of the compiler is actually different. Some flags were renamed between versions, while some were simply marked as obsolete or removed altogether. I decided that trying to catch all the nuances of different compilers in one build file seems to be too difficult a task and its further support even more difficult. Therefore, I decided to explore possible other ways to solve this problem. But what if we create several builds of configurations for the same project directory structure?
In the declaration of inclusion of submodules in the Gradle project, you can specify the directory where the root of the submodule and the name of the file responsible for its configuration will be located. Let's specify the same directory for several imports and create several copies of the build script for each version of the compiler.
rootProject.name = 'test'
include 'java-library'
include 'scala-facade_2.11'
project(':scala-facade_2.11').with {
projectDir = file('scala-facade')
buildFileName = 'build-2.11.gradle'
}
include 'scala-facade_2.12'
project(':scala-facade_2.12').with {
projectDir = file('scala-facade')
buildFileName = 'build-2.12.gradle'
}
include 'scala-facade_2.13'
project(':scala-facade_2.13').with {
projectDir = file('scala-facade')
buildFileName = 'build-2.13.gradle'
}Not bad, but from time to time we can get strange compilation errors related to the fact that all three build scripts use the same build directory. We can fix this by setting them for each build:
plugins {
id 'scala'
}
buildDir = 'build-2.12'
clean {
delete 'build-2.12'
}
// ...Now it’s very beautiful. With only one problem, that such a build will drive your favorite IDE crazy, and most likely further editing of your project will have to be done using instruments. I thought that this is not a big trouble, because you can always simply comment out the excess imports of submodules and turn the cross build into a regular build, with which your IDE most likely knows how to work.
What about additional source sets? Again, with separate files, this turned out to be quite simple, create a new directory and configure it as a source set.
// ...
sourceSets {
compat {
scala {
srcDir 'src/main/scala-2.12-'
}
}
main {
scala {
compileClasspath += compat.output
}
}
test {
scala {
compileClasspath += compat.output
runtimeClasspath += compat.output
}
}
}
// ...// ...
sourceSets {
compat {
scala {
srcDir 'src/main/scala-2.13+'
}
}
main {
scala {
compileClasspath += compat.output
}
}
test {
scala {
compileClasspath += compat.output
runtimeClasspath += compat.output
}
}
}
// ...The final structure of the project looks like this:

Here you can also separate individual common pieces into external configuration files and import them into the build in order to reduce the number of repetitions. But for me it turned out pretty well, declaratively, isolated, and compatible with all possible Gradle plugins.
In total, the problem was solved, Gradle’s flexibility was enough to express quite non-trivial setup quite elegantly, and Scala cross build is possible not only using SBT, and if for one reason or another you use Gradle to build a Scala project, cross compilation as an opportunity for you also available. I hope someone this post will be useful. Thanks for attention.