Writing plugin code is a routine activity for advanced build authors. The activity usually involves writing the plugin implementation, creating custom task type for executing desired functionality and making the runtime behavior configurable for the end user by exposing a declarative and expressive DSL. In this guide you will learn established practices to make you a better plugin developer and how make a plugin as accessible and useful for consumers as possible. Please consider working through the guide on designing Gradle plugins before reading this guide.

The guide assumes you have:

  • Basic understanding of software engineering practices

  • Knowledge of Gradle fundamentals like project organization, task creation and configuration as well as the Gradle build lifecycle

  • Working knowledge in writing Java code

If you happen to be a beginner to Gradle please start by working through the Getting Started Guides on Gradle development first while referencing the Gradle User Manual to go deeper.

1. Practices

1.1. Using the Plugin Development plugin for writing plugins

Setting up a Gradle plugin project should require as little boilerplate code as possible. The Java Gradle Plugin Development plugin provides aid in this concern. To get started add the following code to your build.gradle file:

build.gradle
plugins {
    id 'java-gradle-plugin'
}

By applying the plugin, necessary plugins are applied and relevant dependencies are added. It also helps with validating the plugin metadata before publishing the binary artifact to the Gradle plugin portal. Every plugin project should apply this plugin.

1.2. Prefer writing and using custom task types

Gradle tasks can be defined as ad-hoc tasks, simple task definitions of type DefaultTask with one or many actions, or as enhanced tasks, the ones that use a custom task type and expose its configurability with the help of properties. Generally speaking, custom tasks provide the means for reusability, maintainability, configurability and testability. The same principles hold true when providing tasks as part of plugins. Always prefer custom task types over ad-hoc tasks. Consumers of your plugin will also have the chance to reuse the existing task type if they want to add more tasks to the build script.

Let’s say you implemented a plugin that resolves the latest version of a dependency in a binary repository by making HTTP calls by providing a custom task type. The custom task is provided by a plugin that takes care of communicating via HTTP and processing the response in machine-readable format like XML or JSON.

LatestArtifactVersion.java
package com.company.gradle.binaryrepo;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;

public class LatestArtifactVersion extends DefaultTask {
    private String coordinates;
    private String serverUrl;

    @Input
    public String getCoordinates() {
        return coordinates;
    }

    public void setCoordinates(String coordinates) {
        this.coordinates = coordinates;
    }

    @Input
    public String getServerUrl() {
        return serverUrl;
    }

    public void setServerUrl(String serverUrl) {
        this.serverUrl = serverUrl;
    }

    @TaskAction
    public void resolveLatestVersion() {
        System.out.println("Retrieving artifact " + coordinates + " from " + serverUrl);
        // issue HTTP call and parse response
    }
}

The end user of the task can now easily create multiple tasks of that type with different configuration. All the imperative, potentially complex logic is completely hidden in the custom task implementation.

build.gradle
import com.company.gradle.binaryrepo.LatestArtifactVersion

task latestVersionMavenCentral(type: LatestArtifactVersion) {
    coordinates = 'commons-lang:commons-lang:1.5'
    serverUrl = 'http://repo1.maven.org/maven2/'
}

task latestVersionInhouseRepo(type: LatestArtifactVersion) {
    coordinates = 'commons-lang:commons-lang:2.6'
    serverUrl = 'http://my.company.com/maven2'
}

1.3. Benefiting from incremental tasks

Gradle uses declared inputs and outputs to determine if a task is up-to-date and needs to perform any work. If none of the inputs or outputs have changed, Gradle can skip that task. Gradle calls this mechanism incremental build support. The advantage of incremental build support is that it can significantly improve the performance of a build.

It’s very common for Gradle plugins to introduce custom task types. As a plugin author that means that you’ll have to annotation all properties of a task with input or output annotations. It’s highly recommended to equip every task with the information to run up-to-date checking. Remember: for up-to-date checking to work properly a task needs to define both inputs and outputs.

Let’s consider the following sample task for illustration. The task generates a given number of files in an output directory. The text written to those files is provided by a String property.

Generate.java
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;

