Skip to content

Commit a62538d

Browse files
committed
Configure multi-release support in build
This commit configures multi-release JAR support in the project, in preparation for Java 21+ support for Virtual Threads. Closes gh-1330
1 parent 4d8208d commit a62538d

File tree

9 files changed

+398
-10
lines changed

9 files changed

+398
-10
lines changed

buildSrc/build.gradle

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,12 @@ dependencies {
2424
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}")
2525
implementation("org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}")
2626
implementation("io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}")
27-
implementation "com.tngtech.archunit:archunit:1.4.0"
27+
implementation("com.tngtech.archunit:archunit:1.4.0")
28+
29+
testImplementation("org.assertj:assertj-core:${assertjVersion}")
30+
testImplementation(platform("org.junit:junit-bom:${junitVersion}"))
31+
testImplementation("org.junit.jupiter:junit-jupiter")
32+
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
2833
}
2934

3035
checkstyle {
@@ -41,9 +46,15 @@ gradlePlugin {
4146
id = "org.springframework.graphql.architecture"
4247
implementationClass = "org.springframework.graphql.build.architecture.ArchitecturePlugin"
4348
}
49+
multiReleasePlugin {
50+
id = "org.springframework.graphql.multiReleaseJar"
51+
implementationClass = "org.springframework.graphql.build.multirelease.MultiReleaseJarPlugin"
52+
}
4453
}
4554
}
4655

4756
test {
4857
useJUnitPlatform()
49-
}
58+
}
59+
60+
jar.dependsOn check
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version="1.0"?>
2+
<!--
3+
~ Copyright 2025-present the original author or authors.
4+
~
5+
~ Licensed under the Apache License, Version 2.0 (the "License");
6+
~ you may not use this file except in compliance with the License.
7+
~ You may obtain a copy of the License at
8+
~
9+
~ https://www.apache.org/licenses/LICENSE-2.0
10+
~
11+
~ Unless required by applicable law or agreed to in writing, software
12+
~ distributed under the License is distributed on an "AS IS" BASIS,
13+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
~ See the License for the specific language governing permissions and
15+
~ limitations under the License.
16+
-->
17+
18+
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
19+
<module name="Checker">
20+
21+
<!-- Root Checks -->
22+
<module name="io.spring.javaformat.checkstyle.check.SpringHeaderCheck">
23+
<property name="fileExtensions" value="java"/>
24+
<property name="headerType" value="apache2"/>
25+
<property name="packageInfoHeaderType" value="none"/>
26+
</module>
27+
<module name="com.puppycrawl.tools.checkstyle.checks.NewlineAtEndOfFileCheck"/>
28+
29+
<!-- TreeWalker Checks -->
30+
<module name="TreeWalker">
31+
32+
<!-- Imports -->
33+
<module name="AvoidStarImport"/>
34+
<module name="UnusedImports"/>
35+
<module name="RedundantImport"/>
36+
<!-- Modifiers -->
37+
<module name="com.puppycrawl.tools.checkstyle.checks.modifier.ModifierOrderCheck"/>
38+
39+
</module>
40+
41+
</module>

buildSrc/gradle.properties

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
1-
javaFormatVersion=0.0.43
1+
assertjVersion=3.27.3
2+
javaFormatVersion=0.0.47
3+
junitVersion=5.13.4

buildSrc/src/main/java/org/springframework/graphql/build/architecture/ArchitecturePlugin.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,4 @@ private static String taskName(SourceSet sourceSet) {
7171
+ sourceSet.getName().substring(1);
7272
}
7373

74-
}
74+
}

buildSrc/src/main/java/org/springframework/graphql/build/architecture/ArchitectureRules.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,7 @@
2020
import com.tngtech.archunit.core.domain.JavaClass;
2121
import com.tngtech.archunit.lang.ArchRule;
2222
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
23-
import com.tngtech.archunit.library.dependencies.SliceAssignment;
24-
import com.tngtech.archunit.library.dependencies.SliceIdentifier;
2523
import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition;
26-
import java.util.List;
2724

