Revisit compiler configuration in project build

This commit revisit the build configuration to enforce the following:

* A single Java toolchain is used consistently with a recent Java
  version (here, Java 23) and language level
* the main source is compiled with the Java 17 "-release" target
* Multi-Release classes are compiled with their respective "-release"
  target. For now, only "spring-core" ships Java 21 variants.

Closes gh-34507
This commit is contained in:
Brian Clozel 2025-02-25 11:03:42 +01:00
parent 382caac37c
commit 68e9460e9b
13 changed files with 420 additions and 127 deletions

View File

@ -3,12 +3,9 @@ plugins {
// kotlinVersion is managed in gradle.properties // kotlinVersion is managed in gradle.properties
id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false id 'org.jetbrains.kotlin.plugin.serialization' version "${kotlinVersion}" apply false
id 'org.jetbrains.dokka' version '1.9.20' 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 '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 'io.github.goooler.shadow' version '8.1.8' apply false
id 'me.champeau.jmh' version '0.7.2' 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 id "net.ltgt.errorprone" version "4.1.0" apply false
} }
@ -64,7 +61,6 @@ configure([rootProject] + javaProjects) { project ->
apply plugin: "java" apply plugin: "java"
apply plugin: "java-test-fixtures" apply plugin: "java-test-fixtures"
apply plugin: 'org.springframework.build.conventions' apply plugin: 'org.springframework.build.conventions'
apply from: "${rootDir}/gradle/toolchains.gradle"
apply from: "${rootDir}/gradle/ide.gradle" apply from: "${rootDir}/gradle/ide.gradle"
dependencies { dependencies {

View File

@ -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` This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly`
configurations are preferred. 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 ### RuntimeHints Java Agent

View File

@ -20,10 +20,13 @@ ext {
dependencies { dependencies {
checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}"
implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}"
implementation "com.tngtech.archunit:archunit:1.3.0" implementation "com.tngtech.archunit:archunit:1.4.0"
implementation "org.gradle:test-retry-gradle-plugin:1.5.6" implementation "org.gradle:test-retry-gradle-plugin:1.6.2"
implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}"
implementation "io.spring.nohttp:nohttp-gradle:0.0.11" implementation "io.spring.nohttp:nohttp-gradle:0.0.11"
testImplementation("org.assertj:assertj-core:${assertjVersion}")
testImplementation("org.junit.jupiter:junit-jupiter:${junitJupiterVersion}")
} }
gradlePlugin { gradlePlugin {
@ -40,6 +43,10 @@ gradlePlugin {
id = "org.springframework.build.localdev" id = "org.springframework.build.localdev"
implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin" implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin"
} }
multiReleasePlugin {
id = "org.springframework.build.multiReleaseJar"
implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin"
}
optionalDependenciesPlugin { optionalDependenciesPlugin {
id = "org.springframework.build.optional-dependencies" id = "org.springframework.build.optional-dependencies"
implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin"
@ -50,3 +57,9 @@ gradlePlugin {
} }
} }
} }
test {
useJUnitPlatform()
}
jar.dependsOn check

View File

@ -1,2 +1,4 @@
org.gradle.caching=true org.gradle.caching=true
javaFormatVersion=0.0.42 javaFormatVersion=0.0.42
junitJupiterVersion=5.11.4
assertjVersion=3.27.3

View File

