Experiment with reworking wrapper

This commit is contained in:
Johannes Frohnmeyer 2022-09-06 11:15:21 +02:00
parent d777b9f39d
commit 63bf7df080
Signed by: Johannes
GPG Key ID: E76429612C2929F4
31 changed files with 612 additions and 290 deletions

View File

@ -15,11 +15,11 @@ build_test:
stage: build
script:
- TIMESTAMP=$(date +%s)
- gradle --build-cache build publish -Pflavor=nogui -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :Inceptum:publish -Pflavor=fat -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :Inceptum:publish -Pflavor=windows -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :Inceptum:publish -Pflavor=linux -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :Inceptum:publish -Pflavor=macos -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build publish -Pflavor=maven -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :launcher-dist:publish -Pflavor=fat -Pdist.platformOnly -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :launcher-dist:publish -Pflavor=windows -Pdist.platformOnly -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :launcher-dist:publish -Pflavor=linux -Pdist.platformOnly -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache build :launcher-dist:publish -Pflavor=macos -Pdist.platformOnly -Ppublic -Ptimestamp=$TIMESTAMP
- gradle --build-cache :exportMetadata -Ppublic -Ptimestamp=$TIMESTAMP
- mkdir -p build/libs
- cp launcher-shadowed/build/libs/* build/libs/
@ -75,11 +75,11 @@ deploy:
- if: $CI_COMMIT_TAG && '$CI_MERGE_REQUEST_SOURCE_BRANCH_NAME =~ /^master/'
stage: deploy
script:
- gradle --build-cache build publish -Pflavor=nogui -Ppublic -Prelease
- gradle --build-cache build :Inceptum:publish -Pflavor=fat -Ppublic -Prelease
- gradle --build-cache build :Inceptum:publish -Pflavor=windows -Ppublic -Prelease
- gradle --build-cache build :Inceptum:publish -Pflavor=linux -Ppublic -Prelease
- gradle --build-cache build :Inceptum:publish -Pflavor=macos -Ppublic -Prelease
- gradle --build-cache build publish -Pflavor=maven -Ppublic -Prelease
- gradle --build-cache build :launcher-dist:publish -Pflavor=fat -Pdist.platformOnly -Ppublic -Prelease
- gradle --build-cache build :launcher-dist:publish -Pflavor=windows -Pdist.platformOnly -Ppublic -Prelease
- gradle --build-cache build :launcher-dist:publish -Pflavor=linux -Pdist.platformOnly -Ppublic -Prelease
- gradle --build-cache build :launcher-dist:publish -Pflavor=macos -Pdist.platformOnly -Ppublic -Prelease
pages:
image: python:3.8-buster

View File

@ -18,8 +18,8 @@ if (File(".git").exists()) {
println("Building Inceptum $currentVer")
val timestamp: Long =
if (project.hasProperty("timestamp")) "${project.property("timestamp")}".toLong() else (System.currentTimeMillis() / 1000L)
val timestamp: Long = if (project.hasProperty("timestamp")) "${project.property("timestamp")}".toLong()
else (System.currentTimeMillis() / 1000L)
allprojects {
version = currentVer + if (project.hasProperty("release")) "" else "-$timestamp"
@ -32,28 +32,60 @@ val logbackVersion by extra("1.3.0-alpha15")
val jfCommonsVersion by extra("2022.9.4+14-8-34")
val jgitVersion by extra("6.2.0.202206071550-r")
val flavorProp: String by extra(if (project.hasProperty("flavor")) "${project.property("flavor")}" else "custom")
if (flavorProp != "custom" && flavorProp != "maven" && flavorProp != "fat" && flavorProp != "windows" && flavorProp != "linux" && flavorProp != "macos")
throw IllegalStateException("Unsupported flavor: $flavorProp")
val flavor: String by extra(
if (flavorProp != "custom") flavorProp else when (OperatingSystem.current()) {
OperatingSystem.WINDOWS -> "windows"
OperatingSystem.LINUX -> "linux"
OperatingSystem.MAC_OS -> "macos"
else -> throw IllegalStateException()
else -> throw IllegalStateException("Unsupported OS: ${OperatingSystem.current()}")
}
)
val isPublic by extra(project.hasProperty("public"))
val isRelease by extra(project.hasProperty("release"))
val wrapperVersion by extra(1)
tasks.register("exportMetadata") {
doLast {
projectDir.resolve("version.json").writeText(
"""
{
"wrapperVersion": $wrapperVersion,
"version": "$version",
"isPublic": $isPublic,
"isRelease": $isRelease,
"jvm": ${project(":common").extra["javaVersion"]}
"jvm": ${project(":common").extra["javaVersion"]},
"repositories": [
"https://repo.maven.apache.org/maven2/",
"https://gitlab.com/api/v4/projects/35745143/packages/maven"
],
"natives": {
"windows": [
"org.lwjgl:lwjgl:$lwjglVersion:natives-windows",
"org.lwjgl:lwjgl-opengl:$lwjglVersion:natives-windows",
"org.lwjgl:lwjgl-glfw:$lwjglVersion:natives-windows",
"org.lwjgl:lwjgl-tinyfd:$lwjglVersion:natives-windows",
"io.github.spair:imgui-java-natives-windows:$imguiVersion"
],
"linux": [
"org.lwjgl:lwjgl:$lwjglVersion:natives-linux",
"org.lwjgl:lwjgl-opengl:$lwjglVersion:natives-linux",
"org.lwjgl:lwjgl-glfw:$lwjglVersion:natives-linux",
"org.lwjgl:lwjgl-tinyfd:$lwjglVersion:natives-linux",
"io.github.spair:imgui-java-natives-linux:$imguiVersion"
],
"macos": [
"org.lwjgl:lwjgl:$lwjglVersion:natives-macos",
"org.lwjgl:lwjgl-opengl:$lwjglVersion:natives-macos",
"org.lwjgl:lwjgl-glfw:$lwjglVersion:natives-macos",
"org.lwjgl:lwjgl-tinyfd:$lwjglVersion:natives-macos",
"io.github.spair:imgui-java-natives-macos:$imguiVersion"
]
}
}
""".trimIndent()
""".trimIndent()
)
}
}

View File

@ -43,14 +43,4 @@ val nativeExe by tasks.registering(FileOutput::class) {
if (rootProject.extra["flavor"] == "windows") {
tasks.build.get().dependsOn(nativeExe)
}
publishing {
publications {
create<MavenPublication>("mavenJava") {
artifact(tasks.shadowJar) {
builtBy(tasks.shadowJar)
}
}
}
}

View File

@ -22,5 +22,6 @@ projectDir.resolve("src/main/java/io/gitlab/jfronny/inceptum/common/BuildMetadat
public static final boolean IS_PUBLIC = ${rootProject.extra["isPublic"]};
public static final boolean IS_RELEASE = ${rootProject.extra["isRelease"]};
public static final int VM_VERSION = $javaVersion;
public static final int WRAPPER_VERSION = ${rootProject.extra["wrapperVersion"]};
}
""".trimIndent())
""".trimIndent())

View File

@ -23,6 +23,7 @@ public class MetaHolder {
public static final Path ACCOUNTS_PATH = BASE_PATH.resolve("accounts.json");
public static final Path CONFIG_PATH = BASE_PATH.resolve("inceptum.json");
public static final Path WRAPPER_CONFIG_PATH = BASE_PATH.resolve("wrapper.json");
public static final Path NATIVES_DIR = BASE_PATH.resolve("natives");
public static final Path FORCE_LOAD_PATH = NATIVES_DIR.resolve("forceload");
public static final Path LIBRARIES_DIR = BASE_PATH.resolve("libraries");

View File

@ -84,6 +84,10 @@ public class Net {
return res.toString();
}
public static String downloadString(String url) throws IOException, URISyntaxException {
return HttpUtils.get(url).sendString();
}
public static String downloadString(String url, String sha1) throws IOException, URISyntaxException {
return new String(downloadData(url, sha1), StandardCharsets.UTF_8);
}

View File

@ -0,0 +1,7 @@
package io.gitlab.jfronny.inceptum.common;
public class OutdatedException extends RuntimeException {
public OutdatedException(String message) {
super(message);
}
}

View File

@ -1,82 +0,0 @@
package io.gitlab.jfronny.inceptum.common;
import io.gitlab.jfronny.commons.ComparableVersion;
import io.gitlab.jfronny.inceptum.common.api.GitlabApi;
import io.gitlab.jfronny.inceptum.common.model.gitlab.*;
import io.gitlab.jfronny.inceptum.common.model.inceptum.*;
import java.io.IOException;
import java.net.URISyntaxException;
import java.util.function.Consumer;
public class UpdateChecker {
private static final long PROJECT_ID = 30862253L;
public static UpdateInfo check(UpdateChannel channel, ComparableVersion current, String flavor, Consumer<UpdateChannel> channelInvalid) {
try {
int jvm = Runtime.version().feature();
if (flavor.equals("custom")) {
Utils.LOGGER.error("Custom build, skipping update check");
return null;
}
GitlabProject project = GitlabApi.getProject(PROJECT_ID);
GitlabPackage experimental = null;
ComparableVersion experimentalVersion = null;
GitlabPackage stable = null;
ComparableVersion stableVersion = null;
packageLoop:
for (GitlabPackage info : GitlabApi.getPackages(project)) {
if (info.status.equals("default") && info.name.equals("io/gitlab/jfronny/inceptum/Inceptum")) {
for (GitlabPipeline pipeline : info.pipelines) {
for (GitlabJob job : GitlabApi.getJobs(project, pipeline.id)) {
if (!job.name.equals("build_test")) continue;
try {
VersionMetadata iv = Net.downloadObject(GitlabApi.PROJECTS + project.id + "/jobs/" + job.id + "/artifacts/version.json", VersionMetadata.class);
if (iv.jvm > jvm) {
Utils.LOGGER.error("A newer JVM is required to use the latest inceptum version. Please update!");
continue packageLoop;
}
} catch (IOException e) {
continue packageLoop;
}
}
if (pipeline.ref.equals("master") && pipeline.status.equals("success")) {
ComparableVersion cvNew = new ComparableVersion(info.version);
if (experimentalVersion == null || experimentalVersion.compareTo(cvNew) < 0) {
experimental = info;
experimentalVersion = cvNew;
}
if (!info.version.contains("-") && (stableVersion == null || stableVersion.compareTo(cvNew) < 0)) {
stable = info;
stableVersion = cvNew;
}
}
}
}
}
if (experimental == null) {
throw new IOException("No version could be found");
} else if (stable == null && channel == UpdateChannel.Stable) {
channel = UpdateChannel.CI;
channelInvalid.accept(channel);
}
GitlabPackage info = switch (channel) {
case CI -> experimental;
case Stable -> stable;
};
Utils.LOGGER.info("Latest version is " + info.version + ", current is " + current);
if (current.compareTo(new ComparableVersion(info.version)) >= 0) {
Utils.LOGGER.info("Up-to-date");
return null;
}
GitlabPackageFile file = GitlabApi.getFile(project, info, f -> f.file_name.endsWith('-' + flavor + ".jar"));
if (file == null)
Utils.LOGGER.error("No valid package was discovered");
else
return new UpdateInfo("https://gitlab.com/" + project.path_with_namespace + "/-/package_files/" + file.id + "/download", file.file_sha1, new ComparableVersion(info.version));
} catch (IOException | URISyntaxException e) {
Utils.LOGGER.error("Could not check for updates", e);
}
return null;
}
}

View File

@ -0,0 +1,200 @@
package io.gitlab.jfronny.inceptum.common;
import io.gitlab.jfronny.commons.ComparableVersion;
import io.gitlab.jfronny.commons.OSUtils;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.common.api.GitlabApi;
import io.gitlab.jfronny.inceptum.common.api.MavenApi;
import io.gitlab.jfronny.inceptum.common.model.gitlab.*;
import io.gitlab.jfronny.inceptum.common.model.inceptum.*;
import io.gitlab.jfronny.inceptum.common.model.maven.MavenDependency;
import io.gitlab.jfronny.inceptum.common.model.maven.Pom;
import org.jetbrains.annotations.Nullable;
import org.xml.sax.SAXException;
import javax.xml.stream.XMLStreamException;
import java.io.File;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class Updater {
private static final long PROJECT_ID = 30862253L;
private static final String PROJECT_MAVEN = "https://gitlab.com/api/v4/projects/" + PROJECT_ID + "/packages/maven/";
public static UpdateMetadata getUpdate() {
return Updater.check(ConfigHolder.CONFIG.channel, BuildMetadata.VERSION, channel -> {
Utils.LOGGER.error("No stable version was found, switching to experimental channel");
ConfigHolder.CONFIG.channel = channel;
ConfigHolder.saveConfig();
});
}
public static void update(UpdateMetadata source, boolean relaunch) throws IOException, URISyntaxException {
if (Runtime.version().feature() < source.jvm) {
throw new OutdatedException("A newer JVM version is required for the current build of Inceptum. Please update!");
} else if (source.wrapperVersion > BuildMetadata.WRAPPER_VERSION) {
throw new OutdatedException("The current build of Inceptum requires a newer wrapper version. Please update!");
} else if (source.wrapperVersion < BuildMetadata.WRAPPER_VERSION) {
throw new OutdatedException("The current build of Inceptum requires an older wrapper version. Please update!");
}
Utils.LOGGER.info("Downloading version " + source.version);
WrapperConfig config = new WrapperConfig();
config.natives = new HashMap<>();
config.libraries = new LinkedList<>();
config.repositories = new LinkedList<>(source.repositories);
source.natives.forEach((k, v) -> config.natives.put(k, new LinkedList<>(v)));
downloadLibrary(source.repositories, "io.gitlab.jfronny.inceptum:launcher-dist:" + source.version, config.libraries);
List<String> currentLibraries = new LinkedList<>(config.libraries);
if (source.natives.containsKey(Utils.getCurrentFlavor())) {
List<String> natives = new LinkedList<>();
for (String lib : source.natives.get(Utils.getCurrentFlavor())){
downloadLibrary(source.repositories, lib, natives);
}
currentLibraries.addAll(natives);
config.natives.put(Utils.getCurrentFlavor(), natives);
}
JFiles.writeObject(MetaHolder.WRAPPER_CONFIG_PATH, config);
if (relaunch) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
new ProcessBuilder(OSUtils.getJvmBinary(),
"-cp",
buildClasspath(currentLibraries.stream())
.map(Path::toString)
.collect(Collectors.joining("" + File.pathSeparatorChar))
).inheritIO().start();
} catch (IOException e) {
Utils.LOGGER.error("Could not relaunch", e);
}
}));
}
}
public static List<Path> getLaunchClasspath(WrapperConfig wrapperConfig) throws IOException, URISyntaxException {
List<String> natives = wrapperConfig.natives.get(Utils.getCurrentFlavor());
if (natives == null) natives = new LinkedList<>();
List<String> libs = wrapperConfig.libraries;
if (libs == null) libs = new LinkedList<>();
boolean configChanged = false;
for (String lib : libs) {
Path p = artifactToPath(lib);
if (!Files.exists(p)) {
configChanged = true;
downloadLibrary(wrapperConfig.repositories, lib, libs);
}
}
for (String lib : natives) {
Path p = artifactToPath(lib);
if (!Files.exists(p)) {
configChanged = true;
downloadLibrary(wrapperConfig.repositories, lib, natives);
}
}
if (configChanged) JFiles.writeObject(MetaHolder.WRAPPER_CONFIG_PATH, wrapperConfig);
return buildClasspath(Stream.concat(libs.stream(), natives.stream())).toList();
}
private static Stream<Path> buildClasspath(Stream<String> libraries) {
return libraries.map(Updater::artifactToPath);
}
private static void downloadLibrary(List<String> repositories, final String artifact, List<String> libraries) throws IOException, URISyntaxException {
for (String repository : repositories) {
Pom pom;
try {
pom = MavenApi.getPom(repository, artifact);
} catch (IOException | URISyntaxException | XMLStreamException | SAXException ignored) {
continue;
}
for (MavenDependency dependency : pom.dependencies) {
String mvnName = dependency.groupId + ":" + dependency.artifactId + ":" + dependency.version;
downloadLibrary(repositories, mvnName, libraries);
}
MavenApi.downloadLibrary(repository, artifact);
libraries.add(artifact);
return;
}
throw new IOException("Could not find any repository containing the artifact " + artifact + " (searched: " + String.join(", ", repositories) + ")");
}
private static Path artifactToPath(String artifact) {
return MetaHolder.LIBRARIES_DIR.resolve(MavenApi.mavenNotationToJarPath(artifact)).toAbsolutePath();
}
public static @Nullable UpdateMetadata check(UpdateChannel channel, ComparableVersion current, Consumer<UpdateChannel> channelInvalid) {
try {
int jvm = Runtime.version().feature();
GitlabProject project = GitlabApi.getProject(PROJECT_ID);
UpdateMetadata experimental = null;
UpdateMetadata stable = null;
packageLoop:for (GitlabPackage info : GitlabApi.getPackages(project)) {
if (info.status.equals("default") && info.name.equals("io/gitlab/jfronny/inceptum/Inceptum")) {
pipelineLoop:for (GitlabPipeline pipeline : info.pipelines) {
if (!pipeline.ref.equals("master")) continue pipelineLoop;
if (!pipeline.status.equals("success")) {
Utils.LOGGER.warn("Skipping failed CI build");
continue pipelineLoop;
}
for (GitlabJob job : GitlabApi.getJobs(project, pipeline.id)) {
if (!job.name.equals("build_test")) continue;
try {
UpdateMetadata update = Net.downloadObject(GitlabApi.PROJECTS + project.id + "/jobs/" + job.id + "/artifacts/version.json", UpdateMetadata.class);
if (update.jvm > jvm) {
Utils.LOGGER.error("A newer JVM is required to use the latest inceptum version. Please update!");
continue packageLoop;
}
if (experimental == null || experimental.version.compareTo(update.version) < 0) {
experimental = update;
}
if (!info.version.contains("-") && (stable == null || stable.version.compareTo(update.version) < 0)) {
stable = update;
}
} catch (IOException e) {
continue packageLoop;
}
}
}
}
}
if (experimental == null) {
throw new IOException("No version could be found");
} else if (stable == null && channel == UpdateChannel.Stable) {
channel = UpdateChannel.CI;
channelInvalid.accept(channel);
}
UpdateMetadata info = switch (channel) {
case CI -> experimental;
case Stable -> stable;
};
Utils.LOGGER.info("Latest version is " + info.version + ", current is " + current);
if (current.compareTo(info.version) >= 0) {
Utils.LOGGER.info("Up-to-date");
return null;
}
return info;
} catch (IOException | URISyntaxException e) {
Utils.LOGGER.error("Could not check for updates", e);
}
return null;
}
public static String getShadowJarUrl(UpdateMetadata metadata) {
return PROJECT_MAVEN + "io/gitlab/jfronny/inceptum/Inceptum/" + metadata.version + "/Inceptum-" + metadata.version + "-" + Utils.getCurrentFlavor() + ".jar";
}
}

View File

@ -11,7 +11,9 @@ import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.FileSystem;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
public class Utils {
public static final int CACHE_SIZE = 128;
@ -50,4 +52,27 @@ public class Utils {
public static void setSystemLoader(ClassLoader loader) {
SYSTEM_LOADER = loader;
}
/**
* Joins strings with the provided separator but removes separators from the start and end of the strings
* Example: join('/', "some/path/", "/some/subpath/", "example/") -> "some/path/some/subpath/example
* @param separator The separator to join with
* @param segments The strings to join
* @return The joined string
*/
public static String join(String separator, String... segments) {
return Arrays.stream(segments)
.map(s -> s.startsWith(separator) ? s.substring(separator.length()) : s)
.map(s -> s.endsWith(separator) ? s.substring(0, s.length() - separator.length()) : s)
.filter(s -> !s.isEmpty())
.collect(Collectors.joining(separator));
}
public static String getCurrentFlavor() {
return switch (OSUtils.TYPE) {
case WINDOWS -> "windows";
case MAC_OS -> "macos";
case LINUX -> "linux";
};
}
}

