From 6c6cb46f3d01cc5c8ce39150579e0f2178228e02 Mon Sep 17 00:00:00 2001 From: Sebastian Hartte Date: Sun, 24 Nov 2024 20:19:15 +0100 Subject: [PATCH] Getting the production client to launch --- .../neodev/e2e/InstallProductionServer.java | 2 - .../neodev/e2e/RunProductionClient.java | 179 ++++++++++++------ .../net/neoforged/neodev/e2e/RunUtils.java | 34 ---- .../neodev/utils/MavenIdentifier.java | 40 ++++ 4 files changed, 165 insertions(+), 90 deletions(-) diff --git a/buildSrc/src/main/java/net/neoforged/neodev/e2e/InstallProductionServer.java b/buildSrc/src/main/java/net/neoforged/neodev/e2e/InstallProductionServer.java index f7bb1919fe..e562cf9e19 100644 --- a/buildSrc/src/main/java/net/neoforged/neodev/e2e/InstallProductionServer.java +++ b/buildSrc/src/main/java/net/neoforged/neodev/e2e/InstallProductionServer.java @@ -111,9 +111,7 @@ public void install() throws Exception { var jvmArgs = argFileContent.substring(0, startOfSplit); var programArgs = argFileContent.substring(startOfSplit + mainClass.length() + 1); - // We need to sanitize all JVM args by removing modular args var jvmArgParams = RunUtils.splitJvmArgs(jvmArgs); - RunUtils.cleanJvmArgs(jvmArgParams); // This is read by the JVM using the native platform encoding Files.write(getNeoForgeJvmArgFile().getAsFile().get().toPath(), jvmArgParams, Charset.forName(System.getProperty("native.encoding"))); diff --git a/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunProductionClient.java b/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunProductionClient.java index 6c274d938a..2c59b65de3 100644 --- a/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunProductionClient.java +++ b/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunProductionClient.java @@ -3,6 +3,7 @@ import com.google.gson.Gson; import com.google.gson.JsonObject; import net.neoforged.neodev.installer.IdentifiedFile; +import net.neoforged.neodev.utils.MavenIdentifier; import org.apache.tools.ant.taskdefs.condition.Os; import org.gradle.api.GradleException; import org.gradle.api.file.DirectoryProperty; @@ -28,6 +29,7 @@ import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; @@ -125,37 +127,112 @@ public void exec() { placeholders.put("library_directory", librariesDir.toAbsolutePath().toString()); placeholders.put("classpath_separator", File.pathSeparator); - // Copy all libraries to the production installation directory - // The classpath items must match the locations that might be hardcoded in the version profile against {libraries_dir} - var classpath = new ArrayList(); - for (var identifiedFile : getLibraryFiles().get()) { - var libraryFile = identifiedFile.getFile().getAsFile().get().toPath(); - var identifier = identifiedFile.getIdentifier().get(); - var destination = librariesDir.resolve(identifier.repositoryPath()); - try { - if (!Files.exists(destination) - || !Objects.equals(Files.getLastModifiedTime(destination), Files.getLastModifiedTime(libraryFile)) - || Files.size(destination) != Files.size(libraryFile)) { - Files.createDirectories(destination.getParent()); - Files.copy(libraryFile, destination, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); - } - } catch (IOException e) { - throw new GradleException("Failed to copy " + libraryFile + " to " + destination + ": " + e); - } - classpath.add(destination.toAbsolutePath().toString()); - } - placeholders.put("classpath", String.join(File.pathSeparator, classpath)); - execOperations.javaexec(spec -> { + // The JVM args at this point may include debugging options when started through IntelliJ spec.jvmArgs(getAllJvmArgs()); - applyVersionManifest(installDir, "neoforge-" + neoForgeVersion, placeholders, spec); + applyVersionManifest(installDir, "neoforge-" + neoForgeVersion, placeholders, librariesDir, spec); }); } /** * Applies a Vanilla Launcher version manifest to the JavaForkOptions. */ - private static void applyVersionManifest(Path installDir, String versionId, HashMap placeholders, JavaExecSpec spec) { + private void applyVersionManifest(Path installDir, + String versionId, + Map placeholders, + Path librariesDir, + JavaExecSpec spec) { + var manifests = loadVersionManifests(installDir, versionId); + + var mergedProgramArgs = new ArrayList(); + var mergedJvmArgs = new ArrayList(); + + for (var manifest : manifests) { + var mainClass = manifest.getAsJsonPrimitive("mainClass"); + if (mainClass != null) { + spec.getMainClass().set(mainClass.getAsString()); + } + + mergedProgramArgs.addAll(getArguments(manifest, "game")); + mergedJvmArgs.addAll(getArguments(manifest, "jvm")); + } + + // Index all available libraries + var availableLibraries = new HashMap(); + for (var identifiedFile : getLibraryFiles().get()) { + availableLibraries.put( + identifiedFile.getIdentifier().get(), + identifiedFile.getFile().get().getAsFile().toPath() + ); + } + + // The libraries are built in reverse, and libraries already added are not added again from parent manifests + var librariesAdded = new HashSet(); + var classpathItems = new ArrayList(); + for (var i = manifests.size() - 1; i >= 0; i--) { + var manifest = manifests.get(i); + + var libraries = manifest.getAsJsonArray("libraries"); + for (var library : libraries) { + var libraryObj = library.getAsJsonObject(); + + // Skip if disabled by rule + if (isDisabledByRules(libraryObj)) { + getLogger().info("Skipping library {} since it's condition is not met.", libraryObj); + continue; + } + + var id = MavenIdentifier.parse(libraryObj.get("name").getAsString()); + + // We use this to deduplicate the same library in different versions across manifests + var idWithoutVersion = new MavenIdentifier( + id.group(), + id.artifact(), + "", + id.classifier(), + id.extension() + ); + + if (!librariesAdded.add(idWithoutVersion)) { + continue; // The library was overridden by a child profile + } + + // Try finding the library in the classpath we got from Gradle + var availableLibrary = availableLibraries.get(id); + if (availableLibrary == null) { + throw new GradleException("Version manifest asks for " + id + " but this library is not available through Gradle."); + } + + // Copy over the library to the libraries directory, since our loader only deduplicates class-path + // items with module-path items when they are at the same location (and the module-path is defined + // relative to the libraries directory). + Path destination = null; + try { + destination = librariesDir.resolve(id.repositoryPath()); + if (!Files.exists(destination) + || !Objects.equals(Files.getLastModifiedTime(destination), Files.getLastModifiedTime(availableLibrary)) + || Files.size(destination) != Files.size(availableLibrary)) { + Files.createDirectories(destination.getParent()); + Files.copy(availableLibrary, destination, StandardCopyOption.COPY_ATTRIBUTES, StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new GradleException("Failed to copy library " + availableLibrary + " to " + destination + ": " + e, e); + } + classpathItems.add(destination.toAbsolutePath().toString()); + } + } + + var classpath = String.join(File.pathSeparator, classpathItems); + placeholders.putIfAbsent("classpath", classpath); + + expandPlaceholders(mergedProgramArgs, placeholders); + spec.args(mergedProgramArgs); + expandPlaceholders(mergedJvmArgs, placeholders); + spec.jvmArgs(mergedJvmArgs); + } + + // Returns the inherited manifests first + private static List loadVersionManifests(Path installDir, String versionId) { // Read back the version manifest and get the startup arguments var manifestPath = installDir.resolve("versions").resolve(versionId).resolve(versionId + ".json"); JsonObject manifest; @@ -165,25 +242,15 @@ private static void applyVersionManifest(Path installDir, String versionId, Hash throw new GradleException("Failed to read launcher profile " + manifestPath, e); } + var result = new ArrayList(); var inheritsFrom = manifest.getAsJsonPrimitive("inheritsFrom"); if (inheritsFrom != null) { - applyVersionManifest(installDir, inheritsFrom.getAsString(), placeholders, spec); + result.addAll(loadVersionManifests(installDir, inheritsFrom.getAsString())); } - var mainClass = manifest.getAsJsonPrimitive("mainClass").getAsString(); - spec.getMainClass().set(mainClass); + result.add(manifest); - // Vanilla Arguments - var programArgs = getArguments(manifest, "game"); - expandPlaceholders(programArgs, placeholders); - spec.args(programArgs); - // TODO Is this needed? RunUtils.escapeJvmArgs(programArgs); - - var jvmArgs = getArguments(manifest, "jvm"); - expandPlaceholders(jvmArgs, placeholders); - // TODO RunUtils.cleanJvmArgs(jvmArgs); - // TODO is this needed? RunUtils.escapeJvmArgs(jvmArgs); - spec.jvmArgs(jvmArgs); + return result; } private static void expandPlaceholders(List args, Map variables) { @@ -204,7 +271,17 @@ private static List getArguments(JsonObject manifest, String kind) { var gameArgs = manifest.getAsJsonObject("arguments").getAsJsonArray(kind); for (var gameArg : gameArgs) { if (gameArg.isJsonObject()) { - evaluateRule(gameArg.getAsJsonObject(), result); + var conditionalArgument = gameArg.getAsJsonObject(); + if (!isDisabledByRules(conditionalArgument)) { + var value = conditionalArgument.get("value"); + if (value.isJsonPrimitive()) { + result.add(value.getAsString()); + } else { + for (var valueEl : value.getAsJsonArray()) { + result.add(valueEl.getAsString()); + } + } + } } else { result.add(gameArg.getAsString()); } @@ -213,11 +290,13 @@ private static List getArguments(JsonObject manifest, String kind) { return result; } - /** - * Given a "rule" object from a Vanilla launcher profile, evaluate it into the effective arguments. - */ - private static void evaluateRule(JsonObject ruleObject, List out) { - for (var ruleEl : ruleObject.getAsJsonArray("rules")) { + private static boolean isDisabledByRules(JsonObject ruleObject) { + var rules = ruleObject.getAsJsonArray("rules"); + if (rules == null) { + return false; + } + + for (var ruleEl : rules) { var rule = ruleEl.getAsJsonObject(); boolean allow = "allow".equals(rule.getAsJsonPrimitive("action").getAsString()); // We only care about "os" rules @@ -227,22 +306,14 @@ private static void evaluateRule(JsonObject ruleObject, List out) { var arch = os.getAsJsonPrimitive("arch"); boolean ruleMatches = (name == null || isCurrentOsName(name.getAsString())) && (arch == null || isCurrentOsArch(arch.getAsString())); if (ruleMatches != allow) { - return; + return true; } } else { // We assume unknown rules do not apply - return; - } - } - - var value = ruleObject.get("value"); - if (value.isJsonPrimitive()) { - out.add(value.getAsString()); - } else { - for (var valueEl : value.getAsJsonArray()) { - out.add(valueEl.getAsString()); + return true; } } + return false; } private static boolean isCurrentOsName(String os) { diff --git a/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunUtils.java b/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunUtils.java index 0106e8a9f3..efcfc8862b 100644 --- a/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunUtils.java +++ b/buildSrc/src/main/java/net/neoforged/neodev/e2e/RunUtils.java @@ -26,38 +26,4 @@ static List splitJvmArgs(String jvmArgs) throws IOException { return args; } - /** - * We remove any classpath or module-path arguments since both have to be set up with project artifacts, - * and not artifacts from the installation. - */ - static void cleanJvmArgs(List jvmArgs) { - for (int i = 0; i < jvmArgs.size(); i++) { - var jvmArg = jvmArgs.get(i); - // Remove the classpath argument - if ("-cp".equals(jvmArg) || "-classpath".equals(jvmArg)) { - if (i + 1 < jvmArgs.size() && jvmArgs.get(i + 1).equals("${classpath}")) { - jvmArgs.remove(i + 1); - } - jvmArgs.remove(i--); - } else if ("-p".equals(jvmArg) || "--module-path".equals(jvmArg)) { - if (i + 1 < jvmArgs.size()) { - jvmArgs.remove(i + 1); - } - jvmArgs.remove(i--); - } - } - } - - static void escapeJvmArgs(List jvmArgs) { - jvmArgs.replaceAll(RunUtils::escapeJvmArg); - } - - static String escapeJvmArg(String arg) { - var escaped = arg.replace("\\", "\\\\").replace("\"", "\\\""); - if (escaped.contains(" ")) { - return "\"" + escaped + "\""; - } - return escaped; - } - } diff --git a/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java b/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java index 6a22312fd0..21dc04655b 100644 --- a/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java +++ b/buildSrc/src/main/java/net/neoforged/neodev/utils/MavenIdentifier.java @@ -10,4 +10,44 @@ public String artifactNotation() { public String repositoryPath() { return group.replace(".", "/") + "/" + artifact + "/" + version + "/" + artifact + "-" + version + (classifier.isEmpty() ? "" : "-" + classifier) + "." + extension; } + + /** + * Valid forms: + *
    + *
  • {@code groupId:artifactId:version}
  • + *
  • {@code groupId:artifactId:version:classifier}
  • + *
  • {@code groupId:artifactId:version:classifier@extension}
  • + *
  • {@code groupId:artifactId:version@extension}
  • + *
+ */ + public static MavenIdentifier parse(String coordinate) { + var coordinateAndExt = coordinate.split("@"); + String extension = "jar"; + if (coordinateAndExt.length > 2) { + throw new IllegalArgumentException("Malformed Maven coordinate: " + coordinate); + } else if (coordinateAndExt.length == 2) { + extension = coordinateAndExt[1]; + coordinate = coordinateAndExt[0]; + } + + var parts = coordinate.split(":"); + if (parts.length != 3 && parts.length != 4) { + throw new IllegalArgumentException("Malformed Maven coordinate: " + coordinate); + } + + var groupId = parts[0]; + var artifactId = parts[1]; + var version = parts[2]; + var classifier = parts.length == 4 ? parts[3] : ""; + return new MavenIdentifier(groupId, artifactId, version, classifier, extension); + } + + @Override + public String toString() { + if (classifier != null) { + return group + ":" + artifact + ":" + version + ":" + classifier + "@" + extension; + } else { + return group + ":" + artifact + ":" + version + "@" + extension; + } + } }