Rewrite dependency extraction to use mod list system (#4841)

This commit is contained in:
LexManos 2018-04-10 16:01:35 -07:00 committed by GitHub
parent 3f4dfbb367
commit 9d0771b3d7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1497 additions and 414 deletions

View file

@ -87,6 +87,11 @@
},
{
"name": "net.sf.trove4j:trove4j:3.0.3"
},
{
"name": "org.apache.maven:maven-artifact:3.5.3",
"children" : ["sources"],
"url" : "http://repo.maven.apache.org/maven2"
}
]
}

View file

@ -138,6 +138,12 @@
"name": "net.sf.trove4j:trove4j:3.0.3",
"clientreq":true,
"serverreq":true
},
{
"name": "org.apache.maven:maven-artifact:3.5.3",
"url" : "http://files.minecraftforge.net/maven/",
"serverreq":true,
"clientreq":true
}
]
},

View file

@ -32,14 +32,12 @@ import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.google.common.io.ByteStreams;
import com.google.gson.Gson;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.InjectedModContainer;
import net.minecraftforge.fml.common.Loader;
import net.minecraftforge.fml.common.ModContainer;

View file

@ -32,8 +32,10 @@ import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Matcher;
import net.minecraft.util.ResourceLocation;
import net.minecraftforge.classloading.FMLForgePlugin;
import net.minecraftforge.common.ForgeVersion;
import net.minecraftforge.common.capabilities.CapabilityManager;
import net.minecraftforge.common.config.ConfigManager;
@ -42,6 +44,8 @@ import net.minecraftforge.fml.common.LoaderState.ModState;
import net.minecraftforge.fml.common.ModContainer.Disableable;
import net.minecraftforge.fml.common.ProgressManager.ProgressBar;
import net.minecraftforge.fml.common.discovery.ASMDataTable;
import net.minecraftforge.fml.common.discovery.ContainerType;
import net.minecraftforge.fml.common.discovery.ModCandidate;
import net.minecraftforge.fml.common.discovery.ModDiscoverer;
import net.minecraftforge.fml.common.event.FMLInterModComms;
import net.minecraftforge.fml.common.event.FMLLoadEvent;
@ -54,8 +58,11 @@ import net.minecraftforge.fml.common.toposort.ModSortingException.SortingExcepti
import net.minecraftforge.fml.common.versioning.ArtifactVersion;
import net.minecraftforge.fml.common.versioning.DependencyParser;
import net.minecraftforge.fml.common.versioning.VersionParser;
import net.minecraftforge.fml.relauncher.ModListHelper;
import net.minecraftforge.fml.relauncher.CoreModManager;
import net.minecraftforge.fml.relauncher.Side;
import net.minecraftforge.fml.relauncher.libraries.Artifact;
import net.minecraftforge.fml.relauncher.libraries.LibraryManager;
import net.minecraftforge.fml.relauncher.libraries.Repository;
import net.minecraftforge.registries.GameData;
import net.minecraftforge.registries.ObjectHolderRegistry;
@ -384,18 +391,42 @@ public class Loader
mods.add(new InjectedModContainer(mc,mc.getSource()));
}
ModDiscoverer discoverer = new ModDiscoverer();
FMLLog.log.debug("Attempting to load mods contained in the minecraft jar file and associated classes");
discoverer.findClasspathMods(modClassLoader);
FMLLog.log.debug("Minecraft jar mods loaded successfully");
FMLLog.log.info("Found {} mods from the command line. Injecting into mod discoverer", ModListHelper.additionalMods.size());
FMLLog.log.info("Searching {} for mods", canonicalModsDir.getAbsolutePath());
discoverer.findModDirMods(canonicalModsDir, ModListHelper.additionalMods.values().toArray(new File[0]));
File versionSpecificModsDir = new File(canonicalModsDir,mccversion);
if (versionSpecificModsDir.isDirectory())
if (!FMLForgePlugin.RUNTIME_DEOBF) //Only descover mods in the classpath if we're in the dev env.
{ //TODO: Move this to GradleStart?
FMLLog.log.debug("Attempting to load mods contained in the minecraft jar file and associated classes");
discoverer.findClasspathMods(modClassLoader);
FMLLog.log.debug("Minecraft jar mods loaded successfully");
}
List<Artifact> maven_canidates = LibraryManager.flattenLists(minecraftDir);
List<File> file_canidates = LibraryManager.gatherLegacyCanidates(minecraftDir);
for (Artifact artifact : maven_canidates)
{
FMLLog.log.info("Also searching {} for mods", versionSpecificModsDir);
discoverer.findModDirMods(versionSpecificModsDir);
artifact = Repository.resolveAll(artifact);
if (artifact != null)
{
File target = artifact.getFile();
if (!file_canidates.contains(target))
file_canidates.add(target);
}
}
//Do we want to sort the full list after resolving artifacts?
//TODO: Add dependency gathering?
for (File mod : file_canidates)
{
// skip loaded coremods
if (CoreModManager.getIgnoredMods().contains(mod.getName()))
{
FMLLog.log.trace("Skipping already parsed coremod or tweaker {}", mod.getName());
}
else
{
FMLLog.log.debug("Found a candidate zip or jar file {}", mod.getName());
discoverer.addCandidate(new ModCandidate(mod, mod, ContainerType.JAR));
}
}
mods.addAll(discoverer.identifyMods());

View file

