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.
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
, completebuilds
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.
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.