/*
* 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.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.*;
import java.net.URL;
import java.util.LinkedHashSet;
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.
*/
@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.
*
* @param mapper the mapper
* @param manifest the 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) {
if (prettyPrint) {
writer = mapper.writerWithDefaultPrettyPrinter();
} else {
writer = mapper.writer();
}
this.prettyPrint = prettyPrint;
}
public void scan(File dir) throws IOException {
logSection("Scanning for .info.json files...");
FileInfoScanner scanner = new FileInfoScanner(mapper);
scanner.walk(dir);
for (FeaturePattern pattern : scanner.getPatterns()) {
applicator.register(pattern);
}
}
public void addFiles(File dir, File destDir) throws IOException {
logSection("Adding files to modpack...");
ClientFileCollector collector = new ClientFileCollector(this.manifest, applicator, destDir);
collector.walk(dir);
}
public void addLoaders(File dir, File librariesDir) {
logSection("Checking for mod loaders to install...");
LinkedHashSet<Library> collected = new LinkedHashSet<Library>();
File[] files = dir.listFiles(new JarFileFilter());
if (files != null) {
for (File file : files) {
try {
processLoader(collected, file, librariesDir);
} catch (IOException e) {
log.log(Level.WARNING, "Failed to add the loader at " + file.getAbsolutePath(), e);
}
}
}
this.loaderLibraries.addAll(collected);
VersionManifest version = manifest.getVersionManifest();
collected.addAll(version.getLibraries());
version.setLibraries(collected);
}
private void processLoader(LinkedHashSet<Library> loaderLibraries, 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) {
for (Library library : libraries) {
if (!version.getLibraries().contains(library)) {
loaderLibraries.add(library);
}
}
}
// 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");
}
public void readConfig(File path) throws IOException {
if (path != null) {
BuilderConfig config = read(path, BuilderConfig.class);
config.update(manifest);
config.registerProperties(applicator);
}
}
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) {
versionManifest.setId(manifest.getGameVersion());
}
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;
}
private <V> V read(File path, Class<V> clazz) throws IOException {
try {
if (path == null) {
return clazz.newInstance();
} else {
return mapper.readValue(path, clazz);
}
} catch (InstantiationException e) {
throw new IOException("Failed to create " + clazz.getCanonicalName(), e);
} catch (IllegalAccessException e) {
throw new IOException("Failed to create " + clazz.getCanonicalName(), e);
}
}
/**
* Build a package given the arguments.
*
* @param args arguments
* @throws IOException thrown on I/O error
* @throws InterruptedException on interruption
*/
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();
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_DEFAULT);
Manifest manifest = new Manifest();
manifest.setMinimumVersion(Manifest.MIN_PROTOCOL_VERSION);
PackageBuilder builder = new PackageBuilder(mapper, manifest);
builder.setPrettyPrint(options.isPrettyPrinting());
// From config
builder.readConfig(options.getConfigPath());
builder.readVersionManifest(options.getVersionManifestPath());
// From options
manifest.updateName(options.getName());
manifest.updateTitle(options.getTitle());
manifest.updateGameVersion(options.getGameVersion());
manifest.setVersion(options.getVersion());
manifest.setLibrariesLocation(options.getLibrariesLocation());
manifest.setObjectsLocation(options.getObjectsLocation());
builder.scan(options.getFilesDir());
builder.addFiles(options.getFilesDir(), options.getObjectsDir());
builder.addLoaders(options.getLoadersDir(), options.getLibrariesDir());
builder.downloadLibraries(options.getLibrariesDir());
builder.writeManifest(options.getManifestPath());
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 + " ---");
}
}