import org.gradle.api.DefaultTask;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;

public class Generate extends DefaultTask {
    private int fileCount;
    private String content;
    private File generatedFileDir;

    @Input
    public int getFileCount() {
        return fileCount;
    }

    public void setFileCount(int fileCount) {
        this.fileCount = fileCount;
    }

    @Input
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    @OutputDirectory
    public File getGeneratedFileDir() {
        return generatedFileDir;
    }

    public void setGeneratedFileDir(File generatedFileDir) {
        this.generatedFileDir = generatedFileDir;
    }

    @TaskAction
    public void perform() throws IOException {
        for (int i = 1; i <= fileCount; i++) {
            writeFile(new File(generatedFileDir, i + ".txt"), content);
        }
    }

    private void writeFile(File destination, String content) throws IOException {
        BufferedWriter output = null;
        try {
            output = new BufferedWriter(new FileWriter(destination));
            output.write(content);
        } finally {
            if (output != null) {
                output.close();
            }
        }
    }
}

The first section of this guide talks about the Plugin Development plugin. As an added benefit of applying the plugin to your project, the task validateTaskProperties automatically checks for an existing input/output annotation for every public property define in a custom task type implementation.

1.4. Modeling DSL-like APIs

DSLs exposed by plugins should be readable and easy to understand. For illustration let’s consider the following extension provided by a plugin. In its current form it offers a "flat" list of properties for configuring the creation of a web site.

build.gradle
apply plugin: SitePlugin

site {
    outputDir = file('build/mysite')
    websiteUrl = 'http://gradle.org'
    vcsUrl = 'https://github.com/gradle-guides/gradle-site-plugin'
}

As the number of exposed properties grows, you might want to introduce a nested, more expressive structure. The following code snippet adds a new configuration block named customData as part of the extension. You might have noticed that it provides a stronger indication of what those properties mean.

build.gradle
apply plugin: SitePlugin

site {
    outputDir = file('build/mysite')

    customData {
        websiteUrl = 'http://gradle.org'
        vcsUrl = 'https://github.com/gradle-guides/gradle-site-plugin'
    }
}

It’s fairly easy to implement the backing objects of such an extension. First of all, you’ll need to introduce a new data object for managing the properties websiteUrl and vcsUrl.

CustomData.java
public class CustomData {
    private String websiteUrl;
    private String vcsUrl;

    public void setWebsiteUrl(String websiteUrl) {
        this.websiteUrl = websiteUrl;
    }

    public String getWebsiteUrl() {
        return websiteUrl;
    }

    public void setVcsUrl(String vcsUrl) {
        this.vcsUrl = vcsUrl;
    }

    public String getVcsUrl() {
        return vcsUrl;
    }
}

In the extension, you’ll need to create an instance of the CustomData class and a method that can delegate the captured values to the data instance. To configure underlying data objects define a parameter of type org.gradle.api.Action. The following example demonstrates the use of Action in an extension definition.

SiteExtension.java
import java.io.File;
import org.gradle.api.Action;

public class SiteExtension {
    private File outputDir;
    private final CustomData customData = new CustomData();

    public void setOutputDir(File outputDir) {
        this.outputDir = outputDir;
    }

    public File getOutputDir() {
        return outputDir;
    }

    public CustomData getCustomData() {
        return customData;
    }

    public void customData(Action<? super CustomData> action) {
        action.execute(customData);
    }
}
If you need second- or third-level nesting, you will also have to add overloads that take a Closure, because Gradle cannot instrument nested extensions at the moment.

1.5. Capturing user input to configure plugin runtime behavior

Plugins often times come with default conventions that make sensible assumptions about the consuming project. The Java plugin, for example, searches for Java source files in the directory src/main/java. Default conventions are helpful to streamline project layouts but fall short when dealing with custom project structures, legacy project requirements or a different user preference.

Plugins should expose a way to reconfigure the default runtime behavior. The section Prefer writing and using custom task types describes one way to achieve configurability: by declaring setter methods for task properties. The more sophisticated solution to the problem is to expose an extension. An extension captures user input through a custom DSL that fully blends into the DSL exposed by Gradle core.

