diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index 3bb108610bc..9a90b95d244 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -20,12 +20,17 @@ import java.io.File; import java.util.List; import java.util.Set; import java.util.concurrent.Callable; +import java.util.stream.Stream; import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; import org.gradle.api.Task; import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.DependencyConstraint; +import org.gradle.api.artifacts.component.ModuleComponentIdentifier; +import org.gradle.api.artifacts.dsl.DependencyConstraintHandler; +import org.gradle.api.artifacts.result.ResolvedArtifactResult; import org.gradle.api.attributes.Attribute; import org.gradle.api.attributes.AttributeContainer; import org.gradle.api.file.FileCollection; @@ -292,6 +297,26 @@ final class JavaPluginAction implements PluginApplicationAction { productionRuntimeClasspath.setExtendsFrom(runtimeClasspath.getExtendsFrom()); productionRuntimeClasspath.setCanBeResolved(runtimeClasspath.isCanBeResolved()); productionRuntimeClasspath.setCanBeConsumed(runtimeClasspath.isCanBeConsumed()); + productionRuntimeClasspath.getDependencyConstraints() + .addAllLater(project.getProviders().provider(() -> constraintsFrom(runtimeClasspath, project))); + } + + private Iterable constraintsFrom(Configuration configuration, Project project) { + DependencyConstraintHandler constraints = project.getDependencies().getConstraints(); + return resolvedArtifactsOf(configuration).map((artifact) -> artifact.getId().getComponentIdentifier()) + .filter(ModuleComponentIdentifier.class::isInstance) + .map(ModuleComponentIdentifier.class::cast) + .map(this::asConstraintNotation) + .map(constraints::create) + .toList(); + } + + private Stream resolvedArtifactsOf(Configuration configuration) { + return configuration.getIncoming().getArtifacts().getArtifacts().stream(); + } + + private String asConstraintNotation(ModuleComponentIdentifier identifier) { + return "%s:%s:%s".formatted(identifier.getGroup(), identifier.getModule(), identifier.getVersion()); } private void configureDevelopmentOnlyConfiguration(Project project) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java index a4625822d1e..fc8d61d98a3 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/bundling/AbstractBootArchiveIntegrationTests.java @@ -233,6 +233,21 @@ abstract class AbstractBootArchiveIntegrationTests { } } + @TestTemplate + void versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive() + throws IOException { + assertThat(this.gradleBuild.build(this.taskName).task(":" + this.taskName).getOutcome()) + .isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream libEntryNames = jarFile.stream() + .filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName) + .filter((name) -> name.startsWith(this.libPath)); + assertThat(libEntryNames).containsExactly(this.libPath + "two-1.0.jar", + this.libPath + "commons-io-2.19.0.jar"); + } + } + @TestTemplate void testAndDevelopmentOnlyDependenciesAreNotIncludedInTheArchiveByDefault() throws IOException { File srcMainResources = new File(this.gradleBuild.getProjectDir(), "src/main/resources"); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle new file mode 100644 index 00000000000..c0cb4751337 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle @@ -0,0 +1,21 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() + maven { + url = 'repository' + } +} + +dependencies { + developmentOnly("commons-io-consumer:one:1.0") + implementation("commons-io-consumer:two:1.0") +} + +bootJar { + includeTools = false + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle new file mode 100644 index 00000000000..26281050523 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle @@ -0,0 +1,21 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +repositories { + mavenCentral() + maven { + url = 'repository' + } +} + +dependencies { + developmentOnly("commons-io-consumer:one:1.0") + implementation("commons-io-consumer:two:1.0") +} + +bootWar { + includeTools = false + mainClass = 'com.example.Application' +} diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/one/1.0/one-1.0.pom b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/one/1.0/one-1.0.pom new file mode 100644 index 00000000000..dadd816e115 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/one/1.0/one-1.0.pom @@ -0,0 +1,15 @@ + + + 4.0.0 + commons-io-consumer + one + 1.0 + + + commons-io + commons-io + 2.19.0 + + + diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/two/1.0/two-1.0.pom b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/two/1.0/two-1.0.pom new file mode 100644 index 00000000000..d4ffdd021b8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/two/1.0/two-1.0.pom @@ -0,0 +1,15 @@ + + + 4.0.0 + commons-io-consumer + two + 1.0 + + + commons-io + commons-io + 2.18.0 + + +