text: Stack to the Future - How I migrated an automated build from Maven to Gradle.
  Miles Bardon
  ・6 min read
  Introduction
  When I was introduced to the GazePlay project, I jumped at the chance to try to fix some of the issues on it, and just get to grips with contributing to an Open Source project that was really doing some good.
  One problem; I couldn't build it
  The project had a Maven build system, and everything hinged on it. From build and test, through to releases, everything was written in huge XML files, which (in my humble opinion) makes it difficult to read and understand. There was a huge dependency on the developer working on the project having the JavaFX libraries on their global class-path. Not only this, but the project was built on Java 8, with an out-of-date version of JavaFX (i.e. the one included in older patches of Oracle Java 8).
  This was not a sustainable development environment, so I took it upon myself that my first contribution would be to leverage the power of Gradle to simplify the build, and the new features and support of OpenJDK 11 and OpenJFX 11. The intention was to allow any developer to clone the project and run it with little-to-no setup. This article is a guide to how I acheived this, and is in no way a definitive guide to migration.
  Build, Test and Dependencies
  The first step in this task was to get gradle to do most of the hard work. By running the command
  gradle init in a directory with a Maven project, you can get Gradle to create the wrapper scripts, properties files and a
  build.gradle file for the equivalent
  pom.xml file in the root and inner projects.
  Here is my new root
  build.gradle file:
  /* * This file was generated by the Gradle 'init' task. */ allprojects { group = 'com.github.schwabdidier' version = '1.6.2-SNAPSHOT' } subprojects { apply plugin: 'java' apply plugin: 'maven-publish' repositories { mavenLocal() maven { url = '' } maven { url = '' } maven { url = '' } maven { url = '' } } dependencies { compileOnly 'com.google.code.findbugs:findbugs:3.0.1' } sourceCompatibility = '1.8' publishing { publications { maven(MavenPublication) { from(components.java) } } } tasks.withType(JavaCompile) { options.encoding = 'UTF-8' } }
  And here is one of the subproject
  build.gradle files:
  /* * This file was generated by the Gradle 'init' task. */ dependencies { compile 'com.github.agomezmoron:screen-recorder:0.0.3' compile project(':gazeplay-commons') compile project(':gazeplay-data') ... }
  What this doesn't do, however, is migrate the Maven Plugins. In particular, to get the build working, I had to add in the javafx and lombok annotation processing plugins to the root
  build.gradle. License reporting was also required, so I included this too.
  plugins { id 'application' id 'org.openjfx.javafxplugin' version '0.0.8' id 'io.freefair.lombok' version '4.0.1' id 'com.github.hierynomus.license-report' version '0.15.0' }
  The final steps to get a build working was to add the plugins to each subproject recursively, and configure them as needed.
  Handily, the Gradle initialisation had already provided me with the
  subproject block and I just had to add this configuration to it;
  subprojects { apply plugin: 'java' apply plugin: 'maven-publish' apply plugin: 'org.openjfx.javafxplugin' apply plugin: 'io.freefair.lombok' apply plugin: 'com.github.hierynomus.license-report' javafx { version = "11.0.2" modules = [ 'javafx.controls', 'javafx.swing', 'javafx.media', 'javafx.web' ] } ... }
  Note: At this stage I had set my
  JAVA_HOME to point to my local installation of AdoptOpenJDK 11. As this requires a modular version of OpenJFX, I had to go through all the project imports and determine which modules to include. This wasn't mimicked in the Maven POM as that just needed to import JavaFX Controls.
  A few library upgrades, swapping out deprecated APIs and some documentation later, the build was succeeding and I could open my PR! Huzzah!
  I could test the Gradle configuration worked by running
  gradlew run to run the application from the commandline. The games worked as expected, and I could move onto the next stage.
  Release the
  hounds Jars
  One of the key goals of this task was to be able to bundle releases to users of the application, in the form of a launchable
  .jar file. Fortunately, Gradle recognised all my dependencies and added their Jars to the
  lib folder when I ran the
  distZip task from the
  distribution plugin. What the launch jar couldn't do was FIND any of the dependencies!
  To fix this, I had to adjust the jar manifest configuration to add both the Main-Class to launch, and the Class-Path for all the runtime dependencies.
  jar { manifest { attributes ( "Main-Class": 'net.gazeplay.GazePlayLauncher', "Class-Path": configurations.runtime.collect { it.getName() }.join(' ') ) } }
  By
  collecting all the
  runtime
  configurations, and
  iterating through them all, I could
  getName and
  join them together with a space.
  Now, launching the created
  gazeplay-project-1.6.2-SNAPSHOT.jar would actually load the other Jars into the classpath and allow the games to run!
  Custom Packaging Task
  Since packaging the app for release required a few steps, I decided to create my own custom task in a new
  .gradle file.
  import org.apache.tools.ant.filters.ReplaceTokens task packageApp(dependsOn: ['assembleDist', 'downloadLicenses', 'scriptsForRelease']) { tasks.findByName('assembleDist').mustRunAfter('downloadLicenses') tasks.findByName('assembleDist').mustRunAfter('scriptsForRelease') } task scriptsForRelease(type: Copy) { from "${rootDir}/gazeplay-dist/src/main/bin" into "${rootDir}/build/scripts" filter( ReplaceTokens, tokens: [VERSION: project.version, NAME: project.name] ) outputs.upToDateWhen { false } // Forces the task to rerun every time, without requiring a clean. }
  The
  packageApp task downloads all the licenses for the dependencies, carries out the
  scriptsForRelease task to substitute environment variables for the project version and name, and then packages these all up into a Zip and Tar archive.
  Gradle Release Plugin
  By default, Gradle does not included a direct equivalent to the
  maven-release-plugin that the project had been using up to this point. The release plugin is designed to ensure all files are commited and pushed to master, and then tags the commit with the version number in the project. Finally, it increments this version and commits everything again, ready for further development.
  For this project, I went for the most popular Gradle iteration of the plugin; the Researchgate Release plugin. All that was needed to setup the plugin was specify that the
  packageApp task was executed before the release ran;
  beforeReleaseBuild.dependsOn packageApp.
  Creating a custom JRE
  The main feature of JDK 9 was the introduction of the modular JDK and JRE. This is of course prevalent in Open JDK 11, and is key to creating a custom JRE for distribution along with the app. To do this, I opted to use the Badass Runtime Plugin, which wraps the
  jlink command with a Gradle Task. By defining the exact modules the application requires to run, it's possible to generate a JRE as a build task, with the included
  jre task.
  Here is a snippet of the modules included in the JRE created:
  ext.jreModules=['java.base',\ 'java.compiler',\ 'java.datatransfer',\ 'java.desktop',\ 'java.instrument',\ 'java.logging',\ 'java.management',\ 'java.management.rmi',\ 'java.naming',\ ...
  All that is left to do is configure the runtime plugin as so:
  runtime { options = ['--compress', '2', '--no-header-files', '--no-man-pages'] modules = jreModules jreDir = file("gazeplay-dist/src/jre") }
  The
  jlink command options are specified to keep the folder as small as possible, and the directory is set to match where the JRE 1.8 was located, for consistency with the existing project.
  Conclusion
  It may not have been the most glamourous project, but I learnt a whole lot about Gradle, custom tasks and third-party plugins. Additionally, I'm no expert on custom JREs, or the modularity of Java 9+, but this project gave me the step up needed to start investigating and learning more about it. In the end, I hope this article helps someone solve that little annoying problem they have, or is just an interesting read for someone going through the same process.
  For more details on everything I've discussed here, feel free to check out the PR with all the code.