'Why can't I use Gradle DSL in apply'ed files

In our team we have lot of projects built with Gradle. Some parts in the Gradle files are all the same. For example, we use Java 11 in all our projects. So my idea was that I could split up my build.gradle files into a common part, that is then synced from a central repository into every Gradle project while the project specific parts remain in build.gradle.

build.gradle:

plugins {
    id 'java'
    //...
}

apply from: "common.gradle.kts"

dependencies {
  // ...
}

common.gradle.kts

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

test {
    useJUnitPlatform()
}

Now I get the error message by Gradle

* Where:
Script '/Users/.../common.gradle.kts' line: 4

* What went wrong:
Script compilation errors:

  Line 04:     java {
               ^ Expression 'java' cannot be invoked as a function. The function 'invoke()' is not found

  Line 04:     java {
               ^ Unresolved reference. None of the following candidates is applicable because of receiver type mismatch:
                   public val PluginDependenciesSpec.java: PluginDependencySpec defined in org.gradle.kotlin.dsl

  Line 05:         toolchain {
                   ^ Unresolved reference: toolchain

  Line 06:             languageVersion = JavaLanguageVersion.of(11)
                       ^ Unresolved reference: languageVersion

  Line 09:     test {
               ^ Unresolved reference: test

  Line 10:         useJUnitPlatform()
                   ^ Unresolved reference: useJUnitPlatform

6 errors

For some configurations I found an alternative using a more generic API that works, though it is a lot of effort to find the corresponding alternatives and in the end no one can guarantee that they do exactly the same thing:

tasks.withType<JavaCompile> {
    options.release.set(11)
}

So the question remains: why can't I use the DSL functions java or test in my externalized common.gradle.kts?

It seems it has to do something with the use of Kotlin script, at least if I use Groovy too for my externalized script, it works.



Solution 1:[1]

In your common.gradle.kts, java { } is generated helper Kotlin DSL function. Gradle doesn't know about the Kotlin DSL helpers unless

  1. it's part of a build (not using apply(from = "...")
  2. the java plugin is applied

Understanding when type-safe model accessors are available

Only the main project build scripts and precompiled project script plugins have type-safe model accessors. Initialization scripts, settings scripts, script plugins do not. These limitations will be removed in a future Gradle release.

Reacting to plugins

https://docs.gradle.org/current/userguide/implementing_gradle_plugins.html#reacting_to_plugins

It's still possible to have your common.gradle.kts - but it needs to configure the Java Plugin without the Kotlin DSLs

// common.gradle.kts
plugins.withType(JavaBasePlugin::class).configureEach {
    // the project has the Java plugin
    project.extensions.getByType<JavaPluginExtension>().apply {
        toolchain {
            languageVersion.set(JavaLanguageVersion.of(11))
        }
    }
    tasks.withType<Test>().configureEach {
        useJUnitPlatform()
    }
}

This is a little more clunky because the Kotlin DSL helpers aren't available.

buildSrc convention plugins

If you want to create conventions for a single project, then the standard way is to create buildSrc convention plugins.

https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:build_sources

This is best for projects that have lots of subprojects.

// $projectRoot/buildSrc/src/main/kotlin/java-convention.gradle.kts
plugins {
    java
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(11)
    }
}

test {
    useJUnitPlatform()
}

See the answer here for more detail: https://stackoverflow.com/a/71892685/4161471

Sharing plugins between projects

https://docs.gradle.org/current/userguide/implementing_gradle_plugins.html

It's possible to share convention plugins between projects, so long as you have a Maven repo to deploy your plugins.

It's even possible to create your own Gradle distribution, so the plugins are included along with the Gradle wrapper! https://docs.gradle.org/current/userguide/organizing_gradle_projects.html#sec:custom_gradle_distribution

However I'd advise against these approaches. Generally the time invested in creating shared plugins will never be faster than just copy and pasting buildSrc convention plugins. And more importantly, it's best to keep projects independent. While sharing build conventions seems like a good idea, it introduces dependencies that make it hard to track problems, and makes updating the shared plugins hard as you're not sure what the consequences might be. This article explains more https://phauer.com/2016/dont-share-libraries-among-microservices/

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1