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:
-
- Materialize all required dependencies into an specific location.
- Execute the Gradle build.
- Copy generated JAR files to a standard location.
- 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!
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.
thanks for your feedback. The blog post links to a fully functional example (found at the end of the post) that can be cloned/copied to verify the instructions, specifically https://github.com/aalmiray/gradle-in-maven/blob/master/consumer/pom.xml.
With regards to plugin versions, yes, they must be added if you care about reproducible builds, otherwise the defaults are fine.
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