{{announcement.body}}
{{announcement.title}}

The Complete Gradle Plugin Tutorial

DZone 's Guide to

The Complete Gradle Plugin Tutorial

Let's learn how to build, confgiure, and apply a Gradle plugin to your project using Kotlin as the programming language of choice.

· Java Zone ·
Free Resource

Gradle is a very powerful tool that allows you to set up a build process for your project, no matter how complex it is. But when I faced the need of interacting with a Gradle project (set up from scratch, extending or just fix a few lines of code) I hardly managed to do it without additional Googling. If you’ve ever felt the same, you should build your own plugin that might help understand how Gradle works.

This tutorial is useful for developers who also want to build their own plugin. I will describe how to do it in detail from creating a plugin project up to applying a plugin to a project.

Create a Standalone Gradle Plugin Project

First, choose what programming language to use. In general, you can use any language you like that compiles to JVM bytecode.

I recommend using Kotlin as the most suitable language for the task.

Here’s why:

  • It’s statically typed

  • Allows you to produce expressive code

  • Has a lot of "sugar" that allows building a nice DSL

Kotlin has borrowed a lot of features from Groovy, so you can improve your Groovy skills as well.

We will build a sample plugin for counting the lines of code in a project.

Let’s start...

1. Create a New Kotlin Project

  • Name: code-lines-counter
  • ArtifactId: code-lines-counter
  • GroupId: com.github

2. Gradle Project Configuration

Time to add Gradle API tooling. To do this, apply the Java Gradle Plugin. The plugin automatically adds the Gradle API dependency, TestKit, and applies the Java Library plugin. It's a basic setup that helps us to build and test our plugin.

Open your build.gradle file located in the root of the project and add the Java Gradle plugin:

Groovy
 




xxxxxxxxxx
1


1
plugins {
2
   id 'java'
3
   id 'java-gradle-plugin'
4
   id 'org.jetbrains.kotlin.jvm' version '1.3.61'
5
   id 'maven'
6
}



Now we can remove the 'java' plugin because we implicitly got the Java Library plugin. We also applied a 'maven' plugin that will be used to publish our plugin to the local maven repository.

The final step is adding a pluginId that will help Gradle identify and apply your plugin. Configure the 'java-gradle-plugin' that will generate a META-INF file:

Groovy
 




x


1
gradlePlugin {
2
   plugins {
3
       simplePlugin {
4
           id = 'com.github.code-lines'
5
           implementationClass = 'com.github.CodeLinesCounterPlugin'
6
       }
7
   }
8
}



In the code above, we specified the plugin's id as 'com.github.code-lines' and the plugin's main class as 'com.github.CodeLinesCounterPlugin', which will be created later.

That’s it, we finished configuring our plugin! That’s how full build.gradle file looks like now:

Groovy
 




xxxxxxxxxx
1
41


1
plugins {
2
   id 'java-gradle-plugin'
3
   id 'org.jetbrains.kotlin.jvm' version '1.3.61'
4
   id 'maven'
5
}
6
 
          
7
group 'com.github'
8
version '0.0.1'
9
 
          
10
sourceCompatibility = 1.8
11
 
          
12
repositories {
13
   mavenCentral()
14
}
15
 
          
16
dependencies {
17
   implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8"
18
   testCompile group: 'junit', name: 'junit', version: '4.12'
19
}
20
 
          
21
compileKotlin {
22
   kotlinOptions.jvmTarget = "1.8"
23
}
24
 
          
25
compileTestKotlin {
26
   kotlinOptions.jvmTarget = "1.8"
27
}
28
 
          
29
gradlePlugin {
30
   plugins {
31
       simplePlugin {
32
           id = 'com.github.code-lines'
33
           implementationClass = 'com.github.CodeLinesCounterPlugin'
34
       }
35
   }
36
}



3. Coding

Create your com.github package under the src/main/kotlin folder and add a Kotlin class with the name  CodeLinesCounterPlugin (specified earlier for 'java-gradle-plugin') implementing the  Plugin<Project>  interface. Let’s implement it and create our first Gradle task:

Kotlin
 




