Ad-hoc Plugins with Gradle

Gradle code reuse on steroids.

There are many ways to reuse code in Gradle builds. The major and the most powerful one is to write a Gradle plugin either as an independent project or inside buildSrc. You may want to check official documentation here. All of these approaches require additional efforts from both the development and the infrastructure sides. Will you and your team be happy of you doing that?

I want to share the short-cut to make code reuse without too much work. I will consider a Multi-Project Gradle builds. It makes less to no sense for a Single Project Gradle builds, there is nothing to reuse, generally.

What Gradle code to reuse? There are a plenty of places for it. The most trivial is dependencies. You’d like to try using the same versions of libraries along the sub-projects. Repositories, packaging, testing, plugins, configurations are the places of reuse too. Yet another example is Kotlin plugin or Scala plugin configuration.

Subprojects

The most standard feature in Gradle builds. subprojects is the block where you apply the same code snippet to all child (any level) projects.

Projects in Gradle forms a tree structure, it replicates the filesystem. A parent project in Gradle is the project that is in the parent folder of a child project folder. The build.gradle is optional and you are not obliged to have one. For example, the line include ':a:b:c:d' from a settings.gradle file defines the following child projects: a, b, c and d.

I like to use subprojects { .... } block in parent projects to configure common stuff, like dependencies, plugins, repositories. Let’s see the example:

subprojects {
    repositories {
        jcenter()
        mavenCentral()
    }
 
    dependencies {
        compile "org.slf4j:slf4j-api:1.7.25"
        testCompile "junit:junit:4.12"
    }   
}        

Here I set up the default maven repositories in all child projects. Also, I include the standard dependencies, namely, SLF4J and JUnit4. The configuration of JUnit5 is yet another great example to share Gradle code between projects.

It is possible that one does not want to configure all child projects, but some projects only. It is possible. But, It’d be better to move that project somewhere instead. I use the following, configure(project(':a'), project('b').subprojects) { .... } to apply the same settings to several projects only.

Apply From File

The next way to apply common configuration is to use apply: from: file(...foo.gradle) syntax. You move the configuration to a dedicated file somewhere in the project. You include the file to every project, where you need it.

That is the way to re-include the same part of your Gradle script into many projects. The only problem here is that the build.gradle and the ...foo.gradle are executed in an isolated environment. It might be hard to pass parameters back and forth.

A Common Plugin

The most generic approach is to create a Gradle plugin or a custom task. There is the documentation page. Set’s see, how to try writing a plugin just in our .gradle files.

I use IntelliJ IDEA to navigate to interfaces and javadocs. It helps me a lot with code completion and error highlighting directly in .gradle files, which actually use Groovy.

Gradle allows declaring classes in .gradle files. That is the way to create an ad-hoc plugin just in a parent project. You should know, that you will not be able to see the created class outside the .gradle file, where you created it. The good part is that you may still use subprojects and write the following:


subprojects {
  apply plugin: MyPluginClass
}

class MyPluginClass implements Plugin<Project> {
  @Override
  public void apply(Project project) {
    ...
  }
}

That is the most trivial way to create you own Gradle plugin and to enable it for all child projects. It does not require one to invest into infrastructure (e.g. deployment of a plugin, build configurations and so on).

You may use org.gradle.api.plugins.ExtensionContainer#create(...) function to add your own Gradle DSL extension. That is the standard way to include parameters from the plugin usages.

Ext Block

What if I need to apply the ad-hoc plugin only to some selected sub-projects? And I do not like to code it in the parent project build.gradle. Similarly, I may want to reuse some dependencies especially for selected projects.

The ext { .. } block in Gradle is for me. I combine it with the subprojects { .. } block. Closure (or Lambda) is a possible value of a property from ext block too. Let’s combine those features to level-up the code-reuse in our Gradle script. First, in a parent project we add the code:

subprojects {
  ext {
    dep_arrow = {Project project ->
      project.dependencies {
        compile "io.arrow-kt:arrow-core:0.6.1"
        compile "io.arrow-kt:arrow-typeclasses:0.6.1"
        compile "io.arrow-kt:arrow-data:0.6.1"
    }
  }  
}

Then in a selected child project I simply enable Arrow library by writing the only one line:

ext.dep_arrow(project)

Theoretically, It should work with an implicitly captured Project in Closures, without Project parameter, e.g. dep_arrow = { dependencies { ... } }. I was lazy to try that.

The similar trick helps to enable ad-hoc Gradle plugins too.

The Use Case

I work on an project, where we decided to use micro-services. Technically, it means, we split the whole code base into the set of small executables. The same language, namely Kotlin/JVM, is used to implement all services. There are common things one need to have for every service, for example, it includes logging configuration, communication setup, crashes handling. I need the only way to pack those services too.

What did I do? At first, I use Application plugin to implement the executable. It executes the same entry-point class. The entry-point class finds the micro-service class to start it. I plan to use the jib plugin to wrap my services into Docker images.

From the Gradle side of things. I created scripts for one service manually. Then, I turned that code into an ad-hoc Gradle plugin. I enabled the plugin explicitly in the service projects. The plugin configures the Application plugin, adds dependencies, adds an extension to get service-specific parameters.

Right now, the service is created with only a few Gradle lines:

ext.micro_service(project)

micro_service {
    entryClassName = '<THE ENTRY POINT CLASS OF THE SERVICE>'
}

Let your builds be simply. Feed free to ask me for details in the comments below. There is also the official documentation for Gradle.

comments powered by Disqus