Compare commits

...

16 Commits

Author SHA1 Message Date
1d999ddc96 make sure includeLibraries only includes jar files
All checks were successful
CI / build (push) Successful in 1m57s
2025-03-12 23:27:55 +08:00
c2112b9e16 bump lys version
All checks were successful
CI / build (push) Successful in 49s
2025-01-21 15:30:24 +08:00
5cfc65c35a fixed bug with package listing in JPMS modules
All checks were successful
CI / build (push) Successful in 1m32s
2025-01-08 09:35:37 +08:00
d5c93708c5 updated Gradle version
All checks were successful
CI / build (push) Successful in 1m1s
2024-12-31 22:44:53 +08:00
c75afe1073 bumped Gradle version
All checks were successful
CI / build (push) Successful in 26s
2024-12-15 20:35:22 +08:00
dbf99afe2f moved to gitea
All checks were successful
CI / build (push) Successful in 14s
2024-04-04 06:38:19 +08:00
9c63bb50be removed extraclasspath manifest attribute when Gradle task property is empty 2024-02-28 06:49:43 +08:00
26dd1aaf86 update lys-catalog version to 2023.10.05 2023-10-05 15:44:02 +08:00
0f8d64ddd7 fixed typo 2023-10-05 10:05:02 +08:00
9864df4893 added license 2023-09-26 16:31:57 +08:00
a6098174e3 added documentation 2023-09-26 16:29:51 +08:00
07817f80f9 fixed bug with ExtractLauncherTask 2023-08-28 13:48:04 +08:00
995d937b25 added support for service loader in automatic modules 2023-07-09 20:32:55 +08:00
a1139bdc1c added extra classpath manifest attribute 2023-06-24 14:15:07 +08:00
d2040fb02a added module-info.class to envelope-loader library 2023-06-13 19:35:14 +08:00
07de45abe5 version bump to 2023.03.04 2023-03-04 23:35:12 +08:00
20 changed files with 945 additions and 101 deletions

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

View File

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

View File

@@ -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" =&gt; "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;
}
} }

View File

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

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

View File

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

Binary file not shown.

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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