Почему обычный плагин из корневого проекта переопределяет транзитивную зависимость плагина, примененного к подпроекту?

Вопрос или проблема

Настройка сборки

Рассмотрим следующую настройку проекта:

.
├── 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
├── подмодуль-с-буф
│   ├── build.gradle.kts
│   └── src
│       └── main
│           └── proto
│               └── com
│                   └── example
│                       └── constants.proto
├── build.gradle.kts
└── settings.gradle.kts

Проект build-logic в основном содержит 2 “конвенции” Gradle:

  1. одна конвенция для плагина Jib Gradle от Google
  2. другая конвенция для плагина Buf Gradle

Вот конвенция для 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"
    }
}

и конвенция для 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") // <-- используйте эту версию для задачи :подмодуль-с-буф:publishBufImagePublicationPublicationToLocalArtifactsRepository
    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"
        }
    }
}

Важно отметить: плагин Jib Gradle версии 3.4.3 зависит от Jackson v2.15.2, в то время как плагин Buf Gradle версии 0.10.0 ожидает Jackson v2.17.2 в classpath.

Проблема

Проблема заключается в том, что если jib-configuration-convention применяется к корневому проекту, а buf-check-convention применяется к подмодуль-с-буф, то возникает ошибка java.lang.NoSuchMethodError при попытке выполнить ./gradlew :подмодуль-с-буф:writeWorkspaceYaml (как будто плагин Buf Gradle встретил Jackson 2.15.2 в classpath при выполнении задачи writeWorkspaceYaml).

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)

Почему плагин Jib Gradle из моего пользовательского конвенционного плагина, примененного к корневому проекту, переопределяет транзитивную зависимость плагина Buf в подмодуле, и как избежать этого?

Вот как плагины применяются к проектам Gradle:

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

// подмодуль-с-буф/build.gradle.kts

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

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

Также есть минимальный исходный .proto, необходимый для воспроизведения проблемы по адресу подмодуль-с-буф/src/main/proto/com/example/constants.proto:

syntax = "proto3";

package com.example;

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

Точные шаги для воспроизведения

  1. Воссоздайте структуру из вышеуказанного
  2. Измените версию плагина Buf на v0.9.1 и выполните ./gradlew :подмодуль-с-буф:publishBufImagePublicationPublicationToLocalArtifactsRepository, чтобы заполнить локальный репозиторий Maven референсным снэпшотом (к сожалению, плагин Buf v0.10.0 слишком активно разрешает зависимости, поэтому этот шаг необходим)
  3. Вернитесь к плагину Buf v0.10.0 и выполните ./gradlew :подмодуль-с-буф:writeWorkspaceYaml --stacktrace, вы увидите java.lang.NoSuchMethodError

Попытки отладки

  • Я создал специальную задачу, чтобы увидеть, какая версия Jackson используется в classpath плагинов Gradle в каждом проекте Gradle. Удивительно, но она показывает Jackson v2.17.2 (как и ожидалось), если я выполняю ./gradlew :подмодуль-с-буф:printClasspath:
allprojects {
    tasks.register("printClasspath") {
        doLast {
            buildscript.configurations
                .named("classpath").get()
                .asFileTree
                .filter { it.name.contains("jackson")}
                .forEach { println(it) }
        }
    }
}
  • Еще одно интересное наблюдение: если плагин Jib будет применен напрямую (без пользовательского “конвенционного” плагина), то проблема исчезает.

Ответ или решение

При использовании Gradle и его плагинов, проблема с зависимостями и их версиями может быть довольно сложной и требовать тщательного анализа. В данном случае, причина, по которой обычный плагин из корневого проекта переопределяет транзитивную зависимость плагина, применённого к подпроекту, связана с механизмом разрешения зависимостей и их конфигурациями.

Почему возникает конфликт версий?

Когда вы применяете плагины на уровне корневого проекта (в данном случае, плагин Jib), он добавляет свои зависимости (включая Jackson) в проект. Поскольку плагины могут иметь свои транзитивные зависимости, когда вы применяете плагин Buf в подпроекте, он также может требовать определённые версии библиотек, таких как Jackson.

Плагин Jib (версия 3.4.3) требует Jackson 2.15.2, в то время как плагин Buf (версия 0.10.0) ожидает Jackson 2.17.2. В результате, когда оба плагины активны, и вы пытаетесь выполнить задачу буфера, может возникнуть ситуация, когда версия Jackson, необходимая для Buf, не будет найдена, потому что версия из корневого плагина (Jib) переопределит её.

Механизм разрешения зависимостей

Gradle использует механизм "области видимости" зависимостей, и зависимости, добавленные в корневом проекте, могут влиять на идентификацию версий библиотек в подпроектах. Когда плагин Jib добавляет Jackson 2.15.2 в classpath, он может быть использован и в подпроектах, даже если Buf ожидает другую версию. Если в конечном счёте Buf получает версию 2.15.2 вместо 2.17.2, это вызывает ошибку NoSuchMethodError, так как требуемый метод не доступен в версии Jackson, которая фактически загружена.

Как избежать такой ситуации?

Чтобы избежать этой проблемы, вы можете попробовать следующее:

  1. Зафиксируйте зависимости плагинов: Убедитесь, что версии плагинов, которые вы используете, совместимы. В случае плагинов, которые зависят от одних и тех же библиотек, вы можете попытаться использовать рекомендованные версии.

  2. Используйте конфигурации зависимостей: В корневом проекте попытайтесь создать отдельные конфигурации для зависимостей, используемых в разных проектах. Например:

    configurations {
       create("jib") // Конфигурация для Jib
       create("buf") // Конфигурация для Buf
    }
    
    dependencies {
       "jib"( "com.google.cloud.tools:jib-gradle-plugin:3.4.3" )
       "buf"( "build.buf:buf-gradle-plugin:0.10.0" )
    }
  3. Изолируйте плагины: Если возможно, применяйте плагины на уровне подпроектов вместо корневого проекта. Это позволит каждой конфигурации иметь свою собственную изолированную среду, уменьшив вероятность конфликтов.

  4. Используйте версии с предварительным просмотром: Иногда использование версий плагинов с предварительным просмотром (если они доступны) может помочь смягчить проблемы с совместимостью. Это может повысить шансы на наличие совместимых версий библиотек.

  5. Настраивайте сборку: Если вы контролируете зависимости в своих проектах, вы можете вручную управлять версиями зависимостей, чтобы согласовать их между плагинами, например, используя resolutionStrategy в configurations.

Заключение

Разрешение зависимостей в Gradle может быть сложной задачей, особенно когда дело касается взаимодействия нескольких плагинов. Внимательное управление версиями и зависимостями, а также использование отдельных конфигураций могут помочь избежать конфликтов. Если проблема с версиями по-прежнему возникает, стоит рассмотреть возможность обращения к разработчикам плагинов за помощью или рекомендациями по совместимым версиям.

Оцените материал
Добавить комментарий

Капча загружается...