diff --git a/build.gradle b/build.gradle index f27689e29cc4..d523be91131e 100644 --- a/build.gradle +++ b/build.gradle @@ -3,12 +3,9 @@ plugins { // kotlinVersion is managed in gradle.properties id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.dokka' version '1.9.20' - id 'com.github.ben-manes.versions' version '0.51.0' id 'com.github.bjornvester.xjc' version '1.8.2' apply false - id 'de.undercouch.download' version '5.4.0' id 'io.github.goooler.shadow' version '8.1.8' apply false id 'me.champeau.jmh' version '0.7.2' apply false - id 'me.champeau.mrjar' version '0.1.1' id "net.ltgt.errorprone" version "4.1.0" apply false } @@ -64,7 +61,6 @@ configure([rootProject] + javaProjects) { project -> apply plugin: "java" apply plugin: "java-test-fixtures" apply plugin: 'org.springframework.build.conventions' - apply from: "${rootDir}/gradle/toolchains.gradle" apply from: "${rootDir}/gradle/ide.gradle" dependencies { diff --git a/buildSrc/README.md b/buildSrc/README.md index a9fcab370551..3cf8ac690bd3 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -33,6 +33,25 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. +### MultiRelease Jar + +The `org.springframework.build.multiReleaseJar` plugin configures the project with MultiRelease JAR support. +It creates a new SourceSet and dedicated tasks for each Java variant considered. +This can be configured with the DSL, by setting a list of Java variants to configure: + +```groovy +plugins { + id 'org.springframework.build.multiReleaseJar' +} + +multiRelease { + releaseVersions 21, 24 +} +``` + +Note, Java classes will be compiled with the toolchain pre-configured by the project, assuming that its +Java language version is equal or higher than all variants we consider. Each compilation task will only +set the "-release" compilation option accordingly to produce the expected bytecode version. ### RuntimeHints Java Agent diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index dadcb07c4c88..5bfe6021bf74 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,10 +20,13 @@ ext { dependencies { checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" - implementation "com.tngtech.archunit:archunit:1.3.0" - implementation "org.gradle:test-retry-gradle-plugin:1.5.6" + implementation "com.tngtech.archunit:archunit:1.4.0" + implementation "org.gradle:test-retry-gradle-plugin:1.6.2" implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" implementation "io.spring.nohttp:nohttp-gradle:0.0.11" + + testImplementation("org.assertj:assertj-core:${assertjVersion}") + testImplementation("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}") } gradlePlugin { @@ -40,6 +43,10 @@ gradlePlugin { id = "org.springframework.build.localdev" implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin" } + multiReleasePlugin { + id = "org.springframework.build.multiReleaseJar" + implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin" + } optionalDependenciesPlugin { id = "org.springframework.build.optional-dependencies" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" @@ -50,3 +57,9 @@ gradlePlugin { } } } + +test { + useJUnitPlatform() +} + +jar.dependsOn check diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index eacaf4f5b871..48dbb008b69f 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1,2 +1,4 @@ org.gradle.caching=true javaFormatVersion=0.0.42 +junitJupiterVersion=5.11.4 +assertjVersion=3.27.3 \ No newline at end of file diff --git a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java index 54aaf5128dfd..feda5aa59aa5 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -64,7 +64,7 @@ private static void configureNoHttpPlugin(Project project) { NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class); noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines")); noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", - "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**"); + "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**", "buildSrc/**"); List buildFolders = List.of("bin", "build", "out"); project.allprojects(subproject -> { Path rootPath = project.getRootDir().toPath(); diff --git a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java index 4ffd6916c1f6..f0074f64791d 100644 --- a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -17,7 +17,6 @@ package org.springframework.build; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.gradle.api.Plugin; @@ -42,8 +41,18 @@ public class JavaConventions { private static final List TEST_COMPILER_ARGS; + /** + * The Java version we should use as the JVM baseline for building the project + */ + private static final JavaLanguageVersion DEFAULT_LANGUAGE_VERSION = JavaLanguageVersion.of(23); + + /** + * The Java version we should use as the baseline for the compiled bytecode (the "-release" compiler argument) + */ + private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17); + static { - List commonCompilerArgs = Arrays.asList( + List commonCompilerArgs = List.of( "-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann", "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides", "-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options", @@ -51,37 +60,45 @@ public class JavaConventions { ); COMPILER_ARGS = new ArrayList<>(); COMPILER_ARGS.addAll(commonCompilerArgs); - COMPILER_ARGS.addAll(Arrays.asList( + COMPILER_ARGS.addAll(List.of( "-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation", "-Xlint:unchecked", "-Werror" )); TEST_COMPILER_ARGS = new ArrayList<>(); TEST_COMPILER_ARGS.addAll(commonCompilerArgs); - TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes", + TEST_COMPILER_ARGS.addAll(List.of("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes", "-Xlint:-deprecation", "-Xlint:-unchecked")); } public void apply(Project project) { - project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project)); + project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> { + applyToolchainConventions(project); + applyJavaCompileConventions(project); + }); } /** - * Applies the common Java compiler options for main sources, test fixture sources, and - * test sources. + * Configure the Toolchain support for the project. * @param project the current project */ - private void applyJavaCompileConventions(Project project) { + private static void applyToolchainConventions(Project project) { project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> { toolchain.getVendor().set(JvmVendorSpec.BELLSOFT); - toolchain.getLanguageVersion().set(JavaLanguageVersion.of(23)); + toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION); }); - SpringFrameworkExtension frameworkExtension = project.getExtensions().getByType(SpringFrameworkExtension.class); + } + + /** + * Apply the common Java compiler options for main sources, test fixture sources, and + * test sources. + * @param project the current project + */ + private void applyJavaCompileConventions(Project project) { project.afterEvaluate(p -> { p.getTasks().withType(JavaCompile.class) .matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME)) .forEach(compileTask -> { compileTask.getOptions().setCompilerArgs(COMPILER_ARGS); - compileTask.getOptions().getCompilerArgumentProviders().add(frameworkExtension.asArgumentProvider()); compileTask.getOptions().setEncoding("UTF-8"); setJavaRelease(compileTask); }); @@ -90,7 +107,6 @@ private void applyJavaCompileConventions(Project project) { || compileTask.getName().equals("compileTestFixturesJava")) .forEach(compileTask -> { compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS); - compileTask.getOptions().getCompilerArgumentProviders().add(frameworkExtension.asArgumentProvider()); compileTask.getOptions().setEncoding("UTF-8"); setJavaRelease(compileTask); }); @@ -98,8 +114,12 @@ private void applyJavaCompileConventions(Project project) { }); } + /** + * We should pick the {@link #DEFAULT_RELEASE_VERSION} for all compiled classes, + * unless the current task is compiling multi-release JAR code with a higher version. + */ private void setJavaRelease(JavaCompile task) { - int defaultVersion = 17; + int defaultVersion = DEFAULT_RELEASE_VERSION.asInt(); int releaseVersion = defaultVersion; int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt(); for (int version = defaultVersion ; version <= compilerVersion ; version++) { diff --git a/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java b/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java index 78dae151e45d..c4001388eb05 100644 --- a/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java +++ b/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 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. @@ -21,6 +21,8 @@ import org.gradle.api.Project; import org.gradle.api.provider.Property; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.api.tasks.testing.Test; import org.gradle.process.CommandLineArgumentProvider; public class SpringFrameworkExtension { @@ -29,13 +31,18 @@ public class SpringFrameworkExtension { public SpringFrameworkExtension(Project project) { this.enableJavaPreviewFeatures = project.getObjects().property(Boolean.class); + project.getTasks().withType(JavaCompile.class).configureEach(javaCompile -> + javaCompile.getOptions().getCompilerArgumentProviders().add(asArgumentProvider())); + project.getTasks().withType(Test.class).configureEach(test -> + test.getJvmArgumentProviders().add(asArgumentProvider())); + } public Property getEnableJavaPreviewFeatures() { return this.enableJavaPreviewFeatures; } - public CommandLineArgumentProvider asArgumentProvider() { + private CommandLineArgumentProvider asArgumentProvider() { return () -> { if (getEnableJavaPreviewFeatures().getOrElse(false)) { return List.of("--enable-preview"); diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java index 0f1bd09ead27..baa741781c4f 100644 --- a/buildSrc/src/main/java/org/springframework/build/TestConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/TestConventions.java @@ -64,8 +64,6 @@ private void configureTests(Project project, Test test) { "--add-opens=java.base/java.util=ALL-UNNAMED", "-Xshare:off" ); - test.getJvmArgumentProviders().add(project.getExtensions() - .getByType(SpringFrameworkExtension.class).asArgumentProvider()); } private void configureTestRetryPlugin(Project project, Test test) { diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java new file mode 100644 index 000000000000..547c3f480de4 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2025 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. + */ + +package org.springframework.build.multirelease; + +import javax.inject.Inject; + +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.java.archives.Attributes; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.api.tasks.testing.Test; +import org.gradle.language.base.plugins.LifecycleBasePlugin; + +/** + * @author Cedric Champeau + * @author Brian Clozel + */ +public abstract class MultiReleaseExtension { + private final TaskContainer tasks; + private final SourceSetContainer sourceSets; + private final DependencyHandler dependencies; + private final ObjectFactory objects; + private final ConfigurationContainer configurations; + + @Inject + public MultiReleaseExtension(SourceSetContainer sourceSets, + ConfigurationContainer configurations, + TaskContainer tasks, + DependencyHandler dependencies, + ObjectFactory objectFactory) { + this.sourceSets = sourceSets; + this.configurations = configurations; + this.tasks = tasks; + this.dependencies = dependencies; + this.objects = objectFactory; + } + + public void releaseVersions(int... javaVersions) { + releaseVersions("src/main/", "src/test/", javaVersions); + } + + private void releaseVersions(String mainSourceDirectory, String testSourceDirectory, int... javaVersions) { + for (int javaVersion : javaVersions) { + addLanguageVersion(javaVersion, mainSourceDirectory, testSourceDirectory); + } + } + + private void addLanguageVersion(int javaVersion, String mainSourceDirectory, String testSourceDirectory) { + String javaN = "java" + javaVersion; + + SourceSet langSourceSet = sourceSets.create(javaN, srcSet -> srcSet.getJava().srcDir(mainSourceDirectory + javaN)); + SourceSet testSourceSet = sourceSets.create(javaN + "Test", srcSet -> srcSet.getJava().srcDir(testSourceDirectory + javaN)); + SourceSet sharedSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME); + SourceSet sharedTestSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME); + + FileCollection mainClasses = objects.fileCollection().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs()); + dependencies.add(javaN + "Implementation", mainClasses); + + tasks.named(langSourceSet.getCompileJavaTaskName(), JavaCompile.class, task -> + task.getOptions().getRelease().set(javaVersion) + ); + tasks.named(testSourceSet.getCompileJavaTaskName(), JavaCompile.class, task -> + task.getOptions().getRelease().set(javaVersion) + ); + + TaskProvider testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet); + tasks.named("check", task -> task.dependsOn(testTask)); + + configureMultiReleaseJar(javaVersion, langSourceSet); + } + + private TaskProvider createTestTask(int javaVersion, SourceSet testSourceSet, SourceSet sharedTestSourceSet, SourceSet langSourceSet, SourceSet sharedSourceSet) { + Configuration testImplementation = configurations.getByName(testSourceSet.getImplementationConfigurationName()); + testImplementation.extendsFrom(configurations.getByName(sharedTestSourceSet.getImplementationConfigurationName())); + Configuration testCompileOnly = configurations.getByName(testSourceSet.getCompileOnlyConfigurationName()); + testCompileOnly.extendsFrom(configurations.getByName(sharedTestSourceSet.getCompileOnlyConfigurationName())); + testCompileOnly.getDependencies().add(dependencies.create(langSourceSet.getOutput().getClassesDirs())); + testCompileOnly.getDependencies().add(dependencies.create(sharedSourceSet.getOutput().getClassesDirs())); + + Configuration testRuntimeClasspath = configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName()); + // so here's the deal. MRjars are JARs! Which means that to execute tests, we need + // the JAR on classpath, not just classes + resources as Gradle usually does + testRuntimeClasspath.getAttributes() + .attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); + + TaskProvider testTask = tasks.register("java" + javaVersion + "Test", Test.class, test -> { + test.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); + + ConfigurableFileCollection testClassesDirs = objects.fileCollection(); + testClassesDirs.from(testSourceSet.getOutput()); + testClassesDirs.from(sharedTestSourceSet.getOutput()); + test.setTestClassesDirs(testClassesDirs); + ConfigurableFileCollection classpath = objects.fileCollection(); + // must put the MRJar first on classpath + classpath.from(tasks.named("jar")); + // then we put the specific test sourceset tests, so that we can override + // the shared versions + classpath.from(testSourceSet.getOutput()); + + // then we add the shared tests + classpath.from(sharedTestSourceSet.getRuntimeClasspath()); + test.setClasspath(classpath); + }); + return testTask; + } + + private void configureMultiReleaseJar(int version, SourceSet languageSourceSet) { + tasks.named("jar", Jar.class, jar -> { + jar.into("META-INF/versions/" + version, s -> s.from(languageSourceSet.getOutput())); + Attributes attributes = jar.getManifest().getAttributes(); + attributes.put("Multi-Release", "true"); + }); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java new file mode 100644 index 000000000000..1716d016b285 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2025 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. + */ + +package org.springframework.build.multirelease; + +import javax.inject.Inject; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.ExtensionContainer; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.jvm.toolchain.JavaToolchainService; + +/** + * A plugin which adds support for building multi-release jars + * with Gradle. + * @author Cedric Champeau + * @author Brian Clozel + * @see original project + */ +public class MultiReleaseJarPlugin implements Plugin { + + @Inject + protected JavaToolchainService getToolchains() { + throw new UnsupportedOperationException(); + } + + public void apply(Project project) { + project.getPlugins().apply(JavaPlugin.class); + ExtensionContainer extensions = project.getExtensions(); + JavaPluginExtension javaPluginExtension = extensions.getByType(JavaPluginExtension.class); + ConfigurationContainer configurations = project.getConfigurations(); + TaskContainer tasks = project.getTasks(); + DependencyHandler dependencies = project.getDependencies(); + ObjectFactory objects = project.getObjects(); + extensions.create("multiRelease", MultiReleaseExtension.class, + javaPluginExtension.getSourceSets(), + configurations, + tasks, + dependencies, + objects); + } +} diff --git a/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java new file mode 100644 index 000000000000..b4d2f618803f --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2025 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. + */ + +package org.springframework.build.multirelease; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MultiReleaseJarPlugin} + */ +public class MultiReleaseJarPluginTests { + + private File projectDir; + + private File buildFile; + + @BeforeEach + void setup(@TempDir File projectDir) { + this.projectDir = projectDir; + this.buildFile = new File(this.projectDir, "build.gradle"); + } + + @Test + void configureSourceSets() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + multiRelease { releaseVersions 21, 24 } + task printSourceSets { + doLast { + sourceSets.all { println it.name } + } + } + """); + BuildResult buildResult = runGradle("printSourceSets"); + assertThat(buildResult.getOutput()).contains("main", "test", "java21", "java21Test", "java24", "java24Test"); + } + + @Test + void configureToolchainReleaseVersion() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + multiRelease { releaseVersions 21 } + task printReleaseVersion { + doLast { + tasks.all { println it.name } + tasks.named("compileJava21Java") { + println "compileJava21Java releaseVersion: ${it.options.release.get()}" + } + tasks.named("compileJava21TestJava") { + println "compileJava21TestJava releaseVersion: ${it.options.release.get()}" + } + } + } + """); + + BuildResult buildResult = runGradle("printReleaseVersion"); + assertThat(buildResult.getOutput()).contains("compileJava21Java releaseVersion: 21") + .contains("compileJava21TestJava releaseVersion: 21"); + } + + @Test + void packageInJar() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + version = '1.2.3' + multiRelease { releaseVersions 17 } + """); + writeClass("src/main/java17", "Main.java", """ + public class Main {} + """); + BuildResult buildResult = runGradle("assemble"); + File file = new File(this.projectDir, "/build/libs/" + this.projectDir.getName() + "-1.2.3.jar"); + assertThat(file).exists(); + try (JarFile jar = new JarFile(file)) { + Attributes mainAttributes = jar.getManifest().getMainAttributes(); + assertThat(mainAttributes.getValue("Multi-Release")).isEqualTo("true"); + + assertThat(jar.entries().asIterator()).toIterable() + .anyMatch(entry -> entry.getName().equals("META-INF/versions/17/Main.class")); + } + } + + private void writeBuildFile(String buildContent) throws IOException { + try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) { + out.print(buildContent); + } + } + + private void writeClass(String path, String fileName, String fileContent) throws IOException { + Path folder = this.projectDir.toPath().resolve(path); + Files.createDirectories(folder); + Path filePath = folder.resolve(fileName); + Files.createFile(filePath); + Files.writeString(filePath, fileContent); + } + + private BuildResult runGradle(String... args) { + return GradleRunner.create().withProjectDir(this.projectDir).withArguments(args).withPluginClasspath().build(); + } + +} diff --git a/gradle/toolchains.gradle b/gradle/toolchains.gradle deleted file mode 100644 index 8c5d248136da..000000000000 --- a/gradle/toolchains.gradle +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Apply the JVM Toolchain conventions - * See https://docs.gradle.org/current/userguide/toolchains.html - * - * One can choose the toolchain to use for compiling and running the TEST sources. - * These options apply to Java, Kotlin and Groovy test sources when available. - * {@code "./gradlew check -PtestToolchain=22"} will use a JDK22 - * toolchain for compiling and running the test SourceSet. - * - * By default, the main build will fall back to using the a JDK 17 - * toolchain (and 17 language level) for all main sourceSets. - * See {@link org.springframework.build.JavaConventions}. - * - * Gradle will automatically detect JDK distributions in well-known locations. - * The following command will list the detected JDKs on the host. - * {@code - * $ ./gradlew -q javaToolchains - * } - * - * We can also configure ENV variables and let Gradle know about them: - * {@code - * $ echo JDK17 - * /opt/openjdk/java17 - * $ echo JDK22 - * /opt/openjdk/java22 - * $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK22 check - * } - * - * @author Brian Clozel - * @author Sam Brannen - */ - -def testToolchainConfigured() { - return project.hasProperty('testToolchain') && project.testToolchain -} - -def testToolchainLanguageVersion() { - if (testToolchainConfigured()) { - return JavaLanguageVersion.of(project.testToolchain.toString()) - } - return JavaLanguageVersion.of(17) -} - -plugins.withType(JavaPlugin).configureEach { - // Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined - if (testToolchainConfigured()) { - def testLanguageVersion = testToolchainLanguageVersion() - tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach { - javaCompiler = javaToolchains.compilerFor { - languageVersion = testLanguageVersion - } - } - tasks.withType(Test).configureEach{ - javaLauncher = javaToolchains.launcherFor { - languageVersion = testLanguageVersion - } - // Enable Java experimental support in Bytebuddy - // Bytebuddy 1.15.4 supports JDK <= 24 - // see https://github.com/raphw/byte-buddy/blob/master/release-notes.md - if (testLanguageVersion.compareTo(JavaLanguageVersion.of(24)) > 0 ) { - jvmArgs("-Dnet.bytebuddy.experimental=true") - } - } - } -} - -// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode -pluginManager.withPlugin("me.champeau.jmh") { - if (testToolchainConfigured()) { - tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach { - javaLauncher.set(javaToolchains.launcherFor { - languageVersion.set(testToolchainLanguageVersion()) - }) - } - tasks.withType(JavaCompile).matching { it.name.contains("Jmh") }.configureEach { - javaCompiler = javaToolchains.compilerFor { - languageVersion = testToolchainLanguageVersion() - } - } - } -} - -// Store resolved Toolchain JVM information as custom values in the build scan. -rootProject.ext { - resolvedMainToolchain = false - resolvedTestToolchain = false -} -gradle.taskGraph.afterTask { Task task, TaskState state -> - if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { - def metadata = task.javaCompiler.get().metadata - task.project.develocity.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedMainToolchain = true - } - if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) { - def metadata = task.javaLauncher.get().metadata - task.project.develocity.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedTestToolchain = true - } -} diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index 0c2fd914632f..c96a1b6250b4 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.springframework.build.shadow.ShadowSource plugins { - id 'me.champeau.mrjar' + id 'org.springframework.build.multiReleaseJar' } description = "Spring Core" @@ -11,7 +11,7 @@ apply plugin: "kotlin" apply plugin: "kotlinx-serialization" multiRelease { - targetVersions 17, 21 + releaseVersions 21 } def javapoetVersion = "1.13.0"