|
|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
/*
|
|
|
|
|
* Hello Minecraft! Launcher
|
|
|
|
|
* Copyright (C) 2020 huangyuhui <huanghongxun2008@126.com> and contributors
|
|
|
|
|
* Copyright (C) 2021 huangyuhui <huanghongxun2008@126.com> and contributors
|
|
|
|
|
*
|
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
|
@@ -20,6 +20,7 @@ package org.jackhuang.hmcl.download.forge;
|
|
|
|
|
import org.jackhuang.hmcl.download.ArtifactMalformedException;
|
|
|
|
|
import org.jackhuang.hmcl.download.DefaultDependencyManager;
|
|
|
|
|
import org.jackhuang.hmcl.download.LibraryAnalyzer;
|
|
|
|
|
import org.jackhuang.hmcl.download.forge.ForgeNewInstallProfile.Processor;
|
|
|
|
|
import org.jackhuang.hmcl.download.game.GameLibrariesTask;
|
|
|
|
|
import org.jackhuang.hmcl.game.Artifact;
|
|
|
|
|
import org.jackhuang.hmcl.game.DefaultGameRepository;
|
|
|
|
|
@@ -36,6 +37,7 @@ import org.jackhuang.hmcl.util.platform.CommandBuilder;
|
|
|
|
|
import org.jackhuang.hmcl.util.platform.JavaVersion;
|
|
|
|
|
import org.jackhuang.hmcl.util.platform.OperatingSystem;
|
|
|
|
|
import org.jackhuang.hmcl.util.platform.SystemUtils;
|
|
|
|
|
import org.jetbrains.annotations.NotNull;
|
|
|
|
|
|
|
|
|
|
import java.io.FileNotFoundException;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
@@ -45,6 +47,7 @@ import java.nio.file.Files;
|
|
|
|
|
import java.nio.file.Path;
|
|
|
|
|
import java.nio.file.Paths;
|
|
|
|
|
import java.util.*;
|
|
|
|
|
import java.util.concurrent.atomic.AtomicInteger;
|
|
|
|
|
import java.util.jar.Attributes;
|
|
|
|
|
import java.util.jar.JarFile;
|
|
|
|
|
import java.util.zip.ZipException;
|
|
|
|
|
@@ -55,6 +58,118 @@ import static org.jackhuang.hmcl.util.Logging.LOG;
|
|
|
|
|
|
|
|
|
|
public class ForgeNewInstallTask extends Task<Version> {
|
|
|
|
|
|
|
|
|
|
private class ProcessorTask extends Task<Void> {
|
|
|
|
|
|
|
|
|
|
private Processor processor;
|
|
|
|
|
private Map<String, String> vars;
|
|
|
|
|
|
|
|
|
|
public ProcessorTask(@NotNull Processor processor, @NotNull Map<String, String> vars) {
|
|
|
|
|
this.processor = processor;
|
|
|
|
|
this.vars = vars;
|
|
|
|
|
setSignificance(TaskSignificance.MODERATE);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void execute() throws Exception {
|
|
|
|
|
Map<String, String> outputs = new HashMap<>();
|
|
|
|
|
boolean miss = false;
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
|
|
|
|
|
String key = entry.getKey();
|
|
|
|
|
String value = entry.getValue();
|
|
|
|
|
|
|
|
|
|
key = parseLiteral(key, vars, ExceptionalFunction.identity());
|
|
|
|
|
value = parseLiteral(value, vars, ExceptionalFunction.identity());
|
|
|
|
|
|
|
|
|
|
if (key == null || value == null) {
|
|
|
|
|
throw new ArtifactMalformedException("Invalid forge installation configuration");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outputs.put(key, value);
|
|
|
|
|
|
|
|
|
|
Path artifact = Paths.get(key);
|
|
|
|
|
if (Files.exists(artifact)) {
|
|
|
|
|
String code;
|
|
|
|
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
|
|
|
|
code = encodeHex(digest("SHA-1", stream));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Objects.equals(code, value)) {
|
|
|
|
|
Files.delete(artifact);
|
|
|
|
|
LOG.info("Found existing file is not valid: " + artifact);
|
|
|
|
|
|
|
|
|
|
miss = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
miss = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!processor.getOutputs().isEmpty() && !miss) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
|
|
|
|
|
if (!Files.isRegularFile(jar))
|
|
|
|
|
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
|
|
|
|
|
|
|
|
|
|
String mainClass;
|
|
|
|
|
try (JarFile jarFile = new JarFile(jar.toFile())) {
|
|
|
|
|
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (StringUtils.isBlank(mainClass))
|
|
|
|
|
throw new Exception("Game processor jar does not have main class " + jar);
|
|
|
|
|
|
|
|
|
|
List<String> command = new ArrayList<>();
|
|
|
|
|
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
|
|
|
|
|
command.add("-cp");
|
|
|
|
|
|
|
|
|
|
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
|
|
|
|
|
for (Artifact artifact : processor.getClasspath()) {
|
|
|
|
|
Path file = gameRepository.getArtifactFile(version, artifact);
|
|
|
|
|
if (!Files.isRegularFile(file))
|
|
|
|
|
throw new Exception("Game processor dependency missing");
|
|
|
|
|
classpath.add(file.toString());
|
|
|
|
|
}
|
|
|
|
|
classpath.add(jar.toString());
|
|
|
|
|
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
|
|
|
|
|
|
|
|
|
|
command.add(mainClass);
|
|
|
|
|
|
|
|
|
|
List<String> args = new ArrayList<>(processor.getArgs().size());
|
|
|
|
|
for (String arg : processor.getArgs()) {
|
|
|
|
|
String parsed = parseLiteral(arg, vars, ExceptionalFunction.identity());
|
|
|
|
|
if (parsed == null)
|
|
|
|
|
throw new ArtifactMalformedException("Invalid forge installation configuration");
|
|
|
|
|
args.add(parsed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command.addAll(args);
|
|
|
|
|
|
|
|
|
|
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
|
|
|
|
|
int exitCode = SystemUtils.callExternalProcess(command);
|
|
|
|
|
if (exitCode != 0)
|
|
|
|
|
throw new IOException("Game processor exited abnormally with code " + exitCode);
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : outputs.entrySet()) {
|
|
|
|
|
Path artifact = Paths.get(entry.getKey());
|
|
|
|
|
if (!Files.isRegularFile(artifact))
|
|
|
|
|
throw new FileNotFoundException("File missing: " + artifact);
|
|
|
|
|
|
|
|
|
|
String code;
|
|
|
|
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
|
|
|
|
code = encodeHex(digest("SHA-1", stream));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Objects.equals(code, entry.getValue())) {
|
|
|
|
|
Files.delete(artifact);
|
|
|
|
|
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private final DefaultDependencyManager dependencyManager;
|
|
|
|
|
private final DefaultGameRepository gameRepository;
|
|
|
|
|
private final Version version;
|
|
|
|
|
@@ -63,9 +178,13 @@ public class ForgeNewInstallTask extends Task<Version> {
|
|
|
|
|
private final List<Task<?>> dependencies = new LinkedList<>();
|
|
|
|
|
|
|
|
|
|
private ForgeNewInstallProfile profile;
|
|
|
|
|
private List<Processor> processors;
|
|
|
|
|
private Version forgeVersion;
|
|
|
|
|
private final String selfVersion;
|
|
|
|
|
|
|
|
|
|
private Path tempDir;
|
|
|
|
|
private AtomicInteger processorDoneCount = new AtomicInteger(0);
|
|
|
|
|
|
|
|
|
|
ForgeNewInstallTask(DefaultDependencyManager dependencyManager, Version version, String selfVersion, Path installer) {
|
|
|
|
|
this.dependencyManager = dependencyManager;
|
|
|
|
|
this.gameRepository = dependencyManager.getGameRepository();
|
|
|
|
|
@@ -73,7 +192,7 @@ public class ForgeNewInstallTask extends Task<Version> {
|
|
|
|
|
this.installer = installer;
|
|
|
|
|
this.selfVersion = selfVersion;
|
|
|
|
|
|
|
|
|
|
setSignificance(TaskSignificance.MINOR);
|
|
|
|
|
setSignificance(TaskSignificance.MAJOR);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private static String replaceTokens(Map<String, String> tokens, String value) {
|
|
|
|
|
@@ -150,6 +269,7 @@ public class ForgeNewInstallTask extends Task<Version> {
|
|
|
|
|
public void preExecute() throws Exception {
|
|
|
|
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
|
|
|
|
profile = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath("install_profile.json")), ForgeNewInstallProfile.class);
|
|
|
|
|
processors = profile.getProcessors();
|
|
|
|
|
forgeVersion = JsonUtils.fromNonNullJson(FileUtils.readText(fs.getPath(profile.getJson())), Version.class);
|
|
|
|
|
|
|
|
|
|
for (Library library : profile.getLibraries()) {
|
|
|
|
|
@@ -167,151 +287,74 @@ public class ForgeNewInstallTask extends Task<Version> {
|
|
|
|
|
FileUtils.copyFile(mainJar, dest);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (ZipException ex) {
|
|
|
|
|
throw new ArtifactMalformedException("Malformed forge installer file", ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dependents.add(new GameLibrariesTask(dependencyManager, version, true, profile.getLibraries()));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private Task<?> createProcessorTask(Processor processor, Map<String, String> vars) {
|
|
|
|
|
Task<?> task = new ProcessorTask(processor, vars);
|
|
|
|
|
task.onDone().register(
|
|
|
|
|
() -> updateProgress(processorDoneCount.incrementAndGet(), processors.size()));
|
|
|
|
|
return task;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void execute() throws Exception {
|
|
|
|
|
Path temp = Files.createTempDirectory("forge_installer");
|
|
|
|
|
int finished = 0;
|
|
|
|
|
tempDir = Files.createTempDirectory("forge_installer");
|
|
|
|
|
|
|
|
|
|
Map<String, String> vars = new HashMap<>();
|
|
|
|
|
|
|
|
|
|
try (FileSystem fs = CompressingUtils.createReadOnlyZipFileSystem(installer)) {
|
|
|
|
|
List<ForgeNewInstallProfile.Processor> processors = profile.getProcessors();
|
|
|
|
|
Map<String, String> data = profile.getData();
|
|
|
|
|
|
|
|
|
|
updateProgress(0, processors.size());
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : data.entrySet()) {
|
|
|
|
|
for (Map.Entry<String, String> entry : profile.getData().entrySet()) {
|
|
|
|
|
String key = entry.getKey();
|
|
|
|
|
String value = entry.getValue();
|
|
|
|
|
|
|
|
|
|
data.put(key, parseLiteral(value,
|
|
|
|
|
vars.put(key, parseLiteral(value,
|
|
|
|
|
Collections.emptyMap(),
|
|
|
|
|
str -> {
|
|
|
|
|
Path dest = Files.createTempFile(temp, null, null);
|
|
|
|
|
Path dest = Files.createTempFile(tempDir, null, null);
|
|
|
|
|
FileUtils.copyFile(fs.getPath(str), dest);
|
|
|
|
|
return dest.toString();
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
data.put("SIDE", "client");
|
|
|
|
|
data.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
|
|
|
|
|
data.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath());
|
|
|
|
|
data.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath());
|
|
|
|
|
data.put("INSTALLER", installer.toAbsolutePath().toString());
|
|
|
|
|
data.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath());
|
|
|
|
|
|
|
|
|
|
for (ForgeNewInstallProfile.Processor processor : processors) {
|
|
|
|
|
Map<String, String> outputs = new HashMap<>();
|
|
|
|
|
boolean miss = false;
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : processor.getOutputs().entrySet()) {
|
|
|
|
|
String key = entry.getKey();
|
|
|
|
|
String value = entry.getValue();
|
|
|
|
|
|
|
|
|
|
key = parseLiteral(key, data, ExceptionalFunction.identity());
|
|
|
|
|
value = parseLiteral(value, data, ExceptionalFunction.identity());
|
|
|
|
|
|
|
|
|
|
if (key == null || value == null) {
|
|
|
|
|
throw new ArtifactMalformedException("Invalid forge installation configuration");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
outputs.put(key, value);
|
|
|
|
|
|
|
|
|
|
Path artifact = Paths.get(key);
|
|
|
|
|
if (Files.exists(artifact)) {
|
|
|
|
|
String code;
|
|
|
|
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
|
|
|
|
code = encodeHex(digest("SHA-1", stream));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Objects.equals(code, value)) {
|
|
|
|
|
Files.delete(artifact);
|
|
|
|
|
LOG.info("Found existing file is not valid: " + artifact);
|
|
|
|
|
|
|
|
|
|
miss = true;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
miss = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!processor.getOutputs().isEmpty() && !miss) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Path jar = gameRepository.getArtifactFile(version, processor.getJar());
|
|
|
|
|
if (!Files.isRegularFile(jar))
|
|
|
|
|
throw new FileNotFoundException("Game processor file not found, should be downloaded in preprocess");
|
|
|
|
|
|
|
|
|
|
String mainClass;
|
|
|
|
|
try (JarFile jarFile = new JarFile(jar.toFile())) {
|
|
|
|
|
mainClass = jarFile.getManifest().getMainAttributes().getValue(Attributes.Name.MAIN_CLASS);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (StringUtils.isBlank(mainClass))
|
|
|
|
|
throw new Exception("Game processor jar does not have main class " + jar);
|
|
|
|
|
|
|
|
|
|
List<String> command = new ArrayList<>();
|
|
|
|
|
command.add(JavaVersion.fromCurrentEnvironment().getBinary().toString());
|
|
|
|
|
command.add("-cp");
|
|
|
|
|
|
|
|
|
|
List<String> classpath = new ArrayList<>(processor.getClasspath().size() + 1);
|
|
|
|
|
for (Artifact artifact : processor.getClasspath()) {
|
|
|
|
|
Path file = gameRepository.getArtifactFile(version, artifact);
|
|
|
|
|
if (!Files.isRegularFile(file))
|
|
|
|
|
throw new Exception("Game processor dependency missing");
|
|
|
|
|
classpath.add(file.toString());
|
|
|
|
|
}
|
|
|
|
|
classpath.add(jar.toString());
|
|
|
|
|
command.add(String.join(OperatingSystem.PATH_SEPARATOR, classpath));
|
|
|
|
|
|
|
|
|
|
command.add(mainClass);
|
|
|
|
|
|
|
|
|
|
List<String> args = new ArrayList<>(processor.getArgs().size());
|
|
|
|
|
for (String arg : processor.getArgs()) {
|
|
|
|
|
String parsed = parseLiteral(arg, data, ExceptionalFunction.identity());
|
|
|
|
|
if (parsed == null)
|
|
|
|
|
throw new ArtifactMalformedException("Invalid forge installation configuration");
|
|
|
|
|
args.add(parsed);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command.addAll(args);
|
|
|
|
|
|
|
|
|
|
LOG.info("Executing external processor " + processor.getJar().toString() + ", command line: " + new CommandBuilder().addAll(command).toString());
|
|
|
|
|
int exitCode = SystemUtils.callExternalProcess(command);
|
|
|
|
|
if (exitCode != 0)
|
|
|
|
|
throw new IOException("Game processor exited abnormally");
|
|
|
|
|
|
|
|
|
|
for (Map.Entry<String, String> entry : outputs.entrySet()) {
|
|
|
|
|
Path artifact = Paths.get(entry.getKey());
|
|
|
|
|
if (!Files.isRegularFile(artifact))
|
|
|
|
|
throw new FileNotFoundException("File missing: " + artifact);
|
|
|
|
|
|
|
|
|
|
String code;
|
|
|
|
|
try (InputStream stream = Files.newInputStream(artifact)) {
|
|
|
|
|
code = encodeHex(digest("SHA-1", stream));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!Objects.equals(code, entry.getValue())) {
|
|
|
|
|
Files.delete(artifact);
|
|
|
|
|
throw new ChecksumMismatchException("SHA-1", entry.getValue(), code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateProgress(++finished, processors.size());
|
|
|
|
|
}
|
|
|
|
|
} catch (ZipException ex) {
|
|
|
|
|
throw new ArtifactMalformedException("Malformed forge installer file", ex);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
vars.put("SIDE", "client");
|
|
|
|
|
vars.put("MINECRAFT_JAR", gameRepository.getVersionJar(version).getAbsolutePath());
|
|
|
|
|
vars.put("MINECRAFT_VERSION", gameRepository.getVersionJar(version).getAbsolutePath());
|
|
|
|
|
vars.put("ROOT", gameRepository.getBaseDirectory().getAbsolutePath());
|
|
|
|
|
vars.put("INSTALLER", installer.toAbsolutePath().toString());
|
|
|
|
|
vars.put("LIBRARY_DIR", gameRepository.getLibrariesDirectory(version).getAbsolutePath());
|
|
|
|
|
|
|
|
|
|
updateProgress(0, processors.size());
|
|
|
|
|
|
|
|
|
|
Task<?> processorsTask = Task.runSequentially(
|
|
|
|
|
processors.stream()
|
|
|
|
|
.map(processor -> createProcessorTask(processor, vars))
|
|
|
|
|
.toArray(Task<?>[]::new));
|
|
|
|
|
|
|
|
|
|
dependencies.add(
|
|
|
|
|
processorsTask.thenComposeAsync(
|
|
|
|
|
dependencyManager.checkLibraryCompletionAsync(forgeVersion, true)));
|
|
|
|
|
|
|
|
|
|
setResult(forgeVersion
|
|
|
|
|
.setPriority(30000)
|
|
|
|
|
.setId(LibraryAnalyzer.LibraryType.FORGE.getPatchId())
|
|
|
|
|
.setVersion(selfVersion));
|
|
|
|
|
dependencies.add(dependencyManager.checkLibraryCompletionAsync(forgeVersion, true));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
FileUtils.deleteDirectory(temp.toFile());
|
|
|
|
|
@Override
|
|
|
|
|
public boolean doPostExecute() {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Override
|
|
|
|
|
public void postExecute() throws Exception {
|
|
|
|
|
FileUtils.deleteDirectory(tempDir.toFile());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|