v1.0.15.1 — fix server re-extract of launcher zips; correct Forge launch command (-cp per version.json)

This commit is contained in:
SashegDev
2026-08-13 09:33:44 +00:00
parent b6dccd67ac
commit f857d91e4d
4 changed files with 148 additions and 83 deletions
@@ -70,43 +70,36 @@ public class LaunchCommandBuilder {
List<String> allJvmArgs = manifest != null ? manifest.getJvmArguments() : new ArrayList<>();
List<String> allGameArgs = manifest != null ? manifest.getAllGameArguments() : new ArrayList<>();
// Build variable map for placeholder substitution (${classpath},
// ${natives_directory}, ${library_directory}, ${version_name}, ...)
// Build variable map for placeholder substitution (${library_directory},
// ${classpath_separator}, ${version_name}, ${natives_directory}, ...)
Map<String, String> vars = buildVariableMap(options);
// Forge/NeoForge version.json defines its own -p / --module-path /
// --add-modules / --add-opens and references ${classpath}. We substitute
// ${classpath} once (only when referenced) and let the version.json
// arguments drive the classpath/module path exactly as AstralRinth and the
// 1.0.13 launcher did. Do NOT inject a separate -DlegacyClassPath:
// modern Forge's BootstrapLauncher derives the MC-BOOTSTRAP layer from the
// module path, and a redundant -DlegacyClassPath duplicates every library
// and can trigger split-package / duplicate-entry failures.
boolean needsClasspath = referencesClasspath(allJvmArgs) || referencesClasspath(allGameArgs);
if (needsClasspath) {
String classpath;
if (manifest != null) {
classpath = buildClasspathFromManifest(manifest, false);
} else {
classpath = "";
}
Path clientJar = findVersionJar();
if (clientJar != null && isValidJar(clientJar)) {
String sep = System.getProperty("os.name").toLowerCase().contains("win") ? ";" : ":";
classpath = clientJar.toAbsolutePath().toString() + (classpath.isEmpty() ? "" : sep + classpath);
System.out.println(ZAnsi.green(" Added client jar: " + clientJar.getFileName()));
} else {
System.out.println(ZAnsi.yellow(" Client jar not found, falling back to vanilla classpath"));
classpath = buildVanillaClasspath();
}
vars.put("classpath", writeClasspathFile(classpath));
System.out.println(ZAnsi.green(" ${classpath} referenced, classpath built for substitution"));
// Real Forge/NeoForge version.json (1.17+ / 47.x) declares a fixed -p
// module path of bootstrap jars and references ${library_directory} /
// ${classpath_separator} / ${version_name} — but it does NOT reference
// ${classpath} and does NOT contain -cp. To let the JVM find
// cpw.mods.bootstraplauncher.BootstrapLauncher we must supply -cp
// ourselves (version jar + all libraries, exactly like the official
// launcher). If a version.json DOES reference ${classpath}, we substitute
// the same full classpath into it first.
String fullClasspath;
if (manifest != null) {
fullClasspath = buildClasspathFromManifest(manifest, true);
} else {
System.out.println(ZAnsi.cyan(" ${classpath} not referenced by version.json, skipping classpath build"));
fullClasspath = buildVanillaClasspath();
}
boolean referencesClasspath = referencesClasspath(allJvmArgs) || referencesClasspath(allGameArgs);
if (referencesClasspath) {
vars.put("classpath", writeClasspathFile(fullClasspath));
System.out.println(ZAnsi.green(" ${classpath} referenced, substituted full classpath"));
} else {
System.out.println(ZAnsi.cyan(" ${classpath} not referenced by version.json, not substituting"));
}
boolean hasCpInJson = containsCpArg(allJvmArgs);
boolean hasLibraryPathInJson = containsLibraryPathArg(allJvmArgs);
if (!allJvmArgs.isEmpty()) {
for (String arg : allJvmArgs) {
String resolved = resolveVariable(arg, vars);
@@ -120,8 +113,11 @@ public class LaunchCommandBuilder {
command.addAll(getJvmArguments(options));
}
// Forge/NeoForge child version.json doesn't include -Djava.library.path
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
// Forge/NeoForge child version.json usually doesn't include
// -Djava.library.path (47.3.0 doesn't) — add it unless already present.
if (!hasLibraryPathInJson) {
command.add("-Djava.library.path=" + nativesDir.toAbsolutePath());
}
// Append memory/GC args (always after version.json args, like AstralRinth)
int ramMB = options.getMaxMemory() > 0 ? options.getMaxMemory() : 4096;
@@ -139,6 +135,15 @@ public class LaunchCommandBuilder {
command.addAll(options.getExtraJvmArgs());
}
// Real Forge/NeoForge version.json has no -cp, so the JVM would not find the
// main class. Add -cp with version jar + all libraries (official-launcher
// behaviour); skip only if the version.json itself declares -cp.
if (!hasCpInJson) {
command.add("-cp");
command.add(writeClasspathFile(fullClasspath));
System.out.println(ZAnsi.green(" Added -cp classpath for main class lookup"));
}
// Main class from version.json
String mainClass = null;
if (manifest != null) {
@@ -564,6 +569,22 @@ public class LaunchCommandBuilder {
return false;
}
private boolean containsCpArg(List<String> args) {
if (args == null) return false;
for (String arg : args) {
if (arg != null && (arg.equals("-cp") || arg.equals("-classpath"))) return true;
}
return false;
}
private boolean containsLibraryPathArg(List<String> args) {
if (args == null) return false;
for (String arg : args) {
if (arg != null && arg.startsWith("-Djava.library.path=")) return true;
}
return false;
}
private String buildClasspathFromManifest(VersionManifest manifest, boolean includeVersionJar) throws Exception {
List<String> paths = new ArrayList<>();
Path librariesDir = instance.getPath().resolve("libraries");
@@ -75,20 +75,36 @@ class LaunchCommandBuilderTest {
createJarWithModule(dir.resolve("versions/1.20.1/1.20.1.jar"), "minecraft");
createJar(dir.resolve("libraries/net/minecraft/client/1.20.1/client-1.20.1.jar"));
// Forge child
// Forge child — real 1.20.1-47.3.0 format: -p is a fixed bootstrap list,
// NO ${classpath} and NO -cp (the launcher must add -cp itself).
String forgeJson = """
{
"id": "1.20.1-forge-47.3.0",
"inheritsFrom": "1.20.1",
"mainClass": "cpw.mods.bootstraplauncher.BootstrapLauncher",
"arguments": {
"jvm": ["-p", "${classpath}", "--add-modules=ALL-MODULE-PATH", "--add-opens=java.base/java.util.jar=ALL-UNNAMED", "--add-opens=java.base/java.lang.invoke=ALL-UNNAMED"],
"game": ["--launchTarget", "forgeclient", "--fml.forgeVersion", "47.3.0", "--fml.mcVersion", "1.20.1", "--fml.forgeGroup", "net.minecraftforge"]
"jvm": ["-Djava.net.preferIPv6Addresses=system",
"-DignoreList=bootstraplauncher,securejarhandler,asm-commons,asm-util,asm-analysis,asm-tree,asm,JarJarFileSystems,client-extra,fmlcore,javafmllanguage,lowcodelanguage,mclanguage,forge-,${version_name}.jar",
"-DmergeModules=jna-5.10.0.jar,jna-platform-5.10.0.jar",
"-DlibraryDirectory=${library_directory}",
"-p", "${library_directory}/cpw/mods/bootstraplauncher/1.1.2/bootstraplauncher-1.1.2.jar${classpath_separator}${library_directory}/cpw/mods/securejarhandler/2.1.10/securejarhandler-2.1.10.jar",
"--add-modules", "ALL-MODULE-PATH",
"--add-opens", "java.base/java.util.jar=cpw.mods.securejarhandler",
"--add-opens", "java.base/java.lang.invoke=cpw.mods.securejarhandler"],
"game": ["--launchTarget", "forgeclient", "--fml.forgeVersion", "47.3.0",
"--fml.mcVersion", "1.20.1", "--fml.forgeGroup", "net.minecraftforge",
"--fml.mcpVersion", "20230612.114412"]
},
"libraries": [{"name": "net.minecraftforge:forge:1.20.1-47.3.0", "downloads": {"artifact": {"path": "net/minecraftforge/forge/1.20.1-47.3.0/forge-1.20.1-47.3.0.jar"}}}]
"libraries": [
{"name": "cpw.mods:bootstraplauncher:1.1.2", "downloads": {"artifact": {"path": "cpw/mods/bootstraplauncher/1.1.2/bootstraplauncher-1.1.2.jar"}}},
{"name": "cpw.mods:securejarhandler:2.1.10", "downloads": {"artifact": {"path": "cpw/mods/securejarhandler/2.1.10/securejarhandler-2.1.10.jar"}}},
{"name": "net.minecraftforge:forge:1.20.1-47.3.0", "downloads": {"artifact": {"path": "net/minecraftforge/forge/1.20.1-47.3.0/forge-1.20.1-47.3.0.jar"}}}
]
}
""";
writeVersionJson(dir.resolve("versions/" + versionId + "/" + versionId + ".json"), forgeJson);
createJar(dir.resolve("libraries/cpw/mods/bootstraplauncher/1.1.2/bootstraplauncher-1.1.2.jar"));
createJar(dir.resolve("libraries/cpw/mods/securejarhandler/2.1.10/securejarhandler-2.1.10.jar"));
createJar(dir.resolve("libraries/net/minecraftforge/forge/1.20.1-47.3.0/forge-1.20.1-47.3.0.jar"));
Files.createDirectories(dir.resolve("natives"));
@@ -215,21 +231,35 @@ class LaunchCommandBuilderTest {
List<String> command = builder.build(options);
// Parent's -cp must NOT appear (causes split-package with child's -p)
assertFalse(command.contains("-cp"), "Forge must not have -cp from parent");
// Real Forge version.json has no -cp (and no ${classpath}), so the launcher MUST add
// -cp with version jar + all libraries for the JVM to find BootstrapLauncher.
int cpIdx = command.indexOf("-cp");
assertTrue(cpIdx >= 0, "Launcher must add -cp for Forge (version.json has none)");
assertTrue(cpIdx + 1 < command.size(), "-cp must have a value");
String classpath = command.get(cpIdx + 1);
assertNotNull(classpath, "-cp value must not be null");
// -Djava.library.path must be added manually (not in child args)
String expectedLibPath = "-Djava.library.path=" + tempDir.resolve("natives").toAbsolutePath();
assertTrue(command.contains(expectedLibPath), "Forge must have -Djava.library.path");
// Child's -p (module path) must be present
// Child's -p (module path) must be the fixed bootstrap list passed through unchanged
int pIndex = command.indexOf("-p");
assertTrue(pIndex >= 0, "Forge must have -p");
assertTrue(pIndex + 1 < command.size(), "-p must have a value");
assertNotNull(command.get(pIndex + 1), "-p value must not be null");
String modulePath = command.get(pIndex + 1);
assertNotNull(modulePath, "-p value must not be null");
assertTrue(modulePath.contains("bootstraplauncher-1.1.2.jar"), "-p must contain bootstrap jar");
assertTrue(modulePath.contains("securejarhandler-2.1.10.jar"), "-p must contain bootstrap jar");
// Child's --add-modules must be present
assertTrue(command.contains("--add-modules=ALL-MODULE-PATH"), "Forge must have --add-modules=ALL-MODULE-PATH");
// ${classpath} must NOT be substituted / referenced in the real-json JVM args
assertFalse(classpath.contains("${classpath}"), "-cp must not contain unresolved ${classpath} placeholder");
// Child's --add-modules must be present (two-arg format)
int amIdx = command.indexOf("--add-modules");
assertTrue(amIdx >= 0, "Forge must have --add-modules");
assertTrue(amIdx + 1 < command.size(), "--add-modules must have a value");
assertEquals("ALL-MODULE-PATH", command.get(amIdx + 1), "--add-modules must be ALL-MODULE-PATH");
// Memory/GC args
assertTrue(command.contains("-Xmx4096M"), "Forge must have memory args");
@@ -255,9 +285,10 @@ class LaunchCommandBuilderTest {
List<String> command = builder.build(options);
// Find -p's classpath value
int pIdx = command.indexOf("-p");
String cpValue = command.get(pIdx + 1);
// Find -cp's classpath value (launcher-added; version.json has no -cp)
int cpIdx = command.indexOf("-cp");
assertTrue(cpIdx >= 0, "Launcher must add -cp for Forge");
String cpValue = command.get(cpIdx + 1);
// Version jar from ensureVersionJarForForge
assertTrue(cpValue.contains("1.20.1-forge-47.3.0.jar") || cpValue.contains("1.20.1.jar"),
@@ -400,8 +431,9 @@ class LaunchCommandBuilderTest {
List<String> command = builder.build(options);
// Same checks as Forge
assertFalse(command.contains("-cp"), "NeoForge must not have -cp from parent");
// NeoForge child jvm args reference ${classpath} (substituted into -p); version.json
// declares no -cp, so the launcher adds -cp with version jar + all libraries.
assertTrue(command.contains("-cp"), "NeoForge must have -cp (added by launcher)");
String expectedLibPath = "-Djava.library.path=" + tempDir.resolve("natives").toAbsolutePath();
assertTrue(command.contains(expectedLibPath), "NeoForge must have -Djava.library.path");
assertTrue(command.contains("-p"), "NeoForge must have -p");
@@ -494,27 +526,28 @@ class LaunchCommandBuilderTest {
String modulePath = command.get(pIdx + 1);
// Extract --add-modules, -Djava.library.path
String addModules = null;
String libPath = null;
for (String arg : command) {
if (arg.startsWith("-Djava.library.path=")) {
libPath = arg;
}
}
String addModules = null;
for (int i = 0; i < command.size(); i++) {
if ("--add-modules=ALL-MODULE-PATH".equals(command.get(i))) {
addModules = command.get(i);
if ("--add-modules".equals(command.get(i)) && i + 1 < command.size()) {
addModules = command.get(i + 1);
}
}
assertNotNull(addModules, "Must have --add-modules=ALL-MODULE-PATH");
assertNotNull(addModules, "Must have --add-modules");
assertEquals("ALL-MODULE-PATH", addModules, "--add-modules must be ALL-MODULE-PATH");
// Build minimal Java command that validates the module path
// java -Djava.library.path=<path> -p <classpath> --add-modules=ALL-MODULE-PATH -version
// java -Djava.library.path=<path> -p <classpath> --add-modules ALL-MODULE-PATH -version
ProcessBuilder pb = new ProcessBuilder(
"java",
libPath,
"-p", modulePath,
"--add-modules=ALL-MODULE-PATH",
"--add-modules", addModules,
"-version"
);
pb.redirectErrorStream(true);
+1 -1
View File
@@ -19,7 +19,7 @@
<properties>
<revision>1.0.15</revision>
<hotfix>0</hotfix>
<hotfix>1</hotfix>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+38 -27
View File
@@ -1377,6 +1377,37 @@ def generate_launcher_builds_meta():
logger.warning(f"Failed to save meta.json: {e}")
def generate_version_meta(version_path: Path, version: str) -> dict:
"""Generate and cache meta.json for an extracted version directory."""
files = []
for file_path in version_path.rglob("*"):
if file_path.is_file() and file_path.name != "meta.json":
rel_path = str(file_path.relative_to(version_path))
stat = file_path.stat()
file_hash = calculate_file_hash(file_path)
files.append({
"path": rel_path,
"size": stat.st_size,
"hash": f"sha256:{file_hash}"
})
meta = {
"version": version,
"type": "new",
"release_date": datetime.utcnow().isoformat(),
"files": files
}
try:
meta_path = version_path / "meta.json"
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(meta, f, indent=2)
except Exception as e:
logger.warning(f"Failed to save launcher meta for {version}: {e}")
return meta
def scan_launcher_version(version: str) -> Optional[dict]:
"""Scan a launcher version directory and return meta"""
# First check if meta exists in builds/ directly (for new format)
@@ -1407,33 +1438,7 @@ def scan_launcher_version(version: str) -> Optional[dict]:
pass
# Generate meta
files = []
for file_path in version_path.rglob("*"):
if file_path.is_file() and file_path.name != "meta.json":
rel_path = str(file_path.relative_to(version_path))
stat = file_path.stat()
file_hash = calculate_file_hash(file_path)
files.append({
"path": rel_path,
"size": stat.st_size,
"hash": f"sha256:{file_hash}"
})
meta = {
"version": version,
"type": "new",
"release_date": datetime.utcnow().isoformat(),
"files": files
}
# Save meta
try:
with open(meta_path, 'w', encoding='utf-8') as f:
json.dump(meta, f, indent=2)
except Exception as e:
logger.warning(f"Failed to save launcher meta for {version}: {e}")
return meta
return generate_version_meta(version_path, version)
def parse_version_key(v: str) -> tuple:
@@ -1492,6 +1497,12 @@ def extract_new_format_versions():
# Extract all files
zf.extractall(extract_dir)
# Generate meta.json cache for the extracted version. Without this the
# "already extracted" check above always fails: scan_launcher_version()
# short-circuits on BUILDS_DIR/meta.json for the latest version and never
# writes versions/<v>/meta.json, so every scan would re-extract the zip.
generate_version_meta(extract_dir, version)
logger.info(f"Extracted {zip_file.name} successfully")
except Exception as e:
logger.error(f"Failed to extract {zip_file.name}: {e}")