Слияние ui -> main #1
+55
-34
@@ -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");
|
||||
|
||||
+54
-21
@@ -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
@@ -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
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user