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!
Let's follow these steps:
- Create plugins in the Build Script
- Create plugins in the buildSrc module
- Create plugins in a standalone project
- Run this demo
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}!")
}
}
So we can simply execute ./gradlew hello
and check all the plugins that are applied.
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.
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>()
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
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`
}
}
Then for example we can apply the plugin on all projects:
allprojects {
apply<MyBuildProjectPlugin>()
}
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
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}")
}
}
}
}
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"
}
}
}
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()
}
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")
}
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
Notes:
- Apart from unit tests we can also add functional tests to the buildSrc module. I omitted them here for simplicity (you can see an example in the Standalone Project section)
- We cannot define settings plugins on buildSrc since Gradle 5.x because classes from buildSrc are no longer visible to settings scripts
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.
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}")
}
}
}
}
}
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"
}
}
}
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")
}
}
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).
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}")
}
}
}
}
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"
}
}
}
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()
}
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")
}
}
Notes:
- If you check MyProjectPluginFunctionalTest.kt you will see two tests: one for one single-project and one for one multi-module project.
- 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).
To use the standalone plugins locally during development we have two alternatives:
- Using
includeBuild
: see Run using includeBuild - Publishing the plugins locally: see Run using mavenLocal
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"
}
}
We apply the settings plugin in my-gradle-project > settings.gradle.kts:
plugins {
id("com.rogervinas.my-settings-plugin")
}
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")
}
And finally we can publish them to any private or public repository or to Gradle Plugin Portal 🎉
- Edit my-gradle-project > settings.gradle.kts and:
- Remove or comment line
mavenLocal()
in pluginManagement > repositories - Add or uncomment line
includeBuild("../my-gradle-plugins")
- Execute:
cd my-gradle-project
./gradlew hello
If you want to know more about includeBuild
you can read about Composing builds
- Build and publish my-gradle-plugins locally:
cd my-gradle-plugins
./gradlew publishToMavenLocal
- Edit my-gradle-project > settings.gradle.kts and:
- Add or uncomment line
mavenLocal()
in pluginManagement > repositories - Remove or comment line
includeBuild("../my-gradle-plugins")
- Execute:
cd my-gradle-project
./gradlew hello
That's all! Happy coding! 💙