@ -24,20 +24,21 @@ import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import net.minecraftforge.fml.common.FMLLog;
import com.google.common.collect.Maps;
import com.google.common.io.ByteSource;
import com.google.common.io.CharSource;
public class ModAccessTransformer extends AccessTransformer {
public class ModAccessTransformer extends AccessTransformer
{
public static final Attributes.Name FMLAT = new Attributes.Name("FMLAT");
private static Map<String, String> embedded = Maps.newHashMap(); //Needs to be primitive so that both classloaders get the same class.
@SuppressWarnings("unchecked")
public ModAccessTransformer() throws Exception
public ModAccessTransformer() throws Exception
{
super(ModAccessTransformer.class);
//We are in the new ClassLoader here, so we need to get the static field from the other ClassLoader.
@ -59,11 +60,8 @@ public class ModAccessTransformer extends AccessTransformer {
}
}
public static void addJar(JarFile jar) throws IOException
public static void addJar(JarFile jar, String atList) throws IOException
{
Manifest manifest = jar.getManifest();
String atList = manifest.getMainAttributes().getValue("FMLAT");
if (atList == null) return;
for (String at : atList.split(" "))
{
JarEntry jarEntry = jar.getJarEntry("META-INF/"+at);

View file

@ -21,24 +21,17 @@ package net.minecraftforge.fml.common.discovery;
import java.io.File;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.LoaderException;
import net.minecraftforge.fml.common.ModClassLoader;
import net.minecraftforge.fml.common.ModContainer;
import net.minecraftforge.fml.relauncher.CoreModManager;
import net.minecraftforge.fml.relauncher.FileListHelper;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.collect.ObjectArrays;
public class ModDiscoverer
{
private static Pattern zipJar = Pattern.compile("(.+).(zip|jar)$");
private List<ModCandidate> candidates = Lists.newArrayList();
private ASMDataTable dataTable = new ASMDataTable();
@ -89,45 +82,6 @@ public class ModDiscoverer
}
public void findModDirMods(File modsDir)
{
findModDirMods(modsDir, new File[0]);
}
public void findModDirMods(File modsDir, File[] supplementalModFileCandidates)
{
File[] modList = FileListHelper.sortFileList(modsDir, null);
modList = FileListHelper.sortFileList(ObjectArrays.concat(modList, supplementalModFileCandidates, File.class));
for (File modFile : modList)
{
// skip loaded coremods
if (CoreModManager.getIgnoredMods().contains(modFile.getName()))
{
FMLLog.log.trace("Skipping already parsed coremod or tweaker {}", modFile.getName());
}
else if (modFile.isDirectory())
{
//TODO Remove in 1.13+ Mods should never be directory based anymore.
FMLLog.log.debug("Found a candidate mod directory {}", modFile.getName());
addCandidate(new ModCandidate(modFile, modFile, ContainerType.DIR));
}
else
{
Matcher matcher = zipJar.matcher(modFile.getName());
if (matcher.matches())
{
FMLLog.log.debug("Found a candidate zip or jar file {}", matcher.group(0));
addCandidate(new ModCandidate(modFile, modFile, ContainerType.JAR));
}
else
{
FMLLog.log.debug("Ignoring unknown file {} in mods directory", modFile.getName());
}
}
}
}
public List<ModContainer> identifyMods()
{
List<ModContainer> modList = Lists.newArrayList();
@ -165,7 +119,7 @@ public class ModDiscoverer
return nonModLibs;
}
private void addCandidate(ModCandidate candidate)
public void addCandidate(ModCandidate candidate)
{
for (ModCandidate c : candidates)
{

View file

@ -32,7 +32,6 @@ import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.relauncher.FMLLaunchHandler;
import net.minecraftforge.fml.relauncher.FMLSecurityManager;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import com.google.common.collect.Lists;

View file

@ -22,10 +22,8 @@ package net.minecraftforge.fml.relauncher;
import java.io.Closeable;
import java.io.File;
import java.io.FileFilter;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.net.URL;
@ -34,17 +32,15 @@ import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.ToIntFunction;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import net.minecraft.launchwrapper.ITweaker;
import net.minecraft.launchwrapper.Launch;
import net.minecraft.launchwrapper.LaunchClassLoader;
@ -59,6 +55,9 @@ import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.MCVersion;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.Name;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.SortingIndex;
import net.minecraftforge.fml.relauncher.IFMLLoadingPlugin.TransformerExclusions;
import net.minecraftforge.fml.relauncher.libraries.Artifact;
import net.minecraftforge.fml.relauncher.libraries.LibraryManager;
import net.minecraftforge.fml.relauncher.libraries.Repository;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
@ -71,8 +70,6 @@ import com.google.common.primitives.Ints;
public class CoreModManager {
private static final Attributes.Name COREMODCONTAINSFMLMOD = new Attributes.Name("FMLCorePluginContainsFMLMod");
private static final Attributes.Name MODTYPE = new Attributes.Name("ModType");
private static final Attributes.Name MODSIDE = new Attributes.Name("ModSide");
private static final Attributes.Name MODCONTAINSDEPS = new Attributes.Name("ContainedDeps");
private static String[] rootPlugins = { "net.minecraftforge.fml.relauncher.FMLCorePlugin", "net.minecraftforge.classloading.FMLForgePlugin" };
private static List<String> ignoredModFiles = Lists.newArrayList();
private static Map<String, List<String>> transformers = Maps.newHashMap();
@ -83,7 +80,6 @@ public class CoreModManager {
private static List<String> candidateModFiles = Lists.newArrayList();
private static List<String> accessTransformers = Lists.newArrayList();
private static Set<String> rootNames = Sets.newHashSet();
private static final List<String> skipContainedDeps = Arrays.asList(System.getProperty("fml.skipContainedDeps","").split(","));
static
{
@ -197,6 +193,8 @@ public class CoreModManager {
{
CoreModManager.mcDir = mcDir;
CoreModManager.tweaker = tweaker;
//TODO: Detect this better? Support install time deobfusication? Decouple deobf from dev? Add dev flag in GradleStart?
try
{
// Are we in a 'decompiled' environment?
@ -257,7 +255,7 @@ public class CoreModManager {
private static void findDerpMods(LaunchClassLoader classLoader, File modDir, File modDirVer)
{
File[] derplist = listFiles((dir, name) -> name.endsWith(".jar.zip"), modDir, modDirVer);
File[] derplist = listFiles(path -> path.getName().endsWith(".jar.zip"), modDir, modDirVer);
if (derplist != null && derplist.length > 0)
{
FMLLog.log.fatal("FML has detected several badly downloaded jar files, which have been named as zip files. You probably need to download them again, or they may not work properly");
@ -297,20 +295,6 @@ public class CoreModManager {
}
}
private static File[] listFiles(FilenameFilter filter, File ... dirs)
{
File[] ret = null;
for (File dir : dirs)
{
if (!dir.isDirectory() || !dir.exists())
continue;
if (ret == null)
ret = dir.listFiles(filter);
else
ret = ObjectArrays.concat(ret, dir.listFiles(filter), File.class);
}
return ret == null ? new File[0] : ret;
}
private static File[] listFiles(FileFilter filter, File ... dirs)
{
File[] ret = null;
@ -334,18 +318,27 @@ public class CoreModManager {
findDerpMods(classLoader, modsDir, modsDirVer);
extractPackedJars(modsDir, modsDirVer);
ModListHelper.parseModList(mcDir);
//By the time we get here, all bundeled jars should be extracted to the proper repos.
//As well as the mods folders being cleaned up {any files that have maven info being moved to maven folder}
FMLLog.log.debug("Discovering coremods");
File[] coreModList = listFiles((dir, name) -> name.endsWith(".jar"), modsDir, modsDirVer);
List<Artifact> maven_canidates = LibraryManager.flattenLists(mcDir);
List<File> file_canidates = LibraryManager.gatherLegacyCanidates(mcDir);
coreModList = ObjectArrays.concat(coreModList, ModListHelper.additionalMods.values().toArray(new File[0]), File.class);
for (Artifact artifact : maven_canidates)
{
artifact = Repository.resolveAll(artifact);
if (artifact != null)
{
File target = artifact.getFile();
if (!file_canidates.contains(target))
file_canidates.add(target);
}
}
//Do we want to sort the full list after resolving artifacts?
//TODO: Add dependency gathering?
coreModList = FileListHelper.sortFileList(coreModList);
for (File coreMod : coreModList)
for (File coreMod : file_canidates)
{
FMLLog.log.debug("Examining for coremod candidacy {}", coreMod.getName());
JarFile jar = null;
@ -353,14 +346,39 @@ public class CoreModManager {
String fmlCorePlugin;
try
{
jar = new JarFile(coreMod);
if (jar.getManifest() == null)
File manifest = new File(coreMod.getAbsolutePath() + ".meta");
if (LibraryManager.DISABLE_EXTERNAL_MANIFEST || !manifest.exists())
{
// Not a coremod and no access transformer list
jar = new JarFile(coreMod);
mfAttributes = jar.getManifest() == null ? null : jar.getManifest().getMainAttributes();
}
else
{
FileInputStream fis = new FileInputStream(manifest);
mfAttributes = new Manifest(fis).getMainAttributes();
fis.close();
}
if (mfAttributes == null) // Not a coremod and no access transformer list
continue;
String modSide = mfAttributes.getValue(LibraryManager.MODSIDE);
if (!"BOTH".equals(modSide) || FMLLaunchHandler.side().name().equals(modSide))
{
FMLLog.log.debug("Mod {} has ModSide meta-inf value {}, and we're {} It will be ignored", coreMod.getName(), modSide, FMLLaunchHandler.side.name());
ignoredModFiles.add(coreMod.getName());
continue;
}
ModAccessTransformer.addJar(jar);
mfAttributes = jar.getManifest().getMainAttributes();
String ats = mfAttributes.getValue(ModAccessTransformer.FMLAT);
if (ats != null && !ats.isEmpty())
{
if (jar == null) //We could of loaded the external manifest earlier, if so the jar isn't loaded.
jar = new JarFile(coreMod);
ModAccessTransformer.addJar(jar, ats);
}
String cascadedTweaker = mfAttributes.getValue("TweakClass");
if (cascadedTweaker != null)
{
@ -379,13 +397,6 @@ public class CoreModManager {
ignoredModFiles.add(coreMod.getName());
continue;
}
String modSide = mfAttributes.containsKey(MODSIDE) ? mfAttributes.getValue(MODSIDE) : "BOTH";
if (! ("BOTH".equals(modSide) || FMLLaunchHandler.side.name().equals(modSide)))
{
FMLLog.log.debug("Mod {} has ModSide meta-inf value {}, and we're {} It will be ignored", coreMod.getName(), modSide, FMLLaunchHandler.side.name());
ignoredModFiles.add(coreMod.getName());
continue;
}
fmlCorePlugin = mfAttributes.getValue("FMLCorePlugin");
if (fmlCorePlugin == null)
{
@ -428,95 +439,6 @@ public class CoreModManager {
}
}
private static void extractPackedJars(File modsDir, File modsDirVer)
{
for (File dir : new File[]{modsDir, modsDirVer})
{
for (File file : listFiles((d, name) -> name.endsWith(".jar"), dir))
{
JarFile jar = null;
Attributes mfAttributes;
try
{
jar = new JarFile(file);
if (jar.getManifest() == null)
continue;
mfAttributes = jar.getManifest().getMainAttributes();
String modSide = mfAttributes.containsKey(MODSIDE) ? mfAttributes.getValue(MODSIDE) : "BOTH";
if (! ("BOTH".equals(modSide) || FMLLaunchHandler.side.name().equals(modSide)))
continue;
extractContainedDepJars(jar, dir == modsDir ? modsDir : modsDirVer, dir == modsDir ? modsDirVer : modsDir);
}
catch (IOException ioe)
{
FMLLog.log.error("Unable to read the jar file {} - ignoring", file.getName(), ioe);
continue;
}
finally
{
closeQuietly(jar);
}
}
}
}
private static void extractContainedDepJars(JarFile jar, File ... modsDirs) throws IOException
{
if (!jar.getManifest().getMainAttributes().containsKey(MODCONTAINSDEPS)) return;
String deps = jar.getManifest().getMainAttributes().getValue(MODCONTAINSDEPS);
String[] depList = deps.split(" ");
for (String dep : depList)
{
String depEndName = new File(dep).getName(); // extract last part of name
if (skipContainedDeps.contains(dep) || skipContainedDeps.contains(depEndName))
{
FMLLog.log.error("Skipping dep at request: {}", dep);
continue;
}
final JarEntry jarEntry = jar.getJarEntry(dep);
if (jarEntry == null)
{
FMLLog.log.error("Found invalid ContainsDeps declaration {} in {}", dep, jar.getName());
continue;
}
boolean exit = false;
for (File f : modsDirs)
{
File tmp = new File(f, depEndName);
if (tmp.exists())
{
FMLLog.log.debug("Found existing ContainsDep extracted to {}, skipping extraction", tmp.getCanonicalPath());
exit = true;
break;
}
}
if (exit)
continue;
File target = new File(modsDirs[0], depEndName);
FMLLog.log.debug("Extracting ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath());
try
{
Files.createParentDirs(target);
try (
FileOutputStream targetOutputStream = new FileOutputStream(target);
InputStream jarInputStream = jar.getInputStream(jarEntry);
){
ByteStreams.copy(jarInputStream, targetOutputStream);
}
FMLLog.log.debug("Extracted ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath());
} catch (IOException e)
{
FMLLog.log.error("An error occurred extracting dependency", e);
}
}
return;
}
private static Method ADDURL;
private static void handleCascadingTweak(File coreMod, JarFile jar, String cascadedTweaker, LaunchClassLoader classLoader, Integer sortingOrder)
@ -719,51 +641,8 @@ public class CoreModManager {
// Basically a copy of Collections.sort pre 8u20, optimized as we know we're an array list.
// Thanks unhelpful fixer of http://bugs.java.com/view_bug.do?bug_id=8032636
ITweaker[] toSort = tweakers.toArray(new ITweaker[tweakers.size()]);
Arrays.sort(toSort, new Comparator<ITweaker>() {
@Override
public int compare(ITweaker o1, ITweaker o2)
{
Integer first = null;
Integer second = null;
if (o1 instanceof FMLInjectionAndSortingTweaker)
{
first = Integer.MIN_VALUE;
}
if (o2 instanceof FMLInjectionAndSortingTweaker)
{
second = Integer.MIN_VALUE;
}
if (o1 instanceof FMLPluginWrapper)
{
first = ((FMLPluginWrapper) o1).sortIndex;
}
else if (first == null)
{
first = tweakSorting.get(o1.getClass().getName());
}
if (o2 instanceof FMLPluginWrapper)
{
second = ((FMLPluginWrapper) o2).sortIndex;
}
else if (second == null)
{
second = tweakSorting.get(o2.getClass().getName());
}
if (first == null)
{
first = 0;
}
if (second == null)
{
second = 0;
}
return Ints.saturatedCast((long)first - (long)second);
}
});
// Basically a copy of Collections.sort, optimized as we know we're an array list.
// Thanks unhelpful fixer of http://bugs.java.com/view_bug.do?bug_id=8032636
ToIntFunction<ITweaker> getOrder = o -> o instanceof FMLInjectionAndSortingTweaker ? Integer.MIN_VALUE : o instanceof FMLPluginWrapper ? ((FMLPluginWrapper)o).sortIndex : tweakSorting.getOrDefault(o.getClass().getName(), 0);
Arrays.sort(toSort, (o1, o2) -> Ints.saturatedCast((long)getOrder.applyAsInt(o1) - (long)getOrder.applyAsInt(o2)));
for (int j = 0; j < toSort.length; j++) {
tweakers.set(j, toSort[j]);
}

View file

@ -25,6 +25,8 @@ import net.minecraft.launchwrapper.LaunchClassLoader;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.common.TracingPrintStream;
import net.minecraftforge.fml.common.launcher.FMLTweaker;
import net.minecraftforge.fml.relauncher.libraries.LibraryManager;
import org.apache.logging.log4j.LogManager;
public class FMLLaunchHandler
@ -99,6 +101,7 @@ public class FMLLaunchHandler
try
{
LibraryManager.setup(minecraftHome);
CoreModManager.handleLaunch(minecraftHome, classLoader, tweaker);
}
catch (Throwable t)
@ -129,4 +132,6 @@ public class FMLLaunchHandler
{
INSTANCE.injectPostfixTransformers();
}
}

View file

@ -1,166 +0,0 @@
/*
* Minecraft Forge
* Copyright (c) 2016.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraft.launchwrapper.Launch;
import net.minecraftforge.fml.common.FMLLog;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.JsonSyntaxException;
import javax.annotation.Nullable;
public class ModListHelper {
public static class JsonModList {
public String repositoryRoot;
public List<String> modRef;
public String parentList;
}
private static File mcDirectory;
private static Set<File> visitedFiles = Sets.newHashSet();
public static final Map<String,File> additionalMods = Maps.newLinkedHashMap();
static void parseModList(File minecraftDirectory)
{
FMLLog.log.debug("Attempting to load commandline specified mods, relative to {}", minecraftDirectory.getAbsolutePath());
mcDirectory = minecraftDirectory;
@SuppressWarnings("unchecked")
Map<String,String> args = (Map<String, String>) Launch.blackboard.get("launchArgs");
String listFile = args.get("--modListFile");
if (listFile != null)
{
parseListFile(listFile);
}
String extraMods = args.get("--mods");
if (extraMods != null)
{
String[] split = extraMods.split(",");
for (String modFile : split)
{
tryAddFile(modFile, null, modFile);
}
}
String[] extras = new String[]
{
"mods/mod_list.json",
"mods/" + FMLInjectionData.mccversion + "/mod_list.json"
};
for (String extra : extras)
{
if ((new File(mcDirectory, extra)).exists())
parseListFile(extra);
}
}
private static void parseListFile(String listFile) {
File f;
try
{
if (listFile.startsWith("absolute:"))
f = new File(listFile.substring(9)).getCanonicalFile();
else
f = new File(mcDirectory, listFile).getCanonicalFile();
} catch (IOException e2)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Unable to canonicalize path {} relative to {}", listFile, mcDirectory.getAbsolutePath()), e2);
return;
}
if (!f.exists())
{
FMLLog.log.info("Failed to find modList file {}", f.getAbsolutePath());
return;
}
if (visitedFiles.contains(f))
{
FMLLog.log.fatal("There appears to be a loop in the modListFile hierarchy. You shouldn't do this!");
throw new RuntimeException("Loop detected, impossible to load modlistfile");
}
String json;
try {
json = Files.asCharSource(f, StandardCharsets.UTF_8).read();
} catch (IOException e1) {
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to read modList json file {}.", listFile), e1);
return;
}
Gson gsonParser = new Gson();
JsonModList modList;
try {
modList = gsonParser.fromJson(json, JsonModList.class);
} catch (JsonSyntaxException e) {
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to parse modList json file {}.", listFile), e);
return;
}
visitedFiles.add(f);
// We visit parents before children, so the additionalMods list is sorted from parent to child
if (modList.parentList != null)
{
parseListFile(modList.parentList);
}
File repoRoot = new File(modList.repositoryRoot);
if (!repoRoot.exists())
{
FMLLog.log.info("Failed to find the specified repository root {}", modList.repositoryRoot);
return;
}
for (String s : modList.modRef)
{
StringBuilder fileName = new StringBuilder();
StringBuilder genericName = new StringBuilder();
String[] parts = s.split(":");
fileName.append(parts[0].replace('.', File.separatorChar));
genericName.append(parts[0]);
fileName.append(File.separatorChar);
fileName.append(parts[1]).append(File.separatorChar);
genericName.append(":").append(parts[1]);
fileName.append(parts[2]).append(File.separatorChar);
fileName.append(parts[1]).append('-').append(parts[2]);
if (parts.length == 4)
{
fileName.append('-').append(parts[3]);
genericName.append(":").append(parts[3]);
}
fileName.append(".jar");
tryAddFile(fileName.toString(), repoRoot, genericName.toString());
}
}
private static void tryAddFile(String modFileName, @Nullable File repoRoot, String descriptor) {
File modFile = repoRoot != null ? new File(repoRoot,modFileName) : new File(mcDirectory, modFileName);
if (!modFile.exists())
{
FMLLog.log.info("Failed to find mod file {} ({})", descriptor, modFile.getAbsolutePath());
}
else
{
FMLLog.log.debug("Adding {} ({}) to the mod list", descriptor, modFile.getAbsolutePath());
additionalMods.put(descriptor, modFile);
}
}
}

View file

@ -0,0 +1,200 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.File;
import java.text.ParseException;
import java.util.Date;
import java.util.Locale;
import org.apache.maven.artifact.versioning.ComparableVersion;
public class Artifact implements Comparable<Artifact>
{
private final Repository repo;
private final String group;
private final String artifact;
private final String classifier;
private final String extension;
private final String value;
private final ComparableVersion version;
private final String timestamp;
private final Date date;
private String filename;
private String folder;
public Artifact(Repository repo, String value, String timestamp)
{
this.repo = repo;
this.value = value;
int idx = value.indexOf('@');
if (idx > 0)
{
this.extension = value.substring(idx + 1);
value = value.substring(0, idx);
}
else
this.extension = "jar";
String[] parts = value.split(":");
this.group = parts[0];
this.artifact = parts[1];
this.version = new ComparableVersion(parts[2]);
this.classifier = parts.length > 3 ? parts[3] : null;
this.timestamp = isSnapshot() ? timestamp : null;
try
{
this.date = this.timestamp == null ? null : SnapshotJson.TIMESTAMP.parse(this.timestamp);
}
catch (ParseException e)
{
throw new RuntimeException(e); //TODO: Better logging?
}
}
public Artifact(Artifact other, Repository repo, String timestamp)
{
this.repo = repo;
this.group = other.group;
this.artifact = other.artifact;
this.classifier = other.classifier;
this.extension = other.extension;
this.value = other.value;
this.version = other.version;
this.timestamp = timestamp;
try
{
this.date = this.timestamp == null ? null : SnapshotJson.TIMESTAMP.parse(this.timestamp);
}
catch (ParseException e)
{
throw new RuntimeException(e); //TODO: Better logging?
}
}
@Override
public String toString()
{
return this.value;
}
@Override
public int hashCode()
{
return this.value.hashCode();
}
public String getFilename()
{
if (this.filename == null)
{
StringBuilder sb = new StringBuilder();
sb.append(artifact).append('-').append(version);
if (isSnapshot() && timestamp != null)
sb.append('-').append(getTimestamp());
if (classifier != null)
sb.append('-').append(classifier);
sb.append('.').append(extension);
this.filename = sb.toString();
}
return this.filename;
}
public String getFolder()
{
if (folder == null)
{
StringBuilder sb = new StringBuilder();
for (String part : group.split("."))
sb.append(part).append(File.separatorChar);
sb.append(artifact).append(File.separatorChar);
sb.append(version);
folder = sb.toString();
}
return folder;
}
public String getPath()
{
return getFolder() + File.separatorChar + getFilename();
}
public File getFile()
{
return (repo != null ? repo : LibraryManager.getDefaultRepo()).getFile(getPath());
}
public File getSnapshotMeta()
{
if (!isSnapshot())
throw new IllegalStateException("Attempted to call date suffix on non-snapshot");
return (repo != null ? repo : LibraryManager.getDefaultRepo()).getFile(getFolder() + File.separatorChar + SnapshotJson.META_JSON_FILE);
}
public boolean isSnapshot()
{
return version.toString().toLowerCase(Locale.ENGLISH).endsWith("-snapshot");
}
public String getTimestamp()
{
if (!isSnapshot())
throw new IllegalStateException("Attempted to call date suffix on non-snapshot");
return timestamp;
}
public ComparableVersion getVersion()
{
return this.version;
}
public Repository getRepository()
{
return this.repo;
}
public boolean matchesID(Artifact o)
{
if (o == null)
return false;
return group.equals(o.group) && artifact.equals(o.artifact) && (o.classifier == null ? classifier == null : o.classifier.equals(classifier)); //TODO: Case sensitive?
}
public int compareVersion(Artifact o)
{
int ver = version.compareTo(o.version);
if (ver != 0 || !isSnapshot()) return ver;
return timestamp == null ? (o.timestamp == null ? 0 : -1) : o.timestamp == null ? 1 : date.compareTo(o.date);
}
@Override
public int compareTo(Artifact o)
{
if (o == null) return 1;
if (!group.equals(o.group)) return group.compareTo(o.group);
if (!artifact.equals(o.artifact)) return artifact.compareTo(o.artifact);
if (classifier == null && o.classifier != null) return -1;
if (classifier != null && o.classifier == null) return 1;
if (classifier != null && !classifier.equals(o.classifier)) return classifier.compareTo(o.classifier);
return compareVersion(o);
}
}

View file

@ -0,0 +1,466 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.security.CodeSource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.maven.artifact.versioning.ArtifactVersion;
import com.google.common.io.ByteStreams;
import com.google.common.io.Files;
import net.minecraft.launchwrapper.Launch;
import net.minecraftforge.common.ForgeVersion;
import net.minecraftforge.fml.common.FMLLog;
import net.minecraftforge.fml.relauncher.FMLLaunchHandler;
public class LibraryManager
{
public static final boolean DISABLE_EXTERNAL_MANIFEST = Boolean.parseBoolean(System.getProperty("forge.disable_external_manifest", "false"));
public static final boolean ENABLE_AUTO_MOD_MOVEMENT = Boolean.parseBoolean(System.getProperty("forge.enable_auto_mod_movement", "false"));
private static final String LIBRARY_DIRECTORY_OVERRIDE = System.getProperty("forge.lib_folder", null);
private static final List<String> skipContainedDeps = Arrays.asList(System.getProperty("fml.skipContainedDeps","").split(",")); //TODO: Is this used by anyone in the real world? TODO: Remove in 1.13.
private static final FilenameFilter MOD_FILENAME_FILTER = (dir, name) -> name.endsWith(".jar") || name.endsWith(".zip"); //TODO: Disable support for zip in 1.13
private static final Comparator<File> FILE_NAME_SORTER_INSENSITVE = (o1, o2) -> o1.getName().toLowerCase(Locale.ENGLISH).compareTo(o2.getName().toLowerCase(Locale.ENGLISH));
public static final Attributes.Name MODSIDE = new Attributes.Name("ModSide");
private static final Attributes.Name MODCONTAINSDEPS = new Attributes.Name("ContainedDeps");
private static final Attributes.Name MAVEN_ARTIFACT = new Attributes.Name("Maven-Artifact");
private static final Attributes.Name TIMESTAMP = new Attributes.Name("Timestamp");
private static final Attributes.Name MD5 = new Attributes.Name("MD5");
private static Repository libraries_dir = null;
private static Set<File> processed = new HashSet<File>();
public static void setup(File minecraftHome)
{
File libDir = findLibraryFolder(minecraftHome);
FMLLog.log.debug("Determined Minecraft Libraries Root: {}", libDir);
Repository old = Repository.replace(libDir, "libraries");
if (old != null)
FMLLog.log.debug(" Overwriting Previous: {}", old);
libraries_dir = Repository.get("libraries");
File mods = new File(minecraftHome, "mods");
File mods_ver = new File(mods, ForgeVersion.mcVersion);
ModList memory = null;
if (!ENABLE_AUTO_MOD_MOVEMENT)
{
Repository repo = new LinkRepository(new File(mods, "memory_repo"));
memory = new MemoryModList(repo);
ModList.cache.put("MEMORY", memory);
Repository.cache.put("MEMORY", repo);
}
for (File dir : new File[]{mods, mods_ver})
cleanDirectory(dir, ENABLE_AUTO_MOD_MOVEMENT ? ModList.create(new File(dir, "mod_list.json"), minecraftHome) : memory, mods_ver, mods);
for (ModList list : ModList.getKnownLists(minecraftHome))
{
Repository repo = list.getRepository() == null ? libraries_dir : list.getRepository();
for (Artifact artifact : list.getArtifacts())
{
Artifact resolved = repo.resolve(artifact);
if (resolved != null)
{
File target = repo.getFile(resolved.getPath());
if (target.exists())
extractPacked(target, list, mods_ver, mods);
}
}
}
}
private static File findLibraryFolder(File minecraftHome)
{
if (LIBRARY_DIRECTORY_OVERRIDE != null)
{
FMLLog.log.error("System variable set to override Library Directory: {}", LIBRARY_DIRECTORY_OVERRIDE);
return new File(LIBRARY_DIRECTORY_OVERRIDE);
}
CodeSource source = ArtifactVersion.class.getProtectionDomain().getCodeSource();
if (source == null)
{
FMLLog.log.error("Unable to determine codesource for {}. Using default libraries directory.", ArtifactVersion.class.getName());
return new File(minecraftHome, "libraries");
}
try
{
File apache = new File(source.getLocation().toURI());
if (apache.isFile())
apache = apache.getParentFile(); //Get to a directory, this *should* always be the case...
apache = apache.getParentFile(); //Skip the version folder. In case we ever update the version, I don't want to edit this code again.
if (!apache.getAbsolutePath().toLowerCase(Locale.ENGLISH).replace(File.separatorChar, '/').endsWith("/org/apache/maven/maven/"))
{
FMLLog.log.error("Apache Maven library folder was not in the format expected: {}. Using default libraries directory.", new File(source.getLocation().toURI()));
return new File(minecraftHome, "libraries");
}
// maven /maven /apache /org /libraries
return apache.getParentFile().getParentFile().getParentFile().getParentFile();
}
catch (URISyntaxException e)
{
FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Unable to determine file for {}. Using default libraries directory.", ArtifactVersion.class.getName()), e);
}
return new File(minecraftHome, "libraries"); //Everything else failed, return the default.
}
private static void cleanDirectory(File dir, ModList modlist, File... modDirs)
{
if (!dir.exists())
return;
FMLLog.log.debug("Cleaning up mods folder: {}", dir);
for (File file : dir.listFiles(f -> f.isFile() && f.getName().endsWith(".jar")))
{
Pair<Artifact, byte[]> ret = extractPacked(file, modlist, modDirs);
if (ret != null)
{
Artifact artifact = ret.getLeft();
Repository repo = modlist.getRepository() == null ? libraries_dir : modlist.getRepository();
File moved = repo.archive(artifact, file, ret.getRight());
modlist.add(artifact);
processed.add(moved);
}
}
try
{
if (modlist.changed())
modlist.save();
}
catch (IOException e)
{
FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Error updating modlist file {}", modlist.getName()), e);
}
}
private static Pair<Artifact, byte[]> extractPacked(File file, ModList modlist, File... modDirs)
{
if (processed.contains(file))
{
FMLLog.log.debug("File already proccessed {}, Skipping", file.getAbsolutePath());
return null;
}
JarFile jar = null;
try
{
jar = new JarFile(file);
FMLLog.log.debug("Examining file: {}", file.getName());
processed.add(file);
return extractPacked(jar, modlist, modDirs);
}
catch (IOException ioe)
{
FMLLog.log.error("Unable to read the jar file {} - ignoring", file.getName(), ioe);
}
finally
{
try {
if (jar != null)
jar.close();
} catch (IOException e) {}
}
return null;
}
private static Pair<Artifact, byte[]> extractPacked(JarFile jar, ModList modlist, File... modDirs) throws IOException
{
Attributes attrs;
if (jar.getManifest() == null)
return null;
JarEntry manfest_entry = jar.getJarEntry(JarFile.MANIFEST_NAME);
if (manfest_entry == null)
manfest_entry = jar.stream().filter(e -> JarFile.MANIFEST_NAME.equals(e.getName().toUpperCase(Locale.ENGLISH))).findFirst().get(); //We know that getManifest returned non-null so we know there is *some* entry that matches the manifest file. So we dont need to empty check.
attrs = jar.getManifest().getMainAttributes();
String modSide = attrs.getValue(LibraryManager.MODSIDE);
if (!"BOTH".equals(modSide) || FMLLaunchHandler.side().name().equals(modSide))
return null;
if (attrs.containsKey(MODCONTAINSDEPS))
{
for (String dep : attrs.getValue(MODCONTAINSDEPS).split(" "))
{
if (!dep.endsWith(".jar"))
{
FMLLog.log.error("Contained Dep is not a jar file: {}", dep);
throw new IllegalStateException("Invalid contained dep, Must be jar: " + dep);
}
if (jar.getJarEntry(dep) == null && jar.getJarEntry("META-INF/libraries/" + dep) != null)
dep = "META-INF/libraries/" + dep;
JarEntry depEntry = jar.getJarEntry(dep);
if (depEntry == null)
{
FMLLog.log.error("Contained Dep is not in the jar: {}", dep);
throw new IllegalStateException("Invalid contained dep, Missing from jar: " + dep);
}
String depEndName = new File(dep).getName(); // extract last part of name
if (skipContainedDeps.contains(dep) || skipContainedDeps.contains(depEndName))
{
FMLLog.log.error("Skipping dep at request: {}", dep);
continue;
}
Attributes meta = null;
byte[] data = null;
byte[] manifest_data = null;
JarEntry metaEntry = jar.getJarEntry(dep + ".meta");
if (metaEntry != null)
{
manifest_data = readAll(jar.getInputStream(metaEntry));
meta = new Manifest(new ByteArrayInputStream(manifest_data)).getMainAttributes();
}
else
{
data = readAll(jar.getInputStream(depEntry));
try (ZipInputStream zi = new ZipInputStream(new ByteArrayInputStream(data))) //We use zip input stream directly, as the current Oracle implementation of JarInputStream only works when the manifest is the First/Second entry in the jar...
{
ZipEntry ze = null;
while ((ze = zi.getNextEntry()) != null)
{
if (ze.getName().equalsIgnoreCase(JarFile.MANIFEST_NAME))
{
manifest_data = readAll(zi);
meta = new Manifest(new ByteArrayInputStream(manifest_data)).getMainAttributes();
break;
}
}
}
}
if (meta == null || !meta.containsKey(MAVEN_ARTIFACT)) //Ugh I really don't want to do backwards compatibility here, I want to force modders to provide information... TODO: Remove in 1.13?
{
boolean found = false;
for (File dir : modDirs)
{
File target = new File(dir, depEndName);
if (target.exists())
{
FMLLog.log.debug("Found existing ContainDep extracted to {}, skipping extraction", target.getCanonicalPath());
found = true;
}
}
if (!found)
{
File target = new File(modDirs[0], depEndName);
FMLLog.log.debug("Extracting ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath());
try
{
Files.createParentDirs(target);
try
(
FileOutputStream out = new FileOutputStream(target);
InputStream in = data == null ? jar.getInputStream(depEntry) : new ByteArrayInputStream(data)
)
{
ByteStreams.copy(in, out);
}
FMLLog.log.debug("Extracted ContainedDep {} from {} to {}", dep, jar.getName(), target.getCanonicalPath());
extractPacked(target, modlist, modDirs);
}
catch (IOException e)
{
FMLLog.log.error("An error occurred extracting dependency", e);
}
}
}
else
{
try
{
String timestamp = meta.getValue(TIMESTAMP);
if (timestamp != null)
timestamp = SnapshotJson.TIMESTAMP.format(new Date(Integer.parseInt(timestamp)));
Artifact artifact = new Artifact(modlist.getRepository(), meta.getValue(MAVEN_ARTIFACT), timestamp);
File target = artifact.getFile();
if (target.exists())
FMLLog.log.debug("Found existing ContainedDep {}({}) from {} extracted to {}, skipping extraction", dep, artifact.toString(), target.getCanonicalPath(), jar.getName());
else
{
FMLLog.log.debug("Extracting ContainedDep {}({}) from {} to {}", dep, artifact.toString(), jar.getName(), target.getCanonicalPath());
Files.createParentDirs(target);
try
(
FileOutputStream out = new FileOutputStream(target);
InputStream in = data == null ? jar.getInputStream(depEntry) : new ByteArrayInputStream(data)
)
{
ByteStreams.copy(in, out);
}
FMLLog.log.debug("Extracted ContainedDep {}({}) from {} to {}", dep, artifact.toString(), jar.getName(), target.getCanonicalPath());
if (artifact.isSnapshot())
{
SnapshotJson json = SnapshotJson.create(artifact.getSnapshotMeta());
json.add(new SnapshotJson.Entry(artifact.getTimestamp(), meta.getValue(MD5)));
json.write(artifact.getSnapshotMeta());
}
if (!DISABLE_EXTERNAL_MANIFEST)
{
File meta_target = new File(target.getAbsolutePath() + ".meta");
Files.write(manifest_data, meta_target);
}
extractPacked(target, modlist, modDirs);
}
}
catch (NumberFormatException nfe)
{
FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("An error occurred extracting dependency. Invalid Timestamp: {}", meta.getValue(TIMESTAMP)), nfe);
}
catch (IOException e)
{
FMLLog.log.error("An error occurred extracting dependency", e);
}
}
}
}
return attrs.containsKey(MAVEN_ARTIFACT) ? Pair.of(new Artifact(modlist.getRepository(), attrs.getValue(MAVEN_ARTIFACT), attrs.getValue(TIMESTAMP)), readAll(jar.getInputStream(manfest_entry))) : null;
}
private static byte[] readAll(InputStream in) throws IOException
{
ByteArrayOutputStream out = new ByteArrayOutputStream();
int read = -1;
byte[] data = new byte[1024 * 16];
while ((read = in.read(data, 0, data.length)) != -1)
out.write(data, 0, read);
out.flush();
return out.toByteArray();
}
public static List<Artifact> flattenLists(File mcDir)
{
List<Artifact> merged = new ArrayList<>();
for (ModList list : ModList.getBasicLists(mcDir))
{
for (Artifact art : list.flatten())
{
Optional<Artifact> old = merged.stream().filter(art::matchesID).findFirst();
if (!old.isPresent())
{
merged.add(art);
}
else if (old.get().getVersion().compareTo(art.getVersion()) < 0)
{
merged.add(merged.indexOf(old.get()), art);
merged.remove(old.get());
}
}
}
return merged;
}
public static List<File> gatherLegacyCanidates(File mcDir)
{
List<File> list = new ArrayList<>();
@SuppressWarnings("unchecked")
Map<String,String> args = (Map<String, String>)Launch.blackboard.get("launchArgs");
String extraMods = args.get("--mods");
if (extraMods != null)
{
FMLLog.log.info("Found mods from the command line:");
for (String mod : extraMods.split(","))
{
File file = new File(mcDir, mod);
if (!file.exists())
{
FMLLog.log.info(" Failed to find mod file {} ({})", mod, file.getAbsolutePath());
}
else if (!list.contains(file))
{
FMLLog.log.debug(" Adding {} ({}) to the mod list", mod, file.getAbsolutePath());
list.add(file);
}
else if (!list.contains(file))
{
FMLLog.log.debug(" Duplicte command line mod detected {} ({})", mod, file.getAbsolutePath());
}
}
}
for (String dir : new String[]{"mods", "mods" + File.separatorChar + ForgeVersion.mcVersion})
{
File base = new File(mcDir, dir);
if (!base.isDirectory() || !base.exists())
continue;
FMLLog.log.info("Searching {} for mods", base.getAbsolutePath());
for (File f : base.listFiles(MOD_FILENAME_FILTER))
{
if (!list.contains(f))
{
FMLLog.log.debug(" Adding {} to the mod list", f.getName());
list.add(f);
}
}
}
ModList memory = ModList.cache.get("MEMORY");
if (!ENABLE_AUTO_MOD_MOVEMENT && memory != null && memory.getRepository() != null)
memory.getRepository().filterLegacy(list);
list.sort(FILE_NAME_SORTER_INSENSITVE);
return list;
}
public static Repository getDefaultRepo()
{
return libraries_dir;
}
}

View file

@ -0,0 +1,102 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.File;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import net.minecraftforge.fml.common.FMLLog;
public class LinkRepository extends Repository
{
private Map<String, File> artifact_to_file = new HashMap<>();
private Map<String, File> filesystem = new HashMap<>();
private Map<String, Artifact> snapshots = new HashMap<>();
private Set<File> known = new HashSet<>();
LinkRepository(File root)
{
super(root, "MEMORY");
}
public File archive(Artifact artifact, File file, byte[] manifest)
{
String key = artifact.toString();
known.add(file);
if (artifact_to_file.containsKey(key))
{
FMLLog.log.debug("Maven file already exists for {}({}) at {}, ignoring duplicate.", file.getName(), artifact.toString(), artifact_to_file.get(key).getAbsolutePath());
if (artifact.isSnapshot())
{
Artifact old = snapshots.get(key);
if (old == null || old.compareVersion(artifact) < 0)
{
FMLLog.log.debug("Overriding Snapshot {} -> {}", old == null ? "null" : old.getTimestamp(), artifact.getTimestamp());
snapshots.put(key, artifact);
artifact_to_file.put(key, file);
filesystem.put(artifact.getPath(), file);
}
}
}
else
{
FMLLog.log.debug("Making maven link for {} in memory to {}.", key, file.getAbsolutePath());
artifact_to_file.put(key, file);
filesystem.put(artifact.getPath(), file);
if (artifact.isSnapshot())
snapshots.put(key, artifact);
/* Support external meta? screw it.
if (!LibraryManager.DISABLE_EXTERNAL_MANIFEST)
{
File meta_target = new File(target.getAbsolutePath() + ".meta");
Files.write(manifest, meta_target);
}
*/
}
return file;
}
@Override
public void filterLegacy(List<File> list)
{
list.removeIf(e -> known.contains(e));
}
public Artifact resolve(Artifact artifact)
{
String key = artifact.toString();
File file = artifact_to_file.get(key);
if (file == null || !file.exists())
return null;
return new Artifact(artifact, this, artifact.isSnapshot() ? artifact.getTimestamp() : null);
}
@Override
public File getFile(String path)
{
return filesystem.containsKey(path) ? super.getFile(path) : filesystem.get(path);
}
}

View file

@ -0,0 +1,38 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.IOException;
public class MemoryModList extends ModList
{
MemoryModList(Repository repo)
{
super(repo);
}
@Override
public void save() throws IOException {}
@Override
public String getName()
{
return "MEMORY";
}
}

View file

@ -0,0 +1,243 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import com.google.common.collect.ImmutableList;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import net.minecraft.launchwrapper.Launch;
import net.minecraftforge.common.ForgeVersion;
import net.minecraftforge.fml.common.FMLLog;
public class ModList
{
private static final Gson GSON = new GsonBuilder().setLenient().create();
static final Map<String, ModList> cache = new HashMap<>();
public static ModList create(File json, File mcdir)
{
try
{
String key = json.getCanonicalFile().getAbsolutePath();
if (cache.containsKey(key))
return cache.get(key);
}
catch (IOException e)
{
FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Unable to load ModList json at {}.", json.getAbsoluteFile()), e);
}
return new ModList(json, mcdir);
}
@SuppressWarnings("unchecked")
public static List<ModList> getKnownLists(File mcdir)
{
for (String list : new String[] {"mods/mod_list.json", "mods/" + ForgeVersion.mcVersion + "/mod_list.json", ((Map<String, String>)Launch.blackboard.get("launchArgs")).get("--modListFile")})
{
if (list != null)
{
File listFile = getFile(mcdir, list);
if (listFile != null && listFile.exists())
create(listFile, mcdir); //Create will recursively create parent lists, so after this run everything should be populated.
}
}
return ImmutableList.copyOf(cache.values()); //TODO: Do we care about order? I dont think so as we resolve all libs in a flat map.
}
@SuppressWarnings("unchecked")
public static List<ModList> getBasicLists(File mcdir)
{
List<ModList> lst = new ArrayList<>();
ModList memory = cache.get("MEMORY");
if (memory != null)
lst.add(memory);
for (String list : new String[] {"mods/mod_list.json", "mods/" + ForgeVersion.mcVersion + "/mod_list.json", ((Map<String, String>)Launch.blackboard.get("launchArgs")).get("--modListFile")})
{
if (list != null)
{
File listFile = getFile(mcdir, list);
if (listFile != null && listFile.exists())
lst.add(create(listFile, mcdir));
}
}
return lst;
}
private final File path;
private final JsonModList mod_list;
private final Repository repo;
private final ModList parent;
private final List<Artifact> artifacts = new ArrayList<>();
private final List<Artifact> artifacts_imm = Collections.unmodifiableList(artifacts);
private final Map<String, Artifact> art_map = new HashMap<>();
private boolean changed = false;
protected ModList(Repository repo)
{
this.path = null;
this.mod_list = new JsonModList();
this.repo = repo;
this.parent = null;
}
private ModList(File path, File mcdir)
{
this.path = path;
JsonModList temp_list = null;
if (this.path.exists())
{
try
{
String json = Files.asCharSource(path, StandardCharsets.UTF_8).read();
temp_list = GSON.fromJson(json, JsonModList.class);
}
catch (JsonSyntaxException jse)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to parse modList json file {}.", path), jse);
}
catch (IOException ioe)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to read modList json file {}.", path), ioe);
}
}
this.mod_list = temp_list == null ? new JsonModList() : temp_list;
Repository temp = null;
if (mod_list.repositoryRoot != null)
{
try
{
temp = Repository.create(getFile(mcdir, this.mod_list.repositoryRoot));
}
catch (IOException e)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to create repository for modlist at {}.", mod_list.repositoryRoot), e);
}
}
this.repo = temp;
File parent_path = this.mod_list.parentList == null ? null : getFile(mcdir, this.mod_list.parentList);
this.parent = parent_path == null || !parent_path.exists() ? null : ModList.create(parent_path, mcdir);
if (mod_list.modRef != null)
{
for (String ref : mod_list.modRef)
add(new Artifact(this.getRepository(), ref, null));
changed = false;
}
}
public Repository getRepository()
{
return this.repo;
}
public void add(Artifact artifact)
{
Artifact old = art_map.get(artifact.toString());
if (old != null)
{
artifacts.add(artifacts.indexOf(old), artifact);
artifacts.remove(old);
}
else
artifacts.add(artifact);
changed = true;
}
public List<Artifact> getArtifacts()
{
return artifacts_imm;
}
public boolean changed()
{
return changed;
}
public void save() throws IOException
{
mod_list.modRef = artifacts.stream().map(a -> a.toString()).collect(Collectors.toList());
Files.write(GSON.toJson(mod_list), path, StandardCharsets.UTF_8);
}
private static File getFile(File root, String path)
{
try
{
if (path.startsWith("absolute:"))
return new File(path.substring(9)).getCanonicalFile();
else
return new File(root, path).getCanonicalFile();
}
catch (IOException ioe)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Unable to canonicalize path {} relative to {}", path, root.getAbsolutePath()), ioe);
}
return null;
}
private static class JsonModList
{
public String repositoryRoot;
public List<String> modRef;
public String parentList;
}
//TODO: Some form of caching?
public List<Artifact> flatten()
{
List<Artifact> lst = parent == null ? new ArrayList<>() : parent.flatten();
for (Artifact art : artifacts)
{
Optional<Artifact> old = lst.stream().filter(art::matchesID).findFirst();
if (!old.isPresent())
{
lst.add(art);
}
else if (old.get().getVersion().compareTo(art.getVersion()) < 0)
{
lst.add(lst.indexOf(old.get()), art);
lst.remove(old.get());
}
}
return lst;
}
public Object getName()
{
return path.getAbsolutePath();
}
}

View file

@ -0,0 +1,162 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import com.google.common.hash.Hashing;
import com.google.common.io.Files;
import net.minecraftforge.fml.common.FMLLog;
public class Repository
{
static final Map<String, Repository> cache = new LinkedHashMap<>();
public static Repository create(File root) throws IOException
{
return create(root, root.getCanonicalPath());
}
public static Repository create(File root, String name)
{
return cache.computeIfAbsent(name, f -> new Repository(root, name));
}
public static Repository replace(File root, String name)
{
return cache.put(name, new Repository(root, name));
}
public static Repository get(String name)
{
return cache.get(name);
}
public static Artifact resolveAll(Artifact artifact)
{
Artifact ret = null;
for (Repository repo : cache.values())
{
Artifact tmp = repo.resolve(artifact);
if (tmp == null)
continue;
if (!artifact.isSnapshot())
return tmp; //If its a concrete version *assume* any resolved one in any repo will work. As they shouldn't release overriding versions.
ret = ret == null || ret.compareTo(tmp) < 0 ? tmp : ret;
}
return ret;
}
private final String name;
private final File root;
protected Repository(File root) throws IOException
{
this(root, root.getCanonicalPath());
}
protected Repository(File root, String name)
{
this.root = root;
this.name = name;
if (name == null)
throw new IllegalArgumentException("Invalid Repository Name, for " + root);
}
@Override
public int hashCode()
{
return this.name.hashCode();
}
@Override
public boolean equals(Object o)
{
return o instanceof Repository && ((Repository)o).name.equals(name);
}
public Artifact resolve(Artifact artifact)
{
if (!artifact.isSnapshot())
return getFile(artifact.getPath()).exists() ? artifact : null;
File meta = getFile(artifact.getFolder() + File.separatorChar + SnapshotJson.META_JSON_FILE);
if (!meta.exists())
return null;
SnapshotJson json = SnapshotJson.create(getFile(artifact.getFolder() + File.separatorChar + SnapshotJson.META_JSON_FILE));
if (json.getLatest() == null)
return null;
Artifact ret = new Artifact(artifact, this, json.getLatest());
while (json.getLatest() != null && !getFile(ret.getPath()).exists())
{
if (!json.remove(json.getLatest()))
throw new IllegalStateException("Something went wrong, Latest (" + json.getLatest() + ") did not point to an entry in the json list: " + meta.getAbsolutePath());
ret = new Artifact(artifact, this, json.getLatest());
}
return getFile(ret.getPath()).exists() ? ret : null;
}
public File getFile(String path)
{
return new File(root, path);
}
public File archive(Artifact artifact, File file, byte[] manifest)
{
File target = artifact.getFile();
try
{
if (target.exists())
{
FMLLog.log.debug("Maven file already exists for {}({}) at {}, deleting duplicate.", file.getName(), artifact.toString(), target.getAbsolutePath());
file.delete();
}
else
{
FMLLog.log.debug("Moving file {}({}) to maven repo at {}.", file.getName(), artifact.toString(), target.getAbsolutePath());
Files.move(file, target);
if (artifact.isSnapshot())
{
SnapshotJson json = SnapshotJson.create(artifact.getSnapshotMeta());
json.add(new SnapshotJson.Entry(artifact.getTimestamp(), Files.hash(target, Hashing.md5()).toString()));
json.write(artifact.getSnapshotMeta());
}
if (!LibraryManager.DISABLE_EXTERNAL_MANIFEST)
{
File meta_target = new File(target.getAbsolutePath() + ".meta");
Files.write(manifest, meta_target);
}
}
return target;
}
catch (IOException e)
{
FMLLog.log.error(FMLLog.log.getMessageFactory().newMessage("Error moving file {} to {}", file, target.getAbsolutePath()), e);
}
return file;
}
public void filterLegacy(List<File> list){}
}

View file

@ -0,0 +1,163 @@
/*
* Minecraft Forge
* Copyright (c) 2016-2018.
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation version 2.1
* of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
package net.minecraftforge.fml.relauncher.libraries;
import java.io.File;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import com.google.common.io.Files;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonSyntaxException;
import net.minecraftforge.fml.common.FMLLog;
/**
* This is different from the standard maven snapshot metadata.
* Because none of that data is exposed to us as a user of gradle/maven/whatever.
* So we JUST use the timestamp.
*
* {
* "latest": "yyyymmdd.hhmmss",
* "versions": [
* {
* "md5": "md5 in hex lowercase",
* "timestamp": "yyyymmdd.hhmmss"
* ]
* }
*
*/
public class SnapshotJson implements Comparable<SnapshotJson>
{
public static final DateFormat TIMESTAMP = new SimpleDateFormat("yyyymmdd.hhmmss");
public static final String META_JSON_FILE = "maven-metadata.json";
private static final Gson GSON = new GsonBuilder().create();
private static final Comparator<Entry> SORTER = (o1, o2) -> o2.timestamp.compareTo(o2.timestamp);
public static SnapshotJson create(File target)
{
if (!target.exists())
return new SnapshotJson();
try
{
String json = Files.asCharSource(target, StandardCharsets.UTF_8).read();
SnapshotJson obj = GSON.fromJson(json, SnapshotJson.class);
obj.updateLatest();
return obj;
}
catch (JsonSyntaxException jse)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to parse snapshot json file {}.", target), jse);
}
catch (IOException ioe)
{
FMLLog.log.info(FMLLog.log.getMessageFactory().newMessage("Failed to read snapshot json file {}.", target), ioe);
}
return new SnapshotJson();
}
private String latest;
private List<Entry> versions;
public String getLatest()
{
return this.latest;
}
public void add(Entry data)
{
if (versions == null)
versions = new ArrayList<>();
versions.add(data);
updateLatest();
}
public void merge(SnapshotJson o)
{
if (o.versions != null)
{
if (versions == null)
versions = new ArrayList<>(o.versions);
else
o.versions.stream().filter(e -> versions.stream().anyMatch(e2 -> e.timestamp.equals(e2.timestamp))).forEach(e -> versions.add(e));
updateLatest();
}
}
public boolean remove(String timestamp)
{
if (versions == null)
return false;
if (versions.removeIf(e -> e.timestamp.equals(timestamp)))
updateLatest();
return false;
}
public String updateLatest()
{
if (versions == null)
{
latest = null;
return null;
}
Collections.sort(versions, SORTER);
return latest = versions.isEmpty() ? null : versions.get(0).timestamp;
}
public void write(File target) throws IOException
{
Files.write(GSON.toJson(this), target, StandardCharsets.UTF_8);
}
@Override
public int compareTo(SnapshotJson o)
{
return o == null ? 1 : o.latest == null ? latest == null ? 0 : 1 : latest == null ? -1 : o.latest.compareTo(latest);
}
public static class Entry
{
private String timestamp;
private String md5;
public Entry(String timestamp, String md5)
{
this.timestamp = timestamp;
this.md5 = md5;
}
public String getTimestamp()
{
return this.timestamp;
}
public String getMd5()
{
return this.md5;
}
}
}