xxxxxxxxxx
1
19


 
1
package com.github
2
 
          
3
import org.gradle.api.Plugin
4
import org.gradle.api.Project
5
 
          
6
class CodeLinesCounterPlugin : Plugin<Project> {
7
 
          
8
   override fun apply(project: Project) {
9
       project.tasks.create("codeLines") {
10
           task.doLast {
11
               println("Hello from CodeLinesCounterPlugin")
12
           }
13
       }.apply {
14
           group = "stat"
15
       }
16
   }
17
}



We created a task with the name codeLines that prints "Hello from CodeLinesCounterPlugin" message. 

Let’s test our plugin. To do this we need to publish the plugin to maven local repot. Call install gradle task.  When the publishing is complete, go to   {HOME-DIR}/.m2/repository/com/github/code-lines-counter and check that the plugin's artifacts were created.

Applying the 'code-lines-counter' Plugin to a Project

Open build.gradle and add 'code-lines-counter' dependency in the buildscript block:

Groovy
 




x


 
1
buildscript {
2
   repositories {
3
       mavenLocal() // plugin published to maven local
4
   }
5
   dependencies {
6
       classpath 'com.github:code-lines-counter:0.0.1' // plugin’s artifact
7
   }
8
}
9
 
          
10
...
11
   
12
apply plugin: 'com.github.code-lines' // applying plugin



And execute the task:

Shell
 




x


1
gradlew codeLines



The output:

Plain Text
 




x


 
1
> Task :codeLines
2
 
          
3
Hello from CodeLinesCounterPlugin
4
 
          
5
BUILD SUCCESSFUL in 2s



Let’s replace the dummy output with ‘real-world’ logic. We need to walk through sourcesets, read files with code and sum lines:

Kotlin
 




xxxxxxxxxx
1
10
9


 
1
private fun printCodeLinesCount(project: Project) {
2
   var totalCount = 0
3
   project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.forEach { sourceSet ->
4
       sourceSet.allSource.forEach { file ->
5
           totalCount += file.readLines().count()
6
       }
7
   }
8
   println("Total lines: $totalCount")
9
}



Let’s update our plugin with the function:

Kotlin
 




xxxxxxxxxx
1
22


 
1
class CodeLinesCounterPlugin : Plugin<Project> {
2
 
          
3
   override fun apply(project: Project) {
4
       project.tasks.create("codeLines") { task ->
5
           task.doLast {
6
               printCodeLinesCount(project)
7
           }
8
       }.apply {
9
           group = "stat"
10
       }
11
   }
12
 
          
13
   private fun printCodeLinesCount(project: Project) {
14
       var totalCount = 0
15
       project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.forEach { sourceSet ->
16
           sourceSet.allSource.forEach { file ->
17
               totalCount += file.readLines().count()
18
           }
19
       }
20
       println("Total lines: $totalCount")
21
   }
22
}



Checking it. First, reinstall the plugin with install task and run codeLines task again:

Plain Text
 




x


1
> Task :codeLines
2
 
          
3
Total lines: 22
4
 
          
5
BUILD SUCCESSFUL in 2s



Here we go!

Let’s make our plugin configurable. Suppose you want to count only Java/Kotlin code stats or to skip blank lines. We’d like the plugin to be configurable via build.gradle file in the following way:

Groovy
 




xxxxxxxxxx
1


1
codeLinesStat {
2
   sourceFilters.skipBlankLines = true
3
   fileExtensions = ['java', 'kt', 'groovy']
4
}



To make it work, create two data classes that will keep all the settings:

Kotlin
 




xxxxxxxxxx
1


1
open class CodeLinesExtension(
2
    var sourceFilters: SourceFiltersExtension = SourceFiltersExtension(),
3
    var fileExtensions: MutableList<String> = mutableListOf()
4
)
5
 
          
6
open class SourceFiltersExtension(
7
    var skipBlankLines: Boolean = false
8
)



Important! Kotlin classes are final by default. Declare a configuration class as open (those that can be inherited) to make the Gradle processing successful. Also, class fields must be mutable, so a plugin’s user can change the defaults.

Then, we ask Gradle to build an instance of the CodeLinesExtension class based on the build.gradle file.

