package net.minecraftforkage.instsetup;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.Reader;
import java.io.StringReader;
import java.io.Writer;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
import net.minecraftforkage.instsetup.JarTransformer.Stage;
import net.minecraftforkage.instsetup.depsort.DependencySortedObject;
import net.minecraftforkage.instsetup.depsort.DependencySorter;
import net.minecraftforkage.instsetup.depsort.DependencySortingException;
public class SetupEntryPoint {
public static void setupInstance(InstallationArguments args) throws Exception {
if(args.instanceBaseDir == null)
throw new IllegalArgumentException("args.instanceBaseDir must be set");
InstanceEnvironmentData.minecraftDir = args.instanceBaseDir;
InstanceEnvironmentData.modsDir = new File(args.instanceBaseDir, "mods");
InstanceEnvironmentData.configDir = new File(args.instanceBaseDir, "config");
InstanceEnvironmentData.setupTempDir = new File(args.instanceBaseDir, "setup-temp");
if(args.outputLocation == null)
args.outputLocation = new File(args.instanceBaseDir, "mcforkage-baked.jar");
deleteRecursive(InstanceEnvironmentData.setupTempDir);
if(!InstanceEnvironmentData.setupTempDir.mkdir()) {
try {Thread.sleep(500);} catch(Exception e) {}
if(!InstanceEnvironmentData.setupTempDir.mkdir()) {
try {Thread.sleep(500);} catch(Exception e) {}
if(!InstanceEnvironmentData.setupTempDir.mkdir()) {
throw new IOException("Failed to create directory: "+InstanceEnvironmentData.setupTempDir.getAbsolutePath());
}
}
}
List<File> mods = new ArrayList<>();
for(File modFile : InstanceEnvironmentData.getModsDir().listFiles())
if(modFile.getName().endsWith(".zip") || modFile.getName().endsWith(".jar") || modFile.isDirectory())
mods.add(modFile);
File versionSpecificModsDir = new File(InstanceEnvironmentData.getModsDir(), "1.7.10");
if (versionSpecificModsDir.isDirectory())
for(File modFile : versionSpecificModsDir.listFiles())
if(modFile.getName().endsWith(".zip") || modFile.getName().endsWith(".jar") || modFile.isDirectory())
mods.add(modFile);
PackerContext context = new PackerContext();
context.modURLs = new ArrayList<URL>(mods.size());
for(File f : mods)
context.modURLs.add(f.toURI().toURL());
context.modURLs = Collections.unmodifiableList(context.modURLs);
System.out.println("Mods:");
if(mods.size() == 0)
System.out.println(" <none>");
else
for(File f : mods)
System.out.println(" " + f.getAbsolutePath());
long wholeProcessStartTime = System.nanoTime();
createInitialBakedJar(mods, args.coreLocation, args.outputLocation);
System.out.println("Baked JAR: " + args.outputLocation.getAbsolutePath());
List<URL> setupMods = Installer.findSetupClasspathJars(InstanceEnvironmentData.modsDir);
System.out.println("Mods involved in instance setup:");
if(setupMods.size() == 0)
System.out.println(" <none>");
else
for(URL url : setupMods)
System.out.println(" " + url);
FileSystem fs = FileSystems.newFileSystem(Paths.get(args.outputLocation.toURI()), null);
try (ZipFileSystemAdapter bakedJarIZF = new ZipFileSystemAdapter(fs)) {
if(args.standalone) {
writeStandaloneManifest(bakedJarIZF);
}
URLClassLoader setupModClassLoader = new URLClassLoader(setupMods.toArray(new URL[0]), SetupEntryPoint.class.getClassLoader());
Map<JarTransformer.Stage, List<JarTransformer>> transformersByStage = new HashMap<>();
for(JarTransformer jt : loadAllOfClass(JarTransformer.class, setupModClassLoader)) {
JarTransformer.Stage stage = jt.getStage();
if(!transformersByStage.containsKey(stage))
transformersByStage.put(stage, new ArrayList<JarTransformer>());
transformersByStage.get(stage).add(jt);
}
for(Stage stage : new JarTransformer.Stage[] {
JarTransformer.Stage.CLASS_GENERATION_STAGE,
JarTransformer.Stage.MOD_IDENTIFICATION_STAGE,
JarTransformer.Stage.MAIN_STAGE,
JarTransformer.Stage.CLASS_INFO_EXTRACTION_STAGE
}) {
if(transformersByStage.containsKey(stage)) {
for(JarTransformer jt : DependencySorter.sort(transformersByStage.get(stage))) {
final String idString = jt.getID() + " (" + jt.getClass().getName() + ")";
long startTime = System.nanoTime();
jt.transform(bakedJarIZF, context);
long endTime = System.nanoTime();
System.out.println(((endTime - startTime) / 1000000)+" milliseconds: " + idString);
}
}
}
if(args.standalone) {
// For consistency between standalone and non-standalone modpack JARs,
// transformers may not interfere with the copying of libraries.
// (Because with a non-standalone modpack JAR the copying doesn't happen)
copyLibraries(bakedJarIZF, args.libraryDir);
copyNatives(bakedJarIZF, args.nativesDir);
}
writeListFile(bakedJarIZF, InstanceEnvironmentData.extraModContainers, "mcforkage-mod-container-classes.txt");
}
long wholeProcessEndTime = System.nanoTime();
System.out.println("Instance setup completed in "+((wholeProcessEndTime - wholeProcessStartTime) / 1000000)+" milliseconds");
}
private static void copyNatives(ZipFileSystemAdapter bakedJar, File nativesDir) throws IOException {
try(ZipOutputStream out = new ZipOutputStream(bakedJar.write("mcforkage-standalone-natives.zip"))) {
out.setLevel(ZipOutputStream.STORED);
for(File nativeFile : nativesDir.listFiles()) {
out.putNextEntry(new ZipEntry(nativeFile.getName()));
try (FileInputStream in = new FileInputStream(nativeFile)) {
Utils.copyStream(in, out);
}
out.closeEntry();
}
}
}
private static void writeStandaloneManifest(ZipFileSystemAdapter bakedJar) throws IOException {
Manifest mf = new Manifest();
mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
mf.getMainAttributes().put(Attributes.Name.MAIN_CLASS, "net.minecraft.launchwrapper.DirectLaunch");
try (OutputStream out = bakedJar.write("META-INF/MANIFEST.MF")) {
mf.write(out);
}
}
private static void copyLibraries(ZipFileSystemAdapter bakedJarIZF, File libraryDir) throws IOException {
JsonObject json;
try (Reader in = new InputStreamReader(bakedJarIZF.read("mcforkage-launcher-info.json"), StandardCharsets.UTF_8)) {
json = new GsonBuilder().create().fromJson(in, JsonObject.class);
}
for(JsonElement library : json.get("libraries").getAsJsonArray()) {
String name = library.getAsJsonObject().get("name").getAsString();
String[] parts = name.split(":");
File libfile = new File(libraryDir, parts[1]+"-"+parts[2]+".jar");
if(libfile.exists()) {
copyLibrary(bakedJarIZF, libfile);
continue;
}
libfile = new File(libraryDir, parts[0].replace(".",File.separator)+File.separator+parts[1]+File.separator+parts[2]+File.separator+parts[1]+"-"+parts[2]+".jar");
if(libfile.exists()) {
copyLibrary(bakedJarIZF, libfile);
continue;
}
System.err.println("Couldn't find library "+name+" in "+libraryDir);
}
}
private static void copyLibrary(ZipFileSystemAdapter bakedJar, File libfile) throws IOException {
String libname = libfile.getName();
if(libname.contains("."))
libname = libname.substring(0, libname.indexOf('.'));
System.out.println("Merging library "+libfile.getName());
try (JarInputStream jin = new JarInputStream(new FileInputStream(libfile))) {
JarEntry entry;
while((entry = jin.getNextJarEntry()) != null) {
if(entry.isDirectory()) {
bakedJar.createDirectory(entry.getName());
jin.closeEntry();
continue;
}
// File included in multiple Scala jars; ignore it
if(entry.getName().equals("rootdoc.txt")) {
jin.closeEntry();
continue;
}
// Files from Log4J that we want to override
if(entry.getName().equals("org/apache/logging/log4j/core/config/plugins/Log4j2Plugins.dat") || entry.getName().equals("META-INF/log4j-provider.properties")) {
jin.closeEntry();
continue;
}
// Preserve all LICENSE and NOTICE files, even if they have the same
// filename as another.
// Note that modpack JARs are not allowed to be distributed, as they contain
// Minecraft code - therefore we aren't currently concerned with whether users
// are allowed to distribute repackaged libraries.
if(entry.getName().contains("LICENSE") || entry.getName().contains("NOTICE")) {
String name = entry.getName();
String basename, ext;
if(name.contains(".")) {
basename = name.substring(0, name.indexOf('.'));
ext = name.substring(name.indexOf('.'));
} else {
basename = name;
ext = "";
}
name = basename + "-" + libname;
int counter = 0;
while(bakedJar.doesPathExist(name+ext)) {
counter++;
name = basename + "-" + libname + "-" + counter;
}
try (OutputStream entryOut = bakedJar.write(name+ext)) {
Utils.copyStream(jin, entryOut);
}
jin.closeEntry();
continue;
}
if(entry.getName().startsWith("META-INF/")) {
if(entry.getName().equals("META-INF/MANIFEST.MF")) {
jin.closeEntry();
// Ignore manifests in libraries
continue;
}
if(entry.getName().startsWith("META-INF/maven/") || entry.getName().equals("META-INF/DEPENDENCIES") || entry.getName().equals("META-INF/web-fragment.xml")) {
jin.closeEntry();
// Ignore any files under META-INF/maven/ in libraries
continue;
}
if(entry.getName().startsWith("META-INF/services/")) {
if(bakedJar.doesPathExist(entry.getName()))
throw new IOException("Unimplemented: merging two META-INF/services files");
try (OutputStream entryOut = bakedJar.write(entry.getName())) {
Utils.copyStream(jin, entryOut);
}
jin.closeEntry();
continue;
}
if(entry.getName().equals("META-INF/lof4j-provider.properties")) {
try (OutputStream entryOut = bakedJar.write(entry.getName())) {
Utils.copyStream(jin, entryOut);
}
jin.closeEntry();
continue;
}
System.err.println("Ignoring unrecognized META-INF entry: "+entry.getName());
jin.closeEntry();
continue;
}
if(bakedJar.doesPathExist(entry.getName())) {
byte[] existingBytes, newBytes;
try (InputStream in1 = bakedJar.read(entry.getName())) {
existingBytes = Utils.readStream(in1);
}
newBytes = Utils.readStream(jin);
jin.closeEntry();
// If this library has an identical entry, ignore it.
// If it has an entry with the same name but different contents, print a warning.
if(!Arrays.equals(newBytes, existingBytes))
System.err.println("Ignoring entry with duplicate filename: "+entry.getName());
continue;
}
try (OutputStream entryOut = bakedJar.write(entry.getName())) {
Utils.copyStream(jin, entryOut);
}
jin.closeEntry();
}
}
}
private static void writeListFile(AbstractZipFile bakedJarIZF, Collection<String> list, String path) throws IOException {
try (OutputStream out = bakedJarIZF.write(path)) {
for(String item : list) {
out.write(item.getBytes(StandardCharsets.UTF_8));
out.write('\n');
}
}
}
public static void runInstance(File minecraftDir, String[] args, List<URL> libraryURLs) throws Exception {
File bakedJar = new File(minecraftDir, "mcforkage-baked.jar");
List<URL> classpath = new ArrayList<>(libraryURLs);
classpath.add(bakedJar.toURI().toURL());
URLClassLoader minecraftClassLoader = new URLClassLoader(classpath.toArray(new URL[0]), SetupEntryPoint.class.getClassLoader().getParent());
List<String> newArgs = new ArrayList<>(Arrays.asList(args));
newArgs.add("--tweakClass");
newArgs.add("cpw.mods.fml.common.launcher.FMLTweaker");
minecraftClassLoader.loadClass("net.minecraft.launchwrapper.Launch").getMethod("main", String[].class).invoke(null, new Object[] {newArgs.toArray(new String[0])});
}
/** Takes a URL found on the classpath, and checks whether it is a library
* (whether it should be used on the Minecraft classpath) */
private static boolean isClasspathEntryLibrary(URL url) {
if(!url.getProtocol().equals("file"))
return true;
String[] path = url.getPath().split("/");
if(path.length == 0)
return true;
String lastSegment = path[path.length - 1];
return !lastSegment.startsWith("MCForkage-");
}
public static List<URL> findLibrariesFromClasspath() {
List<URL> result = new ArrayList<>();
for(URL url : ((URLClassLoader)SetupEntryPoint.class.getClassLoader()).getURLs()) {
if(isClasspathEntryLibrary(url)) {
result.add(url);
System.out.println("On classpath: " + url + " (is library)");
} else {
System.out.println("On classpath: " + url + " (not library)");
}
}
return result;
}
private static <T extends DependencySortedObject> List<T> loadAllOfClass(Class<T> what, ClassLoader classLoader) throws DependencySortingException {
List<T> result = new ArrayList<>();
for(T t : ServiceLoader.load(what, classLoader))
result.add(t);
return result;
}
private static void createInitialBakedJar(List<File> mods, URL patchedVanillaJarURL, File bakedJarFile) throws IOException {
List<URL> inputURLs = new ArrayList<>();
inputURLs.add(patchedVanillaJarURL);
for(File modFile : mods)
inputURLs.add(modFile.toURI().toURL());
List<byte[]> mcmodInfoFiles = new ArrayList<>();
List<byte[]> versionPropertiesFiles = new ArrayList<>();
Properties classToSourceMap = new Properties();
try (ZipOutputStream z_out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(bakedJarFile)))) {
Set<String> seenEntries = new HashSet<>();
// Minecraft's LaunchClassLoader *requires* a manifest or it won't call definePackage (WTH?)
// TODO: Stop using LaunchClassLoader (and ModClassLoader) since we don't need it
z_out.putNextEntry(new ZipEntry("META-INF/"));
z_out.closeEntry();
seenEntries.add("META-INF/");
z_out.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
z_out.write("Manifest-Version: 1.0\n".getBytes(StandardCharsets.UTF_8));
z_out.closeEntry();
seenEntries.add("META-INF/MANIFEST.MF");
for(URL inputURL : inputURLs) {
System.out.println(inputURL);
try (ZipInputStream z_in = new ZipInputStream(inputURL.openStream())) {
ZipEntry ze_in;
while((ze_in = z_in.getNextEntry()) != null) {
if(ze_in.getName().endsWith("/")) {
z_in.closeEntry();
// don't warn about duplicate directories; just only add them once
if(seenEntries.add(ze_in.getName())) {
z_out.putNextEntry(new ZipEntry(ze_in.getName()));
z_out.closeEntry();
}
continue;
}
if(ze_in.getName().equals("META-INF/MANIFEST.MF")) {
z_in.closeEntry();
continue;
}
if(ze_in.getName().startsWith("META-INF/") && (ze_in.getName().endsWith(".SF") || ze_in.getName().endsWith(".RSA") || ze_in.getName().endsWith(".DSA") || ze_in.getName().endsWith(".EC"))) {
z_in.closeEntry();
continue;
}
if(ze_in.getName().equals("mcmod.info")) {
mcmodInfoFiles.add(Utils.readStream(z_in));
z_in.closeEntry();
continue;
}
if(ze_in.getName().equals("version.properties")) {
versionPropertiesFiles.add(Utils.readStream(z_in));
z_in.closeEntry();
continue;
}
if(!seenEntries.add(ze_in.getName()))
System.err.println("Duplicate entry: "+ze_in.getName());
else {
if(ze_in.getName().endsWith(".class")) {
String className = ze_in.getName();
className = className.substring(0, className.length() - 6).replace('/', '.');
classToSourceMap.put(className, inputURL.toString());
}
z_out.putNextEntry(new ZipEntry(ze_in.getName()));
copyStream(z_in, z_out);
z_in.closeEntry();
z_out.closeEntry();
}
}
}
}
z_out.putNextEntry(new ZipEntry("mcmod.info"));
z_out.write(mergeJsonArrays(mcmodInfoFiles));
z_out.closeEntry();
if(versionPropertiesFiles.size() > 0) {
z_out.putNextEntry(new ZipEntry("version.properties"));
z_out.write(mergePropertiesFiles(versionPropertiesFiles));
z_out.closeEntry();
}
z_out.putNextEntry(new ZipEntry("mcforkage-class-to-source-map.properties"));
classToSourceMap.store(z_out, "");
z_out.closeEntry();
}
}
private static byte[] mergePropertiesFiles(List<byte[]> inputs) {
ByteArrayOutputStream result = new ByteArrayOutputStream();
for(byte[] input : inputs) {
result.write(input, 0, input.length);
result.write('\n');
}
return result.toByteArray();
}
private static byte[] mergeJsonArrays(List<byte[]> inputs) {
List<Object> mods = new ArrayList<>();
Gson gson = new GsonBuilder().setPrettyPrinting().create();
for(byte[] input : inputs) {
JsonElement inputParsed;
try {
inputParsed = gson.fromJson(new String(input, StandardCharsets.UTF_8), JsonElement.class);
} catch (JsonSyntaxException e) {
System.err.println(new String(input, StandardCharsets.UTF_8));
new RuntimeException("Error reading mcmod.info file", e).printStackTrace();
continue;
}
if(inputParsed.isJsonArray())
for(JsonElement mod : inputParsed.getAsJsonArray())
mods.add(mod);
else if(inputParsed.isJsonObject() && inputParsed.getAsJsonObject().has("modList"))
for(JsonElement mod : inputParsed.getAsJsonObject().get("modList").getAsJsonArray())
mods.add(mod);
else
throw new RuntimeException("unrecognized mcmod.info format");
}
return gson.toJson(mods).getBytes(StandardCharsets.UTF_8);
}
private static void copyStream(InputStream in, OutputStream out) throws IOException {
byte[] buffer = new byte[32768];
while(true) {
int read = in.read(buffer);
if(read < 0)
break;
out.write(buffer, 0, read);
}
}
private static void deleteRecursive(File dir) throws IOException {
if(!dir.exists())
return;
if(dir.isDirectory())
for(File child : dir.listFiles())
deleteRecursive(child);
if(!dir.delete())
throw new IOException("Failed to delete "+dir.getAbsolutePath());
}
private SetupEntryPoint() {}
}