Applying a 3rd Party Gradle Plugin as a Composite Plugin

This post shows you how to wrap a 3rd party Gradle plugin in your own plugin, so that you can interactive with it programmatically.

Applying a 3rd Party Gradle Plugin as a Composite Plugin

This post shows you how to wrap a 3rd party Gradle plugin in your own plugin, so that you can interactive with it programmatically. We’ll use Gradle composite builds to allow us to build all at once, decreasing the build, test, release cycle.

Ever used a gradle plugin and wanted to make some tweaks to it, but don’t want the hassle of forking it, or deploying another version to depend on? Using Gradle composite builds you can create a Gradle Plugin that your build can depend on. Then within that plugin, have it apply the third party plugin you want to use / modify.

Note: A 3rd party plugin means one developed separately from your repository. It doesn’t necessarily mean one written by other people. Example’s of such plugins could be ktlint, android gradle plugin, affected module detector, anything you’ve written yourself or by your team/company that sits in a separate Gradle root.

What is a composite build?

A composite build is simply a build that includes other builds. In many ways a composite build is similar to a Gradle multi-project build, except that instead of including single projects, complete builds are included.

Composite builds allow you to:
– combine builds that are usually developed independently, for instance when trying out a bug fix in a library that your application uses

– decompose a large multi-project build into smaller, more isolated chunks that can be worked in independently or together as needed

For this example we are going to use the Affected Module Detector (AMD) Gradle plugin from Dropbox. We will create a Gradle plugin of our own, include it as a composite build and then have that plugin apply the AMD plugin.

The outcome will be that our multi-module project will depend on the AMD plugin and we will be able to run AMD tasks, whilst also being able to augment the AMD plugin behaviour however we like because its wrapped in our own plugin.

All code for this post is available at the repo here. Something we aren’t going to cover is how to apply the AMD plugin in a typical gradle way, however that is covered in the repo, and is available on this branch. If you want to skip straight to the composite solution, then that is available on this branch.

First thing when wanting to create a composite build is creating the folder and Gradle structure. Our project is a basic new Android project from the Android Studio IDE blank template. It has an app module and we have also added an Android library module (mylibrary) so that it is multi-module. Giving you a folder structure like so:

/ 
 build.gradle
 settings.gradle
 app/
    src/
    build.gradle
 mylibrary/
    src/
    build.gradle

We’re going to have our app depend on a plugin we create as a composite plugin. To create a composite plugin, you start with a folder containing a build.gradle a settings.gradle and the other usual Gradle files. (You can use the gradle init command to create these, or copy them from a previous project.) As well as the Gradle root folder files just discussed, we add a sub-project called ‘plugin’ and this also has a build.gradle. Giving you an updated folder structure like so:

/ 
 amd-plugin/
    gradle/
    build.gradle
    gradlew
    settings.gradle
    plugin/
      build.gradle
      src/
 app/
    src/ 
    build.gradle
 mylibrary/
    src/
    build.gradle
 build.gradle
 settings.gradle

Reminder, you can see this folder structure setup, here on GitHub.

Filling out /amd-plugin/settings.gradle looks like this:

pluginManagement { // Declare where we want to find plugin dependencies
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}
dependencyResolutionManagement { // Declare where we want to find code dependencies
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}

rootProject.name = "blundell-amd-plugin" // Name this project

include(":plugin") // Ensure our module (project) is used

Filling out /amd-plugin/build.gradle looks like this:

plugins {
    // We want to use Kotlin
    id("org.jetbrains.kotlin.jvm") version "1.7.21" apply false
    // We want to be able to publish the plugin
    id("com.gradle.plugin-publish") version "1.1.0" apply false
}

Filling out /amd-plugin/plugin/build.gradle looks like this:

plugins {
    id("java-gradle-plugin")
    id("org.jetbrains.kotlin.jvm")
    id("com.gradle.plugin-publish")
}

