Popular Posts

Gradle plugin development

Writing Custom Plugin

Gradle is an open-source build automation system that builds upon the concepts of Apache Ant and Apache Maven and introduces a Groovy-based domain-specific language (DSL) instead of the XML.

Demonstrating gradle plugin development to generate customized sourceSet.

— ThirupathiReddy Vajjala
sourceset

Writing Custom Plugin

Gradle makes it very easy to build custom binary plugins. You simply need to create a class that implements the org.gradle.api.Plugin<T> interface.

The plugin class and its code can be reside in one of the following three locations:

  1. Build script: Can be directly embedded into the build script. This approach limits the reuse value of the plugin

  2. buildSrc project: can reside under the buildSrc project is automatically compiled and is made available in the build scripts classpath.

  3. Stand-alone project: can be bundled as a JAR file that can then be included in the build script’s classpath.

Creating a Java Plugin (buildSrc project)

Create Gradle plugin by creating folder structure as shown below

|my-gradle-plugin
|
|__buildSrc
      |
      |__src
          |__main
              |__java
              |
              |--groovy
                  |__com.tvajjala.plugin
                  |
                  |__com.tvajjala.task

Understanding Gradle Build System

Gradle built system has two major building blocks

  1. project

  2. task

A Gradle build system is made up of one or more projects and each project contains one or more tasks.

Project

A project in Gradle is an abstract concept that represents an artifact that needs to be built.

For each project in the build, Gradle creates an instance of org.gradle.api.Project and associates it with the build script. This allows the build scripts to use Project’s API to access properties and customize build behavior at runtime (for example, by creating new tasks or skipping existing tasks).

add wrapper task to build.gradle to generate wrapper for your project
TestStrategyPlugin.groovy
package com.tvajjala.plugins
import com.tvajjala.actions.SrcSetAction
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.tasks.SourceSetContainer
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import static com.tvajjala.constants.Constants.SOURCE_SET
import static com.tvajjala.constants.TestStrategy.*

/**
 * @author ThirupathiReddy Vajjala
 *
 * Plugin used to generate custom sourceSet for different types of tests execution
 * this allows you to write test cases in a better way
 *
 */
class TestStrategyPlugin implements Plugin<Project> {

    Logger LOG= LoggerFactory.getLogger(TestStrategyPlugin.class)


    @Override
    void apply(Project project) {

        LOG.debug("Generating sourceSet and tasks for testing")

        SourceSetContainer container = (SourceSetContainer) project.getProperties().get(SOURCE_SET)

        container.create(UNIT.sourceSetName, new SrcSetAction(UNIT, project))

        container.create(LAYER.sourceSetName, new SrcSetAction(LAYER, project))

        container.create(CONTRACT.sourceSetName, new SrcSetAction(CONTRACT, project))

        container.create(INTEGRATION.sourceSetName, new SrcSetAction(INTEGRATION, project))

    }
}
Read SourceSetContainer object from project reference.
SourceSetAction.groovy
package com.tvajjala.actions

import com.tvajjala.actions.internal.ResourceDirectoryAction
import com.tvajjala.actions.internal.SrcDirectoryAction
import com.tvajjala.constants.TestStrategy
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.SourceSet
import org.gradle.api.tasks.SourceSetContainer
import org.gradle.api.tasks.TaskContainer
import org.gradle.api.tasks.testing.Test

import static com.tvajjala.constants.Constants.SOURCE_SET

/**
 * @author ThirupathiReddy Vajjala
 *
 * Creates SourceSet and add required classpath to that directory
 *
 */
class SrcSetAction implements Action<SourceSet> {

    TestStrategy testStrategy

    Project project

    SourceSetContainer container

    SrcSetAction(TestStrategy testStrategy, Project project) {
        this.testStrategy = testStrategy
        this.project = project
        this.container = (SourceSetContainer) project.getProperties().get(SOURCE_SET)
    }


    @Override
    void execute(SourceSet sourceSet) {

        def javaSrcSet = sourceSet.java(new SrcDirectoryAction(testStrategy.taskName))
        /** Setting classpath to avoid compilation errors */
        javaSrcSet.setCompileClasspath(javaSrcSet.compileClasspath + container.getByName("main").output.classesDirs+
                container.getByName("main").compileClasspath)

        /** Setting runtime classpath to execute tests */
        javaSrcSet.setRuntimeClasspath(javaSrcSet.runtimeClasspath + container.getByName("main").output.classesDirs+
                container.getByName("main").runtimeClasspath)

        /** Defining resource folder for yml or properties  */
        sourceSet.resources(new ResourceDirectoryAction(testStrategy.taskName))



        TaskContainer taskContainer = project.getTasks()
        /** Define task to execute specific testCases */
        taskContainer.create(testStrategy.taskName, Test.class, new TaskAction(testStrategy, project))
        taskContainer.getByName("clean", new CleanupAction(testStrategy.taskName))

    }


}

