[From the sandbox] Cross-compiling Scala in a Gradle project

[From the sandbox] 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 purposes of creating several versions of a single artifact in a community, it is customary to use SBT, where this feature is available right out of the box and configured in a couple of lines. But what if we want to be confused and create a build for cross-compiling without using SBT?


For one of my Java projects, I decided to create a Scala facade. Historically, the entire project is assembled with the help of 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 only reservation that no cross-compilation in the support is stated. There is an open ticket 2017 and a couple of plug-ins ( 1 , 2 ), which promise to add this feature to your project, but with them there are problems, usually associated with the publication of artifacts. And the whole is nothing. I decided to check how difficult it is to actually configure a build for cross compilation without special plug-ins and SMS.


First, let's 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 is 2.13.0-RC2). And since Scala 2.13 has a lot of backwards 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 in a couple of lines of configuration. Let's see what you can do in Gradle.


Project Structure


The first difficulty that comes up is that the version of the compiler is calculated from the version of the declared dependency on the scala-library. Plus, all dependencies that have the Scala compiler version prefix also need to be changed. Those. For each version of the compiler, the dependency sheet must be different. In addition, the set of flags for different versions of the compiler is actually different. Some flags were renamed between versions, and some were simply marked obsolete or removed altogether. I decided that trying to catch all the nuances of different compilers in the same build file seems like a difficult task and even more difficult is its further support. Therefore, I decided to investigate possible other ways to solve this problem. But what if we create several build configurations for the same project directory structure?


In the submodules inclusion declaration in the Gradle project, you can specify the directory in which the submodule root 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.


settings.gradle
  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 periodically we can get strange compilation errors due to the fact that all three build scripts use the same build directory.We can fix this by setting them for each of the builds:


build-2.12.gradle
  plugins {
  id 'scala'
 }

 buildDir = 'build-2.12'

 clean {
  delete 'build-2.12'
 }
//...  

Now quite beautiful. With only one problem, that such a build will drive your beloved IDE crazy and most likely further editing of your project will have to be done with instruments. I thought it was not a big deal, because you can always just comment out the extra submodule imports and turn the cross build into a regular build that your IDE most likely knows how to work with.


What about additional source sets? Again, with separate files, this turned out to be quite simple, creating a new directory and configuring it as a source set.


build-2.12.gradle
 //...
 sourceSets {
  compat {
  scala {
  srcDir 'src/main/scala-2.12-'
  }
  }
  main {
  scala {
  compileClasspath + = compat.output
  }
  }
  test {
  scala {
  compileClasspath + = compat.output
  runtimeClasspath + = compat.output
  }
  }
 }//...  

build-2.13.gradle
 //...
 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:


Final Project


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 happened so well, declaratively, isolated and compatible with all possible Gradle plugins.


So, the problem was solved, Gradle’s flexibility was enough to express a very non-trivial setup quite gracefully, and the Scala cross build is possible not only using SBT and, if for one reason or another you use Gradle to build the Scala project, cross compilation as an opportunity you are also available. I hope someone this post will be useful. Thank you for your attention.

Source text: [From the sandbox] Cross-compiling Scala in a Gradle project