kazurayam, 26 March 2023
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
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.
In the step1, I will create a skeletal project where I will write a Groovy code as a custom Gradle plugin.
I made a directory named step1
where I located step1/settings.gradle
and step1/build.gradle
.
rootProject.name = 'step1'
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()
}
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"
}
}
}
}
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
}
}
I tried to compile the Groovy code, but it doesn’t compile.
The Gradle API is not accessible for the compiler. Therefore, Groovy compiler failed to find the very core class org.gradle.api.Project
.
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.
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.
I made a directory named step2
where I located step2/settings.gradle
and step2/build.gradle
.
rootProject.name = 'step2'
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:
I could compile the Groovy code and run the junit test.
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.
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.
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'.
...
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.
At first you should have a look at the official Gradle documentation:
I made a directory named step3
where I located step3/settings.gradle
and step3/build.gradle
.
rootProject.name = 'step3'
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")
}
}
}
$ 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
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
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.
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
.
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.
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.
publish
- Publishes all publications produced by this project.
publishAllPublicationsToSketchRepository
- Publishes all Maven publications produced by this project to the sketch
repository.
publishToMavenLocal
- Publishes all Maven publications produced by this project to the local Maven cache.
The rest of commands are unitary ones.
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
.
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.
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.
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.
I made a directory named step4
where I located step4/settings.gradle
and step4/build.gradle
.
rootProject.name = 'step4'
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")
}
}
}
$ 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
build
directory treeWhen 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:
step4/build/publications/MyGreetingPluginMarkerIvy
step4/build/publications/MyGreetingPluginMarkerMaven
step4/build/publications/pluginIvy
step4/build/publications/pluginMaven
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:
step4/build/publications/MyGreetingPluginMarkerBar
step4/build/publications/pluginBar
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.
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.
I made a directory named step5
where I located step5/settings.gradle
and step5/build.gradle
.
rootProject.name = 'step5'
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'
}
}
}
:~/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.
$ 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.
I want to publish my custom Gradle plugin to the Gradle Plugin Portal.
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.
I made a directory named step6
where I located step6/settings.gradle
and step6/build.gradle
.
rootProject.name = 'step6'
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'])
I have experimented at lot and learned the following points:
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.
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.
The com.gradle.plugin-publish
plugin requires me to configure the gradlePlugin { … }
closure only.
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.
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.
The com.gradle.plugin-publish
plugin determines the name of the jar files as designed.
The plugin provides no way for me to specify arbitrary names of my choice to the jar files.
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).
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.
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:
group id = io.github.kazurayam
, plugin id = io.github.kazurayam
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.
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.
I made a directory named step7
where I located step7/settings.gradle
and step7/build.gradle
.
rootProject.name = 'step7'
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'
Here I would summarise what I did for the naming of “group id”, “plugin id” and the package of implementation class.
entity | how I configured in the build.gradle |
---|---|
the build project’s group id |
|
the plugin id |
|
the implementation class |
|
Namespace rul
Now I understood the following:
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.
The name of implementation class can be any. It is not required to use the same namespace as the plugin id.
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.
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.
In the step7, I could publish my custom Gradle plugin to the Gradle Plugin Portal. But I found a problem in it.
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.
I made a directory named step8
where I located step8/settings.gradle
and step8/build.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.
Refer to step8/build.gradle
The step8/build.gradle
is just the same as the step7/build.gradle
.
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!
End of the article