For Scala projects, the provision of binary artifacts compiled for several versions of the Scala compiler is quite common. 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 in 2017 and a couple of plug-ins ( 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 the whole is nothing. I decided to check how difficult it is to actually configure the build for cross compilation without special plug-ins and SMS.
To begin, 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 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.
The first difficulty to face is that the compiler version 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. What if we create multiple build configurations for the same project directory structure?
In the declaration of the inclusion of submodules 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.
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:
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, we 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 is as follows:
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 rather 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 also available. I hope someone this post will be useful. Thanks for attention.
Source: https://habr.com/ru/post/452552/
All Articles