Compare commits
16 Commits
54bf96c29a
...
master
Author | SHA1 | Date | |
---|---|---|---|
1d999ddc96
|
|||
c2112b9e16
|
|||
5cfc65c35a
|
|||
d5c93708c5
|
|||
c75afe1073
|
|||
dbf99afe2f
|
|||
9c63bb50be
|
|||
26dd1aaf86
|
|||
0f8d64ddd7
|
|||
9864df4893
|
|||
a6098174e3
|
|||
07817f80f9
|
|||
995d937b25
|
|||
a1139bdc1c
|
|||
d2040fb02a
|
|||
07de45abe5
|
21
.gitea/workflows/build.yaml
Normal file
21
.gitea/workflows/build.yaml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
name: CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: hostinger
|
||||||
|
steps:
|
||||||
|
- name: Checkout sources
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Java
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: graalvm
|
||||||
|
java-version: 21
|
||||||
|
- name: Setup Gradle
|
||||||
|
uses: gradle/actions/setup-gradle@v3
|
||||||
|
- name: Execute Gradle build
|
||||||
|
env:
|
||||||
|
PUBLISHER_TOKEN: ${{ secrets.PUBLISHER_TOKEN }}
|
||||||
|
run: ./gradlew publish
|
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.
|
122
README.md
Normal file
122
README.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
## 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://gitea.woggioni.net/api/packages/woggioni/maven'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id "net.woggioni.gradle.envelope" version "2025.01.21"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
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 class name 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.
|
||||||
|
|
||||||
|
Java agents configured in this way will always run together with the application and cannot be disabled.
|
||||||
|
|
||||||
|
### 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.
|
||||||
|
|
||||||
|
- When running in JPMS mode (when the `mainModule` property is set), command line arguments like
|
||||||
|
`--add-opens`, `--add-exports`, `--add-reads` won't work as JPMS is initialized after application startup
|
||||||
|
|
||||||
|
- When running in JPMS mode custom stream handler need to added installed using `URL.setURLStreamHandlerFactory`,
|
||||||
|
setting the `java.protocol.handler.pkgs` system property does not work as it tries to load
|
||||||
|
the respective handler using the system classloader which, in an envelope application, can only load envelope own classes
|
20
build.gradle
20
build.gradle
@@ -16,7 +16,7 @@ allprojects {
|
|||||||
|
|
||||||
java {
|
java {
|
||||||
toolchain {
|
toolchain {
|
||||||
languageVersion = JavaLanguageVersion.of(17)
|
languageVersion = JavaLanguageVersion.of(21)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,18 @@ allprojects {
|
|||||||
publishing {
|
publishing {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url = publishMavenRepositoryUrl
|
name = "Gitea"
|
||||||
|
url = uri(getProperty('gitea.maven.url'))
|
||||||
|
|
||||||
|
credentials(HttpHeaderCredentials) {
|
||||||
|
name = "Authorization"
|
||||||
|
value = "token ${System.getenv()["PUBLISHER_TOKEN"]}"
|
||||||
|
}
|
||||||
|
|
||||||
|
authentication {
|
||||||
|
header(HttpHeaderAuthentication)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,11 +115,6 @@ gradlePlugin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
wrapper {
|
|
||||||
gradleVersion = getProperty('version.gradle')
|
|
||||||
distributionType = Wrapper.DistributionType.ALL
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.named('processTestResources') { ProcessResources it ->
|
tasks.named('processTestResources') { ProcessResources it ->
|
||||||
doLast {
|
doLast {
|
||||||
Files.newBufferedWriter(it.destinationDir.toPath().resolve('test-resources.txt')).withCloseable { writer ->
|
Files.newBufferedWriter(it.destinationDir.toPath().resolve('test-resources.txt')).withCloseable { writer ->
|
||||||
|
@@ -1,24 +1,27 @@
|
|||||||
package net.woggioni.envelope;
|
package net.woggioni.envelope;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.SneakyThrows;
|
||||||
|
|
||||||
import java.io.BufferedInputStream;
|
import java.io.BufferedInputStream;
|
||||||
import java.io.BufferedOutputStream;
|
import java.io.BufferedOutputStream;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileInputStream;
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.security.DigestInputStream;
|
import java.security.DigestInputStream;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.MissingFormatArgumentException;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Stream;
|
||||||
import java.util.zip.CRC32;
|
import java.util.zip.CRC32;
|
||||||
import java.util.zip.ZipEntry;
|
import java.util.zip.ZipEntry;
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
import lombok.SneakyThrows;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class Common {
|
public class Common {
|
||||||
@@ -114,4 +117,152 @@ public class Common {
|
|||||||
OutputStream result = new FileOutputStream(file);
|
OutputStream result = new FileOutputStream(file);
|
||||||
return buffered ? new BufferedOutputStream(result) : result;
|
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;
|
package net.woggioni.envelope;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
import java.util.GregorianCalendar;
|
import java.util.GregorianCalendar;
|
||||||
import lombok.AccessLevel;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
public class Constants {
|
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 SYSTEM_PROPERTIES_FILE = METADATA_FOLDER + "/system.properties";
|
||||||
|
|
||||||
public static final String LIBRARIES_TOC = METADATA_FOLDER + "/libraries.txt";
|
public static final String LIBRARIES_TOC = METADATA_FOLDER + "/libraries.txt";
|
||||||
|
public static final char EXTRA_CLASSPATH_ENTRY_SEPARATOR = ';';
|
||||||
|
|
||||||
public static class ManifestAttributes {
|
public static class ManifestAttributes {
|
||||||
public static final String MAIN_MODULE = "Executable-Jar-Main-Module";
|
public static final String MAIN_MODULE = "Executable-Jar-Main-Module";
|
||||||
public static final String MAIN_CLASS = "Executable-Jar-Main-Class";
|
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 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
|
* 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>
|
* <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/
|
gitea.maven.url = https://gitea.woggioni.net/api/packages/woggioni/maven
|
||||||
publishMavenRepositoryUrl=https://mvn.woggioni.net/
|
|
||||||
|
|
||||||
lys.version = 0.2-SNAPSHOT
|
lys.version = 2025.03.08
|
||||||
|
|
||||||
version.envelope=2023.03
|
version.envelope=2025.03.12
|
||||||
version.gradle=7.6
|
|
||||||
|
org.gradle.caching=true
|
||||||
|
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
@@ -1,5 +1,7 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.6-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
|
||||||
|
networkTimeout=10000
|
||||||
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
|
38
gradlew
vendored
38
gradlew
vendored
@@ -15,6 +15,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -55,7 +57,7 @@
|
|||||||
# Darwin, MinGW, and NonStop.
|
# Darwin, MinGW, and NonStop.
|
||||||
#
|
#
|
||||||
# (3) This script is generated from the Groovy template
|
# (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/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
|
||||||
# within the Gradle project.
|
# within the Gradle project.
|
||||||
#
|
#
|
||||||
# You can find Gradle at https://github.com/gradle/gradle/.
|
# You can find Gradle at https://github.com/gradle/gradle/.
|
||||||
@@ -80,13 +82,12 @@ do
|
|||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
|
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
|
# This is normally unused
|
||||||
|
# shellcheck disable=SC2034
|
||||||
APP_NAME="Gradle"
|
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
|
||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -133,22 +134,29 @@ location of your Java installation."
|
|||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
JAVACMD=java
|
JAVACMD=java
|
||||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
if ! command -v java >/dev/null 2>&1
|
||||||
|
then
|
||||||
|
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||||
|
|
||||||
Please set the JAVA_HOME variable in your environment to match the
|
Please set the JAVA_HOME variable in your environment to match the
|
||||||
location of your Java installation."
|
location of your Java installation."
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Increase the maximum file descriptors if we can.
|
# Increase the maximum file descriptors if we can.
|
||||||
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
max*)
|
max*)
|
||||||
|
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
MAX_FD=$( ulimit -H -n ) ||
|
MAX_FD=$( ulimit -H -n ) ||
|
||||||
warn "Could not query maximum file descriptor limit"
|
warn "Could not query maximum file descriptor limit"
|
||||||
esac
|
esac
|
||||||
case $MAX_FD in #(
|
case $MAX_FD in #(
|
||||||
'' | soft) :;; #(
|
'' | soft) :;; #(
|
||||||
*)
|
*)
|
||||||
|
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
|
||||||
|
# shellcheck disable=SC2039,SC3045
|
||||||
ulimit -n "$MAX_FD" ||
|
ulimit -n "$MAX_FD" ||
|
||||||
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
warn "Could not set maximum file descriptor limit to $MAX_FD"
|
||||||
esac
|
esac
|
||||||
@@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Collect all arguments for the java command;
|
|
||||||
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
|
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||||
# shell script including quotes and variable substitutions, so put them in
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
# double quotes to make sure that they get re-expanded; and
|
|
||||||
# * put everything else in single quotes, so that it's not re-expanded.
|
# Collect all arguments for the java command:
|
||||||
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
|
# and any embedded shellness will be escaped.
|
||||||
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
|
# treated as '${Hostname}' itself on the command line.
|
||||||
|
|
||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
|
23
gradlew.bat
vendored
23
gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@@ -26,6 +28,7 @@ if "%OS%"=="Windows_NT" setlocal
|
|||||||
|
|
||||||
set DIRNAME=%~dp0
|
set DIRNAME=%~dp0
|
||||||
if "%DIRNAME%"=="" set DIRNAME=.
|
if "%DIRNAME%"=="" set DIRNAME=.
|
||||||
|
@rem This is normally unused
|
||||||
set APP_BASE_NAME=%~n0
|
set APP_BASE_NAME=%~n0
|
||||||
set APP_HOME=%DIRNAME%
|
set APP_HOME=%DIRNAME%
|
||||||
|
|
||||||
@@ -42,11 +45,11 @@ set JAVA_EXE=java.exe
|
|||||||
%JAVA_EXE% -version >NUL 2>&1
|
%JAVA_EXE% -version >NUL 2>&1
|
||||||
if %ERRORLEVEL% equ 0 goto execute
|
if %ERRORLEVEL% equ 0 goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
|||||||
|
|
||||||
if exist "%JAVA_EXE%" goto execute
|
if exist "%JAVA_EXE%" goto execute
|
||||||
|
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
|
||||||
echo.
|
echo. 1>&2
|
||||||
echo Please set the JAVA_HOME variable in your environment to match the
|
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
|
||||||
echo location of your Java installation.
|
echo location of your Java installation. 1>&2
|
||||||
|
|
||||||
goto fail
|
goto fail
|
||||||
|
|
||||||
|
@@ -7,12 +7,15 @@ ext {
|
|||||||
setProperty('jpms.module.name', 'net.woggioni.envelope')
|
setProperty('jpms.module.name', 'net.woggioni.envelope')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
configurations {
|
configurations {
|
||||||
embedded {
|
embedded {
|
||||||
visible = false
|
visible = false
|
||||||
canBeConsumed = false
|
canBeConsumed = false
|
||||||
}
|
}
|
||||||
compileOnly.extendsFrom(embedded)
|
compileOnly {
|
||||||
|
extendsFrom(embedded)
|
||||||
|
}
|
||||||
tar {
|
tar {
|
||||||
visible = true
|
visible = true
|
||||||
canBeConsumed = true
|
canBeConsumed = true
|
||||||
@@ -22,7 +25,7 @@ configurations {
|
|||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
embedded project(path: ":common", configuration: 'archives')
|
embedded project(path: ":common", configuration: 'archives')
|
||||||
embedded project(path: ":loader", configuration: 'archives')
|
embedded project(path: ":loader", configuration: 'embed')
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
@@ -10,16 +10,27 @@ import java.io.Reader;
|
|||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
|
import java.util.AbstractMap;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
|
import java.util.TreeMap;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.function.Function;
|
||||||
import java.util.jar.Attributes;
|
import java.util.jar.Attributes;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
import java.util.jar.Manifest;
|
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 {
|
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
|
@SneakyThrows
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
Enumeration<URL> it = Launcher.class.getClassLoader().getResources(Constants.SYSTEM_PROPERTIES_FILE);
|
Enumeration<URL> it = Launcher.class.getClassLoader().getResources(Constants.SYSTEM_PROPERTIES_FILE);
|
||||||
@@ -73,8 +147,14 @@ public class Launcher {
|
|||||||
Manifest mf = currentJar.getManifest();
|
Manifest mf = currentJar.getManifest();
|
||||||
Attributes mainAttributes = mf.getMainAttributes();
|
Attributes mainAttributes = mf.getMainAttributes();
|
||||||
|
|
||||||
String mainClassName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_CLASS);
|
String mainClassName = or(
|
||||||
String mainModuleName = mainAttributes.getValue(Constants.ManifestAttributes.MAIN_MODULE);
|
() -> 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();
|
StringBuilder sb = new StringBuilder();
|
||||||
List<JarFile> classpath = new ArrayList<>();
|
List<JarFile> classpath = new ArrayList<>();
|
||||||
URL libraryTocResource = Launcher.class.getClassLoader().getResource(Constants.LIBRARIES_TOC);
|
URL libraryTocResource = Launcher.class.getClassLoader().getResource(Constants.LIBRARIES_TOC);
|
||||||
@@ -94,6 +174,8 @@ public class Launcher {
|
|||||||
else sb.append((char) c);
|
else sb.append((char) c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getExtraClasspath(mainAttributes).forEach(classpath::add);
|
||||||
Consumer<Class<?>> runner = new Consumer<Class<?>>() {
|
Consumer<Class<?>> runner = new Consumer<Class<?>>() {
|
||||||
@Override
|
@Override
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
|
@@ -85,6 +85,7 @@ class MainRunner {
|
|||||||
ModuleLayer layer = controller.layer();
|
ModuleLayer layer = controller.layer();
|
||||||
Module mainModule = layer.findModule(mainModuleName).orElseThrow(
|
Module mainModule = layer.findModule(mainModuleName).orElseThrow(
|
||||||
() -> new IllegalStateException(String.format("Main module '%s' not found", mainModuleName)));
|
() -> new IllegalStateException(String.format("Main module '%s' not found", mainModuleName)));
|
||||||
|
Thread.currentThread().setContextClassLoader(mainModule.getClassLoader());
|
||||||
Optional<String> mainClassOpt = Optional.ofNullable(mainClassName);
|
Optional<String> mainClassOpt = Optional.ofNullable(mainClassName);
|
||||||
runner.accept(Optional.ofNullable(mainClassName)
|
runner.accept(Optional.ofNullable(mainClassName)
|
||||||
.or(() -> mainModule.getDescriptor().mainClass())
|
.or(() -> mainModule.getDescriptor().mainClass())
|
||||||
|
@@ -7,12 +7,15 @@ ext {
|
|||||||
setProperty('jpms.module.name', 'net.woggioni.envelope.loader')
|
setProperty('jpms.module.name', 'net.woggioni.envelope.loader')
|
||||||
}
|
}
|
||||||
|
|
||||||
compileJava11 {
|
configurations {
|
||||||
exclude('module-info.java')
|
embed {
|
||||||
|
canBeResolved = true
|
||||||
|
canBeConsumed = true
|
||||||
|
visible = true
|
||||||
|
transitive = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
publishing {
|
publishing {
|
||||||
publications {
|
publications {
|
||||||
maven(MavenPublication) {
|
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.JarFile;
|
||||||
import net.woggioni.envelope.loader.jar.Handler;
|
import net.woggioni.envelope.loader.jar.Handler;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.lang.module.ModuleDescriptor;
|
import java.lang.module.ModuleDescriptor;
|
||||||
import java.lang.module.ModuleFinder;
|
import java.lang.module.ModuleFinder;
|
||||||
import java.lang.module.ModuleReader;
|
import java.lang.module.ModuleReader;
|
||||||
@@ -17,6 +19,8 @@ import java.net.URLStreamHandler;
|
|||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.AbstractMap;
|
import java.util.AbstractMap;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@@ -27,16 +31,20 @@ import java.util.TreeMap;
|
|||||||
import java.util.jar.Attributes.Name;
|
import java.util.jar.Attributes.Name;
|
||||||
import java.util.jar.JarEntry;
|
import java.util.jar.JarEntry;
|
||||||
import java.util.jar.Manifest;
|
import java.util.jar.Manifest;
|
||||||
|
import java.util.jar.Attributes;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.Stream;
|
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 {
|
public class JarFileModuleFinder implements ModuleFinder {
|
||||||
|
|
||||||
private static final String MODULE_DESCRIPTOR_ENTRY = "module-info.class";
|
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 Name AUTOMATIC_MODULE_NAME_MANIFEST_ENTRY = new Name("Automatic-Module-Name");
|
||||||
|
private static final String SERVICES_PREFIX = "META-INF/services/";
|
||||||
private static class Patterns {
|
private static class Patterns {
|
||||||
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
|
static final Pattern DASH_VERSION = Pattern.compile("-(\\d+(\\.|$))");
|
||||||
static final Pattern NON_ALPHANUM = Pattern.compile("[^A-Za-z0-9]");
|
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 LEADING_DOTS = Pattern.compile("^\\.");
|
||||||
static final Pattern TRAILING_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;
|
private final Map<String, Map.Entry<ModuleReference, Handler>> modules;
|
||||||
|
|
||||||
@SneakyThrows
|
@SneakyThrows
|
||||||
@@ -124,7 +192,7 @@ public class JarFileModuleFinder implements ModuleFinder {
|
|||||||
private static Set<String> collectPackageNames(JarFile jarFile) {
|
private static Set<String> collectPackageNames(JarFile jarFile) {
|
||||||
Set<String> result = jarFile
|
Set<String> result = jarFile
|
||||||
.versionedStream()
|
.versionedStream()
|
||||||
.filter(entry -> entry.getName().endsWith(".class"))
|
.filter(zipEntry -> !zipEntry.isDirectory())
|
||||||
.map(entry -> {
|
.map(entry -> {
|
||||||
String entryName = entry.getName();
|
String entryName = entry.getName();
|
||||||
int lastSlash = entryName.lastIndexOf('/');
|
int lastSlash = entryName.lastIndexOf('/');
|
||||||
@@ -150,20 +218,7 @@ public class JarFileModuleFinder implements ModuleFinder {
|
|||||||
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
moduleDescriptor = ModuleDescriptor.read(is, () -> collectPackageNames(jarFile));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Manifest mf = jarFile.getManifest();
|
moduleDescriptor = deriveModuleDescriptor(jarFile);
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
modules.put(moduleDescriptor.name(),
|
modules.put(moduleDescriptor.name(),
|
||||||
@@ -210,4 +265,234 @@ public class JarFileModuleFinder implements ModuleFinder {
|
|||||||
.stream().map(Map.Entry::getKey)
|
.stream().map(Map.Entry::getKey)
|
||||||
.collect(Collectors.toSet()));
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,7 +9,7 @@ pluginManagement {
|
|||||||
includeModule 'net.woggioni.gradle', 'multi-release-jar'
|
includeModule 'net.woggioni.gradle', 'multi-release-jar'
|
||||||
includeModule 'net.woggioni.gradle.multi-release-jar', 'net.woggioni.gradle.multi-release-jar.gradle.plugin'
|
includeModule 'net.woggioni.gradle.multi-release-jar', 'net.woggioni.gradle.multi-release-jar.gradle.plugin'
|
||||||
}
|
}
|
||||||
url = 'https://woggioni.net/mvn/'
|
url = getProperty('gitea.maven.url')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ pluginManagement {
|
|||||||
dependencyResolutionManagement {
|
dependencyResolutionManagement {
|
||||||
repositories {
|
repositories {
|
||||||
maven {
|
maven {
|
||||||
url = 'https://woggioni.net/mvn/'
|
url = getProperty('gitea.maven.url')
|
||||||
content {
|
content {
|
||||||
includeGroup 'com.lys'
|
includeGroup 'com.lys'
|
||||||
}
|
}
|
||||||
|
@@ -18,8 +18,11 @@ import org.gradle.api.internal.file.copy.CopyActionProcessingStream;
|
|||||||
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
|
import org.gradle.api.internal.file.copy.FileCopyDetailsInternal;
|
||||||
import org.gradle.api.java.archives.internal.DefaultManifest;
|
import org.gradle.api.java.archives.internal.DefaultManifest;
|
||||||
import org.gradle.api.model.ObjectFactory;
|
import org.gradle.api.model.ObjectFactory;
|
||||||
|
import org.gradle.api.plugins.BasePlugin;
|
||||||
import org.gradle.api.plugins.BasePluginExtension;
|
import org.gradle.api.plugins.BasePluginExtension;
|
||||||
import org.gradle.api.plugins.JavaApplication;
|
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.Property;
|
||||||
import org.gradle.api.provider.Provider;
|
import org.gradle.api.provider.Provider;
|
||||||
import org.gradle.api.tasks.Input;
|
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.BEST_COMPRESSION;
|
||||||
import static java.util.zip.Deflater.NO_COMPRESSION;
|
import static java.util.zip.Deflater.NO_COMPRESSION;
|
||||||
|
import static net.woggioni.gradle.envelope.EnvelopePlugin.ENVELOPE_GROUP_NAME;
|
||||||
|
|
||||||
@SuppressWarnings({"unused" })
|
@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 MINIMUM_GRADLE_VERSION = "6.0";
|
||||||
private static final String EXTRACT_LAUNCHER_TASK_NAME = "extractEnvelopeLauncher";
|
private static final String EXTRACT_LAUNCHER_TASK_NAME = "extractEnvelopeLauncher";
|
||||||
|
|
||||||
@@ -75,16 +80,21 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
|
|||||||
|
|
||||||
private final Provider<ExtractLauncherTask> extractLauncherTaskProvider;
|
private final Provider<ExtractLauncherTask> extractLauncherTaskProvider;
|
||||||
|
|
||||||
@Getter(onMethod_ = {@Input, @Optional})
|
@Input
|
||||||
private final Property<String> mainClass;
|
@Optional
|
||||||
|
public abstract Property<String> getMainClass();
|
||||||
|
|
||||||
@Getter(onMethod_ = {@Input, @Optional})
|
@Input
|
||||||
private final Property<String> mainModule;
|
@Optional
|
||||||
|
public abstract Property<String> getMainModule();
|
||||||
|
|
||||||
private final Properties javaAgents = new Properties();
|
private final Properties javaAgents = new Properties();
|
||||||
|
|
||||||
@Getter(onMethod_ = {@Input})
|
@Input
|
||||||
private final Map<String, String> systemProperties = new TreeMap<>();
|
public abstract MapProperty<String, String> getSystemProperties();
|
||||||
|
|
||||||
|
@Input
|
||||||
|
public abstract ListProperty<String> getExtraClasspath();
|
||||||
|
|
||||||
private final org.gradle.api.java.archives.Manifest manifest;
|
private final org.gradle.api.java.archives.Manifest manifest;
|
||||||
|
|
||||||
@@ -124,42 +134,40 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
|
|||||||
javaAgents.put(className, args);
|
javaAgents.put(className, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void systemProperty(String key, String value) {
|
|
||||||
systemProperties.put(key, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void includeLibraries(Object... files) {
|
public void includeLibraries(Object... files) {
|
||||||
into(Constants.LIBRARIES_FOLDER, (copySpec) -> copySpec.from(files));
|
into(Constants.LIBRARIES_FOLDER, (copySpec) -> {
|
||||||
|
copySpec.include("*.jar");
|
||||||
|
copySpec.from(files);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public EnvelopeJarTask(ObjectFactory objects, FileResolver fileResolver) {
|
public EnvelopeJarTask(ObjectFactory objects, FileResolver fileResolver) {
|
||||||
Project rootProject = getProject().getRootProject();
|
Project project = getProject();
|
||||||
TaskContainer rootProjectTasks = rootProject.getTasks();
|
TaskContainer tasks = project.getTasks();
|
||||||
if(rootProjectTasks.getNames().contains(EXTRACT_LAUNCHER_TASK_NAME)) {
|
if(tasks.getNames().contains(EXTRACT_LAUNCHER_TASK_NAME)) {
|
||||||
extractLauncherTaskProvider = rootProjectTasks.named(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
extractLauncherTaskProvider = tasks.named(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
||||||
} else {
|
} else {
|
||||||
extractLauncherTaskProvider = rootProject.getTasks().register(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
extractLauncherTaskProvider = tasks.register(EXTRACT_LAUNCHER_TASK_NAME, ExtractLauncherTask.class);
|
||||||
}
|
}
|
||||||
getInputs().files(extractLauncherTaskProvider);
|
getInputs().files(extractLauncherTaskProvider);
|
||||||
|
|
||||||
setGroup("build");
|
setGroup(BasePlugin.BUILD_GROUP);
|
||||||
setDescription("Creates an executable jar file, embedding all of its runtime dependencies");
|
setDescription("Creates an executable jar file, embedding all of its runtime dependencies");
|
||||||
BasePluginExtension basePluginExtension = getProject().getExtensions().getByType(BasePluginExtension.class);
|
BasePluginExtension basePluginExtension = getProject().getExtensions().getByType(BasePluginExtension.class);
|
||||||
getDestinationDirectory().set(basePluginExtension.getDistsDirectory());
|
getDestinationDirectory().set(basePluginExtension.getLibsDirectory());
|
||||||
getArchiveBaseName().convention(getProject().getName());
|
getArchiveBaseName().convention(getProject().getName());
|
||||||
getArchiveExtension().convention("jar");
|
getArchiveExtension().convention("jar");
|
||||||
getArchiveVersion().convention(getProject().getVersion().toString());
|
getArchiveVersion().convention(getProject().getVersion().toString());
|
||||||
getArchiveAppendix().convention("envelope");
|
getArchiveAppendix().convention(DEFAULT_ARCHIVE_APPENDIX);
|
||||||
|
|
||||||
manifest = new DefaultManifest(fileResolver);
|
manifest = new DefaultManifest(fileResolver);
|
||||||
mainClass = objects.property(String.class);
|
getSystemProperties().convention(new TreeMap<>());
|
||||||
mainModule = objects.property(String.class);
|
|
||||||
JavaApplication javaApplication = getProject().getExtensions().findByType(JavaApplication.class);
|
JavaApplication javaApplication = getProject().getExtensions().findByType(JavaApplication.class);
|
||||||
if(!Objects.isNull(javaApplication)) {
|
if(!Objects.isNull(javaApplication)) {
|
||||||
mainClass.convention(javaApplication.getMainClass());
|
getMainClass().convention(javaApplication.getMainClass());
|
||||||
mainModule.convention(javaApplication.getMainModule());
|
getMainModule().convention(javaApplication.getMainModule());
|
||||||
}
|
}
|
||||||
from(getProject().tarTree(extractLauncherTaskProvider.map(ExtractLauncherTask::getLauncherTar)), copySpec -> exclude(JarFile.MANIFEST_NAME));
|
from(getProject().tarTree(extractLauncherTaskProvider.map(ExtractLauncherTask::getLauncherTar)), copySpec -> exclude(JarFile.MANIFEST_NAME));
|
||||||
}
|
}
|
||||||
@@ -286,11 +294,23 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
|
|||||||
mainAttributes.put(new Attributes.Name("Launcher-Agent-Class"), Constants.AGENT_LAUNCHER);
|
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-Redefine-Classes"), "true");
|
||||||
mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true");
|
mainAttributes.put(new Attributes.Name("Can-Retransform-Classes"), "true");
|
||||||
if(mainClass.isPresent()) {
|
String separator = "" + Constants.EXTRA_CLASSPATH_ENTRY_SEPARATOR;
|
||||||
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_CLASS, mainClass.get());
|
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()) {
|
if(getMainModule().isPresent()) {
|
||||||
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, mainModule.get());
|
mainAttributes.putValue(Constants.ManifestAttributes.MAIN_MODULE, getMainModule().get());
|
||||||
}
|
}
|
||||||
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
||||||
@@ -334,7 +354,7 @@ public class EnvelopeJarTask extends AbstractArchiveTask {
|
|||||||
zipEntry.setMethod(ZipEntry.DEFLATED);
|
zipEntry.setMethod(ZipEntry.DEFLATED);
|
||||||
zipOutputStream.putNextEntry(zipEntry);
|
zipOutputStream.putNextEntry(zipEntry);
|
||||||
Properties props = new Properties();
|
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.setProperty(entry.getKey(), entry.getValue());
|
||||||
}
|
}
|
||||||
props.store(zipOutputStream, null);
|
props.store(zipOutputStream, null);
|
||||||
|
@@ -10,10 +10,16 @@ import org.gradle.api.tasks.JavaExec;
|
|||||||
import org.gradle.api.tasks.bundling.Jar;
|
import org.gradle.api.tasks.bundling.Jar;
|
||||||
|
|
||||||
public class EnvelopePlugin implements Plugin<Project> {
|
public class EnvelopePlugin implements Plugin<Project> {
|
||||||
|
|
||||||
|
public static final String ENVELOPE_GROUP_NAME = "envelope";
|
||||||
|
public static final String ENVELOPE_JAR_TASK_NAME = "envelopeJar";
|
||||||
|
public static final String ENVELOPE_RUN_TASK_NAME = "envelopeRun";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void apply(Project project) {
|
public void apply(Project project) {
|
||||||
Provider<EnvelopeJarTask> envelopeJarTaskProvider = project.getTasks().register("envelopeJar", EnvelopeJarTask.class, t -> {
|
project.getPluginManager().apply(JavaPlugin.class);
|
||||||
t.setGroup(BasePlugin.BUILD_GROUP);
|
Provider<EnvelopeJarTask> envelopeJarTaskProvider = project.getTasks().register(ENVELOPE_JAR_TASK_NAME, EnvelopeJarTask.class, t -> {
|
||||||
|
t.setGroup(ENVELOPE_GROUP_NAME);
|
||||||
t.setDescription("Package the application in a single executable jar file");
|
t.setDescription("Package the application in a single executable jar file");
|
||||||
t.includeLibraries(project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
|
t.includeLibraries(project.getConfigurations().named(JavaPlugin.RUNTIME_CLASSPATH_CONFIGURATION_NAME));
|
||||||
t.includeLibraries(project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class));
|
t.includeLibraries(project.getTasks().named(JavaPlugin.JAR_TASK_NAME, Jar.class));
|
||||||
@@ -21,9 +27,9 @@ public class EnvelopePlugin implements Plugin<Project> {
|
|||||||
project.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME, DefaultTask.class, assembleTask -> {
|
project.getTasks().named(BasePlugin.ASSEMBLE_TASK_NAME, DefaultTask.class, assembleTask -> {
|
||||||
assembleTask.dependsOn(envelopeJarTaskProvider);
|
assembleTask.dependsOn(envelopeJarTaskProvider);
|
||||||
});
|
});
|
||||||
Provider<JavaExec> envelopeRunTaskProvider = project.getTasks().register("envelopeRun", JavaExec.class, t -> {
|
Provider<JavaExec> envelopeRunTaskProvider = project.getTasks().register(ENVELOPE_RUN_TASK_NAME, JavaExec.class, t -> {
|
||||||
t.getInputs().files(envelopeJarTaskProvider);
|
t.getInputs().files(envelopeJarTaskProvider);
|
||||||
t.setGroup("envelope");
|
t.setGroup(ENVELOPE_GROUP_NAME);
|
||||||
t.setDescription("Run the application in the envelope jar");
|
t.setDescription("Run the application in the envelope jar");
|
||||||
t.classpath(envelopeJarTaskProvider);
|
t.classpath(envelopeJarTaskProvider);
|
||||||
});
|
});
|
||||||
|
Reference in New Issue
Block a user