gradlegradle-plugin

Why does conventional plugin from root project overrides transitive dependency of plugin applied to subproject?


Build setup

Consider the following project setup:

.
├── gradle
│   ├── build-logic
│   │   ├── buf-convention
│   │   │   ├── build.gradle.kts
│   │   │   └── src
│   │   │       └── main
│   │   │           └── kotlin
│   │   │               └── buf-check-convention.gradle.kts
│   │   ├── jib-convention
│   │   │   ├── build.gradle.kts
│   │   │   └── src
│   │   │       └── main
│   │   │           └── kotlin
│   │   │               └── jib-configuration-convention.gradle.kts
│   │   └── settings.gradle.kts
├── subproject-with-buf
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── proto
│               └── com
│                   └── example
│                       └── constants.proto
├── build.gradle.kts
└── settings.gradle.kts

The build-logic project basically contains 2 Gradle "conventions":

  1. one with convention for Google's Jib Gradle plugin
  2. another with convention for Buf Gradle plugin

Here is the convention for Jib:

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

repositories {
    gradlePluginPortal()
}

dependencies {
    implementation("com.google.cloud.tools:jib-gradle-plugin:3.4.3")
}

// jib-configuration-convention.gradle.kts

plugins {
    id("com.google.cloud.tools.jib")
}

jib {
    from {
        image = "gcr.io/distroless/java21-debian12:debug"
    }
}

and the convention for Buf:

// build.gradle.kts

plugins {
    `kotlin-dsl`
}

repositories {
    gradlePluginPortal()
}

dependencies {
    implementation("com.google.protobuf:protobuf-gradle-plugin:0.9.4")
//    implementation("build.buf:buf-gradle-plugin:0.9.1") // <-- use this version for :subproject-with-buf:publishBufImagePublicationPublicationToLocalArtifactsRepository task
    implementation("build.buf:buf-gradle-plugin:0.10.0")
}

// buf-check-convention.gradle.kts

plugins {
    id("java")
    id("com.google.protobuf")
    id("build.buf")
    id("maven-publish")
}

buf {
    publishSchema = true
    previousVersion = "0.0.1-SNAPSHOT"

    imageArtifact {
        groupId = "com.buf.image"
        artifactId = "app"
        version = "0.0.1-SNAPSHOT"
    }
}

val repoUri = uri("file://${rootProject.projectDir}/gradle/buf-repo")

repositories {
    mavenCentral()
    maven {
        url = repoUri
    }
}

publishing {
    repositories {
        maven {
            url = repoUri
            name = "LocalArtifacts"
        }
    }
}

Important note: the Jib Gradle plugin v3.4.3 relies on Jackson v2.15.2, while Buf Gradle plugin v0.10.0 expects Jackson v2.17.2 in classpath.

The software project itself consists of the root Gradle project + subproject-with-buf Gradle subporoject.

// settings.gradle.kts

rootProject.name = "gradle-overrides-jackson-with-lower-version"

include("subproject-with-buf")

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

The problem

The problem is: if jib-configuration-convention gets applied to the root project and the buf-check-convention is applied to subproject-with-buf, then there is a java.lang.NoSuchMethodError on attempt to execute ./gradlew :subproject-with-buf:writeWorkspaceYaml (as if Buf Gradle plugin encountered Jackson 2.15.2 in classpath while executing writeWorkspaceYaml task).

java.lang.NoSuchMethodError: 'void com.fasterxml.jackson.core.base.GeneratorBase.<init>(int, com.fasterxml.jackson.core.ObjectCodec, com.fasterxml.jackson.core.io.IOContext)'
        at com.fasterxml.jackson.dataformat.yaml.YAMLGenerator.<init>(YAMLGenerator.java:299)
        at com.fasterxml.jackson.dataformat.yaml.YAMLFactory._createGenerator(YAMLFactory.java:532)
        at com.fasterxml.jackson.dataformat.yaml.YAMLFactory.createGenerator(YAMLFactory.java:481)
        at com.fasterxml.jackson.dataformat.yaml.YAMLFactory.createGenerator(YAMLFactory.java:15)
        at com.fasterxml.jackson.databind.ObjectMapper.createGenerator(ObjectMapper.java:1215)
        at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3964)
        at build.buf.gradle.BufYamlGenerator.generate(BufYamlGenerator.kt:43)

Why does the Jib Gradle plugin coming from my custom conventional plugin applied to the root project overrides transitive dependency of Buf plugin in subproject and how to avoid that?

Here is how plugins get applied to Gradle projects:

// build.gradle.kts
plugins {
    id("java")
    id("jib-configuration-convention")
}

// subproject-with-buf/build.gradle.kts

plugins {
    id("buf-check-convention")
}

sourceSets {
    main {
        proto {
            srcDirs("src/main/proto")
        }
    }
}

There is also a minimal .proto source required to reproduce the issue at subproject-with-buf/src/main/proto/com/example/constants.proto:

syntax = "proto3";

package com.example;

enum Boolean {
  FALSE = 0;
  TRUE = 1;
}

Exact steps to reproduce

  1. Recreate the structure from above
  2. Change the version of Buf plugin to v0.9.1 and execute ./gradlew :subproject-with-buf:publishBufImagePublicationPublicationToLocalArtifactsRepository to populate local Maven repo with the reference snapshot (unfortunately Buf plugin v0.10.0 is too eager at resolving dependencies, so this step is needed)
  3. Switch back to Buf plugin v0.10.0 and execute ./gradlew :subproject-with-buf:writeWorkspaceYaml --stacktrace, you will see the java.lang.NoSuchMethodError

Debugging attempts

allprojects {
    tasks.register("printClasspath") {
        doLast {
            buildscript.configurations
                .named("classpath").get()
                .asFileTree
                .filter { it.name.contains("jackson")}
                .forEach { println(it) }
        }
    }
}

Solution

  • The classpath of the root project's build script is put to a classloader.

    This classloader is the parent of the classloader for the sub project's build script.

    And due to usual classloader logic, a classloader first asks its parent classloader for a class before resolving it self.

    So you cannot avoid this overwriting, except by applying the Jib plugin not to the root project, but maybe to a sibling project of the other subproject.

    Or if Jib could also cope with the newer Jackson version, then you could add the Buf plugin with apply false to the root project, as then conflict resolution which is done within one classpath kicks in and selects the newer Jackson version that then both can use.