Following up with the Gradle POM, where a familiar structure found in Maven projects can be brought to Gradle projects, I'd like to discuss another Maven feature that many see as an advantage and that's lacking in Gradle: the ability to enforce settings in a top-down fashion with hierarchical POM files.
In Maven, a parent POM delivers configuration that will be used by a child POM as is. Children POMs have the option to override and extend these properties. A typical use case in parent POMs is to provide
<dependencyManagement> sections; they also configure default dependencies, properties, plugins, and paths. Many developers see this as an opportunity to close down certain features as well as providing a standard structure across projects within an organization. A parent POM may as well inherit settings from another POM, continuing the chain up to the topmost POM, the Maven Super POM, which is out of reach for modifications, only the Maven team has the access key to make updates (and for good reasons). Don't believe me? Let's have a look at this innocuous POM file
We can generate the effective POM, the resolved POM used by Maven when running this project, by issuing the
mvn help:effective-pom command, resulting in the following content displayed on the screen
Wow, that's a lot of stuff. We can see that the Maven Central repository is defined by default; funny thing is that Maven Local is not found in this file, it appears this repository is always available and its location is set in code. Next we see all the conventional paths for inputs and outputs. Finally a series of plugins are also configured by default; these plugins deliver the basic behavior that we've grown to expect when building a base Java project.
Let's turn to Gradle now. There's no concept of a parent POM, heck there's no concept identical to a Maven POM. Don't get me wrong, Gradle does have conventions and a default structure, it actually offers more than Maven, however the structure is so flexible that it's easy to walk outside of the beaten path without intention. Say we've have come up with our own structure (you may have decided to give the Gradle POM a try), how on earth are we going to enforce such structure in other projects? We have a couple of options at our disposal as a matter of fact.
The first option is to create a Gradle script file, put all the conventions and settings on it, place this script in a location reachable by the target build files (make sure it's versioned!) and simply apply from the URL. Done. You need more hierarchical levels? Simply keep applying them in the super scripts. As tempting as this approach may look like I'd advise against it as included scripts may lead to unexpected classloading errors, specially when combined with the current iteration of the Kotlin DSL.
The second option is to create a plugin project. We gain proper artifact identification (name and version) as well as distribution, as this plugin is just another artifact that may be deployed to your local/remote repository. We also gain better code structure in case the Super POM delivers more features than those that should be packed in a single Plugin class. We however have to deal with artifact deployment, c'est la vie. I do like this approach better as it enables the same growth path as regular builds. Here's for example a sample SuperpomPlugin that configures common settings using the Gradle POM plugin, also default repositories like those found in Maven
If you squint your eyes a little bit you'll see that the contents of the this plugin look a lot like a POM. You may apply any plugins you deem necessary downstream, as well as creating plugin extensions, properties, and more. In case you're wondering, the build file for this plugin looks like so
Still a bit verbose but that's the price we have to pay for bootstrapping. The good news is that the next iteration of this superpom can consume the last, thus reducing the amount of setup in the build file, eating your own dog food as they say. To test out this superpom plugin I simply published it to my local Maven repository by invoking
gradle publish. What's left is to consume this plugin in a project, we have to perform two things given how this plugin is deployed
- Configure the plugin management block to be aware of the repositories where the plugin and its dependencies may be found.
- Apply the plugin to the target project.
The first task can be solved by adding the following code to the
This file is read and executed before the actual build file, thus it's the perfect place to configure such settings as plugin repositories. Now we only have to apply the plugin to the target project, such as
For the final reveal, let's invoke the
effectiveSettings task, showing the info and bintray sections
Notice that some values come from the superpom plugin and others from the target project, this is exactly what we want! Now imagine the superpom plugin adds default dependencies, or applies the
dependency-management plugin that delivers similar features as the
dependencyManagement block in Maven POM files. Another possibility would be to provide type safe dependency definitions, with the possibility to override a particular version. And we're just scratching the surface here.
Summarizing, the technique demonstrated in this post can be used to attain similar behavior as Maven's parent and super POM features, while putting you in full control of which features and settings may be inherited to children projects. Plugins are a powerful feature of Gradle, it's time we get more out of them.
All code found in this post is available for free at this gist.
Keep on coding!