Adding common plugins and their configuration to one plugin

We have several Kotlin projects built in Gradle. All projects have a common part – plugins and their configurations.

build.gradle.kts

plugins {
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.allopen") version "1.9.22"
    id("io.quarkus")
}

Generally, most of our projects have a build.gradle.kts file like this:

plugins {
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.allopen") version "1.9.22"
    id("io.quarkus")
}

repositories {
    mavenCentral()
    mavenLocal()
}

val quarkusPlatformGroupId: String by project
val quarkusPlatformArtifactId: String by project
val quarkusPlatformVersion: String by project

dependencies {
    implementation(enforcedPlatform("${quarkusPlatformGroupId}:${quarkusPlatformArtifactId}:${quarkusPlatformVersion}"))
    implementation("io.quarkus:quarkus-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    implementation("io.quarkus:quarkus-arc")
}

group = "org.acme"
version = "1.0.0-SNAPSHOT"

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

tasks.withType<Test> {
    systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
}
allOpen {
    annotation("jakarta.ws.rs.Path")
    annotation("jakarta.enterprise.context.ApplicationScoped")
    annotation("jakarta.persistence.Entity")
    annotation("io.quarkus.test.junit.QuarkusTest")
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
    kotlinOptions.jvmTarget = JavaVersion.VERSION_21.toString()
    kotlinOptions.javaParameters = true
}

Everything except the dependencies {} section is boilerplate for us. Is it possible to hide these things?

I was thinking about making a plugin that would contain most of these configurations, let’s say these things:

plugins {
    kotlin("jvm") version "1.9.22"
    kotlin("plugin.allopen") version "1.9.22"
    id("io.quarkus")
}

java {
    sourceCompatibility = JavaVersion.VERSION_21
    targetCompatibility = JavaVersion.VERSION_21
}

tasks.withType<Test> {
    systemProperty("java.util.logging.manager", "org.jboss.logmanager.LogManager")
}

allOpen {
    annotation("jakarta.ws.rs.Path")
    annotation("jakarta.enterprise.context.ApplicationScoped")
    annotation("jakarta.persistence.Entity")
    annotation("io.quarkus.test.junit.QuarkusTest")
}

tasks.withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
    kotlinOptions.jvmTarget = JavaVersion.VERSION_21.toString()
    kotlinOptions.javaParameters = true
}

and in our projects we will only add our plugin and the dependencies section:

plugins {
    id("org.example.common-plugin-with-boilerplate")
}

dependencies {
    ...
}

Any tips on how to do this?

As far as I understand, I can’t just enter the common part of the configuration into build.gradle.kts in the plugin, I have to include it in the plugin code, in this section:

class CustomPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val newTask = project.tasks.create("newTask") {
            it.doLast {
                //...
            }
        }
    }
}

I guess I need to modify “project: Project” itself, but I have no idea how to do it.

As a starting point, I wrote two plugins that create files when the application starts. Plugin First creates fileA, plugin Second creates fileB:

class CustomPlugin : Plugin<Project> {
    override fun apply(project: Project) {
        val newTask = project.tasks.create("firstTask") {//secondTask in plugin Second
            it.doLast {
                val resourcesDir = File(project.projectDir, "src/main/resources")
                if (!resourcesDir.exists()) {
                    resourcesDir.mkdirs()
                }

                val configFile = File(resourcesDir, "fileA.txt") // fileB.txt in the plugin Second
                configFile.writeText("some text")
            }
        }
        project.tasks.named("classes") {
            it.dependsOn(newTask)
            it.mustRunAfter(newTask)
        }
    }
}

Then I wanted to add the Second plugin to the First plugin, so that by adding the First plugin to the target application, both tasks from both plugins would be launched when the application started.

But I didn’t manage to do it. If I add plugin Second to the plugins {} section in plugin First:

build.gradle.kts in plugin First

plugins {
    kotlin("jvm") version "1.9.22"
    `java-gradle-plugin`
    `maven-publish`
    id("com.org.example.second-plugin") version "0.0.1"
}

and then I add the First plugin to the plugins {} section of the target application, only one file is created when the application starts - fileA.txt. So the application only executes the task from plugin First.

I tried to add plugin Second to plugin First by declaring in fun apply(project: Project):

and declared in plugins {} :

build.gradle.kts in plugin First

plugins {
    kotlin("jvm") version "1.9.22"
    `java-gradle-plugin`
    `maven-publish`
    id("com.org.example.second-plugin") version "0.0.1"
}

Now when I add the First plugin to the application, I get an error when building:

