PublishingCustomGradlePlugin_StepByStep

Publishing Custom Gradle Plugin explained step by step

kazurayam, 26 March 2023

Introduction

I have made a public GitHub repository of this article and sample codes

I used the gradle version 8.0.2, Java 17.0.7, macOS 12.6

Overview

In this article I will explain the process of publishing custom Gradle plugins. I will present a set of working sample codes.

I started with the official Gradle documentation:

The official docs include tons of detail information. I wandered around and had a hard time to understand. In my humble opinion, the official docs tend to describe the front scene only, tend to hide what is going on behind the scene of publishing Gradle plugins. I studied the publishing process step by step, studied what sort of artifacts are generated in the <projectDir>/build directory by the java-gradle-plugin, the maven-publish plugin and the com.gradle.plugin-publish plugin. I started with writing 2 Groovy scripts, compiling them, configuring them as Plugin, publishing them to some repositories. It was a long way. Actually I had to go 10 steps. I would describe what I found during the course step by step. My story could hopefully help somebody like me who is now wandering in the forest of Gradle documentations.

§1 Start up

Outline

In the step1, I will create a skeletal project where I will write a Groovy code as a custom Gradle plugin.

settings.gradle and build.gradle

I made a directory named step1 where I located step1/settings.gradle and step1/build.gradle.

step1/settings.gradle

rootProject.name = 'step1'

step1/build.gradle

plugins {
    id 'groovy'
}

group = 'com.example'
version = '1.0'

repositories {
    mavenCentral()
}

dependencies {
    // Use the awesome Spock testing and specification framework
    testImplementation 'org.spockframework:spock-core:2.3-groovy-3.0'
}

tasks.named('test') {
    // Use JUnit Jupiter for unit tests.
    useJUnitPlatform()
}

code of custom Gradle plugin

I wrote step1/src/main/groovy/com/example/greeting/GreetingPlguin.

package com.example.greeting

import org.gradle.api.Plugin
import org.gradle.api.Project

class GreetingPlugin implements Plugin<Project> {
    void apply(Project project) {
        project.task('hello') {
            doLast {
                println "Hello from GreetingPlugin"
            }
        }
        project.task('goodbye') {
            doLast {
                println "Goodbye from GreetingPlugin"
            }
        }
    }
}

code of JUnit5 test

step1/src/test/groovy/com/example/greeting/GreetingPluginTest.groovy

package com.example.greeting

import org.gradle.api.Project
import org.gradle.testfixtures.ProjectBuilder
import spock.lang.Specification

class GreetingPluginTest extends Specification {
    def "the plugin registers the tasks"() {
        given:
        Project project = ProjectBuilder.builder().build()

        when:
        project.plugins.apply("org.sample.Greetings")

        then:
        project.tasks.findByName("hello") != null
        project.tasks.findByName("goodbye") != null

    }
}

How the build works

I tried to compile the Groovy code, but it doesn’t compile.

1 1.GradleApiNotAccessible

The Gradle API is not accessible for the compiler. Therefore, Groovy compiler failed to find the very core class org.gradle.api.Project.

What needs to be done

In order to compile the code, I need to introduce the java-gradle-plugin into the build.gradle. I will try it in the step2.

§2 Introducing java-gradle-plugin

Outline

Based on the step1, I will introduce the java-gradle-plugin into the build.gradle file. With the plugin, the Gradle API becomes available to the compileGroovy task. The plugin also generates a “plugin descriptor”, which will be includeded in the distributed jar file.

settings.gradle and build.gradle

I made a directory named step2 where I located step2/settings.gradle and step2/build.gradle.

step2/settings.gradle

rootProject.name = 'step2'

step2/build.gradle

Refer to step2/build.gradle

Please note the 3rd line, we declare we use the java-gradle-plugin:

    id 'java-gradle-plugin'

And line#24, we declare the plugin id and the name of the implementation class:

    // Define the plugin
    plugins {
        MyGreeting {
            id = 'org.sample.Greetings'
            implementationClass = 'com.example.greeting.GreetingPlugin'
        }
    }
}

You should note that the plugin id can be totally different from the class name. In order to demonstrate this point, I intentionally named the plugin id org.sample.Greetings and the fully qualified class name com.example.greeting.GreeingPlugin.

The following Gradle document describe how you can name the plugin id:

How the build works

I could compile the Groovy code and run the junit test.

2.1 success compile and test

Artifacts generated by java-gradle-plugin

In the step2/build directory, the java-gradle-plugin generated a lot of artifacts (files). Let’s look at them to see what the plugin did for us.

build directory

The java-gradle-plugin generated several artifacts in the build directory.

$ tree ./build
./build
├── classes
│   └── ...
├── generated
│   └── ...
├── pluginDescriptors
│   └── org.sample.Greetings.properties
├── pluginUnderTestMetadata
│   └── ...
├── reports
│   └── ...
├── resources
│   └── main
│       └── META-INF
│           └── gradle-plugins
│               └── org.sample.Greetings.properties
├── test-results
│   └── ...
└── tmp
    ├── ...

47 directories, 20 files

Amongst them, the following files are interesting:

step2/build/pluginDescriptors/org.sample.Greetings.properties

implementation-class=com.example.greeting.GreetingPlugin

step2/build/resources/main/META-INF/gradle-plugins/org.sample.Greetings.properties

