Minimal setup for Kotlin back-end app with multi-sub-project, build-logic and convention plugins

Hi!
Firstly, big thanks to all the heroes here; the ones with deep Gradle knowledge who devote so much time to help the rest of us learn!

My goal is to create a simple Gradle template for starting new projects according to how we’ve just started doing it in my team: programming back-end applications solely in Kotlin, using a multi-sub-project approach, and using Quarkus.

I have tried generating a new project with the Quarkus CLI, the IntelliJ Quarkus-Gradle wizard and the Gradle CLI, and gradle init feels like the best tool so far (adding in Quarkus in a later step - dealing with those problems then…). The problem is that it uses the buildSrc approach, but having studied the nowInAndroid project, along with the guides from Jendrik Johannes and an article from Tony Robalik, I want to go with an included “build-logic” build instead.

I tried to remove the buildSrc folder and construct the build-logic project myself, but got stuck and haven’t found any leads anywhere for my situation.

Here is my intended project structure:

my-project/
├─ gradle/
│  ├─ build-logic/
│  │  ├─ kotlin-conventions/
│  │  │  ├─ src/
│  │  │  │  ├─ main/
│  │  │  │  │  ├─ kotlin/
│  │  │  │  │  │  ├─ my.project.kotlin-conventions-plugin.gradle.kts
│  │  │  ├─ build.gradle.kts
│  │  ├─ settings.gradle.kts
├─ app/
│  ├─ build.gradle.kts
├─ domain/
│  ├─ build.gradle.kts
├─ infrastructure/
│  ├─ build.gradle.kts
├─ settings.gradle.kts

These are the files I have started creating and filling out so far:

// ./settings.gradle.kts

pluginManagement {
	includeBuild("./gradle/build-logic")
}

rootProject.name="my-project"
include("app", "domain", "infrastructure")

// We use a proprietary company repository, which we configure in the .gradle directory of our home folders 
// ./gradle/build-logic/settings.gradle.kts

rootProject.name="build-logic"

include("kotlin-conventions")

// ./gradle/build-logic/kotlin-conventions/build.gradle.kts

plugins {
	`kotlin-dsl`
}

repositories {
	gradlePluginPortal()
}

dependencies {
	implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20")
}
// ./gradle/build-logic/kotlin-conventions/src/main/kotlin/my.project.kotlin-conventions-plugin.gradle.kts

plugins {
	kotlin("jvm")
}

dependencies {
	// This is where I get stuck. I cannot find the "implementation" method.
}

As commented in the last file, I get stuck because I don’t have access to the implementation() method. I want to start defining the dependencies that all Kotlin code in my project will use, such as a common logging library.

I read that implementation() is connected to the java and java-library plugins, but adding them do not solve my problem. Also, I am not planning to write any Java at all in this project, so it feels weird having to include them just to define dependencies (something that should not be exclusive to Java…). I got the impression that the kotlin("jvm") plugin should provide the implementation() method as well?

I am of course extremely open to any other details that you all might spot in my intended approach, that could be improved upon!

Thanks a lot to anyone willing to take a look!

The kotlin("jvm") will automatically apply the java plugin and thus “also provide” the implementation configuration.

From a very cursory look it seems to be fine.
Do you maybe just not have synced with the IDE / waited long enough?
Or does it also fail when you try to build from the commandline?
Maybe it would help if you provided your state as complete MCVE.

Sorry for the super late reply, summer and vacation and stuff. Thanks for taking a look, Björn!

Hehe, of course when I started to construct an MCVE, it all worked. :laughing: Unfortunately I did not pay close attention to exactly what I did differently, or the exact order of the steps compared to my problem-project, so I cannot say for certain what the issue was. But I read somewhere that, since I define a separate Gradle project for the convention plug-ins, they do not get built (or “analyzed” or whatever) if they are not needed. And in my problem-project I had not used my convention plug-in anywhere in a sub-projects build.gradle.kts. So maybe that was the reason.

Anyway, I continued on my journey to construct a base example template for Kotlin + Quarkus, and got as far as succeeding in adding in Quarkus manually. But as soon as I add the Quarkus Kotlin Extension, IntelliJ Idea (Ultimate) complains when I go to my “application-test-class” QuarkusStartTest that “Kotlin not configured”. The application starts fine anyway, but why am I getting this warning? Do I need to adjust my Kotlin configurations in my convention plugin because this Quarkus extension is configuring things for me? The Quarkus docs have some recommended settings for Gradle, but I am unsure what effect they have on the ones I have already written before adding the extension:

Here is my MCVE: GitHub - soundsmagic/mcve-gradle-quarkus-kotlin: MCVE for problems with adding Quarkus manually to a Gradle multi-subproject build created with "gradle init", using Kotlin as Gradle DSL and source code language.

But I read somewhere that, since I define a separate Gradle project for the convention plug-ins, they do not get built (or “analyzed” or whatever) if they are not needed.