View File

@ -0,0 +1,176 @@
package io.gitlab.jfronny.inceptum.common.api;
import io.gitlab.jfronny.commons.HttpUtils;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.common.model.maven.MavenDependency;
import io.gitlab.jfronny.inceptum.common.model.maven.Pom;
import org.w3c.dom.*;
import org.xml.sax.SAXException;
import javax.xml.parsers.*;
import javax.xml.stream.XMLStreamException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.*;
public class MavenApi {
private static final DocumentBuilder FACTORY;
static {
try {
FACTORY = DocumentBuilderFactory.newInstance().newDocumentBuilder();
} catch (ParserConfigurationException e) {
throw new RuntimeException("Could not create document builder", e);
}
}
public static Path downloadLibrary(String repo, String artifact) throws IOException, URISyntaxException {
String path = mavenNotationToJarPath(artifact);
Path res = MetaHolder.LIBRARIES_DIR.resolve(path);
Net.downloadFile(Utils.join("/", repo, path), res);
return res;
}
public static Pom getPom(String repo, String artifact) throws IOException, SAXException, URISyntaxException, XMLStreamException {
try (InputStream is = HttpUtils.get(Utils.join("/", repo, mavenNotationToPomPath(artifact))).sendInputStream()) {
Document doc = FACTORY.parse(is);
doc.getDocumentElement().normalize();
Pom result = new Pom();
if (!"project".equals(doc.getDocumentElement().getNodeName())) throw new IOException("Illegal document name");
boolean hasModelVersion = false;
boolean hasGroupId = false;
boolean hasArtifactId = false;
boolean hasVersion = false;
for (Node node : iterable(doc.getDocumentElement().getChildNodes())) {
switch (node.getNodeName()) {
case "modelVersion" -> {
hasModelVersion = true;
result.modelVersion = node.getTextContent();
}
case "groupId" -> {
hasGroupId = true;
result.groupId = node.getTextContent();
}
case "artifactId" -> {
hasArtifactId = true;
result.artifactId = node.getTextContent();
}
case "version" -> {
hasVersion = true;
result.version = node.getTextContent();
}
case "packaging" -> result.packaging = node.getTextContent();
case "dependencies" -> {
result.dependencies = new LinkedList<>();
for (Node dep : iterable(node.getChildNodes())) {
result.dependencies.add(parseDependency(dep));
}
}
default -> {}
}
}
if (!hasModelVersion) throw new IOException("Pom lacks modelVersion");
if (!hasGroupId) throw new IOException("Pom lacks groupId");
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
if (!hasVersion) throw new IOException("Pom lacks version");
return result;
}
}
private static MavenDependency parseDependency(Node doc) throws IOException {
MavenDependency result = new MavenDependency();
boolean hasGroupId = false;
boolean hasArtifactId = false;
boolean hasVersion = false;
boolean hasScope = false;
for (Node node : iterable(doc.getChildNodes())) {
switch (node.getNodeName()) {
case "groupId" -> {
hasGroupId = true;
result.groupId = node.getTextContent();
}
case "artifactId" -> {
hasArtifactId = true;
result.artifactId = node.getTextContent();
}
case "version" -> {
hasVersion = true;
result.version = node.getTextContent();
}
case "scope" -> {
hasScope = true;
result.scope = node.getTextContent();
}
}
}
if (!hasGroupId) throw new IOException("Pom lacks groupId");
if (!hasArtifactId) throw new IOException("Pom lacks artifactId");
if (!hasVersion) throw new IOException("Pom lacks version");
if (!hasScope) throw new IOException("Pom lacks scope");
return result;
}
private static Iterable<Node> iterable(NodeList list) {
return () -> new Iterator<>() {
int index = 0;
@Override
public boolean hasNext() {
return index < list.getLength() - 1;
}
@Override
public Node next() {
if (!hasNext()) throw new NoSuchElementException();
return list.item(index++);
}
};
}
/**
* Converts an artifact in maven notation to a jar file path. The following are supported:
* - some.base.path:artifact:version -> some/base/path/artifact/version/artifact-version.jar
* - some.base.path:artifact:version:classifier -> some/base/path/artifact/version/artifact-version-classifier.jar
* @param mavenNotation An artifact in maven notation
* @return A file path
*/
public static String mavenNotationToJarPath(String mavenNotation) {
if (Objects.requireNonNull(mavenNotation).isEmpty()) throw new IllegalArgumentException("The notation is empty");
String[] lib = mavenNotation.split(":");
if (lib.length <= 1) throw new IllegalArgumentException("Not in maven notation");
if (lib.length == 2) throw new IllegalArgumentException("Skipping versions is not supported");
if (lib.length >= 5) throw new IllegalArgumentException("Unkown elements in maven notation");
String path = lib[0].replace('.', '/') + '/'; // Base
path += lib[1] + '/'; // Artifact name
path += lib[2] + '/'; // Version
if (lib.length == 3) { // artifact-version.jar
path += lib[1] + '-' + lib[2];
} else { // artifact-version-classifier.jar
path += lib[1] + '-' + lib[2] + "-" + lib[3];
}
return path + ".jar";
}
/**
* Converts an artifact in maven notation to a pom file path. The following are supported:
* - some.base.path:artifact:version -> some/base/path/artifact/version/artifact-version.pom
* - some.base.path:artifact:version:classifier -> some/base/path/artifact/version/artifact-version.pom
* @param mavenNotation An artifact in maven notation
* @return A file path
*/
public static String mavenNotationToPomPath(String mavenNotation) {
if (Objects.requireNonNull(mavenNotation).isEmpty()) throw new IllegalArgumentException("The notation is empty");
String[] lib = mavenNotation.split(":");
if (lib.length <= 1) throw new IllegalArgumentException("Not in maven notation");
if (lib.length == 2) throw new IllegalArgumentException("Skipping versions is not supported");
if (lib.length >= 5) throw new IllegalArgumentException("Unkown elements in maven notation");
String path = lib[0].replace('.', '/') + '/'; // Base
path += lib[1] + '/'; // Artifact name
path += lib[2] + '/'; // Version
if (lib.length == 3) { // artifact-version
path += lib[1] + '-' + lib[2];
}
return path + ".pom";
}
}

