DEV Community

Roger Viñas Alcon
Roger Viñas Alcon

Posted on

First Steps Developing Custom Gradle Plugins

Not long ago Gradle scared me a lot 👻 ... maybe it was because of Groovy? 😱

But today I am complete in ❤️ with Gradle! Please don't tell Maven 😜

Gradle plugins allows us to reuse build logic across different projects, and we can implement them in any JVM compatible language: Java, Kotlin, Groovy, ...

In this demo we will implement basic Gradle plugins following the Developing Custom Gradle Plugins documentation. If you have time, take a look at Designing Gradle plugins and among other sections Convention over configuration and Capabilities vs. conventions

It will be fun I promise!

GitHub logo rogervinas / gradle-plugins-first-steps

🧞‍♂️ First Steps Developing Custom Gradle Plugins

Step by step

Let's follow these steps:

gradle-plugins-first-steps

In this demo we will use a sample multi-module Gradle project named my-gradle-project with two modules and a custom hello task defined as:

tasks.create("hello") {
  doLast {
    println("Hello from ${project.name}!")
  }
}
Enter fullscreen mode Exit fullscreen mode

So we can simply execute ./gradlew hello and check all the plugins that are applied.

Create plugins in the Build Script

As a first step, we can define plugins directly on our build script. This is enough if we do not have to reuse them outside the build script they are defined in.

Build Script settings plugin

To create a settings plugin and apply it in my-gradle-project > settings.gradle.kts:

class MyBuildSettingsPlugin : Plugin<Settings> {
  override fun apply(settings: Settings) {
    println("Plugin ${this.javaClass.simpleName} applied on ${settings.rootProject.name}")
    // TODO configure `settings`
  }
}

apply<MyBuildSettingsPlugin>()
Enter fullscreen mode Exit fullscreen mode

For simplicity this example plugin only prints a line whenever is applied. A real plugin should do something with the Settings object that is passed as parameter.

Try it running ./gradlew hello and it should print this line:

Plugin MyBuildSettingsPlugin applied on my-gradle-project
Enter fullscreen mode Exit fullscreen mode

Build Script project plugin

To create a project plugin in my-gradle-project > build.gradle.kts:

class MyBuildProjectPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("Plugin ${this.javaClass.simpleName} applied on ${project.name}")
        // TODO configure `project`
    }
}
Enter fullscreen mode Exit fullscreen mode

Then for example we can apply the plugin on all projects:

allprojects {
    apply<MyBuildProjectPlugin>()
}
Enter fullscreen mode Exit fullscreen mode

Again this example plugin only prints a line whenever is applied. A real plugin should do something with the Project object that is passed as parameter.

Try it running ./gradlew hello and it should print these lines:

> Configure project :
Plugin MyBuildProjectPlugin applied on my-gradle-project
Plugin MyBuildProjectPlugin applied on my-module-1
Plugin MyBuildProjectPlugin applied on my-module-2
Enter fullscreen mode Exit fullscreen mode

Create plugins in the buildSrc module

As a second step, we can define project plugins in a special module named buildSrc. All plugins defined there will be only visible to every build script within the project.

But most important, we can add tests! 🤩

First we create buildSrc module under my-gradle-project using Gradle init and the kotlin-gradle-plugin template

We implement the plugin in MyBuildSrcProjectPlugin.kt:

class MyBuildSrcProjectPlugin : Plugin<Project> {
  override fun apply(project: Project) {
    println("Plugin ${this.javaClass.simpleName} applied on ${project.name}")
    project.tasks.register("my-buildsrc-project-task") { task ->
      task.doLast {
        println("Task ${task.name} executed on ${project.name}")
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

We register it in buildSrc > build.gradle.kts, giving it an id:

gradlePlugin {
    plugins {
        create("my-buildsrc-project-plugin") {
            id = "com.rogervinas.my-buildsrc-project-plugin"
            implementationClass = "com.rogervinas.MyBuildSrcProjectPlugin"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And we unit test it in MyBuildSrcProjectPluginTest.kt:

@Test
fun `should add new task to project`() {
  val project = ProjectBuilder.builder().build()
  project.plugins.apply("com.rogervinas.my-buildsrc-project-plugin")

  assertThat(project.tasks.findByName("my-buildsrc-project-task")).isNotNull()
}
Enter fullscreen mode Exit fullscreen mode

As you can see in this example the plugin registers a new task named my-buildsrc-project-task.

So now we can use it in any build script for example in root my-gradle-project > build.gradle.kts applied to allprojects:

plugins {
  id("com.rogervinas.my-buildsrc-project-plugin")
}

allprojects {
  apply(plugin = "com.rogervinas.my-buildsrc-project-plugin")
}
Enter fullscreen mode Exit fullscreen mode

And then we can try it executing ./gradlew my-buildsrc-project-task and it should print these lines:

> Configure project :
Plugin MyBuildSrcProjectPlugin applied on my-gradle-project
Plugin MyBuildSrcProjectPlugin applied on my-module-1
Plugin MyBuildSrcProjectPlugin applied on my-module-2

> Task :my-buildsrc-project-task
Task my-buildsrc-project-task executed on my-gradle-project

> Task :my-module-1:my-buildsrc-project-task
Task my-buildsrc-project-task executed on my-module-1

> Task :my-module-2:my-buildsrc-project-task
Task my-buildsrc-project-task executed on my-module-2
Enter fullscreen mode Exit fullscreen mode

Notes:

Create plugins in a standalone project

As a final step, if we want to reuse plugins among all our projects and even share them with the rest of the world, we can create them in a separate project.

For this sample I've created a project my-gradle-plugins with two independent modules each using Gradle init and the kotlin-gradle-plugin template. Other templates can be used: java-gradle-plugin or groovy-gradle-plugin

I've decided to create one plugin per module, but you could define many plugins in the same module.

Standalone settings plugin

We implement the plugin in MySettingsPlugin.kt:

class MySettingsPlugin : Plugin<Settings> {
    override fun apply(settings: Settings) {
        println("Plugin ${this.javaClass.simpleName} applied on ${settings.rootProject.name}")
        settings.gradle.allprojects { project ->
            project.tasks.register("my-settings-task") { task ->
                task.doLast {
                    println("Task ${task.name} executed on ${project.name}")
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We register it in build.gradle.kts, giving it an id:

gradlePlugin {
    plugins {
        create("my-settings-plugin") {
            id = "com.rogervinas.my-settings-plugin"
            implementationClass = "com.rogervinas.MySettingsPlugin"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

And we test it in a functional test in MySettingsPluginFunctionalTest.kt, with real gradle projects saved under src/functionalTest/resources:

@Test
fun `should add new task to single-project`() {
    val runner = GradleRunner.create()
    runner.forwardOutput()
    runner.withPluginClasspath()
    runner.withArguments("my-settings-task")
    runner.withProjectDir(File("src/functionalTest/resources/single-project"))
    val result = runner.build()

    assertThat(result.output).all {
        contains("Plugin MySettingsPlugin applied on single-project")
        contains("Task my-settings-task executed on single-project")
    }
}
Enter fullscreen mode Exit fullscreen mode

Notes:

  • If you check MySettingsPluginFunctionalTest.kt you will see two tests: one for one single-project and one for one multi-module project.
  • I have not found any way to unit test a settings plugin. For settings plugins there is no helper class like there is org.gradle.testfixtures.ProjectBuilder for project plugins. If you know a way please let me know! 🙏
  • We use static gradle projects saved under src/functionalTest/resources but we can also generate gradle projects programmatically, saving them on temporary folders (check this sample).

Standalone project plugin

We implement the plugin in MyProjectPlugin.kt:

class MyProjectPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        println("Plugin ${this.javaClass.simpleName} applied on ${project.name}")
        project.tasks.register("my-project-task") { task ->
            task.doLast {
                println("Task ${task.name} executed on ${project.name}")
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We register it in build.gradle.kts, giving it an id:

gradlePlugin {
    plugins {
        create("my-project-plugin") {
            id = "com.rogervinas.my-project-plugin"
            implementationClass = "com.rogervinas.MyProjectPlugin"
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

We test it in a unit test in MyProjectPluginTest.kt:

@Test
fun `should add new task to project`() {
  val project = ProjectBuilder.builder().build()
  project.plugins.apply("com.rogervinas.my-project-plugin")

  assertThat(project.tasks.findByName("my-project-task")).isNotNull()
}
Enter fullscreen mode Exit fullscreen mode

We test it in a functional test in MyProjectPluginFunctionalTest.kt with real gradle projects saved under src/functionalTest/resources:

@Test
fun `should add new task to single-project`() {
  val runner = GradleRunner.create()
  runner.forwardOutput()
  runner.withPluginClasspath()
  runner.withArguments("my-project-task")
  runner.withProjectDir(File("src/functionalTest/resources/single-project"))
  val result = runner.build()

  assertThat(result.output).all {
    contains("Plugin MyProjectPlugin applied on single-project")
    contains("Task my-project-task executed on single-project")
  }
}
Enter fullscreen mode Exit fullscreen mode

Notes:

Using the standalone plugins

To use the standalone plugins locally during development we have two alternatives:

Then we declare which version we want to use just once in my-gradle-project > settings.gradle.kts:

pluginManagement {
  plugins {
    id("com.rogervinas.my-settings-plugin") version "1.0"
    id("com.rogervinas.my-project-plugin") version "1.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

We apply the settings plugin in my-gradle-project > settings.gradle.kts:

plugins {
  id("com.rogervinas.my-settings-plugin")
}
Enter fullscreen mode Exit fullscreen mode

We apply the project plugin in any build script for example in my-gradle-project > build.gradle.kts applied to allprojects:

plugins {
  id("com.rogervinas.my-project-plugin")
}

allprojects {
  apply(plugin="com.rogervinas.my-project-plugin")
}
Enter fullscreen mode Exit fullscreen mode

And finally we can publish them to any private or public repository or to Gradle Plugin Portal 🎉

Run this demo

Run using includeBuild

cd my-gradle-project
./gradlew hello
Enter fullscreen mode Exit fullscreen mode

If you want to know more about includeBuild you can read about Composing builds

Run using mavenLocal

  • Build and publish my-gradle-plugins locally:
cd my-gradle-plugins
./gradlew publishToMavenLocal
Enter fullscreen mode Exit fullscreen mode
cd my-gradle-project
./gradlew hello
Enter fullscreen mode Exit fullscreen mode

That's all! Happy coding! 💙

Top comments (0)