spring-gradle-plugins / dependency-management-plugin

A Gradle plugin that provides Maven-like dependency management functionality
684 stars 85 forks source link

`dependencySubstitution` on parent project is not getting applied on child project when plugin is applied #347

Closed SayakMukhopadhyay closed 1 year ago

SayakMukhopadhyay commented 1 year ago

I have a multi-project layout which uses git submodules such that developers can checkout the sub projects as standalone git repos and work on them. For eg. I have Parent gradle project which has Child1 and Child2 as sub projects.

Let me discuss the reasoning for that so that the context is laid out.

If Child1 depends on Child2 as a library, I have the Child2 dependency added in Child1 as a maven repo (implementation com.example:child2:1.0.0) so that a developer checking out only Child1 doesn't have broken dependencies. At the same time, I want people checking out Parent to be able to use the Child2 dependency as a project (implementation project(:child2)). To implement this, I use dependency substitution in Parent only like below

allprojects {
    configurations.all {
        resolutionStrategy {
            dependencySubstitution {
                substitute module('com.example:child2') with project(':child2')
            }
        }
    }
}

The above substitution is not working when Child1 has the dependency management plugin active

plugins {
    id 'java'
    id 'eclipse'
    id 'idea'
    id 'org.springframework.boot' version '2.7.5'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

The substitution happens as soon as the dependency management plugin is removed. The substitution also works if I use the substitution code in Child1 itself like

configurations.all {
    resolutionStrategy {
        dependencySubstitution {
            substitute module('com.example:child2') with project(':child2')
        }
    }
}

but that's not something that I am looking to do here.

I am able to reproduce this consistently and have created a repo with a simple reproduction (here I am replacing joda-time with the library project in the consumer, both of which are present in the dep-sub-test project, as I need an existing maven package to demonstrate`)

To reproduce:

Pull the project as is and run .\gradlew.bat :consumer:dependencies --configuration compileClasspath to see the dependency tree. The output will have

compileClasspath - Compile classpath for source set 'main'.
+--- org.springframework.boot:spring-boot-starter-web -> 2.7.5
|    +--- org.springframework.boot:spring-boot-starter:2.7.5
|    ...spring stuff
\--- joda-time:joda-time:2.12.0

Comment out the lines id 'io.spring.dependency-management' version '1.0.15.RELEASE' and implementation 'org.springframework.boot:spring-boot-starter-web' and run the above command once again. The output will have

compileClasspath - Compile classpath for source set 'main'.
\--- joda-time:joda-time:2.12.0 -> project :library

NOTE: the org.springframework.boot:spring-boot-starter-web dependency is only there to ensure that there is no corner case that is missed when the plugin is added but no spring dependencies are given.

wilkinsona commented 1 year ago

Thanks for the sample and detailed description of the problem. The cause appears to be related to ordering. If the dependency substitution is configured after the dependency management plugin has been applied, the problem does not occur. This is why it worked when you configured the substitution in the child project. It will also work if you configure it in the root project and react to the dependency management plugin being applied:

allprojects {
    plugins.withId('io.spring.dependency-management') {
        configurations.all {
           resolutionStrategy {
                dependencySubstitution {
                    substitute module('joda-time:joda-time') with project(':library')
                }
            }
        }
    }
}

I'm not sure what, if anything, the dependency management plugin can do about the problem. It'll require some further investigation.

wilkinsona commented 1 year ago

It would appear that Gradle loses the dependency substitution when another resolution strategy configures a version. For example, the following modified consumer project shows the same problem:

plugins {
    id 'java'
}

group = 'com.example'
version = '1.0.0'
sourceCompatibility = 11

repositories {
    mavenCentral()
}

dependencies {
    implementation 'joda-time:joda-time:2.12.0'
}

configurations.all {
    resolutionStrategy {
        eachDependency {
            it.useVersion('2.12.0')
        }
    }
}

The dependency management plugin does the equivalent of this itself so that a version that's specified in a dependency declaration overrides the version from any dependency management that has been configured. You can disable this behavior:

dependencyManagement {
    overriddenByDependencies = false
}

The problem does not occur with this in place. I don't think there's anything more that can be done in the dependency management plugin. Ideally, Gradle would not lose the dependency substitution when another resolution strategy configures a version but that is out of the dependency management plugin's control. If none of the approaches above are suitable, you may want to raise a Gradle issue to see what can be done.

SayakMukhopadhyay commented 1 year ago

Thank you for the detailed explanation. This would be helpful while discussing with the Gradle folks.

EDIT: I have raised an issue at https://github.com/gradle/gradle/issues/23509