diff --git a/CHANGELOG.md b/CHANGELOG.md index 77f8f34..f21e0dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index f04042d..5edd75d 100644 --- a/README.md +++ b/README.md @@ -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 { + // '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` 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). diff --git a/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java b/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java index f7806dc..2160862 100644 --- a/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java +++ b/src/main/java/org/gradlex/javamodule/packaging/JavaModulePackagingExtension.java @@ -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()); diff --git a/src/main/java/org/gradlex/javamodule/packaging/model/Target.java b/src/main/java/org/gradlex/javamodule/packaging/model/Target.java index 84a64e1..00e4d67 100644 --- a/src/main/java/org/gradlex/javamodule/packaging/model/Target.java +++ b/src/main/java/org/gradlex/javamodule/packaging/model/Target.java @@ -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; @@ -26,6 +28,12 @@ public abstract class Target { public abstract Property 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> getPostAppImageStep(); + @Inject public Target(String name) { this.name = name; diff --git a/src/main/java/org/gradlex/javamodule/packaging/tasks/Jpackage.java b/src/main/java/org/gradlex/javamodule/packaging/tasks/Jpackage.java index 3f7b174..ca1fcf9 100644 --- a/src/main/java/org/gradlex/javamodule/packaging/tasks/Jpackage.java +++ b/src/main/java/org/gradlex/javamodule/packaging/tasks/Jpackage.java @@ -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; @@ -112,6 +114,17 @@ public abstract class Jpackage extends DefaultTask { @Input public abstract Property 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. + * + *

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> getPostAppImageStep(); + @OutputDirectory public abstract DirectoryProperty getDestination(); @@ -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(); @@ -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()) { diff --git a/src/test/java/org/gradlex/javamodule/packaging/test/JavaModulePackagingOptionsTest.java b/src/test/java/org/gradlex/javamodule/packaging/test/JavaModulePackagingOptionsTest.java index f28d073..a5bba36 100644 --- a/src/test/java/org/gradlex/javamodule/packaging/test/JavaModulePackagingOptionsTest.java +++ b/src/test/java/org/gradlex/javamodule/packaging/test/JavaModulePackagingOptionsTest.java @@ -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 { + 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 { }) + } + } + """); + + var result = build.fail(":app:jpackage"); + + assertThat(result.getOutput()).contains("'postAppImageStep' cannot be combined with 'singleStepPackaging'"); + } }