implementation-class=com.example.greeting.GreetingPlugin

In the Gradle documentation you can find a description what this properties file is.

What needs to be done

Next, I want to publish the plugin. When I tried it, it failed of course.

$ cd step2
:~/github/PublishingCustomGradlePlugin_StepByStep/step2 (develop *)
$ gradle publish

FAILURE: Build failed with an exception.

* What went wrong:
Task 'publish' not found in root project 'step2'.
...

§3 Introducing maven-publish plugin

Outline

Based on the step2, I will introduce the maven-publish plugin. With it I will publish my custom Gradle plugin into a Maven repository located on the local disk. I will study what the maven-publish plugin does behind the scene.

The official reference doc

At first you should have a look at the official Gradle documentation:

settings.gradle and build.gradle

I made a directory named step3 where I located step3/settings.gradle and step3/build.gradle.

step3/settings.gradle

rootProject.name = 'step3'

step3/build.gradle

Refer to step3/build.gradle

Note the line#5 declares the mave-publish plugin:

plugins {
    ...
    id 'maven-publish'
}

Also the line#35 we have publishing extention closure, where I declared a local Maven repository at the directory step3/build/repos-maven.

    repositories {
        maven {
            name = "sketch"
            url = layout.buildDirectory.dir("repos-maven")
        }
    }
}

How the build works

$ pwd
/Users/kazuakiurayama/github/PublishingCustomGradlePlugin_StepByStep/step3
:~/github/PublishingCustomGradlePlugin_StepByStep/step3 (develop *+)
$ gradle clean

BUILD SUCCESSFUL in 850ms
1 actionable task: 1 executed
:~/github/PublishingCustomGradlePlugin_StepByStep/step3 (develop *+)
$ gradle  publish

BUILD SUCCESSFUL in 881ms
8 actionable tasks: 4 executed, 4 up-to-date

Artifacts generated by the maven-publish plugin

The build directory tree

:~/github/PublishingCustomGradlePlugin_StepByStep/step3 (develop *+)
$ tree build
build
├── classes
│   └── ...
├── generated
│   └── ...
├── libs
│   └── step3-1.0.jar
├── pluginDescriptors
│   └── org.sample.Greetings.properties
├── publications
│   ├── MyGreetingPluginMarkerMaven
│   │   └── pom-default.xml
│   └── pluginMaven
│       └── pom-default.xml
├── repos-maven
│   ├── com
│   │   └── example
│   │       └── step3
│   │           ├── 1.0
│   │           │   ├── step3-1.0.jar
│   │           │   ├── step3-1.0.jar.md5
│   │           │   ├── step3-1.0.jar.sha1
│   │           │   ├── step3-1.0.jar.sha256
│   │           │   ├── step3-1.0.jar.sha512
│   │           │   ├── step3-1.0.pom
│   │           │   ├── step3-1.0.pom.md5
│   │           │   ├── step3-1.0.pom.sha1
│   │           │   ├── step3-1.0.pom.sha256
│   │           │   └── step3-1.0.pom.sha512
│   │           ├── maven-metadata.xml
│   │           ├── maven-metadata.xml.md5
│   │           ├── maven-metadata.xml.sha1
│   │           ├── maven-metadata.xml.sha256
│   │           └── maven-metadata.xml.sha512
│   └── org
│       └── sample
│           └── Greetings
│               └── org.sample.Greetings.gradle.plugin
│                   ├── 1.0
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom.md5
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom.sha1
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom.sha256
│                   │   └── org.sample.Greetings.gradle.plugin-1.0.pom.sha512
│                   ├── maven-metadata.xml
│                   ├── maven-metadata.xml.md5
│                   ├── maven-metadata.xml.sha1
│                   ├── maven-metadata.xml.sha256
│                   └── maven-metadata.xml.sha512
├── resources
│   └── main
│       └── META-INF
│           └── gradle-plugins
│               └── org.sample.Greetings.properties
└── tmp
    ├── ...

37 directories, 38 files

The jar file of the plugin’s binary

step2/build/libs/step3-1.0.jar was created. The jar includes the following content:

:~/github/PublishingCustomGradlePlugin_StepByStep/step3 (develop *+)
$ tar -xvf build/libs/step3-1.0.jar
x META-INF/
x META-INF/MANIFEST.MF
x com/
x com/example/
x com/example/greeting/
x com/example/greeting/GreetingPlugin$_apply_closure2$_closure4.class
x com/example/greeting/GreetingPlugin$_apply_closure2.class
x com/example/greeting/GreetingPlugin$_apply_closure1$_closure3.class
x com/example/greeting/GreetingPlugin.class
x com/example/greeting/GreetingPlugin$_apply_closure1.class
x META-INF/gradle-plugins/
x META-INF/gradle-plugins/org.sample.Greetings.properties