That does not apply here.
Yes, the plugin is not built if it is not use even when running the “parent” build.
But in the IDE it would of course still be synced / analyzed properly.
Otherwise you couldn’t develop a plugin for usage in other projects, that would be quite strange.

As I said, you maybe just did not sync the project / did not wait long enough.

So you are saying your build is working fine, but the IDE does not agree?
Then I’d say this is the wrong location and you need to report it to JetBrains.

But actually no, your MCVE is not building.
You defined mavenCentral() as plugin repository for your main build, not finding the plugins that are only on the Gradle Plugin Portal (which should be most).
And if you fix that, you find that you did not define any repositories for your main builds dependencies.
And if you fix that, everything is fine after sync and I do not see any warning in QuarkusStartTest.

Thanks again for your time and sharp eyes!

Ooh, I knew there was something I forgot to write… I have worked with this at work, where we are using a company-specific Jfrog Artifactory, so I have the repositories defined in my home catalogue. Should have included that info. But yeah, apparently I forgot more stuff…

Okay, since I aim for this to be a self-contained example, I’ll include all repository information directly in the project. I have updated the example repo now, could you have another look? Another question: Since I am curious about how much that can be done in the build-logic project, I tried defining repositories for both regular dependencies and plugins there, but that did not work, why is that?

I have updated the example repo now, could you have another look?

It syncs now ootb, but I still do not see what problem you could have.
After syncing, I open QuarkusStartTest and all looks normal, no warning anywhere.

Since I am curious about how much that can be done in the build-logic project, I tried defining repositories for both regular dependencies and plugins there, but that did not work, why is that?

I can hardly tell you what you did wrong without you telling what you did.
You can perfectly fine do that.
The only exception is plugin repository for resolving settings plugins, as that would be a chicken-and-egg problem.

Thank you for checking again. You were right from the beginning; This was not connected to Gradle, but an issue with a recent change in Quarkus.

Sorry, I did not include anything about performed steps since I just assumed this was the intended behavior in Gradle. I tried defining repositories in dependencyResolutionManagement { } in the settings.gradle.kts of the build-logic on another computer and it worked better, although I am getting an error which I assume is perfectly logical:
:utilities:integrationTest: Cannot resolve external dependency io.github.oshai:kotlin-logging-jvm:7.0.0 because no repositories are defined.
I guess the subproject cannot “see” that repository declaration in the build-logic project, should I define the repository in the convention plugin itself instead?

Another question: I am using your work-around with val libs = the<LibrariesForLibs>() to get my TOML version catalog visible in my convention plugin. Now I am thinking about splitting my one convention plugin into two (one for Kotlin related settings and one for Quarkus related settings), but defining this libs variable in both convention plugins causes a disambiguity error. I am having a hard time grasping how this can be broken out and made visible to both plugins, do you have any suggestions?

I guess the subproject cannot “see” that repository declaration in the build-logic project, should I define the repository in the convention plugin itself instead?

The buildSrc build is a completely separate build that just happens to be executed before your build and its result prepended to all build script classpaths. Which repositories you declare in its settings script or build scripts is only for building that build and has no influence at all on the consuming build, which is your main build. Your main build has to declare all repositories where dependencies or plugins should be resolved from. Of course a convention plugin that you build in buildSrc and that you apply to your main build can configure your main build by adding repositiories. Such a convention plugin can add repositories for normal dependencies and also for plugins. The only thing that does not work is adding plugin repositories from which settings plugins should be resolved, as that would be a chicken-and-egg situation. But a settings plugin could for example add plugin repositories that are then later used to resolve plugins for build scripts.

but defining this libs variable in both convention plugins causes a disambiguity error.

Again, if you want good answers, please ask good questions. :slight_smile:
If you go to the doctor and say “it hurts”, which medication should he give you?
What error do you get exactly?
What did you try exactly?
Can you show an MCVE?
Can you show a build --scan URL?

Thank you again for your help. Good explanation of the repository definitions.

Sorry for once again thinking I did not need to specify more information about my errors.

Sometimes I feel like I’m losing my mind. I went to update my MCVE project to reproduce this dis-ambiguity error, and suddenly it’s gone… Sometimes I get a feeling that IntelliJ is playing with me. The MCVE project is now updated anyway.

Also, for a while a new warning popped up about how “The Kotlin Gradle plugin is applied multiple times”, but that warning disappeared as well before I could reproduce it (or copy the exact warning message). Would it be possible for you to pull down the latest change and see if you get any warnings?

I am doing this testing on my work laptop (Windows 11) and my private laptop (Ubuntu 22), and testing with the company-defined private repository definition in the init.gradle in my .gradle folder in my home directory both active and commented out. Don’t know if this information matters, but just in case.

Only the one about using an auto-provisioned toolchain without having toolchain providers configured.