Gradle Multi-module Projects — Centralized Configuration

Posted by & filed under , .

businessman looking to screen with matrix background

This is a sweet gradle trick for those of you working on multi-module projects, if you are looking to centralize some of the configuration in the parent gradle build, and avoid repeating configuration / build code across sub-modules.

Let’s say you have the following gradle project structure (see image below) where project A is the top level and has 2 modules: project B, C and D. All of the modules are java projects (so they apply plugin: 'java') however, for 2 of those (B and D) you decide to use Spock framework for unit testing while the other one (module C) you decide to stick with normal JUnit testing. As such, your module B and D will also apply plugin: 'groovy'.

Now since you use Groovy in your project, you decide to take it one step further and apply CodeNarc to keep a certain level of code quality. So now you go and apply plugin: 'codenarc' to project B and D too. And since you want to share the same set of rules across projects, you set up your rules file (somewhere in the top level project) and decide to specify its location in these 2 projects via the codenarc closure — something like this:

codenarc {
   configFile = "path/to/my/file"
   ...
}

(and you might add any other CodeNarc configuration in there too I guess.)

project-structure-initial

Because you are dealing with a multi-module project, and since all of your modules are written in java, you decide you don’t need to repeat apply plugin: 'java' in each module, but instead, in project A build.gradle you apply it for all subprojects:

// top level (project A) build.gradle
subprojects {
   apply plugin: 'java'
}

So your project structure and build files will look like this:

project-structure-2

Now as you can see, for project C above the build file is nearly empty; however, there is a lot of duplication in between projects B and D. Even more so, if we say add a new module (E) which we want to use Spock (therefore Groovy!) for testing, and we also want to keep the code in check via CodeNarc, we have to now duplicate the piece of build code found in module B and D in this new module. This can easily get out of hand and open the door to lots of errors and inconsistent configuration.

Ideally, what we want here is the same thing as with the Java plugin and apply that block of code in the top level (A) build file — that would ensure all the subprojects will inherit the same behaviour, plugins and configuration:

subprojects {
   apply plugin: ‘groovy’
   apply plugin: ‘codenarc’
   codenarc {
      configFile =}
}

Trouble is if we do that now we apply Groovy and CodeNarc plugin to all of our modules (including C) — which is not what we want, since we only want this applied to B and D!

Turns out there are (at least) 2 ways of doing this: filtering by module name or filtering by plugins applied to module.

Filtering By Module Name

This method allows you to do filter subprojects (modules) based on their names — and as such only take actions when the name matches (or doesn’t match!) a name. For instance, in our case, we want to apply the above block only to module B and D, so we can check that the sub-project name matches "B" or "D" with a code as follows:

subprojects {
   if( it.name == "B" || it.name == "D" ) {
      apply plugin: ‘groovy’
      apply plugin: ‘codenarc’
      codenarc {
         configFile =}
   }
}

If the above code doesn’t make sense, let me break it down a bit: subprojects takes a closure as a parameter (hence the {..}) and as such this closure will be invoked for each of the modules in your project. And since by default in Groovy, the closure parameter is named "it" if not specified, the above piece of code reads “apply this closure to each one of the modules in my project; and if the name property of the module matches “B” or “D” then apply plugin groovy, codenarc and set the configuration for CodeNarc plugin as follows…”.

This solves our problem but has a small issue: if we want to add module E to the project, similar to B and D, then we need to update the closure above to include “E” in the check too. To avoid that, we can easily change the check to be slightly different:

subprojects {
   if( it.name != "C" ) {
      apply plugin: ‘groovy’
      apply plugin: ‘codenarc’
      codenarc {
         configFile =}
   }
}

This build file now simply applies the above to all modules but C; so if any module is added to the project, it will have the same settings applied to it as module B and D — without changes to the build file.

Filtering By Plugins Applied To Module

The above method works based on module name — on the basis that you know that module B needs this, but module C doesn’t, yet you need it again in module D and E but not F etc. In other words it works because there is a decision made by the dev team ahead of time which modules to get what configurations.

This solution though proposes a more dynamic approach: the dev team only decides in this case where they will need Spock (and therefore Groovy plugin) — and just simply go ahead and apply plugin: 'groovy' in whatever module they want. The top level build file then applies CodeNarc and set up the configuration for any module which applies the groovy plugin!

subprojects {
    plugins.withType( org.gradle.api.plugins.GroovyPlugin ) {
      apply plugin: ‘codenarc’
      codenarc {
         configFile =}
    }
}

So now your project structure and build files looks like this:

project-structure-2

Now you are sharing the same behaviour (applying CodeNarc plugin) and same CodeNarc configuration across all the modules in your project which use the Groovy plugin. And if at any point you need to add module E (as above), you don’t have to change anything in this file — simply create the module E directory structure and if you decide to use Spock, just apply plugin: 'groovy' in your module build file. Then the top level build file will do all the magic. Or, if you don’t want Spock and want to stick for instance with JUnit (so you don’t need Groovy), you simply do not include the Groovy plugin in your module build file — and the top level build file will ensure you do not this time inherit all the other plugins and configuration applied for Groovy!

And as you can probably figure out, you can use this approach for any plugin applied to your modules, so you can easily standardize all of your configuration across the modules.