From a6098174e31db56f4d2afa1040e18ddcc0d19b6a Mon Sep 17 00:00:00 2001 From: Walter Oggioni Date: Tue, 26 Sep 2023 16:29:51 +0800 Subject: [PATCH] added documentation --- README.md | 113 ++++++++++++++++++ .../java/net/woggioni/envelope/Common.java | 9 ++ .../java/net/woggioni/envelope/Constants.java | 7 ++ gradle.properties | 2 +- .../java/net/woggioni/envelope/Launcher.java | 20 +++- .../net/woggioni/envelope/MainRunner.java | 1 + .../gradle/envelope/EnvelopeJarTask.java | 63 +++++----- .../gradle/envelope/EnvelopePlugin.java | 8 +- 8 files changed, 185 insertions(+), 38 deletions(-) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..0030986 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +## Overview +Envelope is a simple Gradle plugin that allows you to create an executable jar file +that includes all runtime dependencies and can be executed with a simple + +```bash +java -jar my-app.jar +``` +It supports JPMS, embedded system properties, Java agents, extra folders to be added to classpath. + +### Usage + +Declare the plugin in your build's `settings.gradle` like this +```groovy + +pluginManagement { + repositories { + maven { + url = 'https://woggioni.net/mvn/' + } + } + + plugins { + id "net.woggioni.gradle.envelope" version "2023.09.25" + } +} +``` + +Then add it to a project's `build.gradle` + +```groovy +plugins { + id 'net.woggioni.gradle.envelope' +} + +envelopeJar { + mainClass = 'your.main.Class' +} +``` + +The plugin adds 2 tasks to your project: + +- `envelopeJar` of type `net.woggioni.gradle.envelope.EnvelopeJarTask` that creates the executable jar in the project's libraries folder +- `envelopeRun` of type `org.gradle.api.tasks.JavaExec` which launches the jar created by the `envelopeJar` task + +### Configuration + +`EnvelopeJarTask` has several properties useful for configuration purposes: + +###### mainClass + +This string property sets the class that will be searched for the `main` method to start the application + +###### mainModule + +When this string property is set, the jar file will be started in JPMS mode (if running on Java 9+) and +this module will be searched for the main class, if the `mainClass` is not set the main class specified +in the module descriptor will be loaded instead + +###### systemProperties + +This is a map that contains Java system properties that will be set before your application starts + +###### extraClasspath + +This is a list of strings representing filesystem paths that will be added to the classpath (if running in classpath mode) +or to the module path (if running in JPMS mode) when the application starts. + +Relative paths and interpolation with Java System properties and environmental variables are supported: + +e.g. + +This looks for a `plugin` folder in the user's home directory +``` +${env:HOME}/plugins +``` + +Same using Java system properties instead +``` +${sys:user.home}/plugins +``` + +###### javaAgent +This is a method accepting 2 strings, the first is the Java agent classname and the second one is the java agent arguments. +It can be invoked multiple times to setup multiple java agents for the same JAR file. +All the java agents will be invoked before the application startup. + +### Example + +```groovy +plugins { + id 'net.woggioni.gradle.envelope' +} + +envelopeJar { + mainClass = 'your.main.Class' + mainModule = 'your.main.module' + + systemProperties = [ + 'some.property' : 'Some value' + ] + + extraClasspath = ["plugins"] + + javaAgent('your.java.agent.Class', 'optional agent arguments') +} +``` + +### Limitations + +This plugin requires Gradle >= 6.0 and Java >=0 8 to build the executable jar file. +The assembled envelope jar requires and Java >= 8 to run, if only `mainClass` is specified, +if both `mainModule` and `mainClass` are specified the generated jar file will (try to) run in classpath mode on Java 8 +and in JPMS mode on Java > 8. \ No newline at end of file diff --git a/common/src/main/java/net/woggioni/envelope/Common.java b/common/src/main/java/net/woggioni/envelope/Common.java index d95238c..13afc92 100644 --- a/common/src/main/java/net/woggioni/envelope/Common.java +++ b/common/src/main/java/net/woggioni/envelope/Common.java @@ -256,4 +256,13 @@ public class Common { public static Stream opt2Stream(Optional opt) { return opt.map(Stream::of).orElse(Stream.empty()); } + + public static Optional or(Supplier ...suppliers) { + Optional result = Optional.empty(); + for(Supplier supplier : suppliers) { + T value = supplier.get(); + if(value != null) return Optional.of(value); + } + return result; + } } diff --git a/common/src/main/java/net/woggioni/envelope/Constants.java b/common/src/main/java/net/woggioni/envelope/Constants.java index 345c169..b1f366d 100644 --- a/common/src/main/java/net/woggioni/envelope/Constants.java +++ b/common/src/main/java/net/woggioni/envelope/Constants.java @@ -26,6 +26,13 @@ public class Constants { public static final String ENTRY_HASH = "SHA-256-Digest"; } + public static class JvmProperties { + private static final String PREFIX = "envelope."; + public static final String MAIN_MODULE = PREFIX + "main.module"; + public static final String MAIN_CLASS = PREFIX + "main.class"; + public static final String EXTRA_CLASSPATH = PREFIX + "extra.classpath"; + } + /** * This value is used as a default file timestamp for all the zip entries when * AbstractArchiveTask.isPreserveFileTimestamps diff --git a/gradle.properties b/gradle.properties index f4c9075..d824e97 100644 --- a/gradle.properties +++ b/gradle.properties @@ -3,5 +3,5 @@ publishMavenRepositoryUrl=https://mvn.woggioni.net/ lys.version = 2023.08.28 -version.envelope=2023.08.28 +version.envelope=2023.09.25 version.gradle=7.6 diff --git a/launcher/src/main/java/net/woggioni/envelope/Launcher.java b/launcher/src/main/java/net/woggioni/envelope/Launcher.java index e35f1b6..5b8dcb0 100644 --- a/launcher/src/main/java/net/woggioni/envelope/Launcher.java +++ b/launcher/src/main/java/net/woggioni/envelope/Launcher.java @@ -19,7 +19,6 @@ import java.util.Collections; import java.util.Enumeration; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Properties; import java.util.TreeMap; import java.util.function.Consumer; @@ -30,6 +29,7 @@ import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; +import static net.woggioni.envelope.Common.or; import static net.woggioni.envelope.Constants.EXTRA_CLASSPATH_ENTRY_SEPARATOR; public class Launcher { @@ -78,7 +78,10 @@ public class Launcher { } private static Stream getExtraClasspath(Attributes mainAttributes) { - return Common.opt2Stream(Optional.ofNullable(mainAttributes.getValue(Constants.ManifestAttributes.EXTRA_CLASSPATH)) + return Common.opt2Stream(or( + () -> System.getProperty(Constants.JvmProperties.EXTRA_CLASSPATH), + () -> mainAttributes.getValue(Constants.ManifestAttributes.EXTRA_CLASSPATH) + ) .map(manifestAttribute -> { Map> dictMap = createContextMap(); return Common.renderTemplate(manifestAttribute, Collections.emptyMap(), dictMap); @@ -93,8 +96,9 @@ public class Launcher { cursor, extraClasspathString.length() ); + String classpathEntry = extraClasspathString.substring(cursor, sep < 0 ? extraClasspathString.length() : sep); + paths.add(classpathEntry); if(sep < 0) break; - paths.add(extraClasspathString.substring(cursor, sep)); cursor = sep + 1; } return paths; @@ -143,8 +147,14 @@ public class Launcher { Manifest mf = currentJar.getManifest(); Attributes mainAttributes = mf.getMainAttributes(); - String mainClassName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_CLASS); - String mainModuleName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_MODULE); + String mainClassName = or( + () -> System.getProperty(Constants.JvmProperties.MAIN_CLASS), + () -> mainAttributes.getValue(Constants.ManifestAttributes.MAIN_CLASS) + ).orElse(null); + String mainModuleName = or( + () -> System.getProperty(Constants.JvmProperties.MAIN_MODULE), + () -> mainAttributes.getValue(Constants.ManifestAttributes.MAIN_MODULE) + ).orElse(null); StringBuilder sb = new StringBuilder(); List classpath = new ArrayList<>(); URL libraryTocResource = Launcher.class.getClassLoader().getResource(Constants.LIBRARIES_TOC); diff --git a/launcher/src/main/java11/net/woggioni/envelope/MainRunner.java b/launcher/src/main/java11/net/woggioni/envelope/MainRunner.java index c69de34..26691e0 100644 --- a/launcher/src/main/java11/net/woggioni/envelope/MainRunner.java +++ b/launcher/src/main/java11/net/woggioni/envelope/MainRunner.java @@ -85,6 +85,7 @@ class MainRunner { ModuleLayer layer = controller.layer(); Module mainModule = layer.findModule(mainModuleName).orElseThrow( () -> new IllegalStateException(String.format("Main module '%s' not found", mainModuleName))); + Thread.currentThread().setContextClassLoader(mainModule.getClassLoader()); Optional mainClassOpt = Optional.ofNullable(mainClassName); runner.accept(Optional.ofNullable(mainClassName) .or(() -> mainModule.getDescriptor().mainClass()) diff --git a/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java b/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java index 8e40246..a3c7e7d 100644 --- a/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java +++ b/src/main/java/net/woggioni/gradle/envelope/EnvelopeJarTask.java @@ -18,9 +18,11 @@ import org.gradle.api.internal.file.copy.CopyActionProcessingStream; import org.gradle.api.internal.file.copy.FileCopyDetailsInternal; import org.gradle.api.java.archives.internal.DefaultManifest; import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.BasePlugin; import org.gradle.api.plugins.BasePluginExtension; import org.gradle.api.plugins.JavaApplication; import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.MapProperty; import org.gradle.api.provider.Property; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.Input; @@ -60,10 +62,12 @@ import java.util.zip.ZipOutputStream; import static java.util.zip.Deflater.BEST_COMPRESSION; import static java.util.zip.Deflater.NO_COMPRESSION; +import static net.woggioni.gradle.envelope.EnvelopePlugin.ENVELOPE_GROUP_NAME; @SuppressWarnings({"unused" }) -public class EnvelopeJarTask extends AbstractArchiveTask { +public abstract class EnvelopeJarTask extends AbstractArchiveTask { + private static final String DEFAULT_ARCHIVE_APPENDIX = ENVELOPE_GROUP_NAME; private static final String MINIMUM_GRADLE_VERSION = "6.0"; private static final String EXTRACT_LAUNCHER_TASK_NAME = "extractEnvelopeLauncher"; @@ -76,19 +80,21 @@ public class EnvelopeJarTask extends AbstractArchiveTask { private final Provider extractLauncherTaskProvider; - @Getter(onMethod_ = {@Input, @Optional}) - private final Property mainClass; + @Input + @Optional + public abstract Property getMainClass(); - @Getter(onMethod_ = {@Input, @Optional}) - private final Property mainModule; + @Input + @Optional + public abstract Property getMainModule(); private final Properties javaAgents = new Properties(); - @Getter(onMethod_ = {@Input}) - private final Map systemProperties = new TreeMap<>(); + @Input + public abstract MapProperty getSystemProperties(); - @Getter(onMethod_ = {@Input}) - private final ListProperty extraClasspath; + @Input + public abstract ListProperty getExtraClasspath(); private final org.gradle.api.java.archives.Manifest manifest; @@ -128,10 +134,6 @@ public class EnvelopeJarTask extends AbstractArchiveTask { javaAgents.put(className, args); } - public void systemProperty(String key, String value) { - systemProperties.put(key, value); - } - public void includeLibraries(Object... files) { into(Constants.LIBRARIES_FOLDER, (copySpec) -> copySpec.from(files)); } @@ -148,23 +150,21 @@ public class EnvelopeJarTask extends AbstractArchiveTask { } getInputs().files(extractLauncherTaskProvider); - setGroup("build"); + setGroup(BasePlugin.BUILD_GROUP); setDescription("Creates an executable jar file, embedding all of its runtime dependencies"); BasePluginExtension basePluginExtension = getProject().getExtensions().getByType(BasePluginExtension.class); - getDestinationDirectory().set(basePluginExtension.getDistsDirectory()); + getDestinationDirectory().set(basePluginExtension.getLibsDirectory()); getArchiveBaseName().convention(getProject().getName()); getArchiveExtension().convention("jar"); getArchiveVersion().convention(getProject().getVersion().toString()); - getArchiveAppendix().convention("envelope"); + getArchiveAppendix().convention(DEFAULT_ARCHIVE_APPENDIX); manifest = new DefaultManifest(fileResolver); - mainClass = objects.property(String.class); - mainModule = objects.property(String.class); - extraClasspath = objects.listProperty(String.class); + getSystemProperties().convention(new TreeMap<>()); JavaApplication javaApplication = getProject().getExtensions().findByType(JavaApplication.class); if(!Objects.isNull(javaApplication)) { - mainClass.convention(javaApplication.getMainClass()); - mainModule.convention(javaApplication.getMainModule()); + getMainClass().convention(javaApplication.getMainClass()); + getMainModule().convention(javaApplication.getMainModule()); } from(getProject().tarTree(extractLauncherTaskProvider.map(ExtractLauncherTask::getLauncherTar)), copySpec -> exclude(JarFile.MANIFEST_NAME)); } @@ -292,15 +292,18 @@ public class EnvelopeJarTask extends AbstractArchiveTask { mainAttributes.put(new Attributes.Name("Can-Redefine-Classes"), "true"); mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true"); String separator = "" + Constants.EXTRA_CLASSPATH_ENTRY_SEPARATOR; - String extraClasspath = EnvelopeJarTask.this.extraClasspath.get().stream() - .map(it -> it.replace(separator, separator + separator) - ).collect(Collectors.joining(separator)); - mainAttributes.put(new Attributes.Name(Constants.ManifestAttributes.EXTRA_CLASSPATH), extraClasspath); - if(mainClass.isPresent()) { - mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, mainClass.get()); + ListProperty extraClasspath = EnvelopeJarTask.this.getExtraClasspath(); + if(extraClasspath.isPresent()) { + String extraClasspathString = extraClasspath.get().stream() + .map(it -> it.replace(separator, separator + separator) + ).collect(Collectors.joining(separator)); + mainAttributes.put(new Attributes.Name(Constants.ManifestAttributes.EXTRA_CLASSPATH), extraClasspathString); } - if(mainModule.isPresent()) { - mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, mainModule.get()); + if(getMainClass().isPresent()) { + mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, getMainClass().get()); + } + if(getMainModule().isPresent()) { + mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, getMainModule().get()); } MessageDigest md = MessageDigest.getInstance("SHA-256"); @@ -344,7 +347,7 @@ public class EnvelopeJarTask extends AbstractArchiveTask { zipEntry.setMethod(ZipEntry.DEFLATED); zipOutputStream.putNextEntry(zipEntry); Properties props = new Properties(); - for(Map.Entry entry : systemProperties.entrySet()) { + for(Map.Entry entry : getSystemProperties().get().entrySet()) { props.setProperty(entry.getKey(), entry.getValue()); } props.store(zipOutputStream, null); diff --git a/src/main/java/net/woggioni/gradle/envelope/EnvelopePlugin.java b/src/main/java/net/woggioni/gradle/envelope/EnvelopePlugin.java index 86f1589..e3f955a 100644 --- a/src/main/java/net/woggioni/gradle/envelope/EnvelopePlugin.java +++ b/src/main/java/net/woggioni/gradle/envelope/EnvelopePlugin.java @@ -10,10 +10,14 @@ import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.bundling.Jar; public class EnvelopePlugin implements Plugin { + + public static final String ENVELOPE_GROUP_NAME = " envelope"; + @Override public void apply(Project project) { + project.getPluginManager().apply(JavaPlugin.class); Provider envelopeJarTaskProvider = project.getTasks().register("envelopeJar", EnvelopeJarTask.class, t -> { - t.setGroup(BasePlugin.BUILD_GROUP); + t.setGroup(ENVELOPE_GROUP_NAME); t.setDescription("Package the application in a single executable jar file"); t.includeLibraries(project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME)); t.includeLibraries(project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class)); @@ -23,7 +27,7 @@ public class EnvelopePlugin implements Plugin { }); Provider envelopeRunTaskProvider = project.getTasks().register("envelopeRun", JavaExec.class, t -> { t.getInputs().files(envelopeJarTaskProvider); - t.setGroup("envelope"); + t.setGroup(ENVELOPE_GROUP_NAME); t.setDescription("Run the application in the envelope jar"); t.classpath(envelopeJarTaskProvider); });