Skip to content

Commit 2492445

Browse files
kopporclaude
andcommitted
Add 'postAppImageStep' to modify the app image before packaging
Creating a JEP 514 AOT cache requires running the packaged launcher once and storing the result inside the app image, which has to happen after jpackage assembled the image but before the OS-specific packages are derived from it. The new option runs an Action on the app image directory at exactly that point; it is rejected together with 'singleStepPackaging', where no intermediate image exists. Closes #135 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JnKVoy56BKWZAkVtvSqNgg
1 parent 80f1a36 commit 2492445

6 files changed

Lines changed: 87 additions & 0 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Java Module Packaging Gradle Plugin - Changelog
22

3+
## Unreleased
4+
* [Added] [#135](https://github.com/gradlex-org/java-module-packaging/issues/135) - `postAppImageStep` option to modify the app image before the packages are built from it
5+
36
## Version 1.3
47
- [#122](https://github.com/gradlex-org/java-module-packaging/issues/122) - 'fatModueJar' tasks to re-package module Jars into one Jar with launcher
58
- [#102](https://github.com/gradlex-org/java-module-packaging/issues/102) - Configure all targets via 'allTargets' notation

‎README.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,26 @@ javaModulePackaging {
280280

281281
You can tell the plugin to perform packaging in one step by setting the `singleStepPackaging = true` option on a target.
282282

283+
### How can I modify the app image before the packages are built from it?
284+
285+
Configure a `postAppImageStep` on a target. It receives the app image directory after all resources have been copied
286+
into it and runs before the OS-specific packages are built from that image, so modifications end up in all packages.
287+
288+
```kotlin
289+
javaModulePackaging {
290+
allTargets {
291+
postAppImageStep = Action<Directory> {
292+
// 'this' is the app image directory, e.g. run the packaged launcher once to train a
293+
// JEP 514 AOT cache and store it in the image
294+
}
295+
}
296+
}
297+
```
298+
299+
The step is not a task input. If its result depends on state outside the app image, register that state as an
300+
additional input of the `jpackage<Target>` task. The option cannot be combined with `singleStepPackaging`, because
301+
single-step packaging builds the packages without an intermediate app image.
302+
283303
### How does a `fatModuleJar` work?
284304

285305
The structure of the Jar follows the structure defined by the [Jenesis Launcher](https://github.com/raphw/jenesis-launcher).

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@ private void registerTargetSpecificTasks(
337337
t.getAppImageOptions().convention(target.getAppImageOptions());
338338
t.getPackageTypes().convention(target.getPackageTypes());
339339
t.getSingleStepPackaging().convention(target.getSingleStepPackaging());
340+
t.getPostAppImageStep().convention(target.getPostAppImageStep());
340341
t.getResources().from(getResources());
341342
t.getTargetResources().from(target.getTargetResources());
342343
t.getVerbose().convention(getVerbose());

‎src/main/java/org/gradlex/javamodule/packaging/model/Target.java‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
package org.gradlex.javamodule.packaging.model;
33

44
import javax.inject.Inject;
5+
import org.gradle.api.Action;
56
import org.gradle.api.file.ConfigurableFileCollection;
7+
import org.gradle.api.file.Directory;
68
import org.gradle.api.provider.ListProperty;
79
import org.gradle.api.provider.Property;
810
import org.jspecify.annotations.NullMarked;
@@ -26,6 +28,12 @@ public abstract class Target {
2628

2729
public abstract Property<Boolean> getSingleStepPackaging();
2830

31+
/**
32+
* An optional step to run on the created app image, after all resources have been copied into it and before
33+
* the OS-specific packages are built from that image. Not supported together with 'singleStepPackaging'.
34+
*/
35+
public abstract Property<Action<Directory>> getPostAppImageStep();
36+
2937
@Inject
3038
public Target(String name) {
3139
this.name = name;

‎src/main/java/org/gradlex/javamodule/packaging/tasks/Jpackage.java‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@
1616
import java.util.List;
1717
import java.util.stream.Collectors;
1818
import javax.inject.Inject;
19+
import org.gradle.api.Action;
1920
import org.gradle.api.DefaultTask;
21+
import org.gradle.api.InvalidUserDataException;
2022
import org.gradle.api.file.ConfigurableFileCollection;
2123
import org.gradle.api.file.Directory;
2224
import org.gradle.api.file.DirectoryProperty;
@@ -112,6 +114,17 @@ public abstract class Jpackage extends DefaultTask {
112114
@Input
113115
public abstract Property<Boolean> getVerbose();
114116

117+
/**
118+
* An optional step to run on the created app image, after all resources have been copied into it and before
119+
* the OS-specific packages are built from that image. Not supported together with 'singleStepPackaging',
120+
* as there is no intermediate app image in that case.
121+
*
122+
* <p>The step is not an input of this task. If its result depends on state outside the app image, register
123+
* that state as an additional task input.
124+
*/
125+
@Internal
126+
public abstract Property<Action<Directory>> getPostAppImageStep();
127+
115128
@OutputDirectory
116129
public abstract DirectoryProperty getDestination();
117130

@@ -138,6 +151,12 @@ public void runJpackage() throws Exception {
138151

139152
validateHostSystem(arch, os);
140153

154+
boolean packagesRequested = getPackageTypes().get().stream().anyMatch(t -> !"app-image".equals(t));
155+
if (getPostAppImageStep().isPresent() && getSingleStepPackaging().get() && packagesRequested) {
156+
throw new InvalidUserDataException("'postAppImageStep' cannot be combined with 'singleStepPackaging', "
157+
+ "because single-step packaging builds the packages without an intermediate app image");
158+
}
159+
141160
Directory resourcesDir = getTempDirectory().get().dir("jpackage-resources");
142161
//noinspection ResultOfMethodCallIgnored
143162
resourcesDir.getAsFile().mkdirs();
@@ -171,6 +190,9 @@ public void runJpackage() throws Exception {
171190
appRootFolder = new File(appImageFolder, "lib");
172191
}
173192
copyAdditionalRessourcesToImageFolder(appRootFolder);
193+
if (getPostAppImageStep().isPresent()) {
194+
getPostAppImageStep().get().execute(getDestination().get().dir(appImageFolder.getName()));
195+
}
174196
}
175197

176198
if (getSingleStepPackaging().get()) {

‎src/test/java/org/gradlex/javamodule/packaging/test/JavaModulePackagingOptionsTest.java‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,4 +221,37 @@ void works_if_module_path_contains_spaces() {
221221

222222
build.build(":app:jpackage");
223223
}
224+
225+
@Test
226+
void can_run_a_post_app_image_step() {
227+
build.appBuildFile.appendText("""
228+
javaModulePackaging {
229+
allTargets {
230+
postAppImageStep.set(Action<Directory> {
231+
file("marker.txt").asFile.writeText("touched")
232+
})
233+
}
234+
}
235+
""");
236+
237+
build.build(":app:jpackage");
238+
239+
assertThat(build.appImageFolder().getAsPath()).isDirectoryRecursivelyContaining("glob:**/marker.txt");
240+
}
241+
242+
@Test
243+
void cannot_combine_post_app_image_step_with_single_step_packaging() {
244+
build.appBuildFile.appendText("""
245+
javaModulePackaging {
246+
allTargets {
247+
singleStepPackaging.set(true)
248+
postAppImageStep.set(Action<Directory> { })
249+
}
250+
}
251+
""");
252+
253+
var result = build.fail(":app:jpackage");
254+
255+
assertThat(result.getOutput()).contains("'postAppImageStep' cannot be combined with 'singleStepPackaging'");
256+
}
224257
}

0 commit comments

Comments
 (0)