added documentation

This commit is contained in:
2023-09-26 16:29:51 +08:00
parent 07817f80f9
commit a6098174e3
8 changed files with 185 additions and 38 deletions

113
README.md Normal file
View 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.

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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);

View File

@@ -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())

View File

@@ -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);

View File

@@ -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);
});