dependencies {
    testImplementation("junit:junit:4.13.2")
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

Those 3 files (settings.gradle, 2x build.gradle) make up the infra of our new project. Gradle should now be able to build that project successfully, however it doesn’t do anything so isn’t much use yet.

You can hopefully notice that everything under amd-plugin looks like a stand-alone gradle project, and that’s because it is. 🙂

Now let’s create a composite build of our original project with this newly created one. Once you have two Gradle projects, its a single line to combine them into a composite build. Here we are composing a plugin so we are including it under pluginManagement, if you wanted to compose source code you would put it with the typical includes at the bottom.

In your root project settings.gradle:

pluginManagement {
    repositories {
        gradlePluginPortal()
        google()
        mavenCentral()
    }
    includeBuild("amd-plugin") // This line allows our plugin project to be included as a composite build
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        google()
        mavenCentral()
    }
}
rootProject.name = "CompositePlugin"
include ':app'
include ':mylibrary'

Once you have declared the composite build like this, you should be able to ‘sync’ your Android Studio IDE and the amd-plugin will start to show as a multi-module project that is included.

The sync’d project (Project View) in Android Studio.

We’ve now connected two Gradle projects together to build as a composite. The last thing left is to fill our our plugin to actually do something. Reminder; we’re going to wrap the Affected Module Detector Plugin from Dropbox, so that we can augment its functionality.

To create a plugin we need to declare to Gradle, what our plugin is and where it is, this helps Gradle create the jar that our plugin exists in. This means changing our /amd-plugin/plugin/build.gradle and adding the gradlePlugin closure:

gradlePlugin {
    plugins {
        blundAffectedModsPlugin {
            id = "com.blundell.amd.plugin" 
            implementationClass = "com.blundell.amd.BlundellAffectedModulesPlugin" // This is the fully qualified name and path to the plugin ( we will create next )
        }
    }
} 

Whilst we are in this file, let’s add a dependency on the 3rd party (AMD) plugin to our dependencies block:

dependencies {
  implementation(
    "com.dropbox.affectedmoduledetector:affectedmoduledetector:0.2.0"
  )
  testImplementation("junit:junit:4.13.2")
}

Now we have a dependency on the AMD plugin, we can access its public api. We also declared our plugin class, let’s create the corresponding source code for that. Create a new file: amd-plugin/plugin/src/main/kotlin/com/blundell/amd/BlundellAffectedModulesPlugin.kt

BlundellAffectedModulesPlugin.kt is our plugin therefore we extend from the Gradle org.gradle.api.Plugin:

package com.blundell.amd

import com.dropbox.affectedmoduledetector.AffectedModuleConfiguration
import com.dropbox.affectedmoduledetector.AffectedModuleDetectorPlugin
import org.gradle.api.Plugin
import org.gradle.api.Project

class BlundellAffectedModulesPlugin : Plugin<Project> {

    override fun apply(project: Project) = project.run {
        project.plugins.apply(AffectedModuleDetectorPlugin::class.java)
        pluginManager.withPlugin("com.dropbox.affectedmoduledetector") {
            val config = rootProject.extensions.findByType(AffectedModuleConfiguration::class.java)!!
            config.logFolder = "${project.buildDir}/amd-output"
            config.logFilename = "output.log"
            logger.lifecycle("We can now interact with the plugin programmatically (as above).")
        }
    }
}

Here is what this code does:

  • We apply the AffectedModuleDetectorPlugin so that anyone applying this plugin, applies the 3rd party plugin
  • With the AMD plugin applied, we get the AMD plugins config and configure it so that it will print out logs into our /build folder when it is running

Once the plugin is created, the last thing we need to do is have our main project apply our BlundellAffectedModulesPlugin so it can be used. This is done in the main project’s root build.gradle:

buildscript {
    ext {
        compose_ui_version = '1.3.2'
    }
}
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
    id 'com.android.application' version '7.3.1' apply false
    id 'com.android.library' version '7.3.1' apply false
    id 'org.jetbrains.kotlin.android' version '1.6.10' apply false
    id 'com.blundell.amd.plugin' // This applies our composite plugin
}

And that’s it! Once you have applied the plugin you can sync your IDE to pick up the latest changes. Then if you run an AMD plugin command such as:

./gradlew runAffectedUnitTests -Paffected_module_detector.enable

You will see your own plugin running and wrapping the 3rd party.

Congratulations on your composed build. You can take this further and create your own tasks so that you can have even more control of what is run and when, the main point being, now that you have a composite build, you are able to programatically interact with other plugins and have all that code in your IDE. Allowing you to debug in one place, things that where typically hard to follow across multiple projects, and decompose your work in smaller more isolated chunks.