Skip to Content
Course content

218: Writing a Custom Gradle Plugin in Kotlin

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Gradle is a powerful tool, but let's be honest: most of us spend 90% of our time just copying snippets from StackOverflow into our build.gradle.kts files. But once your project grows—or you're managing a dozen microservices—you'll find yourself repeating the same build logic everywhere. That's when you stop writing scripts and start writing a custom plugin.

For this lesson, we're going to build a LicenseCheckerPlugin. The goal is simple: the build should fail if the project is missing a LICENSE file in the root directory. It's a small utility, but it perfectly illustrates how to hook into the Gradle lifecycle.

Setting up our buildSrc sandbox

Instead of creating a whole separate project and publishing it to MavenLocal, we'll use the buildSrc directory. If you haven't used it before, buildSrc is a special directory at your project root that Gradle compiles automatically and adds to the classpath of your main build scripts. It's the best place for internal plugins.

First, I'll create the directory structure: buildSrc/src/main/kotlin. Then, I need to tell buildSrc that it's actually a Kotlin-Gradle project. I'll add a build.gradle.kts inside the buildSrc folder:

plugins {
    `kotlin-dsl`
}

repositories {
    mavenCentral()
}

The wrong way: checking during configuration

Now, let's write the plugin. My first instinct is usually to just put the logic inside the apply method. I'll try to check for the file right as the plugin is loaded.

import org.gradle.api.Plugin
import org.gradle.api.Project
import java.io.File

class LicenseCheckerPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val licenseFile = File(project.projectDir, "LICENSE")
        if (!licenseFile.exists()) {
            throw GradleException("Build failed: LICENSE file is missing from the root directory!")
        }
    }
}

I ran this, and it worked... but it worked too well. I noticed that every single time I ran ./gradlew tasks or ./gradlew help, the build failed. This is a classic Gradle mistake: I put execution logic in the Configuration phase.

Gradle has a lifecycle. Configuration is when it builds the graph of tasks. Execution is when it actually runs them. By putting the check in apply, I'm forcing the check to happen every time Gradle even looks at the project. We only want this check to happen when we're actually building the project.

Doing it right with a dedicated Task

To fix this, I need to wrap my logic in a DefaultTask. This moves the check into the Execution phase. I'll define a task class and then register it within the plugin.

import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.TaskAction
import java.io.File

abstract class CheckLicenseTask : DefaultTask() {
    @TaskAction
    fun check() {
        val licenseFile = File(project.projectDir, "LICENSE")
        if (!licenseFile.exists()) {
            throw GradleException("Build failed: LICENSE file is missing from the root directory!")
        }
        println("License file found. We're good to go!")
    }
}

class LicenseCheckerPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        project.tasks.register("checkLicense", CheckLicenseTask::class.java) {
            group = "verification"
            description = "Ensures a LICENSE file exists in the project root."
        }
    }
}

Now, running ./gradlew checkLicense works exactly as expected. If the file is missing, it crashes; if it's there, it prints a success message. More importantly, running other Gradle commands no longer triggers this check.

Hooking it into the build lifecycle

Running a manual task is fine, but we want this to be automatic. We want the checkLicense task to run whenever the project is checked (the standard check task in Java/Kotlin projects).

I'll update the apply method to create a dependency. I'm using dependsOn here to tell Gradle: "Before you run the check task, you must successfully run checkLicense."

override fun apply(project: Project) {
    val checkLicense = project.tasks.register("checkLicense", CheckLicenseTask::class.java) {
        group = "verification"
        description = "Ensures a LICENSE file exists in the project root."
    }

    project.tasks.findByName("check")?.dependsOn(checkLicense)
}

Finally, to actually use this in the main project, I just need to apply it in the root build.gradle.kts. Since it's in buildSrc, I can just reference the class or give it an ID. For simplicity, I'll use the class approach:

apply<LicenseCheckerPlugin>()

Now, if you try to run ./gradlew check without a LICENSE file, Gradle will stop you dead in your tracks. That's the power of a custom plugin: you've codified a project requirement into the build process itself.




📋 Practical Task

Exercise: Build a Semantic Version Validator Plugin

Your goal is to create a custom Gradle plugin in buildSrc that validates the project version. Many companies require versions to follow a strict "Major.Minor.Patch" format (e.g., 1.0.4) and forbid versions like 1.0-SNAPSHOT or latest during release builds.

Requirements:

  • Create a plugin called VersionValidatorPlugin.
  • Register a task called validateVersion.
  • The task should retrieve the project version using project.version.toString().
  • Use a Regular Expression to check if the version matches the pattern: ^\d+\.\d+\.\d+$ (one or more digits, dot, one or more digits, dot, one or more digits).
  • If the version does not match, throw a GradleException with a descriptive message explaining the required format.
  • Ensure the validateVersion task is automatically executed whenever the build task is run.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.