Skip to content

Commit

Permalink
Getting the production client to launch
Browse files Browse the repository at this point in the history
  • Loading branch information
shartte committed Nov 24, 2024
1 parent c6cc8f7 commit 6c6cb46
Show file tree
Hide file tree
Showing 4 changed files with 165 additions and 90 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")));
Expand Down
179 changes: 125 additions & 54 deletions buildSrc/src/main/java/net/neoforged/neodev/e2e/RunProductionClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String>();
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<String, String> placeholders, JavaExecSpec spec) {
private void applyVersionManifest(Path installDir,
String versionId,
Map<String, String> placeholders,
Path librariesDir,
JavaExecSpec spec) {
var manifests = loadVersionManifests(installDir, versionId);

var mergedProgramArgs = new ArrayList<String>();
var mergedJvmArgs = new ArrayList<String>();

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<MavenIdentifier, Path>();
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<MavenIdentifier>();
var classpathItems = new ArrayList<String>();
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<JsonObject> 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;
Expand All @@ -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<JsonObject>();
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<String> args, Map<String, String> variables) {
Expand All @@ -204,7 +271,17 @@ private static List<String> 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());
}
Expand All @@ -213,11 +290,13 @@ private static List<String> 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<String> 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
Expand All @@ -227,22 +306,14 @@ private static void evaluateRule(JsonObject ruleObject, List<String> 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) {
Expand Down
34 changes: 0 additions & 34 deletions buildSrc/src/main/java/net/neoforged/neodev/e2e/RunUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -26,38 +26,4 @@ static List<String> 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<String> 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<String> jvmArgs) {
jvmArgs.replaceAll(RunUtils::escapeJvmArg);
}

static String escapeJvmArg(String arg) {
var escaped = arg.replace("\\", "\\\\").replace("\"", "\\\"");
if (escaped.contains(" ")) {
return "\"" + escaped + "\"";
}
return escaped;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,44 @@ public String artifactNotation() {
public String repositoryPath() {
return group.replace(".", "/") + "/" + artifact + "/" + version + "/" + artifact + "-" + version + (classifier.isEmpty() ? "" : "-" + classifier) + "." + extension;
}

/**
* Valid forms:
* <ul>
* <li>{@code groupId:artifactId:version}</li>
* <li>{@code groupId:artifactId:version:classifier}</li>
* <li>{@code groupId:artifactId:version:classifier@extension}</li>
* <li>{@code groupId:artifactId:version@extension}</li>
* </ul>
*/
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;
}
}
}

0 comments on commit 6c6cb46

Please sign in to comment.