From bdee9c9b19405963c32cb2c24d3187d48e058c0b Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Mon, 23 Jun 2025 09:09:24 +0100 Subject: [PATCH] Ensure consistent versions between dev and production classpaths Prior to this change, versions in the dev and production classpaths could differ. These differing versions could result in a transitive dependency that should have been present in both development and production only being present in the former. This would likely result in failures at runtime. This commit aligns the versions by adding constraints to the production runtime classpath for each dependency in the runtime classpath. Closes gh-46043 --- .../boot/gradle/plugin/JavaPluginAction.java | 25 +++++++++++++++++++ .../AbstractBootArchiveIntegrationTests.java | 15 +++++++++++ ...esNotRemoveDependencyFromTheArchive.gradle | 21 ++++++++++++++++ ...esNotRemoveDependencyFromTheArchive.gradle | 21 ++++++++++++++++ .../commons-io-consumer/one/1.0/one-1.0.pom | 15 +++++++++++ .../commons-io-consumer/two/1.0/two-1.0.pom | 15 +++++++++++ 6 files changed, 112 insertions(+) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-versionMismatchBetweenTransitiveDevelopmentOnlyImplementationDependenciesDoesNotRemoveDependencyFromTheArchive.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/one/1.0/one-1.0.pom create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/repository/commons-io-consumer/two/1.0/two-1.0.pom 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 5d5edd9dcb1..e6cc7b41c4d 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 @@ -232,6 +232,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 + + +