Kotlin
 




x


1
val codeLinesExtension: CodeLinesExtension = project.extensions.create(
2
        "codeLinesStat", 
3
        CodeLinesExtension::class.java
4
)



Now we can process the task according to a user's configuration!

CodeLinesCounterPlugin.kt
Kotlin
 




x


 
1
class CodeLinesCounterPlugin : Plugin<Project> {
2
   override fun apply(project: Project) {
3
       project.tasks.create("codeLines") { task ->
4
           // build extension instance
5
           val codeLinesExtension = project.extensions.create(
6
                   "codeLinesStat", CodeLinesExtension::class.java
7
           )
8
           task.doLast {
9
               printCodeLinesCount(project, codeLinesExtension)
10
           }
11
       }.apply { group = "stat" }
12
   }
13
 
           
14
   private fun printCodeLinesCount(project: Project, codeLinesExtension: CodeLinesExtension) {
15
       val fileFilter = codeLinesExtension.buildFileFilter()
16
       var totalCount = 0
17
       project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.forEach { sourceSet ->
18
           sourceSet.allSource
19
               .filter(fileFilter) // filters files according to desired list of extensions
20
               .forEach { file ->
21
                   val lines = file.readLines()
22
                   totalCount += if (codeLinesExtension.sourceFilters.skipBlankLines) {
23
                       lines.count(CharSequence::isNotBlank) // skips blank lines
24
                   } else {
25
                       lines.count()
26
                   }
27
               }
28
       }
29
       println("Total lines: $totalCount")
30
   }
31
 
           
32
   private fun CodeLinesExtension.buildFileFilter(): (File) -> Boolean = if (fileExtensions.isEmpty()) {
33
       { true } // no-op filter
34
   } else {
35
       { fileExtensions.contains(it.extension) } // filter by extension
36
   }
37
 
           
38
    open class CodeLinesExtension(
39
        var sourceFilters: SourceFiltersExtension = SourceFiltersExtension(),
40
        var fileExtensions: MutableList<String> = mutableListOf()
41
    )
42
    open class SourceFiltersExtension(
43
        var skipBlankLines: Boolean = false
44
    )
45
}



That’s it, we created a simple plugin in 45 lines of code!

If your plugin requires a more complex configuration, you can provide functions to make the plugin API more user friendly:

Kotlin
 




x



1
open class CodeLinesExtension(
2
    var sourceFilters: SourceFiltersExtension = SourceFiltersExtension(),
3
    var fileExtensions: MutableList<String> = mutableListOf()
4
) {
5
    // consumes `action` that contains a configuration for `sourceFilters`
6
    // and overrides `sourceFilters` fields
7
    fun sourceFilters(action: Action<in SourceFiltersExtension>) {
8
      action.execute(sourceFilters)
9
    }
10
}



After this improvement, the plugin can be configured this way:

Groovy
 




x


1
// All five variants are equivalent
2
codeLinesStat {
3
    // 1. direct property override
4
    sourceFilters.skipBlankLines = true
5
 
          
6
    // 2. override using function `fun sourceFilters(action: Action<in SourceFiltersExtension>)`
7
    sourceFilters({
8
        skipBlankLines = true
9
    })
10
    // 3. omit parentheses
11
    sourceFilters {
12
        skipBlankLines = true
13
    }
14
    
15
    // 4. override using setter
16
    sourceFilters.setSkipBlankLines(true)
17
    // 5. omit parentheses
18
    sourceFilters.setSkipBlankLines true
19
}



Check a fully working example here.

Feel free to clone and play with the project 

Shell
 




xxxxxxxxxx
1


1
git clone -b chapter-1 https://github.com/SurpSG/code-lines-counter-gradle-plugin.git



References

https://docs.gradle.org/current/userguide/custom_plugins.html

https://docs.gradle.org/current/userguide/java_gradle_plugin.html 

https://docs.gradle.org/current/userguide/test_kit.html#test_kit

https://kotlinlang.org/docs/reference/server-overview.html 

Topics:
gradle ,gradle plugins ,gradle tutorial ,java ,kotlin ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}