An exception occurred applying plugin request [id: 'com.org.example.first-plugin']
> Failed to apply plugin 'com.org.example.second-plugin'.
   > Plugin with id 'com.org.example.second-plugin' not found.

I also tried, instead of putting plugin Second in the plugins {} section, adding it in buildSrc:

buildscript {
    repositories {
        mavenLocal()
    }
    dependencies {
        classpath("com.org.example:second-plugin:0.0.1")
    }
}

but the same error remains.

Generally, I don’t know if I’m going in the right direction. It seems to me that the approach to configuring the execution of tasks from both plugins in the application may be different from the approach where I want to place plugins like

kotlin(“jvm”) version “1.9.22”
kotlin(“plugin.allopen”) version “1.9.22”
id(“io.quarkus”)

in one plugin. I will be grateful for any help.

I did not fully read all your text, so sorry if I missed something vital.
But yes, moving those things into a so-called convention plugin is exactly the way to centralize build logic. You can do so in buildSrc or in an included build. And you can of course use plain .kt files to write your plugins, but you could also use precompiled script plugins which look almost like normal build scripts if that is easier for you.

Some general advice from a cursory look:

  • do not use mavenLocal() if you can avoid it, it is broken by design in Maven already and has several problems, making your builds slower and flaky. See for example here for more information: Declaring repositories
  • avoid enforcedPlatform except if you really need it. In most situations platform is more appropriate.
  • consider using JVM toolchains instead of *Compatibility to decouple the Java version Gradle runs with from the Java version needed for your product, also the jvmTarget would then be unnecessary

And applying one plugin from another, assuming you have both plugins in the same project in buildSrc, just apply it by type like project.apply<CustomSecondPlugin>().

Hi, thank you for the fast answer. I’m not sure about convention plugin. After reviewing documentation seems like convention plugin helps when project structure looks like this:

├── internal-module
│   └── build.gradle.kts
├── library-a
│   ├── build.gradle.kts
│   └── README.md
├── library-b
│   ├── build.gradle.kts
│   └── README.md
└── settings.gradle.kts

So several subprojects and one common build, and everything under the common root.

In my case, I have several independent projects, without common root.
Is there possibilities to use convention plugin, assuming that my common plugin (containing other plugins) and other projects are completely independent of each other?

Sure.
You said you already started to use convention plugins.
The term just means plugins that implement your conventions and that you apply to your projects.
You can for example publish them and use that published version, or include it as composite build to all the other builds that need it.

Hmm, I’m trying to adapt this sample: Sharing build logic between subprojects Sample to my case, but to be honest - I am no clearly undestand how to do that.

I’m trying to create the simplest convention plugin possible.
Assume that I need to put id("org.jlleitschuh.gradle.ktlint") plugin to my custom plugin.

Then I need to publish that plugin to my maven local repo (in future will be sent to remote).

Last step - I would like to add that custom plugin to my project (which is absolutely independent, and has other root than custom plugin), as below:

plugins {
    id("org.example.my-custom-plugin")
}

and I expect to see ktlinFormat in my tasks:
Tasks -> formatting -> ktlintFormat

So first I created that kind of project:
image

build.gradle.kts:

plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}

dependencies {
    implementation("org.jlleitschuh.gradle:ktlint-gradle:11.5.1")
    implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22")
}

settings.gradle.kts
rootProject.name="my-org-conventions"

myproject.java-conventions.gradle.kts:

plugins {
    id("org.jetbrains.kotlin.jvm")
    id("org.jlleitschuh.gradle.ktlint")
}

repositories {
    mavenCentral()
}

myproject.library-conventions.gradle.kts:

plugins {
    `maven-publish`
    id("myproject.java-conventions")
}

group = "com.example"

publishing {
    publications {
        create<MavenPublication>("myRelease") {
            groupId = "com.example.library"
            artifactId = "my-buildsrc"
            from(components["java"])
        }
    }
    repositories {
        mavenLocal()
    }
}

But when I try to publish I get a failure:

user@Yoga:~/Downloads/my-buildsrc-sample/buildSrc$ ./gradlew publish -Pversion=0.1.0

FAILURE: Build failed with an exception.

* What went wrong:
Task 'publish' not found in root project 'my-org-conventions'.

* Try:
> Run gradlew tasks to get a list of available tasks.
> For more on name expansion, please refer to https://docs.gradle.org/8.5/userguide/command_line_interface.html#sec:name_abbreviation in the Gradle documentation.
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 358ms

I definitely feel that I am doing something wrong :thinking:
Could you help please with any suggestions?

buildSrc is not meant for something that is published, but only for build logic local to a build. Besides that, why do you expect a publish task to be present? You did not configure publishing for that project.