Commonly used Project API Properties and methods

Table 1. Project API
Property/Method Description

name

Name of the project and can be changed using the settings.gradle file. By default, the project directory name.

defaultTasks

Configures the names of the default tasks to run for a project

parent

Returns the parent project

task

Overloaded method to create new task

INFO: SourceSetContainer , add new actions to create/customize default folder structure.

Tasks

Gradle projects are made up of one or more tasks that perform build steps.

Tasks execute actions such as compile Java source code and generate classes or clean target folders. For each task in build file, Gradle creates an instance of org.gradle.api.Task. By default it would be org.gradle.api.DefaultTask.

Table 2. Task APIs
Property/Method Description

name

Name of the task

enabled

Decides if a task is enabled or disabled

doFirst

Adds an action to the beginning of the task’s action list

doLast

Adds an action to the end of tasks action list

onlyIf

Runs a task only if the passed in closure returns true

dependson

Configures task dependencies

TaskAction.groovy
package com.tvajjala.actions

import com.tvajjala.constants.TestStrategy
import org.gradle.api.Action
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.tasks.SourceSetContainer
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import static com.tvajjala.constants.Constants.GROUP_NAME
import static com.tvajjala.constants.Constants.SOURCE_SET

/**
 * @author ThirupathiReddy Vajjala
 *
 * Registers tasks to project
 *
 */
class TaskAction implements Action<Task> {

    static Logger LOG= LoggerFactory.getLogger(TaskAction.class)

    TestStrategy testStrategy

    Project project

    SourceSetContainer container

    TaskAction(TestStrategy testStrategy, Project project) {

        this.testStrategy = testStrategy
        this.project = project
        this.container = (SourceSetContainer) project.getProperties().get(SOURCE_SET)
    }


    @Override
    void execute(Task task) {

        /** Group name where task is defined */
        task.setGroup(GROUP_NAME)

        LOG.info("Adding $testStrategy.taskName to Group $GROUP_NAME")

        /** Description of the task */
        task.setDescription(testStrategy.description)

        /** Specify test classes location to run */
        task.setTestClassesDirs(container.getByName(testStrategy.sourceSetName).output.classesDirs)

        /** Specify classpath for runtime dependencies */
        task.setClasspath(container.getByName(testStrategy.sourceSetName).runtimeClasspath)

        LOG.info("Setting classpath to task")
    }
}

Short Plugin Name

Plugins by default are referred to by their fully qualified class names. it is however possible to give your plugins short, easy-to-use names.

This can be achieved by creating file in the src/main/resources/META-INF/gradle-plugin folder. The name of the properties file becomes the plugin’s short name.

Since this plugin deals with build numbers, we can call the plugin build-number-plugin.

to accomplish this, create a build-number-plugin.properties file under the gradle-plugins folder.

Inside the file, you simply create a property with the key implementation-class and the fully qualified class as its value:

com.tvajjala.test-strategy.properties
implementation-class=com.tvajjala.plugins.TestStrategyPlugin

use below line to register plugin to your project

apply plugin: 'com.tvajjala.test-strategy'

Plugin Source code dependencies

Plugin source code dependencies can be defined under buildscript section.

build.gradle
    buildscript {
        ext {
            springBootVersion = '2.1.0.RELEASE'
        }
        repositories {
            mavenLocal()
            mavenCentral()
            jcenter()
        }
        dependencies {
            classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
        }
    }

Semantic versioning 2.0.0 guidelines

Software projects typically have version numbers that are expressed using three numbers separated by periods:

<major-version>.<minor-version>.<patch/incremental-version>.<build-number>

Ex: 1.0.0.56

  1. major feature changes that are usually not backward compatible.

  2. minor versions are incremented when implementing minor features or major bugs.

  3. patch/incremental versions are incremented for minor bugs, text changes , etc.

  4. build number for incremental builds from development teams. build numbers can be obtained

from source code check-ins or build timestamps or continuous integration (CI) server-generated numbers.

1 comment:

  1. This is really nice post, I found and love this content also visit Serp Api. Thanks for sharing.

    ReplyDelete