@ -64,7 +64,7 @@ public class CheckstyleConventions {
NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class); NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class);
noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines")); noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines"));
noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", noHttp.getSource().exclude("**/test-output/**", "**/.settings/**",
"**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**"); "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**", "buildSrc/**");
List<String> buildFolders = List.of("bin", "build", "out"); List<String> buildFolders = List.of("bin", "build", "out");
project.allprojects(subproject -> { project.allprojects(subproject -> {
Path rootPath = project.getRootDir().toPath(); Path rootPath = project.getRootDir().toPath();

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,7 +17,6 @@
package org.springframework.build; package org.springframework.build;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; import java.util.List;
import org.gradle.api.Plugin; import org.gradle.api.Plugin;
@ -42,8 +41,18 @@ public class JavaConventions {
private static final List<String> TEST_COMPILER_ARGS; private static final List<String> 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 { static {
List<String> commonCompilerArgs = Arrays.asList( List<String> commonCompilerArgs = List.of(
"-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann", "-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann",
"-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides", "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides",
"-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options", "-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options",
@ -51,37 +60,45 @@ public class JavaConventions {
); );
COMPILER_ARGS = new ArrayList<>(); COMPILER_ARGS = new ArrayList<>();
COMPILER_ARGS.addAll(commonCompilerArgs); COMPILER_ARGS.addAll(commonCompilerArgs);
COMPILER_ARGS.addAll(Arrays.asList( COMPILER_ARGS.addAll(List.of(
"-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation", "-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation",
"-Xlint:unchecked", "-Werror" "-Xlint:unchecked", "-Werror"
)); ));
TEST_COMPILER_ARGS = new ArrayList<>(); TEST_COMPILER_ARGS = new ArrayList<>();
TEST_COMPILER_ARGS.addAll(commonCompilerArgs); 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")); "-Xlint:-deprecation", "-Xlint:-unchecked"));
} }
public void apply(Project project) { 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 * Configure the Toolchain support for the project.
* @param project the current project
*/
private static void applyToolchainConventions(Project project) {
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION);
});
}
/**
* Apply the common Java compiler options for main sources, test fixture sources, and
* test sources. * test sources.
* @param project the current project * @param project the current project
*/ */
private void applyJavaCompileConventions(Project project) { private void applyJavaCompileConventions(Project project) {
project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> {
toolchain.getVendor().set(JvmVendorSpec.BELLSOFT);
toolchain.getLanguageVersion().set(JavaLanguageVersion.of(23));
});
SpringFrameworkExtension frameworkExtension = project.getExtensions().getByType(SpringFrameworkExtension.class);
project.afterEvaluate(p -> { project.afterEvaluate(p -> {
p.getTasks().withType(JavaCompile.class) p.getTasks().withType(JavaCompile.class)
.matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME)) .matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME))
.forEach(compileTask -> { .forEach(compileTask -> {
compileTask.getOptions().setCompilerArgs(COMPILER_ARGS); compileTask.getOptions().setCompilerArgs(COMPILER_ARGS);
compileTask.getOptions().getCompilerArgumentProviders().add(frameworkExtension.asArgumentProvider());
compileTask.getOptions().setEncoding("UTF-8"); compileTask.getOptions().setEncoding("UTF-8");
setJavaRelease(compileTask); setJavaRelease(compileTask);
}); });
@ -90,7 +107,6 @@ public class JavaConventions {
|| compileTask.getName().equals("compileTestFixturesJava")) || compileTask.getName().equals("compileTestFixturesJava"))
.forEach(compileTask -> { .forEach(compileTask -> {
compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS); compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS);
compileTask.getOptions().getCompilerArgumentProviders().add(frameworkExtension.asArgumentProvider());
compileTask.getOptions().setEncoding("UTF-8"); compileTask.getOptions().setEncoding("UTF-8");
setJavaRelease(compileTask); setJavaRelease(compileTask);
}); });
@ -98,8 +114,12 @@ public class JavaConventions {
}); });
} }
/**
* 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) { private void setJavaRelease(JavaCompile task) {
int defaultVersion = 17; int defaultVersion = DEFAULT_RELEASE_VERSION.asInt();
int releaseVersion = defaultVersion; int releaseVersion = defaultVersion;
int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt(); int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt();
for (int version = defaultVersion ; version <= compilerVersion ; version++) { for (int version = defaultVersion ; version <= compilerVersion ; version++) {

View File

@ -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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -21,6 +21,8 @@ import java.util.List;
import org.gradle.api.Project; import org.gradle.api.Project;
import org.gradle.api.provider.Property; 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; import org.gradle.process.CommandLineArgumentProvider;
public class SpringFrameworkExtension { public class SpringFrameworkExtension {
@ -29,13 +31,18 @@ public class SpringFrameworkExtension {
public SpringFrameworkExtension(Project project) { public SpringFrameworkExtension(Project project) {
this.enableJavaPreviewFeatures = project.getObjects().property(Boolean.class); 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<Boolean> getEnableJavaPreviewFeatures() { public Property<Boolean> getEnableJavaPreviewFeatures() {
return this.enableJavaPreviewFeatures; return this.enableJavaPreviewFeatures;
} }
public CommandLineArgumentProvider asArgumentProvider() { private CommandLineArgumentProvider asArgumentProvider() {
return () -> { return () -> {
if (getEnableJavaPreviewFeatures().getOrElse(false)) { if (getEnableJavaPreviewFeatures().getOrElse(false)) {
return List.of("--enable-preview"); return List.of("--enable-preview");

View File

@ -64,8 +64,6 @@ class TestConventions {
"--add-opens=java.base/java.util=ALL-UNNAMED", "--add-opens=java.base/java.util=ALL-UNNAMED",
"-Xshare:off" "-Xshare:off"
); );
test.getJvmArgumentProviders().add(project.getExtensions()
.getByType(SpringFrameworkExtension.class).asArgumentProvider());
} }
private void configureTestRetryPlugin(Project project, Test test) { private void configureTestRetryPlugin(Project project, Test test) {

View File

@ -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<Test> testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet);
tasks.named("check", task -> task.dependsOn(testTask));
configureMultiReleaseJar(javaVersion, langSourceSet);
}
private TaskProvider<Test> 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<Test> 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");
});
}
}

View File

@ -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 <a href="https://github.com/melix/mrjar-gradle-plugin">original project</a>
*/
public class MultiReleaseJarPlugin implements Plugin<Project> {
@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);
}
}

View File

@ -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();
}
}

View File

@ -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
}
}

View File

@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
import org.springframework.build.shadow.ShadowSource import org.springframework.build.shadow.ShadowSource
plugins { plugins {
id 'me.champeau.mrjar' id 'org.springframework.build.multiReleaseJar'
} }
description = "Spring Core" description = "Spring Core"
@ -11,7 +11,7 @@ apply plugin: "kotlin"
apply plugin: "kotlinx-serialization" apply plugin: "kotlinx-serialization"
multiRelease { multiRelease {
targetVersions 17, 21 releaseVersions 21
} }
def javapoetVersion = "1.13.0" def javapoetVersion = "1.13.0"