Browse Source
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-34507pull/34511/head
13 changed files with 419 additions and 126 deletions
@ -1,2 +1,4 @@
@@ -1,2 +1,4 @@
|
||||
org.gradle.caching=true |
||||
javaFormatVersion=0.0.42 |
||||
junitJupiterVersion=5.11.4 |
||||
assertjVersion=3.27.3 |
||||
@ -0,0 +1,139 @@
@@ -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"); |
||||
}); |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,61 @@
@@ -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); |
||||
} |
||||
} |
||||
@ -0,0 +1,137 @@
@@ -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(); |
||||
} |
||||
|
||||
} |
||||
@ -1,99 +0,0 @@
@@ -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 |
||||
} |
||||
} |
||||
Loading…
Reference in new issue