View File

@ -1,6 +0,0 @@
package io.gitlab.jfronny.inceptum.common.model.inceptum;
import io.gitlab.jfronny.commons.ComparableVersion;
public record UpdateInfo(String url, String sha1, ComparableVersion newVersion) {
}

View File

@ -2,9 +2,15 @@ package io.gitlab.jfronny.inceptum.common.model.inceptum;
import io.gitlab.jfronny.commons.ComparableVersion;
public class VersionMetadata {
import java.util.List;
import java.util.Map;
public class UpdateMetadata {
public Integer wrapperVersion;
public ComparableVersion version;
public Boolean isPublic;
public Boolean isRelease;
public Integer jvm;
public List<String> repositories;
public Map<String, List<String>> natives;
}

View File

@ -0,0 +1,10 @@
package io.gitlab.jfronny.inceptum.common.model.inceptum;
import java.util.List;
import java.util.Map;
public class WrapperConfig {
public List<String> libraries;
public List<String> repositories;
public Map<String, List<String>> natives;
}

View File

@ -0,0 +1,8 @@
package io.gitlab.jfronny.inceptum.common.model.maven;
public class MavenDependency {
public String groupId;
public String artifactId;
public String version;
public String scope;
}

View File

@ -0,0 +1,12 @@
package io.gitlab.jfronny.inceptum.common.model.maven;
import java.util.List;
public class Pom {
public String modelVersion;
public String groupId;
public String artifactId;
public String version;
public String packaging;
public List<MavenDependency> dependencies;
}

View File

@ -19,10 +19,10 @@ This module contains a dear-imgui-based frontend for Inceptum.
Builds of this module are platform-specific and dependents must manually ensure the correct imgui and lwjgl natives are imported.
A build without natives can be obtained through the maven and a build with natives through the shadowed Inceptum jar
## launcher-shadowed/Inceptum
## launcher-dist/Inceptum
This module builds a shadowed jar of launcher-cli and launcher-imgui to be used by normal users.
It also adds additional, platform-specific commands to the CLI.
A build can be obtained through the maven (with the correct suffix) or as a jar with shadowed dependencies.
A shadowed build can be obtained as "Inceptum" from maven, a build with dependencies as "launcher-dist"
Windows users can also obtain a binary built using fabric-installer-native-bootstrap.
## wrapper

View File

@ -0,0 +1,40 @@
plugins {
id("inceptum.application-standalone-conventions")
}
application {
mainClass.set("io.gitlab.jfronny.inceptum.Inceptum")
}
dependencies {
implementation(project(":launcher"))
implementation(project(":launcher-cli"))
implementation(project(":launcher-imgui"))
}
tasks.shadowJar {
archiveClassifier.set(rootProject.extra["flavorProp"] as String)
archiveBaseName.set("Inceptum")
exclude("about.html")
exclude("plugin.properties")
exclude("META-INF/**")
}
publishing {
publications {
if (rootProject.hasProperty("dist.platformOnly")) {
create<MavenPublication>("shadowed") {
artifact(tasks.shadowJar) {
builtBy(tasks.shadowJar)
artifactId = "Inceptum"
}
}
} else {
create<MavenPublication>("mavenJava") {
artifact(tasks.jar) {
builtBy(tasks.jar)
}
}
}
}
}

View File

@ -2,10 +2,8 @@ package io.gitlab.jfronny.inceptum;
import io.gitlab.jfronny.inceptum.cli.Command;
import io.gitlab.jfronny.inceptum.cli.CommandArgs;
import io.gitlab.jfronny.inceptum.common.Utils;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateInfo;
import io.gitlab.jfronny.inceptum.imgui.ImBuildMetadata;
import io.gitlab.jfronny.inceptum.launcher.Updater;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateMetadata;
import java.io.IOException;
import java.net.URISyntaxException;
@ -32,7 +30,7 @@ public class UpdateCheckCommand extends Command {
Utils.LOGGER.error("Automatic updates are not supported without the wrapper");
return;
}
UpdateInfo updateUrl = Updater.getUpdate(ImBuildMetadata.FLAVOR);
UpdateMetadata updateUrl = BuildMetadata.IS_PUBLIC ? Updater.getUpdate() : null;
if (updateUrl == null) {
Utils.LOGGER.info("No update was found");
} else {

View File

@ -2,38 +2,25 @@ plugins {
id("inceptum.application-conventions")
}
group = "io.gitlab.jfronny.inceptum"
version = "0.2.0-1662303777"
dependencies {
val flavor: String by rootProject.extra
val lwjglVersion: String by rootProject.extra
val imguiVersion: String by rootProject.extra
implementation(project(":launcher"))
fun native(name: String) {
if (flavor == "windows" || flavor == "fat") implementation(name.replace("@platform", "windows"))
if (flavor == "linux" || flavor == "fat") implementation(name.replace("@platform", "linux"))
if (flavor == "macos" || flavor == "fat") implementation(name.replace("@platform", "macos"))
}
implementation(platform("org.lwjgl:lwjgl-bom:$lwjglVersion"))
implementation(project(":launcher"))
arrayOf("", "-opengl", "-glfw", "-tinyfd").forEach {
implementation("org.lwjgl:lwjgl$it:$lwjglVersion")
if (flavor == "windows" || flavor == "fat") implementation("org.lwjgl:lwjgl$it::natives-windows")
if (flavor == "linux" || flavor == "fat") implementation("org.lwjgl:lwjgl$it::natives-linux")
if (flavor == "macos" || flavor == "fat") implementation("org.lwjgl:lwjgl$it::natives-macos")
native("org.lwjgl:lwjgl$it:$lwjglVersion:natives-@platform")
}
implementation("io.github.spair:imgui-java-binding:$imguiVersion") // https://github.com/SpaiR/imgui-java
implementation("io.github.spair:imgui-java-lwjgl3:$imguiVersion")
if (flavor == "windows" || flavor == "fat") implementation("io.github.spair:imgui-java-natives-windows:$imguiVersion")
if (flavor == "linux" || flavor == "fat") implementation("io.github.spair:imgui-java-natives-linux:$imguiVersion")
if (flavor == "macos" || flavor == "fat") implementation("io.github.spair:imgui-java-natives-macos:$imguiVersion")
native("io.github.spair:imgui-java-natives-@platform:$imguiVersion")
}
projectDir.resolve("src/main/java/io/gitlab/jfronny/inceptum/imgui/ImBuildMetadata.java").writeText(
"""
package io.gitlab.jfronny.inceptum.imgui;
public class ImBuildMetadata {
public static final String FLAVOR = "${rootProject.extra["flavorProp"]}";
}
""".trimIndent())

View File

@ -6,12 +6,11 @@ import imgui.flag.ImGuiConfigFlags;
import imgui.gl3.ImGuiImplGl3;
import imgui.glfw.ImGuiImplGlfw;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateInfo;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateMetadata;
import io.gitlab.jfronny.inceptum.imgui.window.MainWindow;
import io.gitlab.jfronny.inceptum.launcher.LauncherEnv;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.imgui.window.Window;
import io.gitlab.jfronny.inceptum.launcher.Updater;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.launcher.api.account.AccountManager;
import io.gitlab.jfronny.inceptum.launcher.system.mds.ModsDirScanner;
@ -41,7 +40,7 @@ public class GuiMain {
public static void main(String[] args) throws IOException {
LauncherEnv.checkClassLoaderState();
Utils.LOGGER.info("Launching Inceptum v" + BuildMetadata.VERSION + " (" + ImBuildMetadata.FLAVOR + ")");
Utils.LOGGER.info("Launching Inceptum v" + BuildMetadata.VERSION);
Utils.LOGGER.info("Loading from " + MetaHolder.BASE_PATH);
LauncherEnv.initialize(new GuiEnvBackend());
try {
@ -52,7 +51,7 @@ public class GuiMain {
}
public static void main(boolean wrapped) {
UpdateInfo update = Updater.getUpdate(ImBuildMetadata.FLAVOR);
UpdateMetadata update = BuildMetadata.IS_PUBLIC ? Updater.getUpdate() : null;
AccountManager.loadAccounts();
Utils.LOGGER.info("Initializing UI");
try {
@ -78,7 +77,7 @@ public class GuiMain {
} else {
LauncherEnv.showOkCancel("An update was found. Automatic installs are not supported without the wrapper but you can download it nonetheless", "Update found", () -> {
try {
Utils.openWebBrowser(new URI(update.url()));
Utils.openWebBrowser(new URI(Updater.getShadowJarUrl(update)));
exit();
} catch (URISyntaxException e) {
LauncherEnv.showError("Could not download update", e);

View File

@ -1,20 +0,0 @@
plugins {
id("inceptum.application-standalone-conventions")
}
application {
mainClass.set("io.gitlab.jfronny.inceptum.Inceptum")
}
dependencies {
implementation(project(":launcher"))
implementation(project(":launcher-cli"))
implementation(project(":launcher-imgui"))
}
tasks.shadowJar {
archiveClassifier.set(rootProject.extra["flavorProp"] as String)
exclude("about.html")
exclude("plugin.properties")
exclude("META-INF/**")
}

View File

@ -1,44 +0,0 @@
package io.gitlab.jfronny.inceptum.launcher;
import io.gitlab.jfronny.commons.OSUtils;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateInfo;
import java.io.IOException;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
public class Updater {
public static UpdateInfo getUpdate(String flavor) {
return UpdateChecker.check(ConfigHolder.CONFIG.channel, BuildMetadata.VERSION, flavor, channel -> {
Utils.LOGGER.error("No stable version was found, switching to experimental channel");
ConfigHolder.CONFIG.channel = channel;
ConfigHolder.saveConfig();
});
}
//TODO (possibly) rework for new structure
public static void update(UpdateInfo source, boolean relaunch) throws IOException, URISyntaxException {
Utils.LOGGER.info("Downloading " + source.url());
Path jarPath = MetaHolder.LIBRARIES_DIR.resolve("io/gitlab/jfronny/inceptum/Inceptum")
.resolve(source.newVersion().toString())
.resolve("Inceptum-" + source.newVersion() + '-' + OSUtils.TYPE.getMojName() + ".jar")
.toAbsolutePath();
Files.createDirectories(jarPath.getParent());
Net.downloadFile(source.url(), source.sha1(), jarPath);
if (relaunch) {
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
try {
new ProcessBuilder(OSUtils.getJvmBinary(),
"-jar",
jarPath.toString())
.inheritIO()
.start();
} catch (IOException e) {
Utils.LOGGER.error("Could not relaunch", e);
}
}));
}
}
}

View File

@ -2,6 +2,7 @@ package io.gitlab.jfronny.inceptum.launcher.api;
import io.gitlab.jfronny.gson.reflect.TypeToken;
import io.gitlab.jfronny.inceptum.common.Net;
import io.gitlab.jfronny.inceptum.common.api.MavenApi;
import io.gitlab.jfronny.inceptum.launcher.model.fabric.FabricVersionLoaderInfo;
import io.gitlab.jfronny.inceptum.launcher.model.inceptum.InstanceMeta;
import io.gitlab.jfronny.inceptum.launcher.model.mojang.*;
@ -64,9 +65,7 @@ public class FabricMetaApi {
res.downloads = new VersionInfo.Library.Downloads();
res.downloads.classifiers = null;
res.downloads.artifact = new VersionInfo.Library.Downloads.Artifact();
String[] lib = library.name.split(":");
assert lib.length == 3;
res.downloads.artifact.path = lib[0].replace('.', '/') + '/' + lib[1] + '/' + lib[2] + '/' + lib[1] + '-' + lib[2] + ".jar";
res.downloads.artifact.path = MavenApi.mavenNotationToJarPath(library.name);
res.downloads.artifact.size = -1;
res.downloads.artifact.sha1 = null;
res.downloads.artifact.url = library.url + res.downloads.artifact.path;

@ -1 +1 @@
Subproject commit 336e431c44a10036a24a5d4960789e1eedad6c27
Subproject commit fe0b2fdc3361a5a085a316b73e25aec635c50863

View File

@ -5,6 +5,4 @@ include("wrapper")
include("launcher")
include("launcher-cli")
include("launcher-imgui")
include("launcher-shadowed")
project(":launcher-shadowed").name = "Inceptum"
include("launcher-dist")

View File

@ -1,83 +1,44 @@
package io.gitlab.jfronny.inceptum;
import io.gitlab.jfronny.commons.ComparableVersion;
import io.gitlab.jfronny.commons.OSUtils;
import io.gitlab.jfronny.commons.io.JFiles;
import io.gitlab.jfronny.inceptum.common.*;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateChannel;
import io.gitlab.jfronny.inceptum.common.model.inceptum.UpdateInfo;
import io.gitlab.jfronny.inceptum.common.model.inceptum.*;
import java.io.*;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Optional;
import java.util.stream.Stream;
import java.util.List;
public class Wrapper {
private static final Path INCEPTUM_LIB_DIR = MetaHolder.BASE_PATH.resolve("libraries/io/gitlab/jfronny/inceptum/Inceptum");
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, URISyntaxException {
Utils.setSystemLoader(WrapperStrap.SYSTEM_LOADER);
MetaHolder.setWrapperFlag();
System.out.println("Inceptum Wrapper v" + BuildMetadata.VERSION);
InceptumEnvironmentInitializer.initialize();
if (!Files.exists(INCEPTUM_LIB_DIR)) downloadLatest();
Optional<Path> localBinary = selectLocalBinary();
if (localBinary.isEmpty()) {
downloadLatest();
localBinary = selectLocalBinary();
}
if (localBinary.isEmpty()) {
throw new FileNotFoundException("Could not identify or download a valid binary");
}
List<Path> classpath = getClasspath();
String[] newArgs = new String[args.length + 1];
newArgs[0] = "wrapper";
System.arraycopy(args, 0, newArgs, 1, args.length);
System.out.println("Starting Inceptum ClassLoader");
WrapperStrap.switchEnv(localBinary.get(),
WrapperStrap.switchEnv(classpath,
"io.gitlab.jfronny.inceptum.Inceptum",
newArgs);
}
private static void downloadLatest() throws IOException {
System.err.println("No Inceptum jar was identified");
UpdateInfo ui = UpdateChecker.check(UpdateChannel.Stable, new ComparableVersion("0"), OSUtils.TYPE.getMojName(), R::nop);
if (ui == null) {
throw new FileNotFoundException("Could not identify a valid inceptum version. Are you connected to the internet?");
}
System.err.println("Downloading " + ui.url());
Path dir = INCEPTUM_LIB_DIR.resolve(ui.newVersion().toString());
Files.createDirectories(dir);
try (InputStream is = new URL(ui.url()).openStream()) {
Files.write(dir.resolve("Inceptum-" + ui.newVersion() + '-' + OSUtils.TYPE.getMojName() + ".jar"), is.readAllBytes());
} catch (Throwable t) {
Files.delete(dir);
}
}
private static Optional<Path> selectLocalBinary() throws IOException {
Path pathChosen = null;
ComparableVersion versionChosen = new ComparableVersion("0");
try (Stream<Path> inceptumLibVersionDirStream = Files.list(INCEPTUM_LIB_DIR)) {
for (Path inceptumVersionPath : inceptumLibVersionDirStream.toList()) {
ComparableVersion versionCurrent = new ComparableVersion(inceptumVersionPath.getFileName().toString());
if (versionCurrent.compareTo(versionChosen) > 0) {
inceptumVersionPath = inceptumVersionPath.resolve("Inceptum-" + versionCurrent + '-' + OSUtils.TYPE.getMojName() + ".jar");
if (Files.exists(inceptumVersionPath)) {
versionChosen = versionCurrent;
pathChosen = inceptumVersionPath;
} else {
Utils.LOGGER.error("Candidate " + inceptumVersionPath + " failed: doesn't exist");
}
}
private static List<Path> getClasspath() throws IOException, URISyntaxException {
if (!Files.exists(MetaHolder.WRAPPER_CONFIG_PATH)) {
UpdateMetadata update = Updater.check(UpdateChannel.Stable, new ComparableVersion("0"), R::nop);
if (update == null) {
throw new FileNotFoundException("Could not identify a valid inceptum version. Are you connected to the internet?");
}
Updater.update(update, false);
if (!Files.exists(MetaHolder.WRAPPER_CONFIG_PATH))
throw new FileNotFoundException("Something went wrong while downloading the latest version.");
}
if (pathChosen == null) {
Utils.LOGGER.error("Something went wrong, please try again");
return Optional.empty();
}
return Optional.of(pathChosen);
return Updater.getLaunchClasspath(JFiles.readObject(MetaHolder.WRAPPER_CONFIG_PATH, WrapperConfig.class));
}
}

View File

@ -1,26 +1,35 @@
package io.gitlab.jfronny.inceptum;
import io.gitlab.jfronny.inceptum.common.Utils;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;
import java.io.IOException;
import java.net.*;
import java.nio.file.*;
import java.util.*;
public class WrapperClassLoader extends ClassLoader implements Closeable {
private final Path source;
private final FileSystem fs;
private final Set<Path> sources = new LinkedHashSet<>();
private final Set<Closeable> closeables = new HashSet<>();
public WrapperClassLoader(Path zipArchive) throws IOException, URISyntaxException {
public WrapperClassLoader(Iterable<Path> jars) throws IOException, URISyntaxException {
super(null);
this.fs = Utils.openZipFile(zipArchive, false);
this.source = Files.isDirectory(zipArchive) ? zipArchive : fs.getPath("");
for (Path jar : jars) {
if (Files.isDirectory(jar)) {
sources.add(jar);
} else {
FileSystem fs = Utils.openZipFile(jar, false);
closeables.add(fs);
sources.add(fs.getPath(""));
}
}
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Path path = getPath(name);
if (!Files.exists(path)) return WrapperStrap.SYSTEM_LOADER.loadClass(name);
Path path = findClassSource(name);
if (path == null) return WrapperStrap.SYSTEM_LOADER.loadClass(name);
byte[] clazz;
try {
clazz = Files.readAllBytes(path);
@ -32,7 +41,7 @@ public class WrapperClassLoader extends ClassLoader implements Closeable {
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
if (!Files.exists(getPath(name)) || name.equals("io.gitlab.jfronny.inceptum.WrapperStrap"))
if (findClassSource(name) == null || name.equals("io.gitlab.jfronny.inceptum.WrapperStrap"))
return WrapperStrap.SYSTEM_LOADER.loadClass(name);
synchronized (this.getClassLoadingLock(name)) {
Class<?> c = this.findLoadedClass(name);
@ -44,8 +53,8 @@ public class WrapperClassLoader extends ClassLoader implements Closeable {
@Override
public URL getResource(String name) {
Path p = source.resolve(name);
if (!Files.exists(p)) return WrapperStrap.SYSTEM_LOADER.getResource(name);
Path p = findSource(name);
if (p == null) return WrapperStrap.SYSTEM_LOADER.getResource(name);
try {
return p.toUri().toURL();
} catch (MalformedURLException e) {
@ -53,12 +62,22 @@ public class WrapperClassLoader extends ClassLoader implements Closeable {
}
}
private Path getPath(String className) {
return source.resolve(className.replace('.', '/') + ".class");
private @Nullable Path findClassSource(String className) {
return findSource(className.replace('.', '/') + ".class");
}
private @Nullable Path findSource(String path) {
for (Path source : sources) {
Path p = source.resolve(path);
if (Files.exists(p)) return p;
}
return null;
}
@Override
public void close() throws IOException {
fs.close();
for (Closeable closeable : closeables) {
closeable.close();
}
}
}

View File

@ -7,19 +7,20 @@ import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.net.URISyntaxException;
import java.nio.file.Path;
import java.util.*;
public class WrapperStrap {
public static final ClassLoader SYSTEM_LOADER = ClassLoader.getSystemClassLoader();
public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, URISyntaxException {
Utils.LOGGER.info("Starting Inceptum Wrapper ClassLoader");
switchEnv(new File(WrapperStrap.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toPath(),
switchEnv(Set.of(new File(WrapperStrap.class.getProtectionDomain().getCodeSource().getLocation().toURI()).toPath()),
"io.gitlab.jfronny.inceptum.Wrapper",
args);
}
public static void switchEnv(Path jar, String mainClass, String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, URISyntaxException {
try (WrapperClassLoader loader = new WrapperClassLoader(jar)) {
public static void switchEnv(Iterable<Path> jars, String mainClass, String[] args) throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException, URISyntaxException {
try (WrapperClassLoader loader = new WrapperClassLoader(jars)) {
Thread.currentThread().setContextClassLoader(loader);
loader.loadClass(mainClass)
.getDeclaredMethod("main", String[].class)