- * Copyright (c) contributors
- *
- * 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.
- */
-package org.spongepowered.vanilla.installer;
-
-import org.checkerframework.checker.nullness.qual.Nullable;
-import org.tinylog.Logger;
-
-import java.io.File;
-import java.io.IOException;
-import java.lang.instrument.Instrumentation;
-import java.lang.reflect.Field;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.URISyntaxException;
-import java.net.URL;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.util.Map;
-import java.util.Set;
-import java.util.jar.JarFile;
-import java.util.jar.Manifest;
-
-/**
- * Agent, used to add downloaded jars to the system classpath and open modules
- * for deep reflection.
- *
- * This needs to be compiled for exactly java 9, since it runs before we have
- * an opportunity to provide a friendly warning message.
- */
-public class Agent {
-
- private static Instrumentation instrumentation;
- private static boolean usingFallback;
-
- public static void premain(final String agentArgs, final Instrumentation instrumentation) {
- Agent.instrumentation = instrumentation;
- }
-
- public static void agentmain(final String agentArgs, final Instrumentation instrumentation) {
- Agent.instrumentation = instrumentation;
- }
-
- static void addJarToClasspath(final Path jar) {
- if (Agent.instrumentation == null) {
- throw new IllegalStateException("The SpongeVanilla jar must be run as a java agent in order to add downloaded libraries to the classpath!");
- }
- try {
- final Path normalized = Paths.get(jar.toRealPath().toUri().toURL().toURI());
-
- if (Agent.usingFallback) {
- Fallback.addToSystemClasspath(jar);
- return;
- }
-
- try {
- // x.X The URL escaping done by appendToSystemClassLoaderSearch differs from the
- try (final JarFile jf = new JarFile(new File(normalized.toUri()))) {
- Agent.instrumentation.appendToSystemClassLoaderSearch(jf);
- }
- } catch (final IllegalArgumentException ex) {
- // For some reason, the Agent method on Windows can't handle some non-ASCII characters
- // This is fairly awful, but it makes things work (and hopefully won't be reached often)
- Logger.debug(ex, "Failed to add library {} to classpath, transitioning to fallback (more unsafe!) method", jar);
- Agent.usingFallback = true;
- Fallback.addToSystemClasspath(jar);
- }
- } catch (final IOException | URISyntaxException ex) {
- Logger.error(ex, "Failed to create jar file for archive '{}'!", jar);
- }
- }
-
- static void crackModules() {
- final Set systemUnnamed = Set.of(ClassLoader.getSystemClassLoader().getUnnamedModule());
- Agent.instrumentation.redefineModule(
- Manifest.class.getModule(),
- Set.of(),
- Map.of("sun.security.util", systemUnnamed), // ModLauncher
- Map.of(
- // ModLauncher -- needs Manifest.jv, and various JarVerifier methods
- "java.util.jar", systemUnnamed
- ),
- Set.of(),
- Map.of()
- );
- }
-
- static final class Fallback {
-
- private static final Object SYSTEM_CLASS_PATH; /* a URLClassPath */
- private static final Method ADD_URL; /* URLClassPath.addURL(java.net.URL) */
-
- static {
- Logger.debug("Initializing fallback classpath modification. This is only expected when using non-ASCII characters in file paths on Windows");
- // Crack the java.base module to allow us to use reflection
- final Set systemUnnamed = Set.of(ClassLoader.getSystemClassLoader().getUnnamedModule());
- Agent.instrumentation.redefineModule(
- ClassLoader.class.getModule(), /* java.base */
- Set.of(),
- Map.of("jdk.internal.loader", systemUnnamed),
- Map.of("jdk.internal.loader", systemUnnamed),
- Set.of(),
- Map.of()
- );
-
- final ClassLoader loader = ClassLoader.getSystemClassLoader();
-
- Field ucp = Fallback.fieldOrNull(loader.getClass(), "ucp");
- if (ucp == null) {
- ucp = Fallback.fieldOrNull(loader.getClass().getSuperclass(), "ucp");
- }
-
- if (ucp == null) {
- // Did they change something?
- throw new ExceptionInInitializerError("Unable to initialize fallback classpath handling on your system. Perhaps try a different Java version?");
- }
-
- try {
- SYSTEM_CLASS_PATH = ucp.get(loader);
- ADD_URL = Fallback.SYSTEM_CLASS_PATH.getClass().getDeclaredMethod("addURL", URL.class);
- } catch (final NoSuchMethodException | IllegalAccessException ex) {
- throw new ExceptionInInitializerError(ex);
- }
- }
-
- private static @Nullable Field fieldOrNull(final @Nullable Class> clazz, final String name) {
- if (clazz == null) {
- return null;
- }
-
- try {
- final Field f = clazz.getDeclaredField(name);
- f.setAccessible(true);
- return f;
- } catch (final NoSuchFieldException ex) {
- return null;
- }
- }
-
- static void addToSystemClasspath(final Path file) {
- try {
- Fallback.ADD_URL.invoke(Fallback.SYSTEM_CLASS_PATH, file.toUri().toURL());
- } catch (final IllegalAccessException | InvocationTargetException | IOException ex) {
- Logger.error(ex, "Failed to add file {} to the system classpath", file);
- throw new RuntimeException(ex);
- }
- }
-
- }
-
-}
diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java
index 3e6f96caa00..992785c98d9 100644
--- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java
+++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/InstallerMain.java
@@ -43,9 +43,14 @@
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.URI;
import java.net.URL;
+import java.net.URLClassLoader;
import java.net.URLConnection;
import java.nio.file.AccessDeniedException;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
@@ -63,12 +68,13 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.jar.JarFile;
+import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
public final class InstallerMain {
- private static final String COLLECTION_BOOTSTRAP = "bootstrap"; // goes on app
- private static final String COLLECTION_MAIN = "main"; // goes on TCL
+ private static final String COLLECTION_BOOTSTRAP = "bootstrap"; // boot layer
+ private static final String COLLECTION_MAIN = "main"; // game layer
private static final int MAX_TRIES = 2;
private final Installer installer;
@@ -126,41 +132,72 @@ public void downloadAndRun() throws Exception {
}
assert remappedMinecraftJar != null; // always assigned or thrown
- // MC itself and mojang dependencies are on the main layer, other libs are only on the bootstrap layer
+ // Minecraft itself is on the main layer
libraryManager.addLibrary(InstallerMain.COLLECTION_MAIN, new LibraryManager.Library("minecraft", remappedMinecraftJar.server()));
- for (final Map.Entry library : remappedMinecraftJar.libraries().entrySet()) {
- if (library.getKey().group().equals("com.mojang") || library.getKey().group().equals("net.minecraft")) {
- libraryManager.addLibrary(InstallerMain.COLLECTION_MAIN, new LibraryManager.Library(library.getKey().toString(), library.getValue()));
- } else {
- libraryManager.addLibrary(InstallerMain.COLLECTION_BOOTSTRAP, new LibraryManager.Library(library.getKey().toString(), library.getValue()));
- }
+
+ // Other libs are on the bootstrap layer
+ for (final Map.Entry entry : remappedMinecraftJar.libraries().entrySet()) {
+ final GroupArtifactVersion artifact = entry.getKey();
+ final Path path = entry.getValue();
+
+ libraryManager.addLibrary(InstallerMain.COLLECTION_BOOTSTRAP, new LibraryManager.Library(artifact.toString(), path));
}
+
this.installer.getLibraryManager().finishedProcessing();
Logger.info("Environment has been verified.");
final Set seenLibs = new HashSet<>();
- this.installer.getLibraryManager().getAll(InstallerMain.COLLECTION_BOOTSTRAP).stream()
- .peek(lib -> seenLibs.add(lib.getName()))
- .map(LibraryManager.Library::getFile)
- .forEach(path -> {
- Logger.debug("Adding jar {} to bootstrap classpath", path);
- Agent.addJarToClasspath(path);
- });
-
- final Path[] transformableLibs = this.installer.getLibraryManager().getAll(InstallerMain.COLLECTION_MAIN).stream()
- .filter(lib -> !seenLibs.contains(lib.getName()))
- .map(LibraryManager.Library::getFile)
+ final Path[] bootLibs = this.installer.getLibraryManager().getAll(InstallerMain.COLLECTION_BOOTSTRAP).stream()
+ .peek(lib -> seenLibs.add(lib.name()))
+ .map(LibraryManager.Library::file)
.toArray(Path[]::new);
+ final Path[] gameLibs = this.installer.getLibraryManager().getAll(InstallerMain.COLLECTION_MAIN).stream()
+ .filter(lib -> !seenLibs.contains(lib.name()))
+ .map(LibraryManager.Library::file)
+ .toArray(Path[]::new);
+
+ final URL rootJar = InstallerMain.class.getProtectionDomain().getCodeSource().getLocation();
+ final URI fsURI = new URI("jar", rootJar.toString(), null);
+ System.setProperty("sponge.rootJarFS", fsURI.toString());
+
+ final FileSystem fs = FileSystems.newFileSystem(fsURI, Map.of());
+ final Path spongeBoot = newJarInJar(fs.getPath("jars", "spongevanilla-boot.jar"));
+
+ String launchTarget = LauncherCommandLine.launchTarget;
+ if (launchTarget == null) {
+ final Path manifestFile = fs.getPath("META-INF", "MANIFEST.MF");
+ try (final InputStream stream = Files.newInputStream(manifestFile)) {
+ final Manifest manifest = new Manifest(stream);
+ launchTarget = manifest.getMainAttributes().getValue(Constants.ManifestAttributes.LAUNCH_TARGET);
+ }
+ }
+
+ final StringBuilder gameLibsEnv = new StringBuilder();
+ for (final Path lib : gameLibs) {
+ gameLibsEnv.append(lib.toAbsolutePath()).append(';');
+ }
+ gameLibsEnv.setLength(gameLibsEnv.length() - 1);
+ System.setProperty("sponge.gameResources", gameLibsEnv.toString());
+
final List gameArgs = new ArrayList<>(LauncherCommandLine.remainingArgs);
+ gameArgs.add("--launchTarget");
+ gameArgs.add(launchTarget);
Collections.addAll(gameArgs, this.installer.getLauncherConfig().args.split(" "));
- // Suppress illegal reflection warnings on newer java
- Agent.crackModules();
+ InstallerMain.bootstrap(bootLibs, spongeBoot, gameArgs.toArray(new String[0]));
+ }
- final String className = "org.spongepowered.vanilla.applaunch.Main";
- InstallerMain.invokeMain(className, gameArgs.toArray(new String[0]), transformableLibs);
+ private static Path newJarInJar(final Path jar) {
+ try {
+ URI jij = new URI("jij:" + jar.toAbsolutePath().toUri().getRawSchemeSpecificPart()).normalize();
+ final Map env = Map.of("packagePath", jar);
+ FileSystem jijFS = FileSystems.newFileSystem(jij, env);
+ return jijFS.getPath("/"); // root of the archive to load
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
}
private ServerAndLibraries recoverFromMinecraftDownloadError(final T ex) throws T {
@@ -175,17 +212,33 @@ private ServerAndLibraries recoverFromMinecraftDownloadErr
}
}
- private static void invokeMain(final String className, final String[] args, final Path[] extraCpEntries) {
+ private static void bootstrap(final Path[] bootLibs, final Path spongeBoot, final String[] args) throws Exception {
+ final URL[] urls = new URL[bootLibs.length];
+ for (int i = 0; i < bootLibs.length; i++) {
+ urls[i] = bootLibs[i].toAbsolutePath().toUri().toURL();
+ }
+
+ final List classpath = new ArrayList<>();
+ for (final Path lib : bootLibs) {
+ classpath.add(new Path[] { lib });
+ }
+ classpath.add(new Path[] { spongeBoot });
+
+ URLClassLoader loader = new URLClassLoader(urls, ClassLoader.getPlatformClassLoader());
+ ClassLoader previousLoader = Thread.currentThread().getContextClassLoader();
try {
- Class.forName(className)
- .getMethod("main", String[].class, Path[].class)
- .invoke(null, args, extraCpEntries);
- } catch (final InvocationTargetException ex) {
- Logger.error(ex.getCause(), "Failed to invoke main class {} due to an error", className);
- System.exit(1);
- } catch (final ClassNotFoundException | NoSuchMethodException | IllegalAccessException ex) {
- Logger.error(ex, "Failed to invoke main class {} due to an error", className);
+ Thread.currentThread().setContextClassLoader(loader);
+ final Class> cl = Class.forName("net.minecraftforge.bootstrap.Bootstrap", false, loader);
+ final Object instance = cl.getDeclaredConstructor().newInstance();
+ final Method m = cl.getDeclaredMethod("bootstrapMain", String[].class, List.class);
+ m.setAccessible(true);
+ m.invoke(instance, args, classpath);
+ } catch (final Exception ex) {
+ final Throwable cause = ex instanceof InvocationTargetException ? ex.getCause() : ex;
+ Logger.error(cause, "Failed to invoke bootstrap main due to an error");
System.exit(1);
+ } finally {
+ Thread.currentThread().setContextClassLoader(previousLoader);
}
}
@@ -391,47 +444,46 @@ private ServerAndLibraries remapMinecraft(final ServerAndLibraries minecraft, fi
final Renamer.Builder renamerBuilder = Renamer.builder()
.add(Transformer.parameterAnnotationFixerFactory())
.add(ctx -> {
- final Transformer backing = Transformer.renamerFactory(mappings, false).create(ctx);
- return new Transformer() {
-
- @Override
- public ClassEntry process(final ClassEntry entry) {
- final String name = entry.getName();
- if (name.startsWith("it/unimi")
- || name.startsWith("com/google")
- || name.startsWith("com/mojang/datafixers")
- || name.startsWith("com/mojang/brigadier")
- || name.startsWith("org/apache")) {
- return entry;
+ final Transformer backing = Transformer.renamerFactory(mappings, false).create(ctx);
+ return new Transformer() {
+ @Override
+ public ClassEntry process(final ClassEntry entry) {
+ final String name = entry.getName();
+ if (name.startsWith("it/unimi")
+ || name.startsWith("com/google")
+ || name.startsWith("com/mojang/datafixers")
+ || name.startsWith("com/mojang/brigadier")
+ || name.startsWith("org/apache")) {
+ return entry;
+ }
+ return backing.process(entry);
}
- return backing.process(entry);
- }
-
- @Override
- public ManifestEntry process(final ManifestEntry entry) {
- return backing.process(entry);
- }
- @Override
- public ResourceEntry process(final ResourceEntry entry) {
- return backing.process(entry);
- }
+ @Override
+ public ManifestEntry process(final ManifestEntry entry) {
+ return backing.process(entry);
+ }
- @Override
- public Collection extends Entry> getExtras() {
- return backing.getExtras();
- }
+ @Override
+ public ResourceEntry process(final ResourceEntry entry) {
+ return backing.process(entry);
+ }
- };
+ @Override
+ public Collection extends Entry> getExtras() {
+ return backing.getExtras();
+ }
+ };
})
.add(Transformer.recordFixerFactory())
.add(Transformer.parameterAnnotationFixerFactory())
.add(Transformer.sourceFixerFactory(SourceFixerConfig.JAVA))
.add(Transformer.signatureStripperFactory(SignatureStripperConfig.ALL))
.logger(Logger::debug); // quiet
- try (final Renamer ren = renamerBuilder.build()) {
- ren.run(minecraft.server.toFile(), tempOutput.toFile());
- }
+
+ try (final Renamer ren = renamerBuilder.build()) {
+ ren.run(minecraft.server.toFile(), tempOutput.toFile());
+ }
// Restore file
try {
diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LauncherCommandLine.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LauncherCommandLine.java
index f52c34a3169..89cc81e3075 100644
--- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LauncherCommandLine.java
+++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LauncherCommandLine.java
@@ -39,12 +39,16 @@
public final class LauncherCommandLine {
private static final OptionParser PARSER = new OptionParser();
- private static final ArgumentAcceptingOptionSpec INSTALLER_DIRECTORY_ARG = LauncherCommandLine.PARSER.accepts("installerDir",
- "Alternative installer directory").withRequiredArg().withValuesConvertedBy(new PathConverter(PathProperties.DIRECTORY_EXISTING))
+ private static final ArgumentAcceptingOptionSpec INSTALLER_DIRECTORY_ARG = LauncherCommandLine.PARSER
+ .accepts("installerDir", "Alternative installer directory").withRequiredArg()
+ .withValuesConvertedBy(new PathConverter(PathProperties.DIRECTORY_EXISTING))
.defaultsTo(Paths.get("."));
- private static final ArgumentAcceptingOptionSpec LIBRARIES_DIRECTORY_ARG = LauncherCommandLine.PARSER.accepts("librariesDir",
- "Alternative libraries directory").withRequiredArg().withValuesConvertedBy(new PathConverter(PathProperties.DIRECTORY_EXISTING))
+ private static final ArgumentAcceptingOptionSpec LIBRARIES_DIRECTORY_ARG = LauncherCommandLine.PARSER
+ .accepts("librariesDir", "Alternative libraries directory").withRequiredArg()
+ .withValuesConvertedBy(new PathConverter(PathProperties.DIRECTORY_EXISTING))
.defaultsTo(Paths.get("libraries"));
+ private static final ArgumentAcceptingOptionSpec LAUNCH_TARGET_ARG = LauncherCommandLine.PARSER
+ .accepts("launchTarget", "Launch target").withRequiredArg();
private static final NonOptionArgumentSpec REMAINDER = LauncherCommandLine.PARSER.nonOptions().ofType(String.class);
static {
@@ -52,6 +56,7 @@ public final class LauncherCommandLine {
}
public static Path installerDirectory, librariesDirectory;
+ public static String launchTarget;
public static List remainingArgs;
private LauncherCommandLine() {
@@ -61,6 +66,7 @@ public static void configure(final String[] args) {
final OptionSet options = LauncherCommandLine.PARSER.parse(args);
LauncherCommandLine.installerDirectory = options.valueOf(LauncherCommandLine.INSTALLER_DIRECTORY_ARG);
LauncherCommandLine.librariesDirectory = options.valueOf(LauncherCommandLine.LIBRARIES_DIRECTORY_ARG);
+ LauncherCommandLine.launchTarget = options.valueOf(LauncherCommandLine.LAUNCH_TARGET_ARG);
LauncherCommandLine.remainingArgs = UnmodifiableCollections.copyOf(options.valuesOf(LauncherCommandLine.REMAINDER));
}
}
diff --git a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java
index c7147a310fd..8c939f4c4e9 100644
--- a/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java
+++ b/vanilla/src/installer/java/org/spongepowered/vanilla/installer/LibraryManager.java
@@ -87,7 +87,7 @@ public Set getAll(final String collection) {
return Collections.unmodifiableSet(this.libraries.getOrDefault(collection, Collections.emptySet()));
}
- protected void addLibrary(final String set, final Library library) {
+ void addLibrary(final String set, final Library library) {
this.libraries.computeIfAbsent(set, $ -> Collections.synchronizedSet(new LinkedHashSet<>())).add(library);
}
@@ -244,24 +244,7 @@ public void finishedProcessing() {
}
}
- public static class Library {
-
- private final String name;
- private final Path file;
-
- public Library(final String name, final Path file) {
- this.name = name;
- this.file = file;
- }
-
- public String getName() {
- return this.name;
- }
-
- public Path getFile() {
- return this.file;
- }
- }
+ public record Library(String name, Path file) {}
private static String asId(final Dependency dep) {
return dep.group + ':' + dep.module + ':' + dep.version;