diff --git a/pom.xml b/pom.xml index ac6ebc9..cf35f51 100644 --- a/pom.xml +++ b/pom.xml @@ -44,6 +44,16 @@ jcommander 1.32 + + org.tukaani + xz + 1.0 + + + org.apache.commons + commons-compress + 1.9 + diff --git a/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java b/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java index f0672d6..793312e 100644 --- a/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java +++ b/src/main/java/com/skcraft/launcher/builder/BuilderOptions.java @@ -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 @@ 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 @@ // 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"); + } + } + } diff --git a/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java b/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java new file mode 100644 index 0000000..8f2e722 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/BuilderUtils.java @@ -0,0 +1,52 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham 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 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 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(); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/Compressor.java b/src/main/java/com/skcraft/launcher/builder/Compressor.java new file mode 100644 index 0000000..40958b1 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/Compressor.java @@ -0,0 +1,48 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham 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); + } + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/JarFileFilter.java b/src/main/java/com/skcraft/launcher/builder/JarFileFilter.java new file mode 100644 index 0000000..31ca410 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/builder/JarFileFilter.java @@ -0,0 +1,19 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham 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"); + } + +} diff --git a/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java b/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java index c7e836d..be0dfcb 100644 --- a/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java +++ b/src/main/java/com/skcraft/launcher/builder/PackageBuilder.java @@ -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 @@ @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 loaderLibraries = Lists.newArrayList(); + private List mavenRepos; /** * Create a new package builder. @@ -42,11 +69,22 @@ * @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>() { + }); + } finally { + closer.close(); + } } public void setPrettyPrint(boolean prettyPrint) { @@ -71,6 +109,156 @@ 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 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 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 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 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 @@ 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 @@ * * @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 @@ 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 + " ---"); } } diff --git a/src/main/java/com/skcraft/launcher/model/loader/InstallData.java b/src/main/java/com/skcraft/launcher/model/loader/InstallData.java new file mode 100644 index 0000000..c346dc6 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/loader/InstallData.java @@ -0,0 +1,19 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham 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; + +} diff --git a/src/main/java/com/skcraft/launcher/model/loader/InstallProfile.java b/src/main/java/com/skcraft/launcher/model/loader/InstallProfile.java new file mode 100644 index 0000000..6205501 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/loader/InstallProfile.java @@ -0,0 +1,21 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham 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; + +} diff --git a/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java b/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java new file mode 100644 index 0000000..f9872c4 --- /dev/null +++ b/src/main/java/com/skcraft/launcher/model/loader/VersionInfo.java @@ -0,0 +1,23 @@ +/* + * SK's Minecraft Launcher + * Copyright (C) 2010-2014 Albert Pham 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 libraries; + +} diff --git a/src/main/java/com/skcraft/launcher/model/minecraft/Library.java b/src/main/java/com/skcraft/launcher/model/minecraft/Library.java index 677d1cc..ccb13de 100644 --- a/src/main/java/com/skcraft/launcher/model/minecraft/Library.java +++ b/src/main/java/com/skcraft/launcher/model/minecraft/Library.java @@ -25,6 +25,8 @@ private transient String group; private transient String artifact; private transient String version; + @JsonProperty("url") + private String baseUrl; private Map natives; private Extract extract; private List rules; @@ -122,6 +124,24 @@ 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; diff --git a/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java b/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java index eee01d4..283bb8a 100644 --- a/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java +++ b/src/main/java/com/skcraft/launcher/model/minecraft/VersionManifest.java @@ -11,7 +11,7 @@ import lombok.Data; import java.util.Date; -import java.util.List; +import java.util.LinkedHashSet; @Data @JsonIgnoreProperties(ignoreUnknown = true) @@ -26,7 +26,7 @@ private String minecraftArguments; private String mainClass; private int minimumLauncherVersion; - private List libraries; + private LinkedHashSet libraries; @JsonIgnore public String getAssetsIndex() { diff --git a/src/main/resources/com/skcraft/launcher/maven_repos.json b/src/main/resources/com/skcraft/launcher/maven_repos.json new file mode 100644 index 0000000..eecbdb4 --- /dev/null +++ b/src/main/resources/com/skcraft/launcher/maven_repos.json @@ -0,0 +1,5 @@ +[ + "https://libraries.minecraft.net/", + "https://central.maven.org/maven2/", + "http://maven.apache.org/" +] \ No newline at end of file