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 section you will learn established practices to make you a better plugin developer and how to make a plugin as accessible and useful for consumers as possible.

This section 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

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.kts
plugins {
    `java-gradle-plugin`
}

gradlePlugin {
    plugins {
        create("simplePlugin") {
            id = "org.example.greeting"
            implementationClass = "org.example.GreetingPlugin"
        }
    }
}
build.gradle
plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        simplePlugin {
            id = 'org.example.greeting'
            implementationClass = 'org.example.GreetingPlugin'
        }
    }
}

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.

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
abstract public class LatestArtifactVersion extends DefaultTask {

    @Input
    abstract public Property<String> getCoordinates();

    @Input
    abstract public Property<String> getServerUrl();

    @TaskAction
    public void resolveLatestVersion() {
        System.out.println("Retrieving artifact " + getCoordinates().get() + " from " + getServerUrl().get());
        // 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.kts
tasks.register<LatestArtifactVersion>("latestVersionMavenCentral") {
    coordinates = "commons-lang:commons-lang"
    serverUrl = "http://repo1.maven.org/maven2"
}

tasks.register<LatestArtifactVersion>("latestVersionInhouseRepo") {
    coordinates = "commons-lang:commons-lang"
    serverUrl = "http://repo1.myorg.org/maven2"
}
build.gradle
tasks.register('latestVersionMavenCentral', LatestArtifactVersion) {
    coordinates = 'commons-lang:commons-lang'
    serverUrl = 'http://repo1.maven.org/maven2'
}

tasks.register('latestVersionInhouseRepo', LatestArtifactVersion) {
    coordinates = 'commons-lang:commons-lang'
    serverUrl = 'http://repo1.myorg.org/maven2'
}

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 annotate 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
public abstract class Generate extends DefaultTask {

    @Input
    abstract public Property<Integer> getFileCount();

    @Input
    abstract public Property<String> getContent();

    @OutputDirectory
    abstract public RegularFileProperty getGeneratedFileDir();

    @TaskAction
    public void perform() throws IOException {
        for (int i = 1; i <= getFileCount().get(); i++) {
            writeFile(new File(getGeneratedFileDir().get().getAsFile(), i + ".txt"), getContent().get());
        }
    }

    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 validatePlugins automatically checks for an existing input/output annotation for every public property defined in a custom task type implementation.

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-flat.gradle.kts
plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = "https://gradle.org"
    vcsUrl = "https://github.com/gradle/gradle-site-plugin"
}
build-flat.gradle
plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")
    websiteUrl = 'https://gradle.org'
    vcsUrl = 'https://github.com/gradle/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.kts
plugins {
    id("org.myorg.site")
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    customData {
        websiteUrl = "https://gradle.org"
        vcsUrl = "https://github.com/gradle/gradle-site-plugin"
    }
}
build.gradle
plugins {
    id 'org.myorg.site'
}

site {
    outputDir = layout.buildDirectory.file("mysite")

    customData {
        websiteUrl = 'https://gradle.org'
        vcsUrl = 'https://github.com/gradle/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
abstract public class CustomData {

    abstract public Property<String> getWebsiteUrl();

    abstract public Property<String> getVcsUrl();
}

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 Action. The following example demonstrates the use of Action in an extension definition.

SiteExtension.java
abstract public class SiteExtension {

    abstract public RegularFileProperty getOutputDir();

    @Nested
    abstract public CustomData getCustomData();

    public void customData(Action<? super CustomData> action) {
        action.execute(getCustomData());
    }
}

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.kts
plugins {
    id("org.myorg.binary-repository-version")
}

binaryRepo {
    coordinates = "commons-lang:commons-lang"
    serverUrl = "http://repo2.myorg.org/maven2"
}
build.gradle
plugins {
    id 'org.myorg.binary-repository-version'
}

binaryRepo {
    coordinates = 'commons-lang:commons-lang'
    serverUrl = 'http://repo2.myorg.org/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 Property 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
public class BinaryRepositoryVersionPlugin implements Plugin<Project> {
    public void apply(Project project) {
        BinaryRepositoryExtension extension =
            project.getExtensions().create("binaryRepo", BinaryRepositoryExtension.class);

        project.getTasks().register("latestArtifactVersion", LatestArtifactVersion.class, task -> {
            task.getCoordinates().set(extension.getCoordinates());
            task.getServerUrl().set(extension.getServerUrl());
        });
    }
}

Instead of using a plain String type, the extension defines the properties coordinates and serverUrl with type Property<String>. The abstract getters for the properties are automatically initialized by Gradle. The values of a property can then be changed on the property object obtained through the corresponding getter method.

The Gradle classloader automatically injects setter methods alongside all getter methods with the return type Property. It allows developers to simplify code like obj.prop.set 'foo' to obj.prop = 'foo' in the Groovy DSL.
BinaryRepositoryExtension.java
abstract public class BinaryRepositoryExtension {

    abstract public Property<String> getCoordinates();

    abstract public Property<String> getServerUrl();
}

The task property also defines the serverUrl with type Property. 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
abstract public class LatestArtifactVersion extends DefaultTask {

    @Input
    abstract public Property<String> getCoordinates();

    @Input
    abstract public Property<String> getServerUrl();

    @TaskAction
    public void resolveLatestVersion() {
        System.out.println("Retrieving artifact " + getCoordinates().get() + " from " + getServerUrl().get());
        // issue HTTP call and parse response
    }
}
We encourage plugin developers to migrate their plugins to the public property 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.

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.kts
plugins {
    id("org.myorg.server-env")
}

environments {
    create("dev") {
        url = "http://localhost:8080"
    }

    create("staging") {
        url = "http://staging.enterprise.com"
    }

    create("production") {
        url = "http://prod.enterprise.com"
    }
}
build.gradle
plugins {
    id 'org.myorg.server-env'
}

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
abstract public class ServerEnvironment {
    private final String name;

    @javax.inject.Inject
    public ServerEnvironment(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    abstract public Property<String> getUrl();
}

Gradle exposes the factory method ObjectFactory.domainObjectContainer(Class, NamedDomainObjectFactory) 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
public class ServerEnvironmentPlugin implements Plugin<Project> {
    @Override
    public void apply(final Project project) {
        ObjectFactory objects = project.getObjects();

        NamedDomainObjectContainer<ServerEnvironment> serverEnvironmentContainer =
            objects.domainObjectContainer(ServerEnvironment.class, name -> objects.newInstance(ServerEnvironment.class, name));
        project.getExtensions().add("environments", serverEnvironmentContainer);

        serverEnvironmentContainer.all(serverEnvironment -> {
            String env = serverEnvironment.getName();
            String capitalizedServerEnv = env.substring(0, 1).toUpperCase() + env.substring(1);
            String taskName = "deployTo" + capitalizedServerEnv;
            project.getTasks().register(taskName, Deploy.class, task -> task.getUrl().set(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.

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 reconfigure the standard source directory.

InhouseStrongOpinionConventionJavaPlugin.java
public class InhouseStrongOpinionConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        // Careful! Eagerly appyling plugins has downsides, and is not always recommended.
        project.getPlugins().apply(JavaPlugin.class);
        SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
        SourceSet main = sourceSets.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
public class InhouseConventionJavaPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getPlugins().withType(JavaPlugin.class, javaPlugin -> {
            SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
            SourceSet main = sourceSets.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
public class InhouseConventionWarPlugin implements Plugin<Project> {
    public void apply(Project project) {
        project.getTasks().withType(War.class).configureEach(war ->
            war.setWebXml(project.file("src/someWeb.xml")));
    }
}

Reacting to build features

Plugins can access the status of build features in the build. The Build Features API allows checking whether the user requested a particular Gradle feature and if it is active in the current build. An example of a build feature is the configuration cache.

There are two main use cases:

  • Using the status of build features in reports or statistics.

  • Incrementally adopting experimental Gradle features by disabling incompatible plugin functionality.

Below is an example of a plugin that utilizes both of the cases.

Reacting to build features
public abstract class MyPlugin implements Plugin<Project> {

    @Inject
    protected abstract BuildFeatures getBuildFeatures(); (1)

    @Override
    public void apply(Project p) {
        BuildFeatures buildFeatures = getBuildFeatures();

        Boolean configCacheRequested = buildFeatures.getConfigurationCache().getRequested() (2)
            .getOrNull(); // could be null if user did not opt in nor opt out
        String configCacheUsage = describeFeatureUsage(configCacheRequested);
        MyReport myReport = new MyReport();
        myReport.setConfigurationCacheUsage(configCacheUsage);

        boolean isolatedProjectsActive = buildFeatures.getIsolatedProjects().getActive() (3)
            .get(); // the active state is always defined
        if (!isolatedProjectsActive) {
            myOptionalPluginLogicIncompatibleWithIsolatedProjects();
        }
    }

    private String describeFeatureUsage(Boolean requested) {
        return requested == null ? "no preference" : requested ? "opt-in" : "opt-out";
    }

    private void myOptionalPluginLogicIncompatibleWithIsolatedProjects() {
    }
}
1 The BuildFeatures service can be injected into plugins, tasks, and other managed types.
2 Accessing the requested status of a feature for reporting.
3 Using the active status of a feature to disable incompatible functionality.

Build feature properties

The status properties of a BuildFeature are represented with Provider<Boolean> types.

The BuildFeature.getRequested() status of a build feature determines if the user requested to enable or disable the feature. If the user did neither, then the value of the provider is undefined.

When the requested provider value is:

  • not present (undefined) — the user neither opted in nor opted out from using the feature;

  • true — the user opted in for using the feature, e.g., using a build option;

  • false — the user opted out from using the feature, e.g., by setting a Gradle property to false.

The BuildFeature.getActive() status of a build feature is always defined. It represents the effective state of the feature in the build.

When the active provider value is:

  • true — the feature may affect the build behavior in a way specific to the feature;

  • false — the feature will not affect the build behavior.

Note that the active status does not depend on the requested status. Even if the user requested a feature, it may still not be active due to other build options used in the build. Gradle can also activate a feature by default, even if the user did not specify a preference.

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 dependency coordinates.

DataProcessingPlugin.java
public class DataProcessingPlugin implements Plugin<Project> {
    public void apply(Project project) {
        Configuration dataFiles = project.getConfigurations().create("dataFiles", c -> {
            c.setVisible(false);
            c.setCanBeConsumed(false);
            c.setCanBeResolved(true);
            c.setDescription("The data artifacts to be processed for this plugin.");
            c.defaultDependencies(d -> d.add(project.getDependencies().create("org.myorg:data:1.4.6")));
        });

        project.getTasks().withType(DataProcessing.class).configureEach(
            dataProcessing -> dataProcessing.getDataFiles().from(dataFiles));
    }
}
DataProcessing.java
abstract public class DataProcessing extends DefaultTask {

    @InputFiles
    abstract public ConfigurableFileCollection getDataFiles();

    @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.kts
plugins {
    id("org.myorg.data-processing")
}

dependencies {
    dataFiles("org.myorg:more-data:2.6")
}
build.gradle
plugins {
    id 'org.myorg.data-processing'
}

dependencies {
    dataFiles 'org.myorg: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. You can 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).

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 shown in code examples use the group ID org.myorg. We could use the same identifier as domain name.

When publishing multiple plugins as part of a single JAR artifact the same naming conventions should apply. This serves as a nice way to group related plugins together. There’s no limitation to the number of plugins that can be registered by identifier. For illustration, the Gradle Android plugin defines two different plugins.

The identifiers for plugins written as a class should be defined in the build script of the project containing the plugin classes. For this, the java-gradle-plugin needs to be applied.

buildSrc/build.gradle.kts
plugins {
    id("java-gradle-plugin")
}

gradlePlugin {
    plugins {
        create("androidApplicationPlugin") {
            id = "com.android.application"
            implementationClass = "com.android.AndroidApplicationPlugin"
        }
        create("androidLibraryPlugin") {
            id = "com.android.library"
            implementationClass = "com.android.AndroidLibraryPlugin"
        }
    }
}
buildSrc/build.gradle
plugins {
    id 'java-gradle-plugin'
}

gradlePlugin {
    plugins {
        androidApplicationPlugin {
            id = 'com.android.application'
            implementationClass = 'com.android.AndroidApplicationPlugin'
        }
        androidLibraryPlugin {
            id = 'com.android.library'
            implementationClass = 'com.android.AndroidLibraryPlugin'
        }
    }
}

Note that identifiers for precompiled script plugins are automatically registered based on the file name of the script plugin.

Providing multiple variants of a plugin for different Gradle versions

The support for multi-variant plugins currently requires you to use the raw variant aware dependency management APIs of Gradle. More conveniences around this may be provided in the future.

Currently, the most convenient way to configure additional plugin variants is to use feature variants, a concept available in all Gradle projects that apply one of the Java plugins. As described in the documentation, there are several options to design feature variants. They may be bundled inside the same Jar, or each variant may come with its own Jar. Here we show how each plugin variant is developed in isolation. That is, in a separate source set that is compiled separately and packaged in a separate Jar. Other setups are possible though.

The following sample demonstrates how to add a variant that is compatible with Gradle 7+ while the "main" variant is compatible with older versions. Note that only Gradle versions 7 or higher can be explicitly targeted by a variant, as support for this was only added in Gradle 7.

build.gradle.kts
val gradle7 = sourceSets.create("gradle7")
java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) (1)
    }
}
configurations.configureEach {
    if (isCanBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
                objects.named("7.0"))
        }
    }
}
tasks.named<Copy>(gradle7.processResourcesTaskName) { (3)
    val copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into("META-INF/gradle-plugins")
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    "gradle7CompileOnly"(gradleApi()) (4)
}
build.gradle
def gradle7 = sourceSets.create('gradle7')
java {
    registerFeature(gradle7.name) {
        usingSourceSet(gradle7)
        capability(project.group.toString(), project.name, project.version.toString()) (1)
    }
}
configurations.configureEach {
    if (canBeConsumed && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE, (2)
                      objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}
tasks.named(gradle7.processResourcesTaskName) { (3)
    def copyPluginDescriptors = rootSpec.addChild()
    copyPluginDescriptors.into('META-INF/gradle-plugins')
    copyPluginDescriptors.from(tasks.pluginDescriptors)
}

dependencies {
    gradle7CompileOnly(gradleApi()) (4)
}

First, we declare a separate source set, and a feature variant based on that, for our Gradle7 plugin variant. We need to do some specific wiring to turn the feature into a proper Gradle plugin variant:

1 Assign the implicit capability that corresponds to the components GAV to the variant.
2 Assign the Gradle API version attribute to all consumable configurations of our Gradle7 variant. This information is used by Gradle to determine which variant to select during plugin resolution.
3 Configure the processGradle7Resources task to make sure the plugin descriptor file is added to the Gradle7 variant Jar.
4 Add a dependency to the gradleApi() for our new variant so that the API is visible during compilation time.

Note that there is currently no convenient way to access the API of other Gradle versions as the one you are building the plugin with. Ideally, every variant should be able to declare a dependency to the API of the minimal Gradle version it supports. This will be improved in the future.

The above snippet assumes that all variants of your plugin have the plugin class at the same location. That is, if you followed this chapter and your plugin class is org.example.GreetingPlugin, you need to create a second variant of that class in src/gradle7/java/org/example.

Using version-specific variants of multi-variant plugins

Given a dependency on a multi-variant plugin, Gradle will automatically choose its variant that best matches the current Gradle version when it resolves any of:

The best matching variant is the variant that targets the highest Gradle API version not exceeding the current build’s Gradle version.

In all other cases, a plugin variant that does not specify the supported Gradle API version is preferred, if such a variant is present.

In projects that use plugins as dependencies, it is possible to request the variants of plugin dependencies that support a different Gradle version. This allows a multi-variant plugin that depends on other plugins to access their APIs which are exclusively provided in their version-specific variants.

This snippet makes the plugin variant gradle7 defined above consume the matching variants of its dependencies on other multi-variant plugins.

build.gradle.kts
configurations.configureEach {
    if (isCanBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named("7.0"))
        }
    }
}
build.gradle
configurations.configureEach {
    if (canBeResolved && name.startsWith(gradle7.name))  {
        attributes {
            attribute(GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                objects.named(GradlePluginApiVersion, '7.0'))
        }
    }
}

Reporting problems

Plugins can report problems through Gradle’s problem-reporting APIs. The APIs report rich, structured information about problems happening during the build. This information can be used by different user interfaces such as Gradle’s console output, Build Scans, or IDEs to communicate problems to the user in the most appropriate way.

The following example shows an issue reported from a plugin:

ProblemReportingPlugin.java
public class ProblemReportingPlugin implements Plugin<Project> {

    private final ProblemReporter problemReporter;

    @Inject
    public ProblemReportingPlugin(Problems problems) { (1)
        this.problemReporter = problems.forNamespace("org.myorg"); (2)
    }

    public void apply(Project project) {
        this.problemReporter.reporting(builder -> builder (3)
            .label("Plugin 'x' is deprecated")
            .details("The plugin 'x' is deprecated since version 2.5")
            .solution("Please use plugin 'y'")
            .severity(Severity.WARNING)
        );
    }
}
1 The Problem service is injected into the plugin.
2 A problem reporter, is created for the plugin. While the namespace is up to the plugin author, it is recommended to use the plugin ID.
3 A problem is reported. This problem is recoverable so that the build will continue.

For a full example, see our end-to-end sample.

Problem building

When reporting a problem, a wide variety of information can be provided. The ProblemSpec describes all the information that can be provided.

Reporting problems

When it comes to reporting problems, we support three different modes:

  • Reporting a problem is used for reporting problems that are recoverable, and the build should continue.

  • Throwing a problem is used for reporting problems that are not recoverable, and the build should fail.

  • Rethrowing a problem is used to wrap an already thrown exception. Otherwise, the behavior is the same as Throwing.

See the ProblemReporter documentation for more details.

Problem aggregation

When reporting problems, Gradle will aggregate similar problems when it sends them through the Tooling API based on the problem’s category label.

  • When a problem is reported, the first occurrence is going to be reported as a ProblemDescriptor, containing the complete information about the problem.

  • Any subsequent occurrences of the same problem will be reported as a ProblemAggregationDescriptor. This descriptor will arrive at the end of the build and contain the number of occurrences of the problem.

  • If for any bucket (i.e., category and label pairing), the number of collected occurrences is greater than 10.000, then it will be sent immediately instead of at the end of the build.