Running Gradle inside Maven

As organizations evolve so do their codebases. Apache Maven and Gradle are the most popular and widely used build tools in the JVM. Usually multi-project builds rely on a single build tool to accomplish the job but there may be uses cases where you need to combine both, having Maven be the one leading the pack. One possible use case is to build a companion Gradle plugin, this is the case for ByteBuddy and Quarkus; another use case is to run a composite build with Maven and Gradle projects mixed together. In this post I'll show how Gradle can be invoked inside Maven.

WARNING: This technique executes Gradle in isolation from the rest of the Maven build. Gradle does not participate in the reactor, thus there are some limitations.

Let's say we have a multi-project build with one module (producer) built with Maven and another one (consumer) built with Gradle. The file structure looks like this

The root pom.xml file must define the modules that belong to the reactor. It also defines common properties and settings that can be applied to any module as long as they inherit from this file as a parent POM (optional). Keeping things simple here's how the root pom.xml files looks

The producer pom.xml is also a simple one. You can add any number of dependencies and build plugins as needed.

Now we can turn to the Gradle project. We need two files: a Maven build file (pom.xml) and a Gradle build file (build.gradle). This particular example creates 3 JAR files

  • the standard artifact jar, i.e, ${project.name}-${project.version}.jar
  • a javadoc jar, i.e, ${project.name}-${project.version}-javadoc.jar
  • a sources jar, i.e, ${project.name}-${project.version}-sources.jar

It also consumes the output of the producer module. This is how the build file looks

We did not define values for the group and version properties as we'll inject them from Maven. Right, we just need to cover one last build file. Here's where the secret sauce comes in as everything we've seen so far is pretty straight forward. In order to run Gradle as a black-box we have to ensure the following conditions:

    1. Materialize all required dependencies into an specific location.
    2. Execute the Gradle build.
    3. Copy generated JAR files to a standard location.
    4. Attached generated JAR files to the build.

Gradle must be able to resolve module dependencies on its own, however as the build does not participate in the Maven reactor we must copy all dependencies to a particular destination. Gradle can resolve dependencies from Maven compatible repositories, Ivy repositories, and directories containing JARs (the flatDir option). We could use the last option but that would mean defining every single dependency in the Gradle build file, including transitive dependencies. It'd be better if we could rely on full artifact resolution, thus a Maven compatible repository is the way to go. Some people think that pushing intermediate artifacts to Maven Local is sufficient enough but that would cause trouble in the reactor as you'd have to invoke Maven in a two-step process making sure that the consumer project is skipped on the first step. It's better if we copy all dependencies to a known location using the standard repository layout, we'll use maven-dependency-plugin for this

Note that all dependencies will be placed under target/dependencies, if you look back to the Gradle build file you'll see there's an additional repository entry that matches this location. Next we have to define how Gradle is invoked, for this we'll use exec-maven-plugin to execute the Gradle wrapper associated with the module.

Here you can observe that the group and version properties are injected into the Gradle build. If you'd like to run the Gradle build in standalone mode then you'll have to define those properties in a way that Gradle can have access, for example in the build file itself, or on a file named gradle.properties that could be refreshed using a Maven goal (perhaps leveraging the antrun plugin). The important bit is that even if you define those values explicitly in the Gradle build they will get overridden when the build is invoked from Maven thanks to the evaluation order of project properties. Once the build is finished (and it's been successful) it's time to copy the generated JARs to the standard Maven location, we can use maven-resources-plugin to make this work

Finally we make sure that these JARs are added to the Maven build in case that any other module or plugin requires them, we'll use build-helper-maven-plugin to accomplish this task

Executing the build from the root yields the following result

Note that the reactor builds the producer project first, then moves to consumer and invokes the Gradle build. You can see the output of Gradle when the exec plugin is invoked. A fully working example can be found here.

Keep on coding!

Liked it? Take a second to support aalmiray on Patreon!
Become a patron at Patreon!

3 comments

  • I wanted to leave some feedback. The article is great, though there are some points that could be improved. Here they are:

    1. The third, fourth, fifth, and sixth pom section belong to the child project, not the parent one. It may be obvious to you, but not to a beginner.

    2. For all four Maven plugins versions should be specified. Namely:
    3.1.1
    1.6.0
    3.1.0
    3.0.0
    Change as needed.

    3. The ${gradle.executable} property is not explained and was not present in my installation. Its setup should be explained.

  • Also, in the Gradle snippet you should change the “compile” dependency for an “implementation” dependency, since “compile” is now deprecated in Gradle; will be removed for good in Gradle 7.0

Trackbacks/Pingbacks

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

ˆ Back To Top