When we have a multi-module project in Gradle we sometimes want to have dependencies, task configuration and other settings shared between the multiple modules. We can use the subprojects or allprojects blocks, but the downside is that it is not clear from the build script of the subproject where the configuration comes from. We must remember it is set from another build script, but there is no reference in the subproject to that connection. It is better to use a plugin with shared configuration and use that plugin in the subprojects. We call this a conventions plugin. This way it is explicitly visible in a subproject that the shared settings come from a plugin. Also it allows Gradle to optimize the build configuration.

The easiest way to implement the shared configuration in a plugin is using a so-called precompiled script plugin. This type of plugin can be written as a build script using the Groovy or Kotlin DSL with a filename ending with .gradle or .gradle.kts. The name of the plugin is the first part of the filename before .gradle or .gradle.kts. In our subproject we can add the plugin to our build script to apply the shared configuration. For a multi-module project we can create such a plugin in the buildSrc directory. For a Groovy plugin we place the file in src/main/groovy, for a Kotlin plugin we place it in src/main/kotlin.

In the following example we write a script plugin using the Kotlin DSL to apply the java-library plugin to a project, set some common dependencies used by all projects, configure the Test tasks and set the Java toolchain. First we create a build.gradle.kts file in the buildSrc directory in the root of our multi-module project and apply the kotlin-dsl plugin:

// File: buildSrc/build.gradle.kts
plugins {
    `kotlin-dsl`
}

repositories.mavenCentral()

Next we create the conventions plugin with our shared configuration:

// File: buildSrc/src/main/kotlin/java-project-conventions.gradle.kts
plugins {
    `java-library`
}

group = "mrhaki.sample"
version = "1.0"

repositories {
    mavenCentral()
}

dependencies {
    val log4jVersion: String by extra("2.14.0")
    val junitVersion: String by extra("5.3.1")
    val assertjVersion: String by extra("3.19.0")

    // Logging
    implementation("org.apache.logging.log4j:log4j-api:${log4jVersion}")
    implementation("org.apache.logging.log4j:log4j-core:${log4jVersion}")

    // Testing
    testImplementation("org.junit.jupiter:junit-jupiter-api:${junitVersion}")
    testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:${junitVersion}")
    testImplementation("org.assertj:assertj-core:${assertjVersion}")
}

java {
    toolchain {
        languageVersion.set(JavaLanguageVersion.of(15))
    }
}

tasks.withType<Test> {
    useJUnitPlatform()
}

The id of our new plugin is java-project-conventions and we can use it in our build script for a subproject as:

// File: rest-api/build.gradle.kts
plugins {
    id("java-project-conventions")  // apply shared config
    application  // apply the Gradle application plugin
}

dependencies {
    val vertxVersion: String by extra("4.0.2")

    implementation(project(":domain"))  // project dependency
    implementation("io.vertx:vertx-core:${vertxVersion}")
}

application {
    mainClass.set("com.mrhaki.web.Api")
}

The rest-api project will have all the configuration and tasks from java-library plugin as configured in the java-project-conventions plugin, so we can build it as a Java project.

Written with Gradle 6.8.2.

shadow-left