Compare commits
10 Commits
54bf96c29a
...
9c63bb50be
Author | SHA1 | Date | |
---|---|---|---|
9c63bb50be
|
|||
26dd1aaf86
|
|||
0f8d64ddd7
|
|||
9864df4893
|
|||
a6098174e3
|
|||
07817f80f9
|
|||
995d937b25
|
|||
a1139bdc1c
|
|||
d2040fb02a
|
|||
07de45abe5
|
22
LICENSE.md
Normal file
22
LICENSE.md
Normal file
@@ -0,0 +1,22 @@
|
||||
Copyright 2023 Walter Oggioni
|
||||
|
||||
Permission is hereby granted, free of charge, to any person
|
||||
obtaining a copy of this software and associated documentation
|
||||
files (the "Software"), to deal in the Software without
|
||||
restriction, including without limitation the rights to use,
|
||||
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following
|
||||
conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
||||
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
||||
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
||||
OTHER DEALINGS IN THE SOFTWARE.
|
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.
|
@@ -1,24 +1,27 @@
|
||||
package net.woggioni.envelope;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Map;
|
||||
import java.util.MissingFormatArgumentException;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.ZipEntry;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import java.io.IOException;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Common {
|
||||
@@ -114,4 +117,152 @@ public class Common {
|
||||
OutputStream result = new FileOutputStream(file);
|
||||
return buffered ? new BufferedOutputStream(result) : result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param template Template text containing the variables to be replaced by this method. <br>
|
||||
* Variables follow the format ${variable_name}. <br>
|
||||
* Example: <br>
|
||||
* "This template was created by ${author}."
|
||||
* @param valuesMap A hashmap with the values of the variables to be replaced. <br>
|
||||
* The key is the variable name and the value is the value to be replaced in the template. <br>
|
||||
* Example: <br>
|
||||
* {"author" => "John Doe"}
|
||||
* @return The template text (String) with the variable names replaced by the values passed in the map. <br>
|
||||
* If any of the variable names is not contained in the map it will be replaced by an empty string. <br>
|
||||
* Example: <br>
|
||||
* "This template was created by John Doe."
|
||||
*/
|
||||
public static String renderTemplate(String template, Map<String, Object> valuesMap) {
|
||||
return renderTemplate(template, valuesMap, null);
|
||||
}
|
||||
|
||||
|
||||
public static int indexOfWithEscape(String haystack, char needle, char escape, int begin, int end) {
|
||||
int result = -1;
|
||||
int cursor = begin;
|
||||
if (end == 0) {
|
||||
end = haystack.length();
|
||||
}
|
||||
int escapeCount = 0;
|
||||
while (cursor < end) {
|
||||
char c = haystack.charAt(cursor);
|
||||
if (escapeCount > 0) {
|
||||
--escapeCount;
|
||||
if (c == escape) {
|
||||
result = -1;
|
||||
}
|
||||
} else if (escapeCount == 0) {
|
||||
if (c == escape) {
|
||||
++escapeCount;
|
||||
}
|
||||
if (c == needle) {
|
||||
result = cursor;
|
||||
}
|
||||
}
|
||||
if (result >= 0 && escapeCount == 0) {
|
||||
break;
|
||||
}
|
||||
++cursor;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public static String renderTemplate(
|
||||
String template,
|
||||
Map<String, Object> valuesMap,
|
||||
Map<String, Map<String, Object>> dictMap) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Object absent = new Object();
|
||||
int cursor = 0;
|
||||
TokenScanner tokenScanner = new TokenScanner(template, '$', '$');
|
||||
while (cursor < template.length()) {
|
||||
tokenScanner.next();
|
||||
int nextPlaceHolder;
|
||||
switch (tokenScanner.getTokenType()) {
|
||||
case TOKEN: {
|
||||
nextPlaceHolder = tokenScanner.getTokenIndex();
|
||||
while (cursor < nextPlaceHolder) {
|
||||
char ch = template.charAt(cursor++);
|
||||
sb.append(ch);
|
||||
}
|
||||
if (cursor + 1 < template.length() && template.charAt(cursor + 1) == '{') {
|
||||
String key;
|
||||
String context = null;
|
||||
String defaultValue = null;
|
||||
Object value;
|
||||
int end = template.indexOf('}', cursor + 1);
|
||||
int colon;
|
||||
if (dictMap == null)
|
||||
colon = -1;
|
||||
else {
|
||||
colon = indexOfWithEscape(template, ':', '\\', cursor + 1, template.length());
|
||||
if (colon >= end) colon = -1;
|
||||
}
|
||||
if (colon < 0) {
|
||||
key = template.substring(cursor + 2, end);
|
||||
value = valuesMap.getOrDefault(key, absent);
|
||||
} else {
|
||||
context = template.substring(cursor + 2, colon);
|
||||
int secondColon = indexOfWithEscape(template, ':', '\\', colon + 1, end);
|
||||
if (secondColon < 0) {
|
||||
key = template.substring(colon + 1, end);
|
||||
} else {
|
||||
key = template.substring(colon + 1, secondColon);
|
||||
defaultValue = template.substring(secondColon + 1, end);
|
||||
}
|
||||
value = Optional.ofNullable(dictMap.get(context))
|
||||
.map(m -> m.get(key))
|
||||
.orElse(absent);
|
||||
}
|
||||
if (value != absent) {
|
||||
sb.append(value.toString());
|
||||
} else {
|
||||
if (defaultValue != null) {
|
||||
sb.append(defaultValue);
|
||||
} else {
|
||||
throw new MissingFormatArgumentException(
|
||||
String.format("Missing value for placeholder '%s'",
|
||||
context == null ? key : context + ':' + key
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
cursor = end + 1;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case ESCAPE:
|
||||
nextPlaceHolder = tokenScanner.getTokenIndex();
|
||||
while (cursor < nextPlaceHolder) {
|
||||
char ch = template.charAt(cursor++);
|
||||
sb.append(ch);
|
||||
}
|
||||
cursor = nextPlaceHolder + 1;
|
||||
sb.append(template.charAt(cursor++));
|
||||
break;
|
||||
case END:
|
||||
default:
|
||||
nextPlaceHolder = template.length();
|
||||
while (cursor < nextPlaceHolder) {
|
||||
char ch = template.charAt(cursor++);
|
||||
sb.append(ch);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,10 @@
|
||||
package net.woggioni.envelope;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.GregorianCalendar;
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class Constants {
|
||||
@@ -16,13 +17,22 @@ public class Constants {
|
||||
public static final String SYSTEM_PROPERTIES_FILE = METADATA_FOLDER + "/system.properties";
|
||||
|
||||
public static final String LIBRARIES_TOC = METADATA_FOLDER + "/libraries.txt";
|
||||
public static final char EXTRA_CLASSPATH_ENTRY_SEPARATOR = ';';
|
||||
|
||||
public static class ManifestAttributes {
|
||||
public static final String MAIN_MODULE = "Executable-Jar-Main-Module";
|
||||
public static final String MAIN_CLASS = "Executable-Jar-Main-Class";
|
||||
public static final String EXTRA_CLASSPATH = "Executable-Jar-Extra-Classpath";
|
||||
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>
|
||||
|
74
common/src/main/java/net/woggioni/envelope/TokenScanner.java
Normal file
74
common/src/main/java/net/woggioni/envelope/TokenScanner.java
Normal file
@@ -0,0 +1,74 @@
|
||||
package net.woggioni.envelope;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
public class TokenScanner {
|
||||
|
||||
public enum TokenType {
|
||||
ESCAPE, TOKEN, END;
|
||||
}
|
||||
private final String haystack;
|
||||
private final char needle;
|
||||
private final char escape;
|
||||
private int begin;
|
||||
private final int end;
|
||||
|
||||
@Getter
|
||||
private int tokenIndex = -1;
|
||||
|
||||
@Getter
|
||||
private TokenType tokenType = null;
|
||||
|
||||
public TokenScanner(String haystack, char needle, char escape, int begin, int end) {
|
||||
this.haystack = haystack;
|
||||
this.needle = needle;
|
||||
this.escape = escape;
|
||||
this.begin = begin;
|
||||
this.end = end;
|
||||
}
|
||||
|
||||
public TokenScanner(String haystack, char needle, char escape, int begin) {
|
||||
this(haystack, needle, escape, begin, haystack.length());
|
||||
}
|
||||
|
||||
public TokenScanner(String haystack, char needle, char escape) {
|
||||
this(haystack, needle, escape, 0, haystack.length());
|
||||
}
|
||||
|
||||
public void next() {
|
||||
int result = -1;
|
||||
int cursor = begin;
|
||||
int escapeCount = 0;
|
||||
while(true) {
|
||||
if(cursor < end) {
|
||||
char c = haystack.charAt(cursor);
|
||||
if (escapeCount > 0) {
|
||||
--escapeCount;
|
||||
if(c == escape || c == needle) {
|
||||
tokenIndex = cursor - 1;
|
||||
tokenType = TokenType.ESCAPE;
|
||||
break;
|
||||
}
|
||||
} else if (escapeCount == 0) {
|
||||
if (c == escape) {
|
||||
++escapeCount;
|
||||
}
|
||||
if (c == needle) {
|
||||
result = cursor;
|
||||
}
|
||||
}
|
||||
if (result >= 0 && escapeCount == 0) {
|
||||
tokenIndex = result;
|
||||
tokenType = TokenType.TOKEN;
|
||||
break;
|
||||
}
|
||||
++cursor;
|
||||
} else {
|
||||
tokenIndex = result;
|
||||
tokenType = result < 0 ? TokenType.END :TokenType.TOKEN;
|
||||
break;
|
||||
}
|
||||
}
|
||||
begin = cursor + 1;
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
woggioniMavenRepositoryUrl=https://woggioni.net/mvn/
|
||||
publishMavenRepositoryUrl=https://mvn.woggioni.net/
|
||||
|
||||
lys.version = 0.2-SNAPSHOT
|
||||
lys.version = 2024.02.24
|
||||
|
||||
version.envelope=2023.03
|
||||
version.envelope=2024.02.28
|
||||
version.gradle=7.6
|
||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,6 @@
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-bin.zip
|
||||
networkTimeout=10000
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
12
gradlew
vendored
12
gradlew
vendored
@@ -55,7 +55,7 @@
|
||||
# Darwin, MinGW, and NonStop.
|
||||
#
|
||||
# (3) This script is generated from the Groovy template
|
||||
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||
# within the Gradle project.
|
||||
#
|
||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||
@@ -80,10 +80,10 @@ do
|
||||
esac
|
||||
done
|
||||
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
APP_NAME="Gradle"
|
||||
# This is normally unused
|
||||
# shellcheck disable=SC2034
|
||||
APP_BASE_NAME=${0##*/}
|
||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||
@@ -143,12 +143,16 @@ fi
|
||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||
case $MAX_FD in #(
|
||||
max*)
|
||||
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
MAX_FD=$( ulimit -H -n ) ||
|
||||
warn "Could not query maximum file descriptor limit"
|
||||
esac
|
||||
case $MAX_FD in #(
|
||||
'' | soft) :;; #(
|
||||
*)
|
||||
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||
# shellcheck disable=SC3045
|
||||
ulimit -n "$MAX_FD" ||
|
||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||
esac
|
||||
|
1
gradlew.bat
vendored
1
gradlew.bat
vendored
@@ -26,6 +26,7 @@ if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%"=="" set DIRNAME=.
|
||||
@rem This is normally unused
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
|
@@ -7,12 +7,15 @@ ext {
|
||||
setProperty('jpms.module.name', 'net.woggioni.envelope')
|
||||
}
|
||||
|
||||
|
||||
configurations {
|
||||
embedded {
|
||||
visible = false
|
||||
canBeConsumed = false
|
||||
}
|
||||
compileOnly.extendsFrom(embedded)
|
||||
compileOnly {
|
||||
extendsFrom(embedded)
|
||||
}
|
||||
tar {
|
||||
visible = true
|
||||
canBeConsumed = true
|
||||
@@ -22,7 +25,7 @@ configurations {
|
||||
|
||||
dependencies {
|
||||
embedded project(path: ":common", configuration: 'archives')
|
||||
embedded project(path: ":loader", configuration: 'archives')
|
||||
embedded project(path: ":loader", configuration: 'embed')
|
||||
}
|
||||
|
||||
java {
|
||||
|
@@ -10,16 +10,27 @@ import java.io.Reader;
|
||||
import java.lang.reflect.Method;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Enumeration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Properties;
|
||||
import java.util.TreeMap;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarEntry;
|
||||
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 {
|
||||
|
||||
@@ -53,6 +64,69 @@ public class Launcher {
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<String, Map<String, Object>> createContextMap() {
|
||||
Map<String, Map<String, Object>> dictMap = new TreeMap<>();
|
||||
dictMap.put("env", Collections.unmodifiableMap(System.getenv()
|
||||
.entrySet()
|
||||
.stream()
|
||||
.map(entry -> new AbstractMap.SimpleEntry<>(entry.getKey(), (Object) entry.getValue()))
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
|
||||
dictMap.put("sys", Collections.unmodifiableMap(System.getProperties().entrySet().stream()
|
||||
.map((Map.Entry<? super String, Object> entry) -> (Map.Entry<String, Object>) entry)
|
||||
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue))));
|
||||
return Collections.unmodifiableMap(dictMap);
|
||||
}
|
||||
|
||||
private static Stream<JarFile> getExtraClasspath(Attributes mainAttributes) {
|
||||
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);
|
||||
}).map(extraClasspathString -> {
|
||||
List<String> paths = new ArrayList<>();
|
||||
int cursor = 0;
|
||||
while(true) {
|
||||
int sep = Common.indexOfWithEscape(
|
||||
extraClasspathString,
|
||||
EXTRA_CLASSPATH_ENTRY_SEPARATOR,
|
||||
EXTRA_CLASSPATH_ENTRY_SEPARATOR,
|
||||
cursor,
|
||||
extraClasspathString.length()
|
||||
);
|
||||
String classpathEntry = extraClasspathString.substring(cursor, sep < 0 ? extraClasspathString.length() : sep);
|
||||
paths.add(classpathEntry);
|
||||
if(sep < 0) break;
|
||||
cursor = sep + 1;
|
||||
}
|
||||
return paths;
|
||||
}))
|
||||
.flatMap(List::stream)
|
||||
.map(Paths::get)
|
||||
.flatMap(new Function<Path, Stream<Path>>() {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public Stream<Path> apply(Path path) {
|
||||
if(Files.isDirectory(path)) {
|
||||
return Files.list(path).filter(childPath -> !Files.isDirectory(childPath));
|
||||
} else {
|
||||
return Stream.of(path);
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".jar"))
|
||||
.map(Path::toFile)
|
||||
.map(new Function<File, JarFile>() {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
public JarFile apply(File file) {
|
||||
return new JarFile(file);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public static void main(String[] args) {
|
||||
Enumeration<URL> it = Launcher.class.getClassLoader().getResources(Constants.SYSTEM_PROPERTIES_FILE);
|
||||
@@ -73,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);
|
||||
@@ -94,6 +174,8 @@ public class Launcher {
|
||||
else sb.append((char) c);
|
||||
}
|
||||
}
|
||||
|
||||
getExtraClasspath(mainAttributes).forEach(classpath::add);
|
||||
Consumer<Class<?>> runner = new Consumer<Class<?>>() {
|
||||
@Override
|
||||
@SneakyThrows
|
||||
|
@@ -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())
|
||||
|
@@ -7,12 +7,15 @@ ext {
|
||||
setProperty('jpms.module.name', 'net.woggioni.envelope.loader')
|
||||
}
|
||||
|
||||
compileJava11 {
|
||||
exclude('module-info.java')
|
||||
configurations {
|
||||
embed {
|
||||
canBeResolved = true
|
||||
canBeConsumed = true
|
||||
visible = true
|
||||
transitive = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
publishing {
|
||||
publications {
|
||||
maven(MavenPublication) {
|
||||
@@ -24,3 +27,24 @@ publishing {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
tasks.register('embedJar', Jar) { jar ->
|
||||
Provider<Jar> jarTaskProvider = tasks.named(JavaPlugin.JAR_TASK_NAME)
|
||||
jar.inputs.files(jarTaskProvider)
|
||||
archiveClassifier = 'embed'
|
||||
from(zipTree(jarTaskProvider.map { it.archiveFile} )) {
|
||||
exclude '**/module-info.class'
|
||||
}
|
||||
manifest{
|
||||
attributes([
|
||||
'Multi-Release': 'true'
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
artifacts {
|
||||
embed(embedJar)
|
||||
}
|
||||
|
||||
|
@@ -4,9 +4,11 @@ import lombok.SneakyThrows;
|
||||
import net.woggioni.envelope.loader.JarFile;
|
||||
import net.woggioni.envelope.loader.jar.Handler;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.module.ModuleDescriptor;
|
||||
import java.lang.module.ModuleFinder;
|
||||
import java.lang.module.ModuleReader;
|
||||
@@ -17,6 +19,8 @@ import java.net.URLStreamHandler;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.AbstractMap;
|
||||
import java.util.Arrays;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
@@ -27,16 +31,20 @@ import java.util.TreeMap;
|
||||
import java.util.jar.Attributes.Name;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import java.lang.module.FindException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.lang.module.InvalidModuleDescriptorException;
|
||||
|
||||
public class JarFileModuleFinder implements ModuleFinder {
|
||||
|
||||
private static final String MODULE_DESCRIPTOR_ENTRY = "module-info.class";
|
||||
private static final Name AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY = new Name("Automatic-Module-Name");
|
||||
|
||||
private static final String SERVICES_PREFIX = "META-INF/services/";
|
||||
private static class Patterns {
|
||||
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
|
||||
static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
|
||||
@@ -44,6 +52,66 @@ public class JarFileModuleFinder implements ModuleFinder {
|
||||
static final Pattern LEADING_DOTS = Pattern.compile("^\\.");
|
||||
static final Pattern TRAILING_DOTS = Pattern.compile("\\.$");
|
||||
}
|
||||
|
||||
|
||||
// keywords, boolean and null literals, not allowed in identifiers
|
||||
private static final Set<String> RESERVED = Set.of(
|
||||
"abstract",
|
||||
"assert",
|
||||
"boolean",
|
||||
"break",
|
||||
"byte",
|
||||
"case",
|
||||
"catch",
|
||||
"char",
|
||||
"class",
|
||||
"const",
|
||||
"continue",
|
||||
"default",
|
||||
"do",
|
||||
"double",
|
||||
"else",
|
||||
"enum",
|
||||
"extends",
|
||||
"final",
|
||||
"finally",
|
||||
"float",
|
||||
"for",
|
||||
"goto",
|
||||
"if",
|
||||
"implements",
|
||||
"import",
|
||||
"instanceof",
|
||||
"int",
|
||||
"interface",
|
||||
"long",
|
||||
"native",
|
||||
"new",
|
||||
"package",
|
||||
"private",
|
||||
"protected",
|
||||
"public",
|
||||
"return",
|
||||
"short",
|
||||
"static",
|
||||
"strictfp",
|
||||
"super",
|
||||
"switch",
|
||||
"synchronized",
|
||||
"this",
|
||||
"throw",
|
||||
"throws",
|
||||
"transient",
|
||||
"try",
|
||||
"void",
|
||||
"volatile",
|
||||
"while",
|
||||
"true",
|
||||
"false",
|
||||
"null",
|
||||
"_"
|
||||
);
|
||||
|
||||
private final Map<String, Map.Entry<ModuleReference, Handler>> modules;
|
||||
|
||||
@SneakyThrows
|
||||
@@ -150,20 +218,7 @@ public class JarFileModuleFinder implements ModuleFinder {
|
||||
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
||||
}
|
||||
} else {
|
||||
Manifest mf = jarFile.getManifest();
|
||||
moduleName = mf.getMainAttributes().getValue(AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY);
|
||||
if(moduleName == null) {
|
||||
moduleName = moduleNameFromURI(uri);
|
||||
}
|
||||
ModuleDescriptor.Builder mdb = ModuleDescriptor.newAutomaticModule(moduleName);
|
||||
mdb.packages(collectPackageNames(jarFile));
|
||||
|
||||
// Main-Class attribute if it exists
|
||||
String mainClass = mf.getMainAttributes().getValue(Name.MAIN_CLASS);
|
||||
if (mainClass != null) {
|
||||
mdb.mainClass(mainClass);
|
||||
}
|
||||
moduleDescriptor = mdb.build();
|
||||
moduleDescriptor = deriveModuleDescriptor(jarFile);
|
||||
}
|
||||
|
||||
modules.put(moduleDescriptor.name(),
|
||||
@@ -210,4 +265,233 @@ public class JarFileModuleFinder implements ModuleFinder {
|
||||
.stream().map(Map.Entry::getKey)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
private ModuleDescriptor deriveModuleDescriptor(JarFile jf)
|
||||
throws IOException
|
||||
{
|
||||
// Read Automatic-Module-Name attribute if present
|
||||
Manifest man = jf.getManifest();
|
||||
Attributes attrs = null;
|
||||
String moduleName = null;
|
||||
if (man != null) {
|
||||
attrs = man.getMainAttributes();
|
||||
if (attrs != null) {
|
||||
moduleName = attrs.getValue(AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY);
|
||||
}
|
||||
}
|
||||
|
||||
// Derive the version, and the module name if needed, from JAR file name
|
||||
String fn = jf.getName();
|
||||
int i = fn.lastIndexOf(File.separator);
|
||||
if (i != -1)
|
||||
fn = fn.substring(i + 1);
|
||||
|
||||
// drop ".jar"
|
||||
String name = fn.substring(0, fn.length() - 4);
|
||||
String vs = null;
|
||||
|
||||
// find first occurrence of -${NUMBER}. or -${NUMBER}$
|
||||
Matcher matcher = Patterns.DASH_VERSION.matcher(name);
|
||||
if (matcher.find()) {
|
||||
int start = matcher.start();
|
||||
|
||||
// attempt to parse the tail as a version string
|
||||
try {
|
||||
String tail = name.substring(start + 1);
|
||||
ModuleDescriptor.Version.parse(tail);
|
||||
vs = tail;
|
||||
} catch (IllegalArgumentException ignore) { }
|
||||
|
||||
name = name.substring(0, start);
|
||||
}
|
||||
|
||||
// Create builder, using the name derived from file name when
|
||||
// Automatic-Module-Name not present
|
||||
ModuleDescriptor.Builder builder;
|
||||
if (moduleName != null) {
|
||||
try {
|
||||
builder = ModuleDescriptor.newAutomaticModule(moduleName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
throw new FindException(AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY + ": " + e.getMessage());
|
||||
}
|
||||
} else {
|
||||
builder = ModuleDescriptor.newAutomaticModule(cleanModuleName(name));
|
||||
}
|
||||
|
||||
// module version if present
|
||||
if (vs != null)
|
||||
builder.version(vs);
|
||||
|
||||
// scan the names of the entries in the JAR file
|
||||
Map<Boolean, Set<String>> map = jf.versionedStream()
|
||||
.filter(e -> !e.isDirectory())
|
||||
.map(JarEntry::getName)
|
||||
.filter(e -> (e.endsWith(".class") ^ e.startsWith(SERVICES_PREFIX)))
|
||||
.collect(Collectors.partitioningBy(e -> e.startsWith(SERVICES_PREFIX),
|
||||
Collectors.toSet()));
|
||||
|
||||
Set<String> classFiles = map.get(Boolean.FALSE);
|
||||
Set<String> configFiles = map.get(Boolean.TRUE);
|
||||
|
||||
// the packages containing class files
|
||||
Set<String> packages = classFiles.stream()
|
||||
.map(this::toPackageName)
|
||||
.flatMap(Optional::stream)
|
||||
.distinct()
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// all packages are exported and open
|
||||
builder.packages(packages);
|
||||
|
||||
// map names of service configuration files to service names
|
||||
Set<String> serviceNames = configFiles.stream()
|
||||
.map(this::toServiceName)
|
||||
.flatMap(Optional::stream)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
// parse each service configuration file
|
||||
for (String sn : serviceNames) {
|
||||
JarEntry entry = jf.getJarEntry(SERVICES_PREFIX + sn);
|
||||
List<String> providerClasses = new ArrayList<>();
|
||||
try (InputStream in = jf.getInputStream(entry)) {
|
||||
BufferedReader reader
|
||||
= new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8));
|
||||
String cn;
|
||||
while ((cn = nextLine(reader)) != null) {
|
||||
if (!cn.isEmpty()) {
|
||||
String pn = packageName(cn);
|
||||
if (!packages.contains(pn)) {
|
||||
String msg = "Provider class " + cn + " not in module";
|
||||
throw new InvalidModuleDescriptorException(msg);
|
||||
}
|
||||
providerClasses.add(cn);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!providerClasses.isEmpty())
|
||||
builder.provides(sn, providerClasses);
|
||||
}
|
||||
|
||||
// Main-Class attribute if it exists
|
||||
if (attrs != null) {
|
||||
String mainClass = attrs.getValue(Attributes.Name.MAIN_CLASS);
|
||||
if (mainClass != null) {
|
||||
mainClass = mainClass.replace('/', '.');
|
||||
if (isClassName(mainClass)) {
|
||||
String pn = packageName(mainClass);
|
||||
if (packages.contains(pn)) {
|
||||
builder.mainClass(mainClass);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Optional<String> toServiceName(String cf) {
|
||||
assert cf.startsWith(SERVICES_PREFIX);
|
||||
int index = cf.lastIndexOf("/") + 1;
|
||||
if (index < cf.length()) {
|
||||
String prefix = cf.substring(0, index);
|
||||
if (prefix.equals(SERVICES_PREFIX)) {
|
||||
String sn = cf.substring(index);
|
||||
if (isClassName(sn))
|
||||
return Optional.of(sn);
|
||||
}
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
private static String packageName(String cn) {
|
||||
int index = cn.lastIndexOf('.');
|
||||
return (index == -1) ? "" : cn.substring(0, index);
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps the name of an entry in a JAR or ZIP file to a package name.
|
||||
*
|
||||
* @throws InvalidModuleDescriptorException if the name is a class file in
|
||||
* the top-level directory of the JAR/ZIP file (and it's not
|
||||
* module-info.class)
|
||||
*/
|
||||
private Optional<String> toPackageName(String name) {
|
||||
assert !name.endsWith("/");
|
||||
int index = name.lastIndexOf("/");
|
||||
if (index == -1) {
|
||||
if (name.endsWith(".class") && !name.equals(MODULE_DESCRIPTOR_ENTRY)) {
|
||||
String msg = name + " found in top-level directory"
|
||||
+ " (unnamed package not allowed in module)";
|
||||
throw new InvalidModuleDescriptorException(msg);
|
||||
}
|
||||
return Optional.empty();
|
||||
}
|
||||
|
||||
String pn = name.substring(0, index).replace('/', '.');
|
||||
if (isPackageName(pn)) {
|
||||
return Optional.of(pn);
|
||||
} else {
|
||||
// not a valid package name
|
||||
return Optional.empty();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next line from the given reader and trims it of comments and
|
||||
* leading/trailing white space.
|
||||
*
|
||||
* Returns null if the reader is at EOF.
|
||||
*/
|
||||
private String nextLine(BufferedReader reader) throws IOException {
|
||||
String ln = reader.readLine();
|
||||
if (ln != null) {
|
||||
int ci = ln.indexOf('#');
|
||||
if (ci >= 0)
|
||||
ln = ln.substring(0, ci);
|
||||
ln = ln.trim();
|
||||
}
|
||||
return ln;
|
||||
}
|
||||
|
||||
private static boolean isClassName(String name) {
|
||||
return isTypeName(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns {@code true} if the given name is a legal type name.
|
||||
*/ private static boolean isPackageName(String name) {
|
||||
return isTypeName(name);
|
||||
}
|
||||
|
||||
private static boolean isTypeName(String name) {
|
||||
int next;
|
||||
int off = 0;
|
||||
while ((next = name.indexOf('.', off)) != -1) {
|
||||
String id = name.substring(off, next);
|
||||
if (!isJavaIdentifier(id))
|
||||
return false;
|
||||
off = next+1;
|
||||
}
|
||||
String last = name.substring(off);
|
||||
return isJavaIdentifier(last);
|
||||
}
|
||||
|
||||
private static boolean isJavaIdentifier(String str) {
|
||||
if (str.isEmpty() || RESERVED.contains(str))
|
||||
return false;
|
||||
|
||||
int first = Character.codePointAt(str, 0);
|
||||
if (!Character.isJavaIdentifierStart(first))
|
||||
return false;
|
||||
|
||||
int i = Character.charCount(first);
|
||||
while (i < str.length()) {
|
||||
int cp = Character.codePointAt(str, i);
|
||||
if (!Character.isJavaIdentifierPart(cp))
|
||||
return false;
|
||||
i += Character.charCount(cp);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -18,8 +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;
|
||||
@@ -59,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";
|
||||
|
||||
@@ -75,16 +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();
|
||||
|
||||
@Input
|
||||
public abstract ListProperty<String> getExtraClasspath();
|
||||
|
||||
private final org.gradle.api.java.archives.Manifest manifest;
|
||||
|
||||
@@ -124,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));
|
||||
}
|
||||
@@ -135,31 +141,30 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
|
||||
|
||||
@Inject
|
||||
public EnvelopeJarTask(ObjectFactory objects, FileResolver fileResolver) {
|
||||
Project rootProject = getProject().getRootProject();
|
||||
TaskContainer rootProjectTasks = rootProject.getTasks();
|
||||
if(rootProjectTasks.getNames().contains(EXTRACT_LAUNCHER_TASK_NAME)) {
|
||||
extractLauncherTaskProvider = rootProjectTasks.named(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
||||
Project project = getProject();
|
||||
TaskContainer tasks = project.getTasks();
|
||||
if(tasks.getNames().contains(EXTRACT_LAUNCHER_TASK_NAME)) {
|
||||
extractLauncherTaskProvider = tasks.named(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
||||
} else {
|
||||
extractLauncherTaskProvider = rootProject.getTasks().register(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
||||
extractLauncherTaskProvider = tasks.register(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
||||
}
|
||||
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);
|
||||
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));
|
||||
}
|
||||
@@ -286,11 +291,23 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
|
||||
mainAttributes.put(new Attributes.Name("Launcher-Agent-Class"), Constants.AGENT_LAUNCHER);
|
||||
mainAttributes.put(new Attributes.Name("Can-Redefine-Classes"), "true");
|
||||
mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true");
|
||||
if(mainClass.isPresent()) {
|
||||
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, mainClass.get());
|
||||
String separator = "" + Constants.EXTRA_CLASSPATH_ENTRY_SEPARATOR;
|
||||
ListProperty<String> extraClasspathProperty = EnvelopeJarTask.this.getExtraClasspath();
|
||||
java.util.Optional.of(extraClasspathProperty)
|
||||
.filter(ListProperty::isPresent)
|
||||
.map(ListProperty::get)
|
||||
.filter(l -> !l.isEmpty())
|
||||
.ifPresent(extraClasspath -> {
|
||||
String extraClasspathString = extraClasspath.stream()
|
||||
.map(it -> it.replace(separator, separator + separator)
|
||||
).collect(Collectors.joining(separator));
|
||||
mainAttributes.put(new Attributes.Name(Constants.ManifestAttributes.EXTRA_CLASSPATH), extraClasspathString);
|
||||
});
|
||||
if(getMainClass().isPresent()) {
|
||||
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, getMainClass().get());
|
||||
}
|
||||
if(mainModule.isPresent()) {
|
||||
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, mainModule.get());
|
||||
if(getMainModule().isPresent()) {
|
||||
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, getMainModule().get());
|
||||
}
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||
@@ -334,7 +351,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