From 43ca2d2cb01fb7afbef48f92c5220efdbcc130ff Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Tue, 8 Feb 2022 15:57:33 +0000 Subject: [PATCH] Access classpath lazily to allow later changes to be picked up Previously, the classpath of bootJar, bootWar, and bootRun was configured directly as a FileCollection derived from the main source set's runtime classpath. This direct configuration meant that subsequent changes to the main source set's runtime classpath may not have been picked up. This commit changes the configuration of the classpath to use a Callable. This indirection allows subsequent changes to the main source set's runtime classpath to be picked up as long as they occur before Gradle calls the callable. Closes gh-29672 --- .../boot/gradle/plugin/JavaPluginAction.java | 7 ++--- .../gradle/plugin/ResolveMainClassName.java | 17 +++++++++--- .../boot/gradle/plugin/WarPluginAction.java | 6 +++-- .../AbstractBootArchiveIntegrationTests.java | 26 ++++++++++++++++++- .../tasks/run/BootRunIntegrationTests.java | 12 ++++++++- ...ySourceSetCanBeIncludedInTheArchive.gradle | 15 +++++++++++ ...ySourceSetCanBeIncludedInTheArchive.gradle | 15 +++++++++++ ...condarySourceSetCanBeOnTheClasspath.gradle | 15 +++++++++++ 8 files changed, 103 insertions(+), 10 deletions(-) create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle create mode 100644 spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle 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 b698ce9b12..61752d1169 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -20,6 +20,7 @@ import java.io.File; import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.Callable; import org.gradle.api.Action; import org.gradle.api.Plugin; @@ -105,7 +106,7 @@ final class JavaPluginAction implements PluginApplicationAction { .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); Configuration productionRuntimeClasspath = project.getConfigurations() .getByName(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); - FileCollection classpath = mainSourceSet.getRuntimeClasspath() + Callable classpath = () -> mainSourceSet.getRuntimeClasspath() .minus((developmentOnly.minus(productionRuntimeClasspath))).filter(new JarTypeFileSpec()); TaskProvider resolveMainClassName = ResolveMainClassName .registerForTask(SpringBootPlugin.BOOT_JAR_TASK_NAME, project, classpath); @@ -137,7 +138,7 @@ final class JavaPluginAction implements PluginApplicationAction { } private void configureBootRunTask(Project project) { - FileCollection classpath = javaPluginConvention(project).getSourceSets() + Callable classpath = () -> javaPluginConvention(project).getSourceSets() .findByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath().filter(new JarTypeFileSpec()); TaskProvider resolveProvider = ResolveMainClassName.registerForTask("bootRun", project, classpath); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java index 76e668ce87..0628330f53 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/ResolveMainClassName.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -23,6 +23,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardOpenOption; import java.util.Objects; +import java.util.concurrent.Callable; import org.gradle.api.DefaultTask; import org.gradle.api.InvalidUserDataException; @@ -86,7 +87,17 @@ public class ResolveMainClassName extends DefaultTask { * @param classpath the classpath */ public void setClasspath(FileCollection classpath) { - this.classpath = classpath; + setClasspath((Object) classpath); + } + + /** + * Sets the classpath to include in the archive. The given {@code classpath} is + * evaluated as per {@link Project#files(Object...)}. + * @param classpath the classpath + * @since 2.5.9 + */ + public void setClasspath(Object classpath) { + this.classpath = getProject().files(classpath); } /** @@ -142,7 +153,7 @@ public class ResolveMainClassName extends DefaultTask { } static TaskProvider registerForTask(String taskName, Project project, - FileCollection classpath) { + Callable classpath) { TaskProvider resolveMainClassNameProvider = project.getTasks() .register(taskName + "MainClassName", ResolveMainClassName.class, (resolveMainClassName) -> { Convention convention = project.getConvention(); diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java index 5ecc72a23d..7879ae7288 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/main/java/org/springframework/boot/gradle/plugin/WarPluginAction.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -16,6 +16,8 @@ package org.springframework.boot.gradle.plugin; +import java.util.concurrent.Callable; + import org.gradle.api.Action; import org.gradle.api.Plugin; import org.gradle.api.Project; @@ -71,7 +73,7 @@ class WarPluginAction implements PluginApplicationAction { .getByName(SpringBootPlugin.DEVELOPMENT_ONLY_CONFIGURATION_NAME); Configuration productionRuntimeClasspath = project.getConfigurations() .getByName(SpringBootPlugin.PRODUCTION_RUNTIME_CLASSPATH_CONFIGURATION_NAME); - FileCollection classpath = project.getConvention().getByType(SourceSetContainer.class) + Callable classpath = () -> project.getConvention().getByType(SourceSetContainer.class) .getByName(SourceSet.MAIN_SOURCE_SET_NAME).getRuntimeClasspath() .minus(providedRuntimeConfiguration(project)).minus((developmentOnly.minus(productionRuntimeClasspath))) .filter(new JarTypeFileSpec()); 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 9e50befd75..d1c82787fb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -462,6 +462,30 @@ abstract class AbstractBootArchiveIntegrationTests { assertExtractedLayers(layerNames, indexedLayers); } + @TestTemplate + void classesFromASecondarySourceSetCanBeIncludedInTheArchive() throws IOException { + writeMainClass(); + File examplePackage = new File(this.gradleBuild.getProjectDir(), "src/secondary/java/example"); + examplePackage.mkdirs(); + File main = new File(examplePackage, "Secondary.java"); + try (PrintWriter writer = new PrintWriter(new FileWriter(main))) { + writer.println("package example;"); + writer.println(); + writer.println("public class Secondary {}"); + } + catch (IOException ex) { + throw new RuntimeException(ex); + } + BuildResult build = this.gradleBuild.build(this.taskName); + assertThat(build.task(":" + this.taskName).getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + try (JarFile jarFile = new JarFile(new File(this.gradleBuild.getProjectDir(), "build/libs").listFiles()[0])) { + Stream classesEntryNames = jarFile.stream().filter((entry) -> !entry.isDirectory()) + .map(JarEntry::getName).filter((name) -> name.startsWith(this.classesPath)); + assertThat(classesEntryNames).containsExactly(this.classesPath + "example/Main.class", + this.classesPath + "example/Secondary.class"); + } + } + private void copyMainClassApplication() throws IOException { copyApplication("main"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java index 67590476fa..ac753ed56b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/java/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -138,6 +138,16 @@ class BootRunIntegrationTests { assertThat(result.getOutput()).contains("standard.jar").doesNotContain("starter.jar"); } + @TestTemplate + void classesFromASecondarySourceSetCanBeOnTheClasspath() throws IOException { + File output = new File(this.gradleBuild.getProjectDir(), "src/secondary/java/com/example/bootrun/main"); + output.mkdirs(); + FileSystemUtils.copyRecursively(new File("src/test/java/com/example/bootrun/main"), output); + BuildResult result = this.gradleBuild.build("bootRun"); + assertThat(result.task(":bootRun").getOutcome()).isEqualTo(TaskOutcome.SUCCESS); + assertThat(result.getOutput()).contains("com.example.bootrun.main.CustomMainClass"); + } + private void copyMainClassApplication() throws IOException { copyApplication("main"); } diff --git a/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle new file mode 100644 index 0000000000..eeca788d36 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootJarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + secondary + main { + runtimeClasspath += secondary.output + } +} + +bootJar { + 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-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle new file mode 100644 index 0000000000..e107dfd956 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/bundling/BootWarIntegrationTests-classesFromASecondarySourceSetCanBeIncludedInTheArchive.gradle @@ -0,0 +1,15 @@ +plugins { + id 'war' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + secondary + main { + runtimeClasspath += secondary.output + } +} + +bootWar { + 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/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle new file mode 100644 index 0000000000..315eb8a0b8 --- /dev/null +++ b/spring-boot-project/spring-boot-tools/spring-boot-gradle-plugin/src/test/resources/org/springframework/boot/gradle/tasks/run/BootRunIntegrationTests-classesFromASecondarySourceSetCanBeOnTheClasspath.gradle @@ -0,0 +1,15 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '{version}' +} + +sourceSets { + secondary + main { + runtimeClasspath += secondary.output + } +} + +springBoot { + mainClass = 'com.example.bootrun.main.CustomMainClass' +}