2825
abstract class ArchitectureRules {
2926

@@ -84,4 +81,4 @@ public boolean test(JavaClass javaClass) {
8481
.allowEmptyShould(true);
8582
}
8683

87-
}
84+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.build.multirelease;
18+
19+
import javax.inject.Inject;
20+
21+
import org.gradle.api.artifacts.Configuration;
22+
import org.gradle.api.artifacts.ConfigurationContainer;
23+
import org.gradle.api.artifacts.dsl.DependencyHandler;
24+
import org.gradle.api.attributes.LibraryElements;
25+
import org.gradle.api.file.ConfigurableFileCollection;
26+
import org.gradle.api.file.FileCollection;
27+
import org.gradle.api.java.archives.Attributes;
28+
import org.gradle.api.model.ObjectFactory;
29+
import org.gradle.api.tasks.SourceSet;
30+
import org.gradle.api.tasks.SourceSetContainer;
31+
import org.gradle.api.tasks.TaskContainer;
32+
import org.gradle.api.tasks.TaskProvider;
33+
import org.gradle.api.tasks.bundling.Jar;
34+
import org.gradle.api.tasks.compile.JavaCompile;
35+
import org.gradle.api.tasks.testing.Test;
36+
import org.gradle.language.base.plugins.LifecycleBasePlugin;
37+
38+
/**
39+
* @author Cedric Champeau
40+
* @author Brian Clozel
41+
*/
42+
public abstract class MultiReleaseExtension {
43+
private final TaskContainer tasks;
44+
private final SourceSetContainer sourceSets;
45+
private final DependencyHandler dependencies;
46+
private final ObjectFactory objects;
47+
private final ConfigurationContainer configurations;
48+
49+
@Inject
50+
public MultiReleaseExtension(SourceSetContainer sourceSets,
51+
ConfigurationContainer configurations,
52+
TaskContainer tasks,
53+
DependencyHandler dependencies,
54+
ObjectFactory objectFactory) {
55+
this.sourceSets = sourceSets;
56+
this.configurations = configurations;
57+
this.tasks = tasks;
58+
this.dependencies = dependencies;
59+
this.objects = objectFactory;
60+
}
61+
62+
public void releaseVersions(int... javaVersions) {
63+
releaseVersions("src/main/", "src/test/", javaVersions);
64+
}
65+
66+
private void releaseVersions(String mainSourceDirectory, String testSourceDirectory, int... javaVersions) {
67+
for (int javaVersion : javaVersions) {
68+
addLanguageVersion(javaVersion, mainSourceDirectory, testSourceDirectory);
69+
}
70+
}
71+
72+
private void addLanguageVersion(int javaVersion, String mainSourceDirectory, String testSourceDirectory) {
73+
String javaN = "java" + javaVersion;
74+
75+
SourceSet langSourceSet = sourceSets.create(javaN, srcSet -> srcSet.getJava().srcDir(mainSourceDirectory + javaN));
76+
SourceSet testSourceSet = sourceSets.create(javaN + "Test", srcSet -> srcSet.getJava().srcDir(testSourceDirectory + javaN));
77+
SourceSet sharedSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME);
78+
SourceSet sharedTestSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME);
79+
80+
FileCollection mainClasses = objects.fileCollection().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs());
81+
dependencies.add(javaN + "Implementation", mainClasses);
82+
83+
tasks.named(langSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
84+
task.getOptions().getRelease().set(javaVersion)
85+
);
86+
tasks.named(testSourceSet.getCompileJavaTaskName(), JavaCompile.class, task ->
87+
task.getOptions().getRelease().set(javaVersion)
88+
);
89+
90+
TaskProvider<Test> testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet);
91+
tasks.named("check", task -> task.dependsOn(testTask));
92+
93+
configureMultiReleaseJar(javaVersion, langSourceSet);
94+
}
95+
96+
private TaskProvider<Test> createTestTask(int javaVersion, SourceSet testSourceSet, SourceSet sharedTestSourceSet, SourceSet langSourceSet, SourceSet sharedSourceSet) {
97+
Configuration testImplementation = configurations.getByName(testSourceSet.getImplementationConfigurationName());
98+
testImplementation.extendsFrom(configurations.getByName(sharedTestSourceSet.getImplementationConfigurationName()));
99+
Configuration testCompileOnly = configurations.getByName(testSourceSet.getCompileOnlyConfigurationName());
100+
testCompileOnly.extendsFrom(configurations.getByName(sharedTestSourceSet.getCompileOnlyConfigurationName()));
101+
testCompileOnly.getDependencies().add(dependencies.create(langSourceSet.getOutput().getClassesDirs()));
102+
testCompileOnly.getDependencies().add(dependencies.create(sharedSourceSet.getOutput().getClassesDirs()));
103+
104+
Configuration testRuntimeClasspath = configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName());
105+
// so here's the deal. MRjars are JARs! Which means that to execute tests, we need
106+
// the JAR on classpath, not just classes + resources as Gradle usually does
107+
testRuntimeClasspath.getAttributes()
108+
.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR));
109+
110+
TaskProvider<Test> testTask = tasks.register("java" + javaVersion + "Test", Test.class, test -> {
111+
test.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP);
112+
113+
ConfigurableFileCollection testClassesDirs = objects.fileCollection();
114+
testClassesDirs.from(testSourceSet.getOutput());
115+
testClassesDirs.from(sharedTestSourceSet.getOutput());
116+
test.setTestClassesDirs(testClassesDirs);
117+
ConfigurableFileCollection classpath = objects.fileCollection();
118+
// must put the MRJar first on classpath
119+
classpath.from(tasks.named("jar"));
120+
// then we put the specific test sourceset tests, so that we can override
121+
// the shared versions
122+
classpath.from(testSourceSet.getOutput());
123+
124+
// then we add the shared tests
125+
classpath.from(sharedTestSourceSet.getRuntimeClasspath());
126+
test.setClasspath(classpath);
127+
});
128+
return testTask;
129+
}
130+
131+
private void configureMultiReleaseJar(int version, SourceSet languageSourceSet) {
132+
tasks.named("jar", Jar.class, jar -> {
133+
jar.into("META-INF/versions/" + version, s -> s.from(languageSourceSet.getOutput()));
134+
Attributes attributes = jar.getManifest().getAttributes();
135+
attributes.put("Multi-Release", "true");
136+
});
137+
}
138+
139+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/*
2+
* Copyright 2002-present the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.graphql.build.multirelease;
18+
19+
import javax.inject.Inject;
20+
21+
import org.gradle.api.Plugin;
22+
import org.gradle.api.Project;
23+
import org.gradle.api.artifacts.ConfigurationContainer;
24+
import org.gradle.api.artifacts.dsl.DependencyHandler;
25+
import org.gradle.api.model.ObjectFactory;
26+
import org.gradle.api.plugins.ExtensionContainer;
27+
import org.gradle.api.plugins.JavaPlugin;
28+
import org.gradle.api.plugins.JavaPluginExtension;
29+
import org.gradle.api.tasks.TaskContainer;
30+
import org.gradle.jvm.toolchain.JavaToolchainService;
31+
32+
/**
33+
* A plugin which adds support for building multi-release jars
34+
* with Gradle.
35+
* @author Cedric Champeau
36+
* @author Brian Clozel
37+
* @see <a href="https://github.com/melix/mrjar-gradle-plugin">original project</a>
38+
*/
39+
public class MultiReleaseJarPlugin implements Plugin<Project> {
40+
41+
@Inject
42+
protected JavaToolchainService getToolchains() {
43+
throw new UnsupportedOperationException();
44+
}
45+
46+
public void apply(Project project) {
47+
project.getPlugins().apply(JavaPlugin.class);
48+
ExtensionContainer extensions = project.getExtensions();
49+
JavaPluginExtension javaPluginExtension = extensions.getByType(JavaPluginExtension.class);
50+
ConfigurationContainer configurations = project.getConfigurations();
51+
TaskContainer tasks = project.getTasks();
52+
DependencyHandler dependencies = project.getDependencies();
53+
ObjectFactory objects = project.getObjects();
54+
extensions.create("multiRelease", MultiReleaseExtension.class,
55+
javaPluginExtension.getSourceSets(),
56+
configurations,
57+
tasks,
58+
dependencies,
59+
objects);
60+
}
61+
}

0 commit comments

Comments
 (0)