added documentation
This commit is contained in:
113
README.md
Normal file
113
README.md
Normal file
@@ -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.
|
@@ -256,4 +256,13 @@ public class Common {
|
||||
public static <T> Stream<T> opt2Stream(Optional<T> opt) {
|
||||
return opt.map(Stream::of).orElse(Stream.empty());
|
||||
}
|
||||
|
||||
public static <T> Optional<T> or(Supplier<T> ...suppliers) {
|
||||
Optional<T> result = Optional.empty();
|
||||
for(Supplier<T> supplier : suppliers) {
|
||||
T value = supplier.get();
|
||||
if(value != null) return Optional.of(value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
@@ -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
|
||||
* <a href="https://docs.gradle.org/current/javadoc/org/gradle/api/tasks/bundling/AbstractArchiveTask.html#isPreserveFileTimestamps--">AbstractArchiveTask.isPreserveFileTimestamps</a>
|
||||
|
@@ -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
|
||||
|
@@ -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<JarFile> 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<String, Map<String, Object>> 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<JarFile> classpath = new ArrayList<>();
|
||||
URL libraryTocResource = Launcher.class.getClassLoader().getResource(Constants.LIBRARIES_TOC);
|
||||
|
@@ -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<String> mainClassOpt = Optional.ofNullable(mainClassName);
|
||||
runner.accept(Optional.ofNullable(mainClassName)
|
||||
.or(() -> mainModule.getDescriptor().mainClass())
|
||||
|
@@ -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<ExtractLauncherTask> extractLauncherTaskProvider;
|
||||
|
||||
@Getter(onMethod_ = {@Input, @Optional})
|
||||
private final Property<String> mainClass;
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getMainClass();
|
||||
|
||||
@Getter(onMethod_ = {@Input, @Optional})
|
||||
private final Property<String> mainModule;
|
||||
@Input
|
||||
@Optional
|
||||
public abstract Property<String> getMainModule();
|
||||
|
||||
private final Properties javaAgents = new Properties();
|
||||
|
||||
@Getter(onMethod_ = {@Input})
|
||||
private final Map<String, String> systemProperties = new TreeMap<>();
|
||||
@Input
|
||||
public abstract MapProperty<String, String> getSystemProperties();
|
||||
|
||||
@Getter(onMethod_ = {@Input})
|
||||
private final ListProperty<String> extraClasspath;
|
||||
@Input
|
||||
public abstract ListProperty<String> 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<String> 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<String, String> entry : systemProperties.entrySet()) {
|
||||
for(Map.Entry<String, String> entry : getSystemProperties().get().entrySet()) {
|
||||
props.setProperty(entry.getKey(), entry.getValue());
|
||||
}
|
||||
props.store(zipOutputStream, null);
|
||||
|
@@ -10,10 +10,14 @@ import org.gradle.api.tasks.JavaExec;
|
||||
import org.gradle.api.tasks.bundling.Jar;
|
||||
|
||||
public class EnvelopePlugin implements Plugin<Project> {
|
||||
|
||||
public static final String ENVELOPE_GROUP_NAME = " envelope";
|
||||
|
||||
@Override
|
||||
public void apply(Project project) {
|
||||
project.getPluginManager().apply(JavaPlugin.class);
|
||||
Provider<EnvelopeJarTask> 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<Project> {
|
||||
});
|
||||
Provider<JavaExec> 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);
|
||||
});
|
||||
|
Reference in New Issue
Block a user