Skip to content

Commit d299bd0

Browse files
committed
Add support for building modular fat jars with launcher
1 parent 9a7d9b9 commit d299bd0

10 files changed

Lines changed: 405 additions & 43 deletions

File tree

‎.github/workflows/ci-build.yml‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ jobs:
1111
strategy:
1212
matrix:
1313
os: [ubuntu, macos, windows]
14+
fail-fast: false
1415
runs-on: ${{ matrix.os }}-latest
1516
steps:
1617
- name: standardize git line endings
@@ -21,7 +22,9 @@ jobs:
2122
- uses: actions/setup-java@v5
2223
with:
2324
distribution: temurin
24-
java-version: 17
25+
java-version: |
26+
25.0.3
27+
17.0.19
2528
- uses: gradle/actions/setup-gradle@v6
2629
with:
2730
build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use"

‎.github/workflows/release.yml‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ jobs:
1111
- uses: actions/setup-java@v5
1212
with:
1313
distribution: temurin
14-
java-version: 17
14+
java-version: |
15+
25.0.3
16+
17.0.19
1517
- uses: gradle/actions/setup-gradle@v6
1618
with:
1719
build-scan-terms-of-use-url: "https://gradle.com/help/legal-terms-of-use"

‎build.gradle.kts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,12 @@ testingConventions { testGradleVersions("7.4", "7.6.5", "8.0.2", "8.14.2") }
2020
tasks.test {
2121
inputs.property("operatingSystemName", System.getProperty("os.name"))
2222
inputs.property("operatingSystemArch", System.getProperty("os.arch"))
23+
environment["JAVA_25"] =
24+
javaToolchains
25+
.launcherFor { languageVersion = JavaLanguageVersion.of(25) }
26+
.get()
27+
.metadata
28+
.installationPath
29+
.asFile
30+
.absolutePath
2331
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
# 17.0.19
12
toolchainVersion=17

‎src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java‎

Lines changed: 82 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.gradle.util.GradleVersion;
4646
import org.gradlex.javamodule.packaging.internal.HostIdentification;
4747
import org.gradlex.javamodule.packaging.model.Target;
48+
import org.gradlex.javamodule.packaging.tasks.FatModuleJar;
4849
import org.gradlex.javamodule.packaging.tasks.Jpackage;
4950
import org.gradlex.javamodule.packaging.tasks.ValidateHostSystemAction;
5051
import org.jspecify.annotations.NullMarked;
@@ -56,6 +57,7 @@ public abstract class JavaModulePackagingExtension {
5657
private static final Attribute<Boolean> JAVA_MODULE_ATTRIBUTE = Attribute.of("javaModule", Boolean.class);
5758
private static final String INTERNAL = "internal";
5859
private static final String JPACKAGE = "jpackage";
60+
private static final String FAT_MODULE_JAR = "fatModuleJar";
5961

6062
private final Project project;
6163

@@ -95,7 +97,6 @@ public JavaModulePackagingExtension(Project project) {
9597
/**
9698
* Retrieve the target with the given 'label'. If the target does not yet exist, it will be created.
9799
*/
98-
@SuppressWarnings("unused")
99100
public Target target(String label) {
100101
return target(label, target -> {});
101102
}
@@ -120,7 +121,6 @@ public Target target(String label, Action<? super Target> action) {
120121
/**
121122
* Configure all targets for the given OS.
122123
*/
123-
@SuppressWarnings("unused")
124124
public void targetsWithOs(String operatingSystem, Action<? super Target> action) {
125125
NamedDomainObjectSet<Target> matches =
126126
targets.matching(t -> t.getOperatingSystem().isPresent()
@@ -132,7 +132,6 @@ public void targetsWithOs(String operatingSystem, Action<? super Target> action)
132132
* Set a 'primary target'. Standard Gradle tasks that are not bound to a specific target – like 'assemble' – use
133133
* this 'primary target'.
134134
*/
135-
@SuppressWarnings("unused")
136135
public Target primaryTarget(Target target) {
137136
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
138137
ConfigurationContainer configurations = project.getConfigurations();
@@ -155,7 +154,6 @@ public Target primaryTarget(Target target) {
155154
/**
156155
* Set a test suite to be 'multi-target'. This registers an additional 'test' task for each target.
157156
*/
158-
@SuppressWarnings({"unused", "UnstableApiUsage"})
159157
public TestSuite multiTargetTestSuite(TestSuite testSuite) {
160158
if (!(testSuite instanceof JvmTestSuite)) {
161159
return testSuite;
@@ -183,7 +181,18 @@ public TestSuite multiTargetTestSuite(TestSuite testSuite) {
183181
return testSuite;
184182
}
185183

184+
void maybeAddSingleDefaultTarget(Target target) {
185+
if (targets.isEmpty()) {
186+
targets.add(target);
187+
newTarget(target, true);
188+
}
189+
}
190+
186191
private void newTarget(Target target) {
192+
newTarget(target, false);
193+
}
194+
195+
private void newTarget(Target target, boolean singleDefault) {
187196
target.getPackageTypes().convention(target.getOperatingSystem().map(os -> {
188197
switch (os) {
189198
case WINDOWS:
@@ -200,35 +209,41 @@ private void newTarget(Target target) {
200209
SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class);
201210

202211
sourceSets.all(sourceSet -> {
203-
Configuration internal = maybeCreateInternalConfiguration();
204-
configurations.create(
205-
target.getName() + capitalize(sourceSet.getCompileClasspathConfigurationName()), c -> {
206-
c.setCanBeConsumed(false);
207-
setInvisible(c);
208-
configureJavaStandardAttributes(c, Usage.JAVA_API);
209-
configureTargetAttributes(c, target);
210-
c.extendsFrom(
211-
configurations.getByName(sourceSet.getImplementationConfigurationName()),
212-
configurations.getByName(sourceSet.getCompileOnlyConfigurationName()),
213-
internal);
214-
});
215-
Configuration runtimeClasspath = configurations.create(
216-
target.getName() + capitalize(sourceSet.getRuntimeClasspathConfigurationName()), c -> {
217-
c.setCanBeConsumed(false);
218-
setInvisible(c);
219-
configureJavaStandardAttributes(c, Usage.JAVA_RUNTIME);
220-
configureTargetAttributes(c, target);
221-
c.extendsFrom(
222-
configurations.getByName(sourceSet.getImplementationConfigurationName()),
223-
configurations.getByName(sourceSet.getRuntimeOnlyConfigurationName()),
224-
internal);
225-
});
212+
Configuration runtimeClasspath;
213+
if (singleDefault) {
214+
runtimeClasspath = configurations.getByName(sourceSet.getRuntimeClasspathConfigurationName());
215+
} else {
216+
Configuration internal = maybeCreateInternalConfiguration();
217+
configurations.create(
218+
target.getName() + capitalize(sourceSet.getCompileClasspathConfigurationName()), c -> {
219+
c.setCanBeConsumed(false);
220+
setInvisible(c);
221+
configureJavaStandardAttributes(c, Usage.JAVA_API);
222+
configureTargetAttributes(c, target);
223+
c.extendsFrom(
224+
configurations.getByName(sourceSet.getImplementationConfigurationName()),
225+
configurations.getByName(sourceSet.getCompileOnlyConfigurationName()),
226+
internal);
227+
});
228+
runtimeClasspath = configurations.create(
229+
target.getName() + capitalize(sourceSet.getRuntimeClasspathConfigurationName()), c -> {
230+
c.setCanBeConsumed(false);
231+
setInvisible(c);
232+
configureJavaStandardAttributes(c, Usage.JAVA_RUNTIME);
233+
configureTargetAttributes(c, target);
234+
c.extendsFrom(
235+
configurations.getByName(sourceSet.getImplementationConfigurationName()),
236+
configurations.getByName(sourceSet.getRuntimeOnlyConfigurationName()),
237+
internal);
238+
});
239+
}
226240

227241
if (SourceSet.isMain(sourceSet)) {
228242
project.getPlugins()
229243
.withType(
230244
ApplicationPlugin.class,
231-
p -> registerTargetSpecificTasks(target, sourceSet.getJarTaskName(), runtimeClasspath));
245+
p -> registerTargetSpecificTasks(
246+
target, singleDefault, sourceSet.getJarTaskName(), runtimeClasspath));
232247
}
233248
});
234249
}
@@ -268,13 +283,16 @@ private void configureTargetAttributes(Configuration resolvable, Target target)
268283
target.getArchitecture().map(name -> getObjects().named(MachineArchitecture.class, name)));
269284
}
270285

271-
private void registerTargetSpecificTasks(Target target, String applicationJarTask, Configuration runtimeClasspath) {
286+
private void registerTargetSpecificTasks(
287+
Target target, boolean singleDefaultTarget, String applicationJarTask, Configuration runtimeClasspath) {
272288
TaskContainer tasks = project.getTasks();
273289

274290
JavaPluginExtension java = project.getExtensions().getByType(JavaPluginExtension.class);
275291
JavaApplication application = project.getExtensions().getByType(JavaApplication.class);
276292

277293
TaskProvider<Jpackage> jpackage = tasks.register(JPACKAGE + capitalize(target.getName()), Jpackage.class, t -> {
294+
t.setDescription("Runs 'jpackage' command to create an image or s package.");
295+
278296
t.getJavaInstallation()
279297
.convention(getJavaToolchains()
280298
.compilerFor(java.getToolchain())
@@ -318,12 +336,43 @@ private void registerTargetSpecificTasks(Target target, String applicationJarTas
318336
t.setJvmArgs(application.getApplicationDefaultJvmArgs());
319337
t.classpath(tasks.named("jar"), runtimeClasspath);
320338
});
321-
maybeAddJpackageLifecycleTask(tasks, target, jpackage);
339+
340+
registerFatModuleJarTask(target, singleDefaultTarget, applicationJarTask, runtimeClasspath);
341+
342+
maybeAddJpackageLifecycleTask(target, jpackage);
343+
}
344+
345+
private void registerFatModuleJarTask(
346+
Target target, boolean singleDefaultTarget, String applicationJarTask, Configuration runtimeClasspath) {
347+
TaskContainer tasks = project.getTasks();
348+
JavaApplication application = project.getExtensions().getByType(JavaApplication.class);
349+
ConfigurationContainer configurations = project.getConfigurations();
350+
351+
String taskName = FAT_MODULE_JAR + (singleDefaultTarget ? "" : capitalize(target.getName()));
352+
tasks.register(taskName, FatModuleJar.class, t -> {
353+
String classifier = singleDefaultTarget ? "all" : "all-" + target.getName();
354+
355+
t.setDescription("Assembles a fat jar archive containing the complete module path and a launcher.");
356+
357+
t.getMainModule().convention(application.getMainModule());
358+
t.getMainClass().convention(application.getMainClass());
359+
360+
t.getLauncherPath().from(configurations.named("fatModuleJarLauncherPath"));
361+
t.getLauncherMainClass().convention("build.jenesis.launcher.Launcher");
362+
363+
t.getModulePath().from(tasks.named(applicationJarTask));
364+
t.getModulePath().from(runtimeClasspath);
365+
366+
t.setZip64(true);
367+
368+
t.getArchiveClassifier().set(classifier);
369+
});
322370
}
323371

324-
private void maybeAddJpackageLifecycleTask(
325-
TaskContainer tasks, Target target, TaskProvider<Jpackage> targetJpackage) {
326-
// if a task already exists, do nothing to avoid conflciting with other plugins
372+
private void maybeAddJpackageLifecycleTask(Target target, TaskProvider<Jpackage> targetJpackage) {
373+
TaskContainer tasks = project.getTasks();
374+
375+
// if a task already exists, do nothing to avoid conflict with other plugins
327376
TaskProvider<Task> jpackage;
328377
if (tasks.getNames().contains(JPACKAGE)) {
329378
jpackage = tasks.named(JPACKAGE);

‎src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingPlugin.java‎

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,25 @@
11
// SPDX-License-Identifier: Apache-2.0
22
package org.gradlex.javamodule.packaging;
33

4-
import javax.inject.Inject;
54
import org.gradle.api.Plugin;
65
import org.gradle.api.Project;
6+
import org.gradle.api.artifacts.Configuration;
7+
import org.gradle.api.artifacts.ConfigurationContainer;
8+
import org.gradle.api.artifacts.dsl.DependencyHandler;
9+
import org.gradle.api.attributes.Usage;
710
import org.gradle.api.file.SourceDirectorySet;
11+
import org.gradle.api.model.ObjectFactory;
812
import org.gradle.api.plugins.JavaPlugin;
13+
import org.gradle.api.provider.Provider;
914
import org.gradle.api.tasks.SourceSetContainer;
10-
import org.gradle.jvm.toolchain.JavaToolchainService;
1115
import org.gradle.util.GradleVersion;
1216
import org.gradlex.javamodule.packaging.internal.HostIdentification;
17+
import org.gradlex.javamodule.packaging.model.Target;
1318
import org.jspecify.annotations.NullMarked;
1419

1520
@NullMarked
1621
public abstract class JavaModulePackagingPlugin implements Plugin<Project> {
1722

18-
@Inject
19-
protected abstract JavaToolchainService getJavaToolchains();
20-
2123
@Override
2224
public void apply(Project project) {
2325
if (GradleVersion.current().compareTo(GradleVersion.version("7.4")) < 0) {
@@ -36,6 +38,34 @@ public void apply(Project project) {
3638
.getProjectDirectory()
3739
.dir(mainResources.getSrcDirs().iterator().next().getParent() + "/resourcesPackage")));
3840
javaModulePackaging.getVerbose().convention(false);
39-
javaModulePackaging.primaryTarget(HostIdentification.hostTarget(project.getObjects()));
41+
42+
Target hostTarget = HostIdentification.hostTarget(project.getObjects());
43+
javaModulePackaging.primaryTarget(hostTarget);
44+
project.afterEvaluate(__ -> javaModulePackaging.maybeAddSingleDefaultTarget(hostTarget));
45+
46+
registerFatModuleJarLauncherScope(project);
47+
}
48+
49+
private void registerFatModuleJarLauncherScope(Project project) {
50+
ObjectFactory objects = project.getObjects();
51+
ConfigurationContainer configurations = project.getConfigurations();
52+
DependencyHandler dependencies = project.getDependencies();
53+
54+
Provider<Configuration> fatModuleJarLauncher = configurations.register("fatModuleJarLauncher", c -> {
55+
c.setCanBeConsumed(false);
56+
c.setCanBeResolved(false);
57+
c.withDependencies(deps -> {
58+
if (deps.isEmpty()) {
59+
deps.add(dependencies.create("build.jenesis:build.jenesis.launcher:0.3.0"));
60+
}
61+
});
62+
});
63+
64+
configurations.register("fatModuleJarLauncherPath", c -> {
65+
c.setCanBeConsumed(false);
66+
c.setCanBeResolved(true);
67+
c.extendsFrom(fatModuleJarLauncher.get());
68+
c.getAttributes().attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.class, Usage.JAVA_RUNTIME));
69+
});
4070
}
4171
}

‎src/main/java/org/gradlex/javamodule/packaging/internal/HostIdentification.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ public static void validateHostSystem(String arch, String os) {
2424
}
2525

2626
public static Target hostTarget(ObjectFactory objects) {
27-
Target host = objects.newInstance(Target.class, "host");
27+
Target host = objects.newInstance(Target.class, hostOs());
2828
host.getOperatingSystem().convention(hostOs());
2929
host.getArchitecture().convention(normalizeArch(System.getProperty("os.arch")));
3030
return host;

0 commit comments

Comments
 (0)