You can easily see, the jar contains the binary class files of my custom Gradle plugin com.example.greeting.GreetingPlugin, and the plugin descriptor which declares the plugin id `org.sample.Greeting.

How the jar is named?

The name of the jar step3-1.0.jar was derived from the rootProject.name=step3 in the step3/settings.gradle file, plus the value of project.version property declared in the step3/build.gradle.

What contents are published in the Maven repository

In the step3/build.gradle I specified

gradlePlugin {
    // Define the plugin
    plugins {
        MyGreeting {
            id = 'org.sample.Greetings'
            implementationClass = 'com.example.greeting.GreetingPlugin'
        }
    }
}

publishing {
    repositories {
        maven {
            url = layout.buildDirectory.dir("repos-maven")
        }
    }
}

This resulted the following content in the local Maven repository:

$ tree build
build
├── publications
│   ├── MyGreetingPluginMarkerMaven
│   │   └── pom-default.xml
│   └── pluginMaven
│       └── pom-default.xml
├── repos-maven
│   ├── com
│   │   └── example
│   │       └── step3
│   │           ├── 1.0
│   │           │   ├── step3-1.0.jar
...
│   └── org
│       └── sample
│           └── Greetings
│               └── org.sample.Greetings.gradle.plugin
│                   ├── 1.0
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom
...

the directory name MyGreetingPluginMarkerMaven is puzzling. It can be parsed into MyGreeting + ‘PluginMarker’ + ‘Maven’, which is “publication name” + ‘PluginMarker’ + “type of repository”.

What is “PluginMarker”? — You should read the official documantation of

Since the plugins {} DSL block only allows for declaring plugins by their globally unique plugin id and version properties, Gradle needs a way to look up the coordinates of the plugin implementation artifact. To do so, Gradle will look for a Plugin Marker Artifact with the coordinates plugin.id:plugin.id.gradle.plugin:plugin.version. This marker needs to have a dependency on the actual plugin implementation. Publishing these markers is automated by the java-gradle-plugin.

Well, this description is very difficult to understand. We need to look at some concrete example. Let’s start with step3/build/publications/MyGreetingPluginMarkerMaven/pom-default.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>org.sample.Greetings</groupId>
  <artifactId>org.sample.Greetings.gradle.plugin</artifactId>
  <version>1.0</version>
  <packaging>pom</packaging>
  <dependencies>
    <dependency>
      <groupId>com.example</groupId>
      <artifactId>step3</artifactId>
      <version>1.0</version>
    </dependency>
  </dependencies>
</project>

Here you can see the plugin id org.sample.Greetings is encoded as <groupId>org.sample.Greetings</groupId>. And the maven-publish plugin generated <artifactId>org.sample.Greetings.gradle.plugin</artifactId>, which is followed by <version>1.0</version>. So this pom-default.xml file is declaring my custom Gradle plugin as an entity registered in the Maven repository.

And you can find that the pom-default.xml declares <dependency> to another entity in the maven repository, which can be addressed by the coordinate com.example:step3:1.0.

Now we need to look at step3/build/publications/pluginMaven/pom-default.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd" xmlns="http://maven.apache.org/POM/4.0.0"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.example</groupId>
  <artifactId>step3</artifactId>
  <version>1.0</version>
</project>

This corresponds to the entry in the Maven repository with the coordinate group: "com.example", name: "step3" version: "1.0", which contains the binary jar file.

$ tree build
build
...
├── repos-maven
│   ├── com
│   │   └── example
│   │       └── step3
│   │           ├── 1.0
│   │           │   ├── step3-1.0.jar
...

In short, maven-publish plugin publishes 2 entities for a custom Gradle plugin in Maven repository. One is the plugin’s POM xml file. Another is the jar file of plugin’s implementation jar file.

Any gradle build that uses my org.sample.Greetings plugin, will first get access to the plugin’s POM xml with the coordinate group: "com.example", name: "step3", version: "1.0". The user build will parse the POM xml and will find further dependencies are required. So the user build will get access to the plugin’s implementation jar, which is identified as group: "com.example", name: "step3", version: "1.0".

I initially thought that I am going to publish a Gradle plugin into a repository. The processing behind the scene is not as simple as I thought. There are 2 entities published in the maven repository, which are closely related. The maven-publish plugin manages this compilicated processing silently.

“publish XXX” tasks

maven-publish plugin injects several tasks with name “publish XXXX”. You can see the name of tasks by gradle tasks | grep publish command.

$ gradle tasks | grep publish
publish - Publishes all publications produced by this project.
publishAllPublicationsToSketchRepository - Publishes all Maven publications produced by this project to the sketch repository.
publishMyGreetingPluginMarkerMavenPublicationToMavenLocal - Publishes Maven publication 'MyGreetingPluginMarkerMaven' to the local Maven repository.
publishMyGreetingPluginMarkerMavenPublicationToSketchRepository - Publishes Maven publication 'MyGreetingPluginMarkerMaven' to Maven repository 'sketch'.
publishPluginMavenPublicationToMavenLocal - Publishes Maven publication 'pluginMaven' to the local Maven repository.
publishPluginMavenPublicationToSketchRepository - Publishes Maven publication 'pluginMaven' to Maven repository 'sketch'.
publishToMavenLocal - Publishes all Maven publications produced by this project to the local Maven cache.

Many characters. It is a bit difficult to understand at a glance. I will rewrite this output with a bit of styling for easier read.

The following 3 commands are a sort of macro which invokes other unitary commands.

The rest of commands are unitary ones.

Publications

In the command name you find 2 symbols MyGreetingPlugin and pluginMaven. These symbols are exactly equal to the sub-directories under the build/publications directory.

$ tree build
build
├── publications
│   ├── MyGreetingPluginMarkerMaven
│   │   └── pom-default.xml
│   └── pluginMaven
│       └── pom-default.xml
...

The MyGreetingPluginMarketMaven publication contains a POM.xml file which will be published in the repository hosting services such as the [Gradle Plugin Portal](https://plugins.gradle.org/plugin/com.kazurayam.compareDirectories).

The pluginMaven publication contains a jar of thge custom Gradle plugin binary class files. The jar file is named step3-1.0.jar. The maven-publish plugin generates the jar file implicitly when gradle publshPluginMavenPublicationTo(xxxxx) command is executed. You don’t need to run jar task explicitly.

Repositories “MavenLocal”, “Sketch”

In the command names you find 2 more symbols: “MavenLocal” and “Sketch”.

As for “local Maven repository” or “local Maven cache”, please refer to Baeldung, Where is the Maven Local Repository?

As for “Sketch”, I wrote in the build.gradle file. The symbol “Sketch” was pick up from this configuration.

publishing {
    repositories {
        maven {
            name = "sketch"

Now I am able to interprete all the “publishXXXX” commands, I can guess what they will do when executed.

§4 Maven and Ivy

Outline

Gradle supports published the artifacts into 2 types of repositories: Maven and Ivy. In the previous step3, I published my custom plugin into a local Maven repository. In this step4, I will publish ti into a local Ivy repository as well.

settings.gradle and build.gradle

I made a directory named step4 where I located step4/settings.gradle and step4/build.gradle.

step4/settings.gradle

rootProject.name = 'step4'

step4/build.gradle

Refer to step4/build.gradle

Note the line#6 declares ivy-publish plugin:

plugins {
    ...
    id 'maven-publish'
    id 'ivy-publish'
}

Also the line#36 we have publishing extension closure, where I declared a local Ivy repository at the directory build/repos-ivy as well as a Maven repository.

publishing {
    repositories {
        maven {
            name = "sketchMaven"
            url = layout.buildDirectory.dir("repos-maven")
        }
        ivy {
            name = "sketchIvy"
            url = layout.buildDirectory.dir("repos-ivy")
        }
    }
}

How the build works

$ pwd
/Users/kazuakiurayama/github/PublishingCustomGradlePlugin_StepByStep/step4
:~/github/PublishingCustomGradlePlugin_StepByStep/step4 (develop *+)
$ gradle clean

BUILD SUCCESSFUL in 850ms
1 actionable task: 1 executed
:~/github/PublishingCustomGradlePlugin_StepByStep/step4 (develop *+)
$ gradle  publish

BUILD SUCCESSFUL in 881ms
8 actionable tasks: 4 executed, 4 up-to-date

Artifacts generated by the maven-publish plugin

The build directory tree

When I run gradle publsh, the build generated a lot of files under the build directory.

:~/github/PublishingCustomGradlePlugin_StepByStep/step4 (develop *)
$ tree build
build
├── classes
│   └── ...
├── generated
│   └── ...
├── libs
│   └── step4-1.0.jar
├── pluginDescriptors
│   └── org.sample.Greetings.properties
├── pluginUnderTestMetadata
│   └── plugin-under-test-metadata.properties
├── publications
│   ├── MyGreetingPluginMarkerIvy
│   │   └── ivy.xml
│   ├── MyGreetingPluginMarkerMaven
│   │   ├── org.sample.Greetings.gradle.plugin (1).iml
│   │   └── pom-default.xml
│   ├── pluginIvy
│   │   └── ivy.xml
│   └── pluginMaven
│       ├── pom-default.xml
│       └── step4.iml
├── reports
│   └── ...
├── repos-ivy
│   ├── com.example
│   │   └── step4
│   │       └── 1.0
│   │           ├── ivy-1.0.xml
│   │           ├── ivy-1.0.xml.sha1
│   │           ├── ivy-1.0.xml.sha256
│   │           ├── ivy-1.0.xml.sha512
│   │           ├── step4-1.0.jar
│   │           ├── step4-1.0.jar.sha1
│   │           ├── step4-1.0.jar.sha256
│   │           └── step4-1.0.jar.sha512
│   └── org.sample.Greetings
│       └── org.sample.Greetings.gradle.plugin
│           └── 1.0
│               ├── ivy-1.0.xml
│               ├── ivy-1.0.xml.sha1
│               ├── ivy-1.0.xml.sha256
│               └── ivy-1.0.xml.sha512
├── repos-maven
│   ├── com
│   │   └── example
│   │       └── step4
│   │           ├── 1.0
│   │           │   ├── step4-1.0.jar
│   │           │   ├── step4-1.0.jar.md5
│   │           │   ├── step4-1.0.jar.sha1
│   │           │   ├── step4-1.0.jar.sha256
│   │           │   ├── step4-1.0.jar.sha512
│   │           │   ├── step4-1.0.pom
│   │           │   ├── step4-1.0.pom.md5
│   │           │   ├── step4-1.0.pom.sha1
│   │           │   ├── step4-1.0.pom.sha256
│   │           │   └── step4-1.0.pom.sha512
│   │           ├── maven-metadata.xml
│   │           ├── maven-metadata.xml.md5
│   │           ├── maven-metadata.xml.sha1
│   │           ├── maven-metadata.xml.sha256
│   │           └── maven-metadata.xml.sha512
│   └── org
│       └── sample
│           └── Greetings
│               └── org.sample.Greetings.gradle.plugin
│                   ├── 1.0
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom.md5
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom.sha1
│                   │   ├── org.sample.Greetings.gradle.plugin-1.0.pom.sha256
│                   │   └── org.sample.Greetings.gradle.plugin-1.0.pom.sha512
│                   ├── maven-metadata.xml
│                   ├── maven-metadata.xml.md5
│                   ├── maven-metadata.xml.sha1
│                   ├── maven-metadata.xml.sha256
│                   └── maven-metadata.xml.sha512
├── resources
│   └── main
│       └── META-INF
│           └── gradle-plugins
│               └── org.sample.Greetings.properties
├── test-results
│   └── ...
...
80 directories, 68 files

I looked at the following directories and found it interesting:

I found a sort of symmetry here. If Gradle add a new type of reposotry named “Bar”, then a new plugin bar-publish will be developed, and we will find the following publications added:

§5 Publishing Multiple Plugins out of a single project

Outline

Almost all of articles about publishing custom Gradle plugin assumes a case where a single project publishes a single plugin. I wondered if it is possible to publish 2 or more custom Gradle plugins out of a single Gradle project. So I tried to do it, and succeeded.

Groovy sources of the plugins

In the previous steps up to the step4, I had a single Groovy class com.example.greeting.GreetingPlugin which implements 2 methods hello() and goodbye().

In this step5, I splited the previous class into two:

So, the step5 project is going to publish 2 custom Gradle plugins together.

settings.gradle and build.gradle

I made a directory named step5 where I located step5/settings.gradle and step5/build.gradle.

step5/settings.gradle

rootProject.name = 'step5'

step5/build.gradle

Refer to step5/build.gradle

Note the line#36 we have gradlePlugin extension closure, where I declared 2 plugins: org.sample.Hello and org.sample.Goodbye.

gradlePlugin {
    // Define the plugin
    plugins {
        MyHello {
            id = 'org.sample.Hello'
            implementationClass = 'com.example.hello.HelloPlugin'
        }
        MyGoodbye {
            id = 'org.sample.Goodbye'
            implementationClass = 'com.example.goodbye.GoodbyePlugin'
        }
    }
}

How the build works

:~/github/PublishingCustomGradlePlugin_StepByStep/step5 (develop *)
$ gradle clean

BUILD SUCCESSFUL in 895ms
1 actionable task: 1 executed
:~/github/PublishingCustomGradlePlugin_StepByStep/step5 (develop *)

$ gradle test

BUILD SUCCESSFUL in 7s

$ gradle publish

BUILD SUCCESSFUL in 2s
10 actionable tasks: 10 executed

It worked silently as expected.

Artifacts generated by the maven-publish plugin

$ cd step5
$ tree build
build
├── classes
│   └── ...
├── generated
│   └── ...
├── libs
│   └── step5-1.0.jar
├── pluginDescriptors
│   ├── org.sample.Goodbye.properties
│   └── org.sample.Hello.properties
├── pluginUnderTestMetadata
│   └── ...
├── publications
│   ├── MyGoodbyePluginMarkerMaven
│   │   └── pom-default.xml
│   ├── MyHelloPluginMarkerMaven
│   │   └── pom-default.xml
│   └── pluginMaven
│       └── pom-default.xml
├── reports
│   └── ...
├── repos-maven
│   ├── com
│   │   └── example
│   │       └── step5
│   │           ├── 1.0
│   │           │   ├── step5-1.0.jar
│   │           │   ├── step5-1.0.jar.md5
│   │           │   ├── step5-1.0.jar.sha1
│   │           │   ├── step5-1.0.jar.sha256
│   │           │   ├── step5-1.0.jar.sha512
│   │           │   ├── step5-1.0.pom
│   │           │   ├── step5-1.0.pom.md5
│   │           │   ├── step5-1.0.pom.sha1
│   │           │   ├── step5-1.0.pom.sha256
│   │           │   └── step5-1.0.pom.sha512
│   │           ├── maven-metadata.xml
│   │           ├── maven-metadata.xml.md5
│   │           ├── maven-metadata.xml.sha1
│   │           ├── maven-metadata.xml.sha256
│   │           └── maven-metadata.xml.sha512
│   └── org
│       └── sample
│           ├── Goodbye
│           │   └── org.sample.Goodbye.gradle.plugin
│           │       ├── 1.0
│           │       │   ├── org.sample.Goodbye.gradle.plugin-1.0.pom
│           │       │   ├── org.sample.Goodbye.gradle.plugin-1.0.pom.md5
│           │       │   ├── org.sample.Goodbye.gradle.plugin-1.0.pom.sha1
│           │       │   ├── org.sample.Goodbye.gradle.plugin-1.0.pom.sha256
│           │       │   └── org.sample.Goodbye.gradle.plugin-1.0.pom.sha512
│           │       ├── maven-metadata.xml
│           │       ├── maven-metadata.xml.md5
│           │       ├── maven-metadata.xml.sha1
│           │       ├── maven-metadata.xml.sha256
│           │       └── maven-metadata.xml.sha512
│           └── Hello
│               └── org.sample.Hello.gradle.plugin
│                   ├── 1.0
│                   │   ├── org.sample.Hello.gradle.plugin-1.0.pom
│                   │   ├── org.sample.Hello.gradle.plugin-1.0.pom.md5
│                   │   ├── org.sample.Hello.gradle.plugin-1.0.pom.sha1
│                   │   ├── org.sample.Hello.gradle.plugin-1.0.pom.sha256
│                   │   └── org.sample.Hello.gradle.plugin-1.0.pom.sha512
│                   ├── maven-metadata.xml
│                   ├── maven-metadata.xml.md5
│                   ├── maven-metadata.xml.sha1
│                   ├── maven-metadata.xml.sha256
│                   └── maven-metadata.xml.sha512
├── resources
│   └── main
│       └── META-INF
│           └── gradle-plugins
│               ├── org.sample.Goodbye.properties
│               └── org.sample.Hello.properties
├── test-results
│   └── ...
└── tmp
    ├── ...

78 directories, 71 files

There are quite a lot of files generated by the maven-publsh plugin. But I find nothing surprising.

§6 Publishing a custom plugin to the Gradle Plugin Portal

Outline

I want to publish my custom Gradle plugin to the Gradle Plugin Portal.

Reference documentation

The following doc is the entry point:

I followed the instruction to create an account on the Gradle Plugin Portal. You would need to do the same for you.

settings.gradle and build.gradle

I made a directory named step6 where I located step6/settings.gradle and step6/build.gradle.

step6/settings.gradle

rootProject.name = 'step6'

step6/build.gradle

Refer to step6/build.gradle

Note the line#1..6 I applied the com.`we have `gradlePlugin extension closure.

plugins {
    id 'groovy'
    //id 'java-gradle-plugin'
    //id 'maven-publish'
    id 'com.gradle.plugin-publish' version '1.1.0'
}

Also note that I configured the gradlePlugin closure:

gradlePlugin {
    website = 'https://kazurayam.github.io/PublishingCustomGradlePlugin_StepByStep/'
    vcsUrl = 'https://github.com/kazurayam/PublishingCustomGradlePlugin_StepByStep/'
    plugins {
        MyGreeting {
            id = 'io.github.kazurayam.Greetings'
            displayName = 'Plugin for living nice to others'
            description = 'A plugin that prints hello and goodbye'
            tags.set(['testing', 'lesson'])

The com.gradle.plugin-publish plugin; what it does, what it doesn’t

I have experimented at lot and learned the following points:

  1. In order to publish my plugins to the Gradle Plugin Portal so that others can apply it to their build using the Plugin DSL (plugins { id "…​." version "…​" }}, I need to use the com.gradle.plugin-publish plugin in the build.gradle file The plugin is the MUST. No other choice.

  2. The com.gradle.plugin-publish automatically applies the java-gradle-plugin and the maven-publish plugin. I do not need to apply the latter 2 plugins explicitly.

  3. The com.gradle.plugin-publish plugin requires me to configure the gradlePlugin { …​ } closure only.

  4. The com.gradle.plugin-publish plugin knows where the Gradle Plugin Portal is located. Therefore the plugin does NOT require me to configure the publishing { repository { maven { …​ }}} closure.

  5. The com.gradle.plugin-publish plugin automatically generates the jar files of the binary classes + sources + javadoc as well as metadata (POM xml, etc). The plugin does NOT require me to write any Gradle tasks that generate those jar files.

  6. The com.gradle.plugin-publish plugin determines the name of the jar files as designed.

  7. The plugin provides no way for me to specify arbitrary names of my choice to the jar files.

  8. Of course, I can generate the jar files of the binary + the sources + the javadoc with whatever names I like by writing tasks of type: Jar in the build.gradle file. Of course, by using maven-publish plugin, I can publish those artifacts to any Maven repositories (except the Gradle Plugin Portal).

  9. However, the com.gradle.plugin-publish plugin just ignores the publications I created. The plugin generates jar files as designed and leave my artifacts untouched. I have no chance to publish my artifacts to the Gradle Plugin Portal.

How the build worked — I got an error!

I clean the step6/build directory.

:~/github/PublishingCustomGradlePlugin_StepByStep/step6 (develop *)
$ gradle clean

BUILD SUCCESSFUL in 895ms
1 actionable task: 1 executed

I executed gradle compileGroovy task

$ gradle publishToMavenLocal

BUILD SUCCESSFUL in 1s
10 actionable tasks: 6 executed, 4 up-to-date

By gradle publishToMavenLocal command, I could confirm that the plugin has been published in the local Maven cache_, which is located in the ~/m2 directory, as follows:

$ tree ~/.m2/repository/io/github/kazurayam/
/Users/kazuakiurayama/.m2/repository/io/github/kazurayam/
├── Greetings
│   └── io.github.kazurayam.Greetings.gradle.plugin
│       ├── 1.0
│       │   └── io.github.kazurayam.Greetings.gradle.plugin-1.0.pom
│       └── maven-metadata-local.xml
├── step6
│   ├── 1.0
│   │   ├── step6-1.0-javadoc.jar
│   │   ├── step6-1.0-sources.jar
│   │   ├── step6-1.0.jar
│   │   └── step6-1.0.pom
│   └── maven-metadata-local.xml
...

I could check the content of the step6-1.0.jar as follows:

$ tar -xvf ~/.m2/repository/io/github/kazurayam/step6/1.0/step6-1.0.jar
x META-INF/
x META-INF/MANIFEST.MF
x com/
x com/example/
x com/example/greeting/
x com/example/greeting/GreetingPlugin$_apply_closure2$_closure4.class
x com/example/greeting/GreetingPlugin$_apply_closure2.class
x com/example/greeting/GreetingPlugin$_apply_closure1$_closure3.class
x com/example/greeting/GreetingPlugin.class
x com/example/greeting/GreetingPlugin$_apply_closure1.class
x META-INF/gradle-plugins/
x META-INF/gradle-plugins/io.github.kazurayam.Greetings.properties

Now I am ready to run gradle publishPlugins command to publish it to the Gradle Plugin Portal.

$ gradle publishPlugins


> Task :publishPlugins FAILED
Publishing plugin io.github.kazurayam.Greetings version 1.0

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':publishPlugins'.
> Failed to post to server.
  Server responded with:
  Plugin id 'io.github.kazurayam.Greetings' and group id 'com.example' must use same top level namespace, like 'com.example' or 'io.github.kazurayam'

Wow. I have got an error! This error is interesting.

I assigned the project’s group id to be com.example whereas I assigned the plugin id to be io.github.kazurayam.XXXX. This inconsistency did not matter when I published the plugin to the local Maven cache. Possibly it would not matter for other Maven repositories. It matters only for the Gradle Plugin Portal.

The Gradle Plugin Portal specifically requires me to change my code to either of:

  1. group id = io.github.kazurayam, plugin id = io.github.kazurayam

  2. group id = com.example, plugin id = com.example

This is the way how the Gradle Plugin Portal is designed. And the com.gradle.plugin-publish plugin checks if my build.gradle script complies with this constraint.

OK. I will fix this error in the text step7.

§7 group id and plugin id must use the same top level namespace

Outline

In the previous step6, I encountered an error when I tried to publish my custom Gradle plugin to the Maven Plugin Portal. The Portal site requires the project’s group id and the plugin id to use the same top level namespace. So I would try to meet this requirement.

settings.gradle and build.gradle

I made a directory named step7 where I located step7/settings.gradle and step7/build.gradle.

step7/settings.gradle

rootProject.name = 'step7'

step7/build.gradle

Refer to step7/build.gradle

Please note in the build.gradle I used io.github.kazurayam as the common top level namespace for both of the project’s group id and the plugin id.

group = 'io.github.kazurayam'
version = '1.1'

and

    plugins {
        MyGreeting {
            id = 'io.github.kazurayam.Greetings'

Namespace rule

Here I would summarise what I did for the naming of “group id”, “plugin id” and the package of implementation class.

Namespace rul
entity how I configured in the build.gradle

the build project’s group id

group="io.github.kazurayam"

the plugin id

gradlePlugin { plugins { MyGreeting { id = 'io.github.kazurayam.Greetings'

the implementation class

com.example.greeting.GreetingPlugin

Namespace rul

Now I understood the following:

  1. The group id and the plugin id must start with the same top level namespace: e.g. io.github.kazuram. The Gradle Plugin Portal requires this.

  2. The name of implementation class can be any. It is not required to use the same namespace as the plugin id.

How the build worked

I published the plugin to the local Maven cache.

:~/github/PublishingCustomGradlePlugin_StepByStep/step7 (develop *)
$ gradle clean

BUILD SUCCESSFUL in 918ms
1 actionable task: 1 executed
:~/github/PublishingCustomGradlePlugin_StepByStep/step7 (develop *+)
$ gradle publishToMavenLocal

BUILD SUCCESSFUL in 3s
10 actionable tasks: 10 executed

I checked how the local Maven Cache looked.

$ tree ~/.m2/repository/io/github/kazurayam/
/Users/kazuakiurayama/.m2/repository/io/github/kazurayam/
├── Greetings
│   └── io.github.kazurayam.Greetings.gradle.plugin
│       ├── 1.1
│       │   └── io.github.kazurayam.Greetings.gradle.plugin-1.1.pom
│       └── maven-metadata-local.xml
├── step7
│   ├── 1.1
│   │   ├── step7-1.1-javadoc.jar
│   │   ├── step7-1.1-sources.jar
│   │   ├── step7-1.1.jar
│   │   └── step7-1.1.pom
│   └── maven-metadata-local.xml
...
12 directories, 23 files

Finally I am going to my custom plugin to the Gradle Plugin Portal.

$ gradle publishPlugin

> Task :publishPlugins
Publishing plugin io.github.kazurayam.Greetings version 1.1
Publishing artifact build/libs/step7-1.1.jar
Publishing artifact build/libs/step7-1.1-javadoc.jar
Publishing artifact build/libs/step7-1.1-sources.jar
Publishing artifact build/publications/pluginMaven/pom-default.xml
Activating plugin io.github.kazurayam.Greetings version 1.1

BUILD SUCCESSFUL in 5s
8 actionable tasks: 2 executed, 6 up-to-date

Finally, I successfully published my custom Gradle plugin to the Gradle Plugin Portal!

I checked the https://plugins.gradle.org/search?term=io.github.kazurayam soon but my plugin was not yet visible on the site. I waited for some hours. After 10 hours of wait, I could see my plugin on the site.

What’s up in the Maven repository named “Gradle Plugin Portal?

I see that the Gradle Plugin Portal has a Maven repository inside. I could find my custom Gradle plugin is already available publicly on the Gradle Plugin Portal at the following URL.

§8 Assigning unique artifactId for PluginMavenPublication

Outline

In the step7, I could publish my custom Gradle plugin to the Gradle Plugin Portal. But I found a problem in it.

Problem to solve — Collision of names

I did the following operation:

$ gradle publishToMavenLocal

BUILD SUCCESSFUL in 1s
10 actionable tasks: 4 executed, 6 up-to-date
:~/github/PublishingCustomGradlePlugin_StepByStep/step7 (develop *)
$ tree ~/.m2/repository/io/github/kazurayam
/Users/kazuakiurayama/.m2/repository/io/github/kazurayam
├── Greetings
│   └── io.github.kazurayam.Greetings.gradle.plugin
│       ├── 1.0
│       │   └── io.github.kazurayam.Greetings.gradle.plugin-1.0.pom
│       ├── 1.1
│       │   └── io.github.kazurayam.Greetings.gradle.plugin-1.1.pom
│       └── maven-metadata-local.xml
├── step6
│   ├── 1.0
│   │   ├── step6-1.0-javadoc.jar
│   │   ├── step6-1.0-sources.jar
│   │   ├── step6-1.0.jar
│   │   └── step6-1.0.pom
│   └── maven-metadata-local.xml
├── step7
│   ├── 1.0
│   │   ├── step7-1.0-javadoc.jar
│   │   ├── step7-1.0-sources.jar
│   │   ├── step7-1.0.jar
│   │   └── step7-1.0.pom
│   ├── 1.1
│   │   ├── step7-1.1-javadoc.jar
│   │   ├── step7-1.1-sources.jar
│   │   ├── step7-1.1.jar
│   │   └── step7-1.1.pom
│   └── maven-metadata-local.xml
└── step9
    ├── 1.0
    │   ├── step9-1.0-javadoc.jar
    │   ├── step9-1.0-sources.jar
    │   ├── step9-1.0.jar
    │   ├── step9-1.0.module
    │   └── step9-1.0.pom
    └── maven-metadata-local.xml

12 directories, 23 files

I was worried about the symbols step6, step7, step9. These symbols were too abstract. They identify nothing.

These files were created by the codes in this “PublishingCustomGradlePlugin_StepByStep” repository. I could easily imagine, sometimes in the near future, I would create another repository, e.g. named “MyGreaterGradlePlugin”. I would likely to create publications named “step6”, “step7” and “step9” in the MyGreaterGradlePlugin project. Then I would suffer from the collisions of the names of directory and files when published to the Maven repository of the Gradle Plugin Portal.

How can I work around this difficulty? — I need to find out a way to give unique artifactId for each of the Gradle plugins I publish. I can achieve it by specifying a unique artifactId as the rootProject.name in the settings.gradle file.

settings.gradle and build.gradle

I made a directory named step8 where I located step8/settings.gradle and step8/build.gradle.

step8/settings.gradle

// step8
rootProject.name = 'GreetingImpl'

Please note, in the settings.gradle, I assigned the rootProject.name property with a string GreetingsImpl. This value would uniquely identify the artifact (the jar that contains the implementation class) created by this build. This is the magic to solve the name collision problem.

step8/build.gradle

Refer to step8/build.gradle

The step8/build.gradle is just the same as the step7/build.gradle.

How the build worked

First I cleaned up the local Maven cache.

$ rm -rf  ~/.m2/repository/io/github/kazurayam

Then I published the plugin into the local Maven cache.

$ gradle publishToMavenLocal

BUILD SUCCESSFUL in 3s
10 actionable tasks: 10 executed

I looked at the tree created in the local Maven cache.

$ tree -xvf ~/.m2/repository/io/github/kazurayam/
/Users/kazuakiurayama/.m2/repository/io/github/kazurayam
├── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl
│   ├── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl/1.2
│   │   ├── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl/1.2/GreetingImpl-1.2-javadoc.jar
│   │   ├── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl/1.2/GreetingImpl-1.2-sources.jar
│   │   ├── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl/1.2/GreetingImpl-1.2.jar
│   │   └── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl/1.2/GreetingImpl-1.2.pom
│   └── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/GreetingImpl/maven-metadata-local.xml
└── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/Greetings
    └── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/Greetings/io.github.kazurayam.Greetings.gradle.plugin
        ├── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/Greetings/io.github.kazurayam.Greetings.gradle.plugin/1.2
        │   └── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/Greetings/io.github.kazurayam.Greetings.gradle.plugin/1.2/io.github.kazurayam.Greetings.gradle.plugin-1.2.pom
        └── /Users/kazuakiurayama/.m2/repository/io/github/kazurayam/Greetings/io.github.kazurayam.Greetings.gradle.plugin/maven-metadata-local.xml

6 directories, 7 files

Cool! How clean it looks! This is what I wanted to create and publish.

So, I published this version into the Gradle Plugin Portal.

$ gradle publishPlugins

> Task :publishPlugins
Publishing plugin io.github.kazurayam.Greetings version 1.2
Publishing artifact build/libs/greeting-gradle-plugin-1.2.jar
Publishing artifact build/libs/greeting-gradle-plugin-1.2-sources.jar
Publishing artifact build/libs/greeting-gradle-plugin-1.2-javadoc.jar
Publishing artifact build/publications/pluginMaven/pom-default.xml
Activating plugin io.github.kazurayam.Greetings version 1.2

BUILD SUCCESSFUL in 12s
8 actionable tasks: 5 executed, 3 up-to-date

Total success!

8.1 published to Gradle Plugin Portal

End of the article