Multi-module build doesn't see configurations from java plugin

Hi folks.
Trying to bootstrap a simple multi-module project in Kotlin. Have “server” and “client” modules inside root one. Here is a folder structure:

/settings.gradle.kts
/build.gradle.kts
/client/src/main/kotlin/...
/server/src/main/kotlin/...
...

I want to configure both “server” and “client” modules in a root build.gradle.kts:

subprojects {
    apply(plugin = "kotlin")
    apply(plugin = "application")
    apply(plugin = "org.jetbrains.kotlin.plugin.serialization")
    repositories {
        mavenCentral()
    }
    dependencies {
        testImplementation("org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version")
    }
}
project(":client") {
    dependencies {
        ...
    }
}
project(":server") {
    apply(plugin = "com.google.cloud.tools.appengine")
    dependencies {
        ...
    }
}

The build fails with Unresolved reference: testImplementation. It’s weird for me since I specified apply(plugin = "kotlin") in the subproject section. That plugin should bring testImplementation and other configurations into scope!

Besides that it is bad practice to cross-configure projects like that, you only get the accessor you expect if you apply a plugin using the plugins DSL, not if you use the legacy apply method you are using there. It should work if you make testImplementation a string iirc though.

What’s the reason behind to not configure multiple projects from within root build.gradle.kts like I did? Why it’s a bad practice?
How can I then apply a bunch of plugins to all subproject without duplicating

plugins {
    application
    kotlin("jvm") version "1.6.10"
    ...
}

across all build.gradle.kts files?
Or I shouldn’t care about such kind of duplicating?

That has multiple reasons and I might miss some, but some are:

  • it’s cleaner if a project defines it’s own build and not configuration comes from other projects injected
  • that also increases maintainability
  • you cannot use the recommended plugins DSL, but only the legacy apply function
  • with Kotlin DSL you don’t get type safe accessor generated for plugins applied the legacy way
  • cross project configuration like that couples projects, which prevents some advanced optimizations like configuration cache or configure on demand

  • There are probably some others in just don’t have on mind right now.

The idiomatic way is to have a convention plugin either in buildSrc or - what I prefer - in an included build, which you then apply to the projects directly. This way you don’t have duplication and the actual project build scripts often just apply one to a few convention plugins and define a list of dependencies. And if you write the convention plugin as precompiled script plugin, it looks almost like a normal build script.

onepiece.software has a nice video explaining convention plugins: Understanding Gradle #03 – Plugins - YouTube

Should any usage of allprojects { ... } or subprojects { ... } be considered as a bad practice?

I think so, yes.
Well, let’s say 98.7 % to keep a small window open. :smiley: