The Complete Gradle Plugin Tutorial
Let's learn how to build, configure, and apply a Gradle plugin to your project using Kotlin as the programming language of choice.
Join the DZone community and get the full member experience.
Join For FreeGradle 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 plugins. 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 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:
plugins {
id 'java'
id 'java-gradle-plugin'
id 'org.jetbrains.kotlin.jvm' version '1.5.21'
id 'maven-publish'
}
Now we can remove the 'java'
plugin because we implicitly got the Java Library plugin. We also applied a 'maven-publish'
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:
x
gradlePlugin {
plugins {
simplePlugin {
id = 'com.github.code-lines'
implementationClass = 'com.github.CodeLinesCounterPlugin'
}
}
}
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:
plugins {
id 'java-gradle-plugin'
id 'org.jetbrains.kotlin.jvm' version '1.5.21'
id 'maven'
}
group 'com.github'
version '0.0.1'
sourceCompatibility = 1.8
repositories {
mavenCentral()
}
dependencies {
implementation 'org.jetbrains.kotlin:kotlin-stdlib-jdk8'
testImplementation 'junit:junit:4.12'
}
compileKotlin {
kotlinOptions.jvmTarget = "1.8"
}
compileTestKotlin {
kotlinOptions.jvmTarget = "1.8"
}
gradlePlugin {
plugins {
simplePlugin {
id = 'com.github.code-lines'
implementationClass = 'com.github.CodeLinesCounterPlugin'
}
}
}
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:
xxxxxxxxxx
package com.github
import org.gradle.api.Plugin
import org.gradle.api.Project
class CodeLinesCounterPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.create("codeLines") {
task.doLast {
println("Hello from CodeLinesCounterPlugin")
}
}.apply {
group = "stat"
}
}
}
We created a task with the name codeLines that prints the "Hello from CodeLinesCounterPlugin" message.
Let’s test our plugin. To do this we need to publish the plugin to maven local repository. Call publishToMavenLocal 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:
x
buildscript {
repositories {
mavenLocal() // plugin published to maven local
}
dependencies {
classpath 'com.github:code-lines-counter:0.0.1' // plugin’s artifact
}
}
...
apply plugin: 'com.github.code-lines' // applying plugin
And execute the task:
x
gradlew codeLines
The output:
x
> Task :codeLines
Hello from CodeLinesCounterPlugin
BUILD SUCCESSFUL in 2s
Let’s replace the dummy output with ‘real-world’ logic. We need to walk through source sets, read files with code and sum lines:
xxxxxxxxxx
private fun printCodeLinesCount(project: Project) {
var totalCount = 0
project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.forEach { sourceSet ->
sourceSet.allSource.forEach { file ->
totalCount += file.readLines().count()
}
}
println("Total lines: $totalCount")
}
Let’s update our plugin with the function:
xxxxxxxxxx
class CodeLinesCounterPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.create("codeLines") { task ->
task.doLast {
printCodeLinesCount(project)
}
}.apply {
group = "stat"
}
}
private fun printCodeLinesCount(project: Project) {
var totalCount = 0
project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.forEach { sourceSet ->
sourceSet.allSource.forEach { file ->
totalCount += file.readLines().count()
}
}
println("Total lines: $totalCount")
}
}
Checking it. First, reinstall the plugin with install task and run codeLines task again:
x
> Task :codeLines
Total lines: 22
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:
xxxxxxxxxx
codeLinesStat {
sourceFilters.skipBlankLines = true
fileExtensions = ['java', 'kt', 'groovy']
}
To make it work, create two data classes that will keep all the settings:
xxxxxxxxxx
open class CodeLinesExtension(
var sourceFilters: SourceFiltersExtension = SourceFiltersExtension(),
var fileExtensions: MutableList<String> = mutableListOf()
)
open class SourceFiltersExtension(
var skipBlankLines: Boolean = false
)
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.
x
val codeLinesExtension: CodeLinesExtension = project.extensions.create(
"codeLinesStat",
CodeLinesExtension::class.java
)
Now we can process the task according to a user's configuration!
CodeLinesCounterPlugin.kt
x
class CodeLinesCounterPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.tasks.create("codeLines") { task ->
// build extension instance
val codeLinesExtension = project.extensions.create(
"codeLinesStat", CodeLinesExtension::class.java
)
task.doLast {
printCodeLinesCount(project, codeLinesExtension)
}
}.apply { group = "stat" }
}
private fun printCodeLinesCount(project: Project, codeLinesExtension: CodeLinesExtension) {
val fileFilter = codeLinesExtension.buildFileFilter()
var totalCount = 0
project.convention.getPlugin(JavaPluginConvention::class.java).sourceSets.forEach { sourceSet ->
sourceSet.allSource
.filter(fileFilter) // filters files according to desired list of extensions
.forEach { file ->
val lines = file.readLines()
totalCount += if (codeLinesExtension.sourceFilters.skipBlankLines) {
lines.count(CharSequence::isNotBlank) // skips blank lines
} else {
lines.count()
}
}
}
println("Total lines: $totalCount")
}
private fun CodeLinesExtension.buildFileFilter(): (File) -> Boolean = if (fileExtensions.isEmpty()) {
{ true } // no-op filter
} else {
{ fileExtensions.contains(it.extension) } // filter by extension
}
open class CodeLinesExtension(
var sourceFilters: SourceFiltersExtension = SourceFiltersExtension(),
var fileExtensions: MutableList<String> = mutableListOf()
)
open class SourceFiltersExtension(
var skipBlankLines: Boolean = false
)
}
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:
x
open class CodeLinesExtension(
var sourceFilters: SourceFiltersExtension = SourceFiltersExtension(),
var fileExtensions: MutableList<String> = mutableListOf()
) {
// consumes `action` that contains a configuration for `sourceFilters`
// and overrides `sourceFilters` fields
fun sourceFilters(action: Action<in SourceFiltersExtension>) {
action.execute(sourceFilters)
}
}
After this improvement, the plugin can be configured this way:
x
// All five variants are equivalent
codeLinesStat {
// 1. direct property override
sourceFilters.skipBlankLines = true
// 2. override using function `fun sourceFilters(action: Action<in SourceFiltersExtension>)`
sourceFilters({
skipBlankLines = true
})
// 3. omit parentheses
sourceFilters {
skipBlankLines = true
}
// 4. override using setter
sourceFilters.setSkipBlankLines(true)
// 5. omit parentheses
sourceFilters.setSkipBlankLines true
}
Check a fully working example here.
Feel free to clone and play with the project
xxxxxxxxxx
git clone -b chapter-1 https://github.com/SurpSG/code-lines-counter-gradle-plugin.git
Cover the plugin With Functional Tests
There is a separate article where you could find detailed instructions about configuring and writing functional tests for your plugin.
References
Opinions expressed by DZone contributors are their own.
Comments