The following example applies a plugin that exposes an extension with the name binaryRepo to capture a server URL:

build.gradle
apply plugin: BinaryRepositoryVersionPlugin

binaryRepo {
    serverUrl = 'http://my.company.com/maven2'
}

Let’s assume that you’ll also want to do something with the value of serverUrl once captured. In many cases the exposed extension property is directly mapped to a task property that actually uses the value when performing work. To avoid evaluation order problems you should use the public API PropertyState which was introduced in Gradle 4.0.

Let’s have a look at the internals of the plugin BinaryRepositoryVersionPlugin to give you a better idea. The plugin creates the extension of type BinaryRepositoryExtension and maps the extension property serverUrl to the task property serverUrl.

BinaryRepositoryVersionPlugin.java
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;

public class BinaryRepositoryVersionPlugin implements Plugin<Project> {
    public void apply(Project project) {
        BinaryRepositoryExtension extension = project.getExtensions().create("binaryRepo", BinaryRepositoryExtension.class, project);

        project.getTasks().create("latestArtifactVersion", LatestArtifactVersion.class, new Action<LatestArtifactVersion>() {
            public void execute(LatestArtifactVersion latestArtifactVersion) {
                latestArtifactVersion.setServerUrl(extension.getServerUrlProvider());
            }
        });
    }
}

Instead of using a plain String type, the extension defines the field serverUrl with type PropertyState<String>. The field is initialized in the constructor of the class. It’s state can be set via the exposed setter methods.

BinaryRepositoryExtension.java
import org.gradle.api.Project;
import org.gradle.api.provider.PropertyState;
import org.gradle.api.provider.Provider;

public class BinaryRepositoryExtension {
    private final PropertyState<String> serverUrl;

    public BinaryRepositoryExtension(Project project) {
        serverUrl = project.property(String.class);
    }

    public String getServerUrl() {
        return serverUrl.get();
    }

    public Provider<String> getServerUrlProvider() {
        return serverUrl;
    }

    public void setServerUrl(String serverUrl) {
        this.serverUrl.set(serverUrl);
    }
}

The task property also defines the serverUrl with type PropertyState. It allows for mapping the state of the property without actually accessing its value until needed for processing - that is in the task action.

LatestArtifactVersion.java
import org.gradle.api.DefaultTask;
import org.gradle.api.provider.PropertyState;
import org.gradle.api.provider.Provider;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.TaskAction;

public class LatestArtifactVersion extends DefaultTask {
    private final PropertyState<String> serverUrl;

    public LatestArtifactVersion() {
        serverUrl = getProject().property(String.class);
    }

    @Input
    public String getServerUrl() {
        return serverUrl.get();
    }

    public void setServerUrl(String serverUrl) {
        this.serverUrl.set(serverUrl);
    }

    public void setServerUrl(Provider<String> serverUrl) {
        this.serverUrl.set(serverUrl);
    }

    @TaskAction
    public void resolveLatestVersion() {
        // Access the raw value during the execution phase of the build lifecycle
        System.out.println("Retrieving latest artifact version from URL " + getServerUrl());

        // do additional work
    }
}
We encourage plugin developers to migrate their plugins to the public API as soon as possible. Plugins that are not based on Gradle 4.0 yet may continue to use the internal "convention mapping" API. Please be aware that the "convention mapping" API is undocumented and might be removed with later versions of Gradle.

1.6. Declaring a DSL configuration container

Sometimes you might want to expose a way for users to define multiple, named data objects of the same type. Let’s consider the following build script for illustration purposes.

build.gradle
apply plugin: ServerEnvironmentPlugin

environments {
    dev {
        url = 'http://localhost:8080'
    }

    staging {
        url = 'http://staging.enterprise.com'
    }

    production {
        url = 'http://prod.enterprise.com'
    }
}

The DSL exposed by the plugin exposes a container for defining a set of environments. Each environment configured by the user has an arbitrary but declarative name and is represented with its own DSL configuration block. The example above instantiates a development, staging and production environment including its respective URL.

Obviously, each of these environments needs to have a data representation in code to capture the values. The name of an environment is immutable and can be passed in as constructor parameter. At the moment the only other parameter stored by the data object is an URL. The POJO ServerEnvironment shown below fulfills those requirements.

ServerEnvironment.java
public class ServerEnvironment {
    private final String name;
    private String url;

    public ServerEnvironment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUrl() {
        return url;
    }
}

Gradle exposes the convenience method Project.html#container(java.lang.Class) to create a container of data objects. The parameter the method takes is the class representing the data. The created instance of type NamedDomainObjectContainer can be exposed to the end user by adding it to the extension container with a specific name.

ServerEnvironmentPlugin.java
import org.gradle.api.*;

public class ServerEnvironmentPlugin implements Plugin<Project> {
    @Override
    public void apply(Project project) {
        NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer = project.container(ServerEnvironment.class);
        project.getExtensions().add("environments", serverEnvironmentContainer);

        serverEnvironmentContainer.all(new Action<ServerEnvironment>() {
            public void execute(ServerEnvironment serverEnvironment) {
                String env = serverEnvironment.getName();
                String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
                String taskName = "deployTo" + capitalizedServerEnv;
                Deploy deployTask = project.getTasks().create(taskName, Deploy.class);

                project.afterEvaluate(new Action<Project>() {
                    public void execute(Project project) {
                        deployTask.setUrl(serverEnvironment.getUrl());
                    }
                });
            }
        });
    }
}

It’s very common for a plugin to post-process the captured values within the plugin implementation e.g. to configure tasks. In the example above, a deployment task is created dynamically for every environment that was configured by the user.

1.7. Reacting to plugins

Configuring the runtime behavior of existing plugins and tasks in a build is a common pattern in Gradle plugin implementations. For example a plugin could assume that it is applied to a Java-based project and automatically reconfigures the standard source directory.

InhouseConventionJavaPlugin.java
import java.util.Arrays;

import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;

public class InhouseConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPlugins().apply(JavaPlugin.class);
        JavaPluginConvention javaConvention =
            project.getConvention().getPlugin(JavaPluginConvention.class);
        SourceSet main = javaConvention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME);
        main.getJava().setSrcDirs(Arrays.asList("src"));
    }
}

The drawback to this approach is that it automatically forces the project to apply the Java plugin and therefore imposes a strong opinion on it. In practice, the project applying the plugin might not even deal with Java code. Instead of automatically applying the Java plugin the plugin could just react to the fact that the consuming project applies the Java plugin. Only if that is the case then certain configuration is applied.

InhouseConventionJavaPlugin.java
import java.util.Arrays;

import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.plugins.JavaPlugin;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;

public class InhouseConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPlugins().withType(JavaPlugin.class, new Action<JavaPlugin>() {
            public void execute(JavaPlugin javaPlugin) {
                JavaPluginConvention javaConvention =
                    project.getConvention().getPlugin(JavaPluginConvention.class);
                SourceSet main = javaConvention.getSourceSets().getByName(SourceSet.MAIN_SOURCE_SET_NAME);
                main.getJava().setSrcDirs(Arrays.asList("src"));
            }
        });
    }
}

Reacting to plugins should be preferred over blindly applying other plugins if there is not a good reason for assuming that the consuming project has the expected setup. The same concept applies to task types.

InhouseConventionWarPlugin.java
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.tasks.bundling.War;

public class InhouseConventionWarPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getTasks().withType(War.class, new Action<War>() {
            public void execute(War war) {
                war.setWebXml(project.file("src/someWeb.xml"));
            }
        });
    }
}

1.8. Providing default dependencies for plugins

The implementation of a plugin sometimes requires the use of an external dependency. You might want to automatically download an artifact using Gradle’s dependency management mechanism and later use it in the action of a task type declared in the plugin. Optimally, the plugin implementation doesn’t need to ask the user for the coordinates of that dependency - it can simply predefine a sensible default version.

Let’s have a look at an example. You wrote a plugin that downloads files containing data for further processing. The plugin implementation declares a custom configuration that allows for assigning those external dependencies with default dependency coordinates.

DataProcessingPlugin.java
import org.gradle.api.Action;
import org.gradle.api.Plugin;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.gradle.api.artifacts.DependencySet;

public class DataProcessingPlugin implements Plugin<Project> {
    public void apply(Project project) {
        final Configuration config = project.getConfigurations().create("dataFiles")
            .setVisible(false)
            .setDescription("The data artifacts to be processed for this plugin.");

        config.defaultDependencies(new Action<DependencySet>() {
            public void execute(DependencySet dependencies) {
                dependencies.add(project.getDependencies().create("com.company:data:1.4.6"));
            }
        });

        project.getTasks().withType(DataProcessing.class, new Action<DataProcessing>() {
            public void execute(DataProcessing dataProcessing) {
                dataProcessing.setDataFiles(config);
            }
        });
    }
}
DataProcessing.java
import org.gradle.api.DefaultTask;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.FileCollection;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.TaskAction;

public class DataProcessing extends DefaultTask {
    private final ConfigurableFileCollection dataFiles;

    public DataProcessing() {
        dataFiles = getProject().files();
    }

    @InputFiles
    public FileCollection getDataFiles() {
        return dataFiles;
    }

    public void setDataFiles(FileCollection dataFiles) {
        this.dataFiles.setFrom(dataFiles);
    }

    @TaskAction
    public void process() {
        System.out.println(getDataFiles().getFiles());
    }
}

Now, this approach is very convenient for the end user as there’s no need to actively declare a dependency. The plugin already provides all the knowledge about this implementation detail. But what if the user would like to redefine the default dependency. No problem…​the plugin also exposes the custom configuration that can be used to assign a different dependency. Effectively, the default dependency is overwritten.

build.gradle
apply plugin: DataProcessingPlugin

dependencies {
    dataFiles 'com.company:more-data:2.6'
}

You will find that this pattern works well for tasks that require an external dependency when the action of the task is actually executed. The method is heavily used for custom tasks that execute an external Ant task like many of the Gradle core static analysis plugins do e.g. the FindBugs and Checkstyle plugin. In fact those plugins even go further and abstract the version to be used for the external dependency by exposing an extension property (e.g. toolVersion in the JaCoCo plugin).

1.9. Assigning appropriate plugin identifiers

A descriptive plugin identifier makes it easy for consumers to apply the plugin to a project. The ID should reflect the purpose of the plugin with a single term. Additionally, a domain name should be added to avoid conflicts between other plugins with similar functionality. In the previous sections, dependencies shows in code examples use the group ID com.company. We could use the same identifier as domain name. In case you are not working with a legal entity or should want to publish a open-source plugin then you can just use the domain name hosting the source code e.g. com.github.

When publishing multiple plugins as part of a single JAR artifact (as described in the section Capabilities vs. conventions) the same naming conventions should apply. There’s no limitation to the number of plugins that can be registered by identifier and serves as a nice way to group related plugins together. For illustration, the Gradle Android plugin defines two different plugins in the directory src/main/resources/META-INF/gradle-plugins.

.
└── src
    └── main
        └── resources
            └── META-INF
                └── gradle-plugins
                    ├── com.android.application.properties
                    └── com.android.library.properties

2. Summary

Writing plugins doesn’t have to be hard. With the right techniques you can easily overcome commonly-faced challenges and implement plugins that are maintainable, reusable, declarative, well-documented and tested. Not all presented recommendations and recipes presented in this guide might be applicable to your plugin or your use case. However, the presented solutions should help you move toward the right direction.

The content of this guide will be expanded over time as new functionality becomes available in Gradle core. Please let us know on the Gradle forum if you are still having difficulties implementing specific use cases in your plugin or if you’d like to see other use case covered in this guide.

3. Help improve this guide

Have feedback or a question? Found a typo? Like all Gradle guides, help is just a GitHub issue away. Please add an issue or pull request to gradle-guides/implementing-gradle-plugins and we’ll get back to you.

4. Next steps

There’s far more to Gradle plugins than the actual implementation. You may be interested in: