SKCraft-Launcher/launcher-builder/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java

452 lines
17 KiB
Java

/*
* 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.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.io.ByteStreams;
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.builder.loaders.*;
import com.skcraft.launcher.model.loader.BasicInstallProfile;
import com.skcraft.launcher.model.minecraft.Library;
import com.skcraft.launcher.model.minecraft.ReleaseList;
import com.skcraft.launcher.model.minecraft.Version;
import com.skcraft.launcher.model.minecraft.VersionManifest;
import com.skcraft.launcher.model.modpack.Manifest;
import com.skcraft.launcher.util.HttpRequest;
import com.skcraft.launcher.util.SimpleLogFormatter;
import lombok.Getter;
import lombok.NonNull;
import lombok.Setter;
import lombok.extern.java.Log;
import java.io.*;
import java.net.URL;
import java.util.Collections;
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.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 final Properties properties;
private final ObjectMapper mapper;
private ObjectWriter writer;
private final Manifest manifest;
private final PropertiesApplicator applicator;
@Getter
private boolean prettyPrint = false;
@Getter @Setter
private File baseDir;
private List<Library> loaderLibraries = Lists.newArrayList();
private List<Library> installerLibraries = Lists.newArrayList();
private List<String> mavenRepos;
private List<URL> jarMavens = Lists.newArrayList();
/**
* 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 infoScanner = new FileInfoScanner(mapper);
infoScanner.walk(dir);
for (FeaturePattern pattern : infoScanner.getPatterns()) {
applicator.register(pattern);
}
logSection("Scanning for .url.txt files...");
FileUrlScanner urlScanner = new FileUrlScanner();
urlScanner.walk(dir);
}
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();
ILoaderProcessor processor = null;
try {
ZipEntry profileEntry = BuilderUtils.getZipEntry(jarFile, "install_profile.json");
if (profileEntry != null) {
InputStream stream = jarFile.getInputStream(profileEntry);
InputStreamReader reader = closer.register(new InputStreamReader(stream));
BasicInstallProfile basicProfile = mapper.readValue(BuilderUtils.readStringFromStream(reader),
BasicInstallProfile.class);
if (basicProfile.isLegacy()) {
processor = new OldForgeLoaderProcessor();
} else {
processor = new ModernForgeLoaderProcessor();
}
} else if (BuilderUtils.getZipEntry(jarFile, "fabric-installer.json") != null) {
processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.FABRIC);
} else if (BuilderUtils.getZipEntry(jarFile, "quilt_installer.json") != null) {
processor = new FabricLoaderProcessor(FabricLoaderProcessor.Variant.QUILT);
}
} finally {
closer.close();
jarFile.close();
}
if (processor != null) {
LoaderResult result = processor.process(file, manifest, mapper, baseDir);
if (result == null) {
log.warning("Loader " + file.getName() + " failed to process.");
return;
}
loaderLibraries.addAll(result.getLoaderLibraries());
installerLibraries.addAll(result.getProcessorLibraries());
jarMavens.addAll(result.getJarMavens());
} else {
log.warning("Loader " + file.getName() + " was skipped due to missing metadata. " +
"Is it really a loader JAR?");
}
}
public void downloadLibraries(File librariesDir) throws IOException, InterruptedException {
logSection("Downloading libraries...");
for (Library library : Iterables.concat(loaderLibraries, installerLibraries)) {
library.ensureDownloadsExist();
for (Library.Artifact artifact : library.getDownloads().getAllArtifacts()) {
File outputPath = new File(librariesDir, artifact.getPath());
if (!outputPath.exists()) {
Files.createParentDirs(outputPath);
boolean found = false;
boolean urlEmpty = artifact.getUrl().isEmpty();
// If URL doesn't end with a /, it might be the direct file
if (!urlEmpty && !artifact.getUrl().endsWith("/")) {
found = tryDownloadLibrary(library, artifact, artifact.getUrl(), outputPath);
}
// Look inside the loader JARs
if (!found) {
for (URL base : jarMavens) {
found = tryFetchLibrary(library, new URL(base, artifact.getPath()), outputPath);
if (found) break;
}
}
// Assume artifact URL is a maven repository URL and try that
if (!found && !urlEmpty) {
URL url = LauncherUtils.concat(url(artifact.getUrl()), artifact.getPath());
found = tryDownloadLibrary(library, artifact, url.toString(), outputPath);
}
// Try each repository if not found yet
if (!found) {
for (String baseUrl : mavenRepos) {
found = tryDownloadLibrary(library, artifact, baseUrl + artifact.getPath(),
outputPath);
if (found) break;
}
}
if (!found) {
log.warning("!! Failed to download the library " + library.getName() +
" -- this means your copy of the libraries will lack this file");
}
}
}
}
}
private boolean tryDownloadLibrary(Library library, Library.Artifact artifact, String baseUrl, File outputPath)
throws IOException, InterruptedException {
URL url = new URL(baseUrl);
if (url.getPath().isEmpty() || url.getPath().equals("/")) {
// empty path, this is probably the first "is this a full URL" try.
return false;
}
// Some repositories compress their files
List<Compressor> compressors = BuilderUtils.getCompressors(baseUrl);
for (Compressor compressor : Lists.reverse(compressors)) {
url = new URL(compressor.transformPathname(url.toString()));
}
File tempFile = File.createTempFile("launcherlib", null);
try {
log.info("Downloading library " + library.getName() + " from " + url + "...");
HttpRequest.get(url).execute().expectResponseCode(200)
.expectContentType("application/java-archive", "application/octet-stream", "application/zip")
.saveContent(tempFile);
} catch (IOException e) {
log.info("Could not get file from " + url + ": " + e.getMessage());
return false;
}
writeLibraryToFile(outputPath, tempFile, compressors);
return true;
}
private boolean tryFetchLibrary(Library library, URL url, File outputPath)
throws IOException {
File tempFile = File.createTempFile("launcherlib", null);
Closer closer = Closer.create();
try {
log.info("Reading library " + library.getName() + " from " + url.toString());
InputStream stream = closer.register(url.openStream());
stream = closer.register(new BufferedInputStream(stream));
ByteStreams.copy(stream, closer.register(new FileOutputStream(tempFile)));
} catch (IOException e) {
log.info("Could not get file from " + url + ": " + e.getMessage());
return false;
} finally {
closer.close();
}
writeLibraryToFile(outputPath, tempFile, Collections.<Compressor>emptyList());
return true;
}
private void writeLibraryToFile(File outputPath, File inputFile, List<Compressor> compressors) throws IOException {
// Decompress (if needed) and write to file
Closer closer = Closer.create();
InputStream inputStream = closer.register(new FileInputStream(inputFile));
inputStream = closer.register(new BufferedInputStream(inputStream));
for (Compressor compressor : compressors) {
inputStream = closer.register(compressor.createInputStream(inputStream));
}
ByteStreams.copy(inputStream, closer.register(new FileOutputStream(outputPath)));
inputFile.delete();
closer.close();
}
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(properties.getProperty("versionManifestUrl"));
log.info("Fetching version manifest from " + url + "...");
ReleaseList releases = HttpRequest.get(url)
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(ReleaseList.class);
Version version = releases.find(manifest.getGameVersion());
VersionManifest versionManifest = HttpRequest.get(url(version.getUrl()))
.execute()
.expectResponseCode(200)
.returnContent()
.asJson(VersionManifest.class);
manifest.setVersionManifest(versionManifest);
}
}
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).parse(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_ABSENT);
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());
builder.setBaseDir(options.getOutputPath());
// 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 + " ---");
}
}