1
0
mirror of https://github.com/SKCraft/Launcher.git synced 2024-11-27 12:46:22 +01:00

Add support for mod loader installation.

This commit is contained in:
sk89q 2015-02-17 20:31:37 -08:00
parent 7c887ad5a3
commit 883eaf63eb
12 changed files with 526 additions and 24 deletions

10
pom.xml
View File

@ -44,6 +44,16 @@
<artifactId>jcommander</artifactId>
<version>1.32</version>
</dependency>
<dependency>
<groupId>org.tukaani</groupId>
<artifactId>xz</artifactId>
<version>1.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.9</version>
</dependency>
</dependencies>
<build>

View File

@ -7,6 +7,7 @@
package com.skcraft.launcher.builder;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.ParameterException;
import lombok.Data;
import java.io.File;
@ -15,14 +16,6 @@ import java.io.File;
public class BuilderOptions {
// Configuration
@Parameter(names = "--config")
private File configPath;
@Parameter(names = "--version-file")
private File versionManifestPath;
@Parameter(names = "--libs-url")
private String librariesLocation;
@Parameter(names = "--objects-url")
private String objectsLocation;
// Override config
@Parameter(names = "--name")
@ -35,17 +28,82 @@ public class BuilderOptions {
// Required
@Parameter(names = "--version", required = true)
private String version;
// Paths
@Parameter(names = "--files", required = true)
private File filesDir;
@Parameter(names = "--manifest-dest", required = true)
private File manifestPath;
@Parameter(names = "--objects-dest", required = true)
// Overall paths
@Parameter(names = {"--input", "-i"})
private File inputPath;
@Parameter(names = {"--output", "-o"})
private File outputPath;
// Input paths
@Parameter(names = "--config")
private File configPath;
@Parameter(names = "--version-file")
private File versionManifestPath;
@Parameter(names = "--files")
private File filesDir;
@Parameter(names = "--loaders")
private File loadersDir;
// Output paths
@Parameter(names = "--objects-dest")
private File objectsDir;
@Parameter(names = "--libraries-dest")
private File librariesDir;
@Parameter(names = "--libs-url")
private String librariesLocation = "libraries";
@Parameter(names = "--objects-url")
private String objectsLocation = "objects";
// Misc
@Parameter(names = "--pretty-print")
private boolean prettyPrinting;
public void choosePaths() throws ParameterException {
if (configPath == null) {
requireInputPath("--config");
configPath = new File(inputPath, "modpack.json");
}
if (versionManifestPath == null) {
requireInputPath("--version");
versionManifestPath = new File(inputPath, "version.json");
}
if (filesDir == null) {
requireInputPath("--files");
filesDir = new File(inputPath, "src");
}
if (loadersDir == null) {
requireInputPath("--loaders");
loadersDir = new File(inputPath, "loaders");
}
if (objectsDir == null) {
requireOutputPath("--objects-dest");
objectsDir = new File(outputPath, objectsLocation);
}
if (librariesDir == null) {
requireOutputPath("--libs-dest");
librariesDir = new File(outputPath, librariesLocation);
}
}
private void requireOutputPath(String name) throws ParameterException {
if (outputPath == null) {
throw new ParameterException("Because " + name + " was not specified, --output needs to be specified as the output directory and then " + name + " will be default to a pre-set path within the output directory");
}
}
private void requireInputPath(String name) throws ParameterException {
if (inputPath == null) {
throw new ParameterException("Because " + name + " was not specified, --input needs to be specified as the project directory and then " + name + " will be default to a pre-set path within the project directory");
}
}
}

View File

@ -0,0 +1,52 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.builder;
import com.beust.jcommander.internal.Lists;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import java.util.Collections;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public final class BuilderUtils {
private BuilderUtils() {
}
public static String normalizePath(String path) {
return path.replaceAll("^[/\\\\]*", "").replaceAll("[/\\\\]+", "/");
}
public static ZipEntry getZipEntry(ZipFile jarFile, String path) {
Enumeration<? extends ZipEntry> entries = jarFile.entries();
String expected = normalizePath(path);
while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();
String test = normalizePath(entry.getName());
if (expected.equals(test)) {
return entry;
}
}
return null;
}
public static List<Compressor> getCompressors(String repoUrl) {
if (repoUrl.matches("^https?://files.minecraftforge.net/maven/")) {
return Lists.newArrayList(
new Compressor("xz", CompressorStreamFactory.XZ),
new Compressor("pack", CompressorStreamFactory.PACK200));
} else {
return Collections.emptyList();
}
}
}

View File

@ -0,0 +1,48 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.builder;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.compress.compressors.CompressorStreamFactory;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class Compressor {
private static final CompressorStreamFactory factory = new CompressorStreamFactory();
private final String extension;
private final String format;
public Compressor(String extension, String format) {
this.extension = extension;
this.format = format;
}
public String transformPathname(String filename) {
return filename + "." + extension;
}
public InputStream createInputStream(InputStream inputStream) throws IOException {
try {
return factory.createCompressorInputStream(format, inputStream);
} catch (CompressorException e) {
throw new IOException("Failed to create decompressor", e);
}
}
public OutputStream createOutputStream(OutputStream outputStream) throws IOException {
try {
return factory.createCompressorOutputStream(format, outputStream);
} catch (CompressorException e) {
throw new IOException("Failed to create compressor", e);
}
}
}

View File

@ -0,0 +1,19 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.builder;
import java.io.File;
import java.io.FileFilter;
public class JarFileFilter implements FileFilter {
@Override
public boolean accept(File pathname) {
return pathname.getName().toLowerCase().endsWith(".jar");
}
}

View File

@ -7,21 +7,43 @@
package com.skcraft.launcher.builder;
import com.beust.jcommander.JCommander;
import com.beust.jcommander.ParameterException;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.google.common.base.Strings;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
import com.google.common.io.CharStreams;
import com.google.common.io.Closer;
import com.google.common.io.Files;
import com.skcraft.launcher.Launcher;
import com.skcraft.launcher.LauncherUtils;
import com.skcraft.launcher.model.loader.InstallProfile;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.util.Environment;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SimpleLogFormatter;
import lombok.Getter;
import lombok.NonNull;
import lombok.extern.java.Log;
import java.io.File;
import java.io.IOException;
import java.io.*;
import java.net.URL;
import java.util.List;
import java.util.Properties;
import java.util.jar.JarFile;
import java.util.logging.Level;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Strings.emptyToNull;
import static com.skcraft.launcher.util.HttpRequest.url;
/**
* Builds packages for the launcher.
@ -29,12 +51,17 @@ import static com.google.common.base.Strings.emptyToNull;
@Log
public class PackageBuilder {
private static final Pattern TWEAK_CLASS_ARG = Pattern.compile("--tweakClass\\s+([^\\s]+)");
private final Properties properties;
private final ObjectMapper mapper;
private ObjectWriter writer;
private final Manifest manifest;
private final PropertiesApplicator applicator;
@Getter
private boolean prettyPrint = false;
private List<Library> loaderLibraries = Lists.newArrayList();
private List<String> mavenRepos;
/**
* Create a new package builder.
@ -42,11 +69,22 @@ public class PackageBuilder {
* @param mapper the mapper
* @param manifest the manifest
*/
public PackageBuilder(@NonNull ObjectMapper mapper, @NonNull Manifest manifest) {
public PackageBuilder(@NonNull ObjectMapper mapper, @NonNull Manifest manifest) throws IOException {
this.properties = LauncherUtils.loadProperties(Launcher.class,
"launcher.properties", "com.skcraft.launcher.propertiesFile");
this.mapper = mapper;
this.manifest = manifest;
this.applicator = new PropertiesApplicator(manifest);
setPrettyPrint(false); // Set writer
Closer closer = Closer.create();
try {
mavenRepos = mapper.readValue(closer.register(Launcher.class.getResourceAsStream("maven_repos.json")), new TypeReference<List<String>>() {
});
} finally {
closer.close();
}
}
public void setPrettyPrint(boolean prettyPrint) {
@ -71,6 +109,156 @@ public class PackageBuilder {
collector.walk(dir);
}
public void addLoaders(File dir, File librariesDir) {
logSection("Checking for mod loaders to install...");
File[] files = dir.listFiles(new JarFileFilter());
if (files != null) {
for (File file : files) {
try {
processLoader(file, librariesDir);
} catch (IOException e) {
log.log(Level.WARNING, "Failed to add the loader at " + file.getAbsolutePath(), e);
}
}
}
}
private void processLoader(File file, File librariesDir) throws IOException {
log.info("Installing " + file.getName() + "...");
JarFile jarFile = new JarFile(file);
Closer closer = Closer.create();
try {
ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json");
if (profileEntry != null) {
InputStream stream = jarFile.getInputStream(profileEntry);
// Read file
String data = CharStreams.toString(closer.register(new InputStreamReader(stream)));
data = data.replaceAll(",\\s*\\}", "}"); // Fix issues with trailing commas
InstallProfile profile = mapper.readValue(data, InstallProfile.class);
VersionManifest version = manifest.getVersionManifest();
// Copy tweak class arguments
String args = profile.getVersionInfo().getMinecraftArguments();
if (args != null) {
String existingArgs = Strings.nullToEmpty(version.getMinecraftArguments());
Matcher m = TWEAK_CLASS_ARG.matcher(args);
while (m.find()) {
version.setMinecraftArguments(existingArgs + " " + m.group());
log.info("Adding " + m.group() + " to launch arguments");
}
}
// Add libraries
List<Library> libraries = profile.getVersionInfo().getLibraries();
if (libraries != null) {
version.getLibraries().addAll(libraries);
loaderLibraries.addAll(libraries);
}
// Copy main class
String mainClass = profile.getVersionInfo().getMainClass();
if (mainClass != null) {
version.setMainClass(mainClass);
log.info("Using " + mainClass + " as the main class");
}
// Extract the library
String filePath = profile.getInstallData().getFilePath();
String libraryPath = profile.getInstallData().getPath();
if (filePath != null && libraryPath != null) {
ZipEntry libraryEntry = BuilderUtils.getZipEntry(jarFile, filePath);
if (libraryEntry != null) {
Library library = new Library();
library.setName(libraryPath);
File extractPath = new File(librariesDir, library.getPath(Environment.getInstance()));
Files.createParentDirs(extractPath);
ByteStreams.copy(closer.register(jarFile.getInputStream(libraryEntry)), Files.newOutputStreamSupplier(extractPath));
} else {
log.warning("Could not find the file '" + filePath + "' in " + file.getAbsolutePath() + ", which means that this mod loader will not work correctly");
}
}
} else {
log.warning("The file at " + file.getAbsolutePath() + " did not appear to have an " +
"install_profile.json file inside -- is it actually an installer for a mod loader?");
}
} finally {
closer.close();
jarFile.close();
}
}
public void downloadLibraries(File librariesDir) throws IOException, InterruptedException {
logSection("Downloading libraries...");
// TODO: Download libraries for different environments -- As of writing, this is not an issue
Environment env = Environment.getInstance();
for (Library library : loaderLibraries) {
File outputPath = new File(librariesDir, library.getPath(env));
if (!outputPath.exists()) {
Files.createParentDirs(outputPath);
boolean found = false;
// Gather a list of repositories to download from
List<String> sources = Lists.newArrayList();
if (library.getBaseUrl() != null) {
sources.add(library.getBaseUrl());
}
sources.addAll(mavenRepos);
// Try each repository
for (String baseUrl : sources) {
String pathname = library.getPath(env);
// Some repositories compress their files
List<Compressor> compressors = BuilderUtils.getCompressors(baseUrl);
for (Compressor compressor : Lists.reverse(compressors)) {
pathname = compressor.transformPathname(pathname);
}
URL url = new URL(baseUrl + pathname);
File tempFile = File.createTempFile("launcherlib", null);
try {
log.info("Downloading library " + library.getName() + " from " + url + "...");
HttpRequest.get(url).execute().expectResponseCode(200).saveContent(tempFile);
} catch (IOException e) {
log.info("Could not get file from " + url + ": " + e.getMessage());
continue;
}
// Decompress (if needed) and write to file
Closer closer = Closer.create();
InputStream inputStream = closer.register(new FileInputStream(tempFile));
inputStream = closer.register(new BufferedInputStream(inputStream));
for (Compressor compressor : compressors) {
inputStream = closer.register(compressor.createInputStream(inputStream));
}
ByteStreams.copy(inputStream, closer.register(new FileOutputStream(outputPath)));
tempFile.delete();
found = true;
break;
}
if (!found) {
log.warning("!! Failed to download the library " + library.getName() + " -- this means your copy of the libraries will lack this file");
}
}
}
}
public void validateManifest() {
checkNotNull(emptyToNull(manifest.getName()), "Package name is not defined");
checkNotNull(emptyToNull(manifest.getGameVersion()), "Game version is not defined");
@ -84,14 +272,33 @@ public class PackageBuilder {
}
}
public void readVersionManifest(File path) throws IOException {
if (path != null) {
public void readVersionManifest(File path) throws IOException, InterruptedException {
logSection("Reading version manifest...");
if (path.exists()) {
VersionManifest versionManifest = read(path, VersionManifest.class);
manifest.setVersionManifest(versionManifest);
log.info("Loaded version manifest from " + path.getAbsolutePath());
} else {
URL url = url(String.format(
properties.getProperty("versionManifestUrl"),
manifest.getGameVersion()));
log.info("Fetching version manifest from " + url + "...");
manifest.setVersionManifest(HttpRequest
.get(url)
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(VersionManifest.class));
}
}
public void writeManifest(@NonNull File path) throws IOException {
logSection("Writing manifest...");
manifest.setFeatures(applicator.getFeaturesInUse());
VersionManifest versionManifest = manifest.getVersionManifest();
if (versionManifest != null) {
@ -100,11 +307,14 @@ public class PackageBuilder {
validateManifest();
path.getAbsoluteFile().getParentFile().mkdirs();
writer.writeValue(path, manifest);
log.info("Wrote manifest to " + path.getAbsolutePath());
}
private static BuilderOptions parseArgs(String[] args) {
BuilderOptions options = new BuilderOptions();
new JCommander(options, args);
options.choosePaths();
return options;
}
@ -127,9 +337,18 @@ public class PackageBuilder {
*
* @param args arguments
* @throws IOException thrown on I/O error
* @throws InterruptedException on interruption
*/
public static void main(String[] args) throws IOException {
BuilderOptions options = parseArgs(args);
public static void main(String[] args) throws IOException, InterruptedException {
BuilderOptions options;
try {
options = parseArgs(args);
} catch (ParameterException e) {
new JCommander().usage();
System.err.println("error: " + e.getMessage());
System.exit(1);
return;
}
// Initialize
SimpleLogFormatter.configureGlobalLogger();
@ -155,10 +374,18 @@ public class PackageBuilder {
builder.scan(options.getFilesDir());
builder.addFiles(options.getFilesDir(), options.getObjectsDir());
builder.addLoaders(options.getLoadersDir(), options.getLibrariesDir());
builder.downloadLibraries(options.getLibrariesDir());
builder.writeManifest(options.getManifestPath());
log.info("Wrote manifest to " + options.getManifestPath().getAbsolutePath());
log.info("Done.");
logSection("Done");
log.info("Now upload the contents of " + options.getOutputPath() + " to your web server or CDN!");
}
private static void logSection(String name) {
log.info("");
log.info("--- " + name + " ---");
}
}

View File

@ -0,0 +1,19 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class InstallData {
private String path;
private String filePath;
}

View File

@ -0,0 +1,21 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class InstallProfile {
@JsonProperty("install")
private InstallData installData;
private VersionInfo versionInfo;
}

View File

@ -0,0 +1,23 @@
/*
* SK's Minecraft Launcher
* Copyright (C) 2010-2014 Albert Pham <http://www.sk89q.com> and contributors
* Please see LICENSE.txt for license information.
*/
package com.skcraft.launcher.model.loader;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.skcraft.launcher.model.minecraft.Library;
import lombok.Data;
import java.util.List;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class VersionInfo {
private String minecraftArguments;
private String mainClass;
private List<Library> libraries;
}

View File

@ -25,6 +25,8 @@ public class Library {
private transient String group;
private transient String artifact;
private transient String version;
@JsonProperty("url")
private String baseUrl;
private Map<String, String> natives;
private Extract extract;
private List<Rule> rules;
@ -122,6 +124,24 @@ public class Library {
return path;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Library library = (Library) o;
if (name != null ? !name.equals(library.name) : library.name != null)
return false;
return true;
}
@Override
public int hashCode() {
return name != null ? name.hashCode() : 0;
}
@Data
public static class Rule {
private Action action;

View File

@ -11,7 +11,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.Data;
import java.util.Date;
import java.util.List;
import java.util.LinkedHashSet;
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
@ -26,7 +26,7 @@ public class VersionManifest {
private String minecraftArguments;
private String mainClass;
private int minimumLauncherVersion;
private List<Library> libraries;
private LinkedHashSet<Library> libraries;
@JsonIgnore
public String getAssetsIndex() {

View File

@ -0,0 +1,5 @@
[
"https://libraries.minecraft.net/",
"https://central.maven.org/maven2/",
"http://maven.apache.org/"
]