Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Java Module Packaging Gradle Plugin - Changelog

## Unreleased
* [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

## Version 1.3
- [#122](https://github.com/gradlex-org/java-module-packaging/issues/122) - 'fatModueJar' tasks to re-package module Jars into one Jar with launcher
- [#102](https://github.com/gradlex-org/java-module-packaging/issues/102) - Configure all targets via 'allTargets' notation
Expand Down
20 changes: 20 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,26 @@ javaModulePackaging {

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

### How can I modify the app image before the packages are built from it?

Configure a `postAppImageStep` on a target. It receives the app image directory after all resources have been copied
into it and runs before the OS-specific packages are built from that image, so modifications end up in all packages.

```kotlin
javaModulePackaging {
allTargets {
postAppImageStep = Action<Directory> {
// 'this' is the app image directory, e.g. run the packaged launcher once to train a
// JEP 514 AOT cache and store it in the image
}
}
}
```

The step is not a task input. If its result depends on state outside the app image, register that state as an
additional input of the `jpackage<Target>` task. The option cannot be combined with `singleStepPackaging`, because
single-step packaging builds the packages without an intermediate app image.

### How does a `fatModuleJar` work?

The structure of the Jar follows the structure defined by the [Jenesis Launcher](https://github.com/raphw/jenesis-launcher).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -337,6 +337,7 @@ private void registerTargetSpecificTasks(
t.getAppImageOptions().convention(target.getAppImageOptions());
t.getPackageTypes().convention(target.getPackageTypes());
t.getSingleStepPackaging().convention(target.getSingleStepPackaging());
t.getPostAppImageStep().convention(target.getPostAppImageStep());
t.getResources().from(getResources());
t.getTargetResources().from(target.getTargetResources());
t.getVerbose().convention(getVerbose());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
package org.gradlex.javamodule.packaging.model;

import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.Directory;
import org.gradle.api.provider.ListProperty;
import org.gradle.api.provider.Property;
import org.jspecify.annotations.NullMarked;
Expand All @@ -26,6 +28,12 @@ public abstract class Target {

public abstract Property<Boolean> getSingleStepPackaging();

/**
* An optional step to run on the created app image, after all resources have been copied into it and before
* the OS-specific packages are built from that image. Not supported together with 'singleStepPackaging'.
*/
public abstract Property<Action<Directory>> getPostAppImageStep();

@Inject
public Target(String name) {
this.name = name;
Expand Down
22 changes: 22 additions & 0 deletions src/main/java/org/gradlex/javamodule/packaging/tasks/Jpackage.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@
import java.util.List;
import java.util.stream.Collectors;
import javax.inject.Inject;
import org.gradle.api.Action;
import org.gradle.api.DefaultTask;
import org.gradle.api.InvalidUserDataException;
import org.gradle.api.file.ConfigurableFileCollection;
import org.gradle.api.file.Directory;
import org.gradle.api.file.DirectoryProperty;
Expand Down Expand Up @@ -112,6 +114,17 @@ public abstract class Jpackage extends DefaultTask {
@Input
public abstract Property<Boolean> getVerbose();

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

@OutputDirectory
public abstract DirectoryProperty getDestination();

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

validateHostSystem(arch, os);

boolean packagesRequested = getPackageTypes().get().stream().anyMatch(t -> !"app-image".equals(t));
if (getPostAppImageStep().isPresent() && getSingleStepPackaging().get() && packagesRequested) {
throw new InvalidUserDataException("'postAppImageStep' cannot be combined with 'singleStepPackaging', "
+ "because single-step packaging builds the packages without an intermediate app image");
}

Directory resourcesDir = getTempDirectory().get().dir("jpackage-resources");
//noinspection ResultOfMethodCallIgnored
resourcesDir.getAsFile().mkdirs();
Expand Down Expand Up @@ -171,6 +190,9 @@ public void runJpackage() throws Exception {
appRootFolder = new File(appImageFolder, "lib");
}
copyAdditionalRessourcesToImageFolder(appRootFolder);
if (getPostAppImageStep().isPresent()) {
getPostAppImageStep().get().execute(getDestination().get().dir(appImageFolder.getName()));
}
}

if (getSingleStepPackaging().get()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,4 +221,37 @@ void works_if_module_path_contains_spaces() {

build.build(":app:jpackage");
}

@Test
void can_run_a_post_app_image_step() {
build.appBuildFile.appendText("""
javaModulePackaging {
allTargets {
postAppImageStep.set(Action<Directory> {
file("marker.txt").asFile.writeText("touched")
})
}
}
""");

build.build(":app:jpackage");

assertThat(build.appImageFolder().getAsPath()).isDirectoryRecursivelyContaining("glob:**/marker.txt");
}

@Test
void cannot_combine_post_app_image_step_with_single_step_packaging() {
build.appBuildFile.appendText("""
javaModulePackaging {
allTargets {
singleStepPackaging.set(true)
postAppImageStep.set(Action<Directory> { })
}
}
""");

var result = build.fail(":app:jpackage");

assertThat(result.getOutput()).contains("'postAppImageStep' cannot be combined with 'singleStepPackaging'");
}
}