From 6a432fc79e4fc872de34195cc93023bfbdc8b6d3 Mon Sep 17 00:00:00 2001 From: John Burns Date: Fri, 28 Aug 2026 14:48:41 -0500 Subject: [PATCH] copy configuration attributes from runtimeClasspath to developmentOnly and testAndDevelopmentOnly fixes gh-51492 include refactors in the modified code to better comply with Gradle lazy APIs Signed-off-by: John Burns --- .../boot/gradle/plugin/JavaPluginAction.java | 86 ++++++++++++------- .../JavaPluginActionIntegrationTests.java | 18 ++++ ...AttributesThatMatchRuntimeClasspath.gradle | 38 ++++++++ ...AttributesThatMatchRuntimeClasspath.gradle | 38 ++++++++ 4 files changed, 147 insertions(+), 33 deletions(-) create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-developmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle create mode 100644 build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testAndDevelopmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle diff --git a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java index a78263c8b8da..b0b61f202e8f 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java +++ b/build-plugin/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/JavaPluginAction.java @@ -36,7 +36,6 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.plugins.JavaPluginExtension; import org.gradle.api.provider.Provider; -import org.gradle.api.provider.ProviderFactory; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskProvider; @@ -44,6 +43,8 @@ import org.gradle.api.tasks.compile.JavaCompile; import org.gradle.jvm.toolchain.JavaToolchainService; import org.gradle.jvm.toolchain.JavaToolchainSpec; +import org.gradle.util.GradleVersion; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; import org.springframework.boot.gradle.dsl.SpringBootExtension; @@ -281,48 +282,67 @@ private void configureAdditionalMetadataLocations(JavaCompile compile) { .ifPresent((locations) -> compile.doFirst(new AdditionalMetadataLocationsConfigurer(locations))); } - @SuppressWarnings({ "rawtypes", "unchecked" }) + /** + * In Gradle < 9.1.0, we don't have access to {@link AttributeContainer#addAllLater}, + * so we will just copy the attributes individually. + * @param from configuration from which to copy attributes + * @param to configuration to which attributes will be copied + */ + @SuppressWarnings({ "rawtypes", "unchecked", "NullAway" }) + private static void copyAttributes( + @NonNull Provider from, + @NonNull Configuration to) { + if (GradleVersion.current().compareTo(GradleVersion.version("9.1.0")) < 0) { + to.attributes((attributes) -> { + AttributeContainer sourceAttributes = from.get().getAttributes(); + for (Attribute attribute : sourceAttributes.keySet()) { + attributes.attributeProvider(attribute, + from.map((source) -> source.getAttributes().getAttribute(attribute))); + } + }); + } + else { + to.getAttributes().addAllLater(from.map(Configuration::getAttributes).get()); + } + } + private void configureProductionRuntimeClasspathConfiguration(Project project) { - Configuration productionRuntimeClasspath = project.getConfigurations() - .create(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); - Configuration runtimeClasspath = project.getConfigurations() - .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); - productionRuntimeClasspath.attributes((attributes) -> { - ProviderFactory providers = project.getProviders(); - AttributeContainer sourceAttributes = runtimeClasspath.getAttributes(); - for (Attribute attribute : sourceAttributes.keySet()) { - attributes.attributeProvider(attribute, - providers.provider(() -> sourceAttributes.getAttribute(attribute))); - } + Provider runtimeClasspath = project.getConfigurations() + .named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + project.getConfigurations().create(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME, (prc) -> { + copyAttributes(runtimeClasspath, prc); + prc.setExtendsFrom(runtimeClasspath.get().getExtendsFrom()); + prc.setCanBeResolved(runtimeClasspath.get().isCanBeResolved()); + prc.setCanBeConsumed(runtimeClasspath.get().isCanBeConsumed()); + prc.shouldResolveConsistentlyWith(runtimeClasspath.get()); }); - productionRuntimeClasspath.setExtendsFrom(runtimeClasspath.getExtendsFrom()); - productionRuntimeClasspath.setCanBeResolved(runtimeClasspath.isCanBeResolved()); - productionRuntimeClasspath.setCanBeConsumed(runtimeClasspath.isCanBeConsumed()); - productionRuntimeClasspath.shouldResolveConsistentlyWith(runtimeClasspath); } private void configureDevelopmentOnlyConfiguration(Project project) { - Configuration developmentOnly = project.getConfigurations() - .create(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); - developmentOnly - .setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools."); - Configuration runtimeClasspath = project.getConfigurations() - .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + final var runtimeClasspath = project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); + final var developmentOnly = project.getConfigurations() + .create(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME, (dev) -> { + dev.setDescription("Configuration for development-only dependencies such as Spring Boot's DevTools."); + dev.setCanBeConsumed(false); + copyAttributes(runtimeClasspath, dev); + }); + runtimeClasspath.configure((r) -> r.extendsFrom(developmentOnly)); - runtimeClasspath.extendsFrom(developmentOnly); } private void configureTestAndDevelopmentOnlyConfiguration(Project project) { + final var runtimeClasspath = project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); Configuration testAndDevelopmentOnly = project.getConfigurations() - .create(SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME); - testAndDevelopmentOnly - .setDescription("Configuration for test and development-only dependencies such as Spring Boot's DevTools."); - Configuration runtimeClasspath = project.getConfigurations() - .getByName(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME); - runtimeClasspath.extendsFrom(testAndDevelopmentOnly); - Configuration testImplementation = project.getConfigurations() - .getByName(JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME); - testImplementation.extendsFrom(testAndDevelopmentOnly); + .create(SpringBootPlugin.TEST_AND_DEVELOPMENT_ONLY_CONFIGURATION_NAME, (tado) -> { + tado.setDescription( + "Configuration for test and development-only dependencies such as Spring Boot's DevTools."); + tado.setCanBeConsumed(false); + copyAttributes(runtimeClasspath, tado); + }); + runtimeClasspath.configure((it) -> it.extendsFrom(testAndDevelopmentOnly)); + project.getConfigurations() + .named(JavaPlugin.TEST_IMPLEMENTATION_CONFIGURATION_NAME) + .configure((testImplementation) -> testImplementation.extendsFrom(testAndDevelopmentOnly)); } private void configureSpringBootStarterTestToDependOnJUnitPlatformLauncher(Project project) { diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java index 826797565bb3..8816d8c23573 100644 --- a/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java +++ b/build-plugin/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests.java @@ -213,6 +213,24 @@ void productionRuntimeClasspathIsConfiguredWithAttributesThatMatchRuntimeClasspa assertThat(output).contains("productionRuntimeClasspath: " + attributes); } + @TestTemplate + void developmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath() { + String output = this.gradleBuild.build("build").getOutput(); + Matcher matcher = Pattern.compile("runtimeClasspath: (\\[.*])").matcher(output); + assertThat(matcher.find()).as("%s found in %s", matcher, output).isTrue(); + String attributes = matcher.group(1); + assertThat(output).contains("developmentOnly: " + attributes); + } + + @TestTemplate + void testAndDevelopmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath() { + String output = this.gradleBuild.build("build").getOutput(); + Matcher matcher = Pattern.compile("runtimeClasspath: (\\[.*])").matcher(output); + assertThat(matcher.find()).as("%s found in %s", matcher, output).isTrue(); + String attributes = matcher.group(1); + assertThat(output).contains("testAndDevelopmentOnly: " + attributes); + } + @TestTemplate void productionRuntimeClasspathIsConfiguredWithResolvabilityAndConsumabilityThatMatchesRuntimeClasspath() { String output = this.gradleBuild.build("build").getOutput(); diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-developmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-developmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle new file mode 100644 index 000000000000..b92efe3679c1 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-developmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def collectAttributes(String configurationName) { + def attributes = configurations.findByName(configurationName).attributes + def keys = new TreeSet<>((a1, a2) -> a1.name.compareTo(a2.name)) + keys.addAll(attributes.keySet()) + keys.collect { key -> "${key}: ${attributes.getAttribute(key)}" } +} + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +gradle.taskGraph.whenReady { + def runtimeClasspathAttributes = collectAttributes("runtimeClasspath") + def developmentOnlyAttributes = collectAttributes("developmentOnly") + println("runtimeClasspath: ${runtimeClasspathAttributes}") + println("developmentOnly: ${developmentOnlyAttributes}") +} \ No newline at end of file diff --git a/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testAndDevelopmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testAndDevelopmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle new file mode 100644 index 000000000000..fed71c5c5ea6 --- /dev/null +++ b/build-plugin/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/plugin/JavaPluginActionIntegrationTests-testAndDevelopmentOnlyIsConfiguredWithAttributesThatMatchRuntimeClasspath.gradle @@ -0,0 +1,38 @@ +/* + * Copyright 2012-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +def collectAttributes(String configurationName) { + def attributes = configurations.findByName(configurationName).attributes + def keys = new TreeSet<>((a1, a2) -> a1.name.compareTo(a2.name)) + keys.addAll(attributes.keySet()) + keys.collect { key -> "${key}: ${attributes.getAttribute(key)}" } +} + +plugins { + id 'org.springframework.boot' version '{version}' + id 'java' +} + +springBoot { + mainClass = "com.example.Main" +} + +gradle.taskGraph.whenReady { + def runtimeClasspathAttributes = collectAttributes("runtimeClasspath") + def testAndDevelopmentOnlyAttributes = collectAttributes("testAndDevelopmentOnly") + println("runtimeClasspath: ${runtimeClasspathAttributes}") + println("testAndDevelopmentOnly: ${testAndDevelopmentOnlyAttributes}") +} \ No newline at end of file