diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2b75c4e..fd8fd33 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -17,14 +17,17 @@ jobs: strategy: matrix: command: [ - "--forge --version 1.21.11-61.1.0", - "--forge --version 1.20.1-47.4.0", - "--forge --version 1.12.2-14.23.5.2859 --mappings snapshot:20171003-1.12", - "--forge --version 26.1-62.0.0", - "--client --version 1.20.1", + "--forge --version 1.6.4-9.11.1.964", # FG 1.0 + "--forge --version 1.7.10-10.13.4.1614-1.7.10", # FG 1.1 + "--forge --version 1.8.9-11.15.1.2318-1.8.9", # FG 2.1 + "--forge --version 1.12.2-14.23.5.2864", # FG + "--forge --version 1.20.1-47.4.0", # FG 3 + "--forge --version 1.21.11-61.1.0", # FG 6 Official names + "--forge --version 26.1-62.0.0", # FG 6 No obfusation + "--client --version 1.20.1", # MCPConfig, full decompile/recompile poissible "--server --version 1.20.1", "--mc --version 1.20.1", - "--client --version 26.1-snapshot-1", + "--client --version 26.1-snapshot-1", # No MCPConfig, unobfuscated, No Recompile possible "--server --version 26.1-snapshot-1" ] diff --git a/build.gradle b/build.gradle index d70fa50..439c211 100644 --- a/build.gradle +++ b/build.gradle @@ -50,6 +50,7 @@ license { header = rootProject.file('LICENSE-header.txt') newLine false exclude '**/ComparableVersion.java' // Apache class used as source-level dep as to not need the hundred+ other classes the library pulls in + exclude '**/ContextualPatch.java' // diff4j single file library } application { diff --git a/settings.gradle b/settings.gradle index 7a21cae..d0e0083 100644 --- a/settings.gradle +++ b/settings.gradle @@ -30,18 +30,18 @@ dependencyResolutionManagement.versionCatalogs.register('libs') { library 'gson', 'com.google.code.gson', 'gson' version '2.10.1' library 'jopt', 'net.sf.jopt-simple', 'jopt-simple' version '6.0-alpha-3' - library 'jver', 'net.minecraftforge', 'java-provisioner' version '2.0.4' - library 'srgutils', 'net.minecraftforge', 'srgutils' version '0.6.3' + library 'jver', 'net.minecraftforge', 'java-provisioner' version '2.0.5' + library 'srgutils', 'net.minecraftforge', 'srgutils' version '0.6.6' library 'diff', 'io.codechicken', 'DiffPatch' version '2.0.0.36' // Fuzzy patching library 'fastcsv', 'de.siegmar', 'fastcsv' version '3.4.0' library 'commonsio', 'commons-io', 'commons-io' version '2.18.0' // Utilities - library 'utils-download', 'net.minecraftforge', 'download-utils' version '0.4.0' + library 'utils-download', 'net.minecraftforge', 'download-utils' version '0.4.1' library 'utils-files', 'net.minecraftforge', 'file-utils' version '0.3.3' library 'utils-hash', 'net.minecraftforge', 'hash-utils' version '0.2.2' - library 'utils-data', 'net.minecraftforge', 'json-data-utils' version '0.4.7' + library 'utils-data', 'net.minecraftforge', 'json-data-utils' version '0.4.10' library 'utils-logging', 'net.minecraftforge', 'log-utils' version '0.5.1' library 'utils-os', 'net.minecraftforge', 'os-utils' version '0.1.0' bundle 'utils', ['utils-download', 'utils-files', 'utils-hash', 'utils-data', 'utils-logging', 'utils-os'] diff --git a/src/main/java/net/minecraftforge/mcmaven/cli/MCPTask.java b/src/main/java/net/minecraftforge/mcmaven/cli/MCPTask.java index b796fd9..6647511 100644 --- a/src/main/java/net/minecraftforge/mcmaven/cli/MCPTask.java +++ b/src/main/java/net/minecraftforge/mcmaven/cli/MCPTask.java @@ -21,6 +21,7 @@ import net.minecraftforge.mcmaven.impl.MinecraftMaven; import net.minecraftforge.mcmaven.impl.cache.Cache; import net.minecraftforge.mcmaven.impl.mappings.Mappings; +import net.minecraftforge.mcmaven.impl.mappings.ResolvedMappings; import net.minecraftforge.mcmaven.impl.repo.forge.Patcher; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCP; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPConfigRepo; @@ -197,12 +198,12 @@ static OptionParser run(String[] args, boolean getParser) throws Exception { Util.filter(LOGGER, " SAS: ", sas); LOGGER.info(); - var task = new MCPTask(outputDir, new Cache(cacheRoot, jdkCacheRoot), artifact, pipeline); + var task = new MCPTask(outputDir, new Cache(cacheRoot, jdkCacheRoot), artifact, pipeline, mappings); var ret = task.classes(); if(mappings != null) - task.mappings(mappings); + task.mappings(); if (!disableDecompile) - ret = task.decompile(mappings, ats, sas); + ret = task.decompile(ats, sas); task.data.put("output", ret); try { @@ -222,13 +223,15 @@ static OptionParser run(String[] args, boolean getParser) throws Exception { private final MCP mcp; private final MCPSide side; private final String prefix; + private final ResolvedMappings mappings; - private MCPTask(@Nullable File outputDir, Cache cache, Artifact artifact, String pipeline) { + private MCPTask(@Nullable File outputDir, Cache cache, Artifact artifact, String pipeline, @Nullable Mappings baseMappings) { this.outputDir = outputDir; this.repo = new MCPConfigRepo(cache, false); this.mcp = repo.get(artifact); this.side = mcp.getSide(pipeline); + this.mappings = baseMappings == null ? null : baseMappings.withContext(side); this.data.put("config", artifact.toString()); this.data.put("pipeline", pipeline); @@ -251,15 +254,15 @@ private String classes() { return this.data.get("classes.srg"); } - private void mappings(@Nullable Mappings mappings) { + private void mappings() { this.data.put("mappings.channel", mappings.channel()); this.data.put("mappings.version", mappings.version()); - this.data.put("mappings.zip", local(mappings.getCsvZip(side).execute(), prefix + "/mappings.zip")); - this.data.put("mappings.map2obf", local(mappings.getMapped2Obf(side).execute(), prefix + "/mappings.map2obf.tsrg.gz")); - this.data.put("mappings.map2srg", local(mappings.getMapped2Srg(side).execute(), prefix + "/mappings.map2srg.tsrg.gz")); + this.data.put("mappings.zip", local(mappings.getCsvZip().execute(), prefix + "/mappings.zip")); + this.data.put("mappings.map2obf", local(mappings.getMapped2Obf().execute(), prefix + "/mappings.map2obf.tsrg.gz")); + this.data.put("mappings.map2srg", local(mappings.getMapped2Srg().execute(), prefix + "/mappings.map2srg.tsrg.gz")); } - private String decompile(@Nullable Mappings mappings, List ats, List sas) { + private String decompile(List ats, List sas) { var sourcesTask = side.getSources(); if (!ats.isEmpty() || !sas.isEmpty()) { @@ -299,7 +302,10 @@ private String decompile(@Nullable Mappings mappings, List ats, List LOGGER.info("Renaming MCP Source Jar"); indent = LOGGER.push(); try { - var renameTask = new RenameTask(side.getBuildFolder(), side.getName(), side, sourcesTask, mappings, false); + var mcVersion = side.getMCP().getMinecraftTasks().getVersion(); + var srgTask = side.getTasks().getMappings(); + + var renameTask = new RenameTask(side.getBuildFolder(), side.getName(), sourcesTask, mappings, false, srgTask, mcVersion); this.data.put("sources.named", local(renameTask.execute(), prefix + "/sources.named.jar")); } finally { LOGGER.pop(indent); diff --git a/src/main/java/net/minecraftforge/mcmaven/cli/MavenTask.java b/src/main/java/net/minecraftforge/mcmaven/cli/MavenTask.java index 9c4d3d5..521d89a 100644 --- a/src/main/java/net/minecraftforge/mcmaven/cli/MavenTask.java +++ b/src/main/java/net/minecraftforge/mcmaven/cli/MavenTask.java @@ -7,7 +7,6 @@ import java.io.File; import java.util.ArrayList; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import joptsimple.OptionParser; @@ -211,7 +210,6 @@ static OptionParser run(String[] args, boolean getParser) throws Exception { options.has(globalAuxiliaryVariantsO), options.has(disableGradleO), options.has(stubO), - new HashSet<>(), new ArrayList<>(options.valuesOf(accessTransformerO)), new ArrayList<>(options.valuesOf(facadeConfigO)), options.valueOf(outputJsonO) diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/Mavenizer.java b/src/main/java/net/minecraftforge/mcmaven/impl/Mavenizer.java index 78c5088..0efe4a1 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/Mavenizer.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/Mavenizer.java @@ -21,7 +21,7 @@ private Mavenizer() { } private static boolean offline = false; private static boolean cacheOnly = false; - private static boolean cacheMiss = false; + public static boolean cacheMiss = false; private static boolean ignoreCache = false; public static boolean isOffline() { diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/MinecraftMaven.java b/src/main/java/net/minecraftforge/mcmaven/impl/MinecraftMaven.java index d825364..132e4e0 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/MinecraftMaven.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/MinecraftMaven.java @@ -9,16 +9,17 @@ import net.minecraftforge.mcmaven.impl.mappings.Mappings; import net.minecraftforge.mcmaven.impl.repo.Repo; import net.minecraftforge.mcmaven.impl.repo.Repo.PendingArtifact; -import net.minecraftforge.mcmaven.impl.repo.forge.FGVersion; import net.minecraftforge.mcmaven.impl.repo.forge.ForgeRepo; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCP; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPConfigRepo; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.ComparableVersion; import net.minecraftforge.mcmaven.impl.util.Constants; import net.minecraftforge.mcmaven.impl.util.POMBuilder; import net.minecraftforge.mcmaven.impl.util.ProcessUtils; +import net.minecraftforge.mcmaven.impl.util.StupidHacks; import net.minecraftforge.mcmaven.impl.util.Task; import net.minecraftforge.mcmaven.impl.util.Util; import net.minecraftforge.srgutils.MinecraftVersion; @@ -43,7 +44,6 @@ import java.util.HashSet; import java.util.List; import java.util.Map; -import java.util.Set; import java.util.TreeMap; import java.util.function.Supplier; @@ -72,14 +72,13 @@ public record MinecraftMaven( boolean globalAuxiliaryVariants, boolean disableGradle, boolean stubJars, - Set mcpConfigVersions, List accessTransformer, List facadeConfigs, @Nullable File outputJsonFile ) { // Only 1.14.4+ has official mappings, we can support more when we add more mappings private static final MinecraftVersion MIN_OFFICIAL_MAPPINGS = MinecraftVersion.from("1.14.4"); - private static final ComparableVersion MIN_SUPPORTED_FORGE = new ComparableVersion("1.14.4"); + private static final ComparableVersion MIN_SUPPORTED_FORGE = new ComparableVersion("1.6.4"); public MinecraftMaven { LOGGER.info(" Output: " + output.getAbsolutePath()); @@ -150,28 +149,88 @@ protected void createForge(Artifact artifact, MCPConfigRepo mcprepo, ForgeRepo r if (version == null) throw new IllegalArgumentException("No version specified for Forge"); + ComparableVersion verStart = null; + ComparableVersion verEnd = null; + if (version.charAt(0) == '[' && version.charAt(version.length()-1) == ']') { + var pts = version.split(","); + if (pts[0].length() > 1) + verStart = new ComparableVersion(pts[0].substring(1)); + if (pts[1].length() > 1) + verEnd = new ComparableVersion(pts[1].substring(0, pts[1].length() - 1)); + version = "all"; + } + if ("all".equals(version)) { if (outputJson != null) throw new IllegalArgumentException("Output Json does not support bulk operations"); var versions = this.cache.maven().getVersions(artifact); - var mappingCache = new HashMap(); - for (var ver : versions.reversed()) { - var cver = new ComparableVersion(ver); + var cversions = new ArrayList(versions.size()); + for (var ver : versions) { + cversions.add(new ComparableVersion(ver)); + } + Collections.sort(cversions); + + var mappings = this.mappings; + //var lastMCVersion = ""; + for (var cver : cversions) { if (cver.compareTo(MIN_SUPPORTED_FORGE) < 0) continue; - var fg = FGVersion.fromForge(ver); - if (fg == null || fg.ordinal() < FGVersion.v3.ordinal()) // Unsupported + if (StupidHacks.BLACKLISTED_FORGE_BUILDS.contains(cver.toString())) continue; - var mappings = mappingCache.computeIfAbsent(forgeToMcVersion(ver), this.mappings::withMCVersion); + var ver = cver.toString(); + var mcVersion = Util.forgeToMcVersion(ver); + + if (verStart != null && cver.compareTo(verStart) < 0) + continue; + if (verEnd != null && cver.compareTo(verEnd) > 0) + continue; + if (!ForgeRepo.isSupported(ver)) + continue; + + // Old versions don't have official mappings, and some of them require specific MCP mappigns due to the sources not being in + // full SRG names (we don't remap static imports) So if we're using official mappings (the default arugment) lets use the + // specific bot mappings that the version of Forge was built for. Any other option is at user's pearly and may not work. + if (this.mappings.channel().equals("official")) { + var newMappings = StupidHacks.getDefaultMappings(cver).withMCVersion(mcVersion); + if (!newMappings.channel().equals(mappings.channel()) || !newMappings.version().equals(mappings.version())) { + mappings = newMappings; + LOGGER.info("Using Mappings: " + mappings); + } + } + var art = artifact.withVersion(ver); var artifacts = repo.process(art, mappings, outputJson); - finalize(art, mappings, artifacts); + LOGGER.push(); + try { + finalize(art, mappings, artifacts); + } finally { + LOGGER.pop(); + } } + + ForgeRepo.Info.finish(); } else { - var mappings = this.mappings.withMCVersion(forgeToMcVersion(version)); + if (StupidHacks.BLACKLISTED_FORGE_BUILDS.contains(artifact.getVersion())) + throw new IllegalArgumentException("Forge version " + artifact.getVersion() + " has been blacklisted for technical reasons, pick a different Forge version"); + + var mcVersion = Util.forgeToMcVersion(version); + var mappings = this.mappings.withMCVersion(mcVersion); + + // Old versions don't have official mappings, and some of them require specific MCP mappigns due to the sources not being in + // full SRG names (we don't remap static imports) So if we're using official mappings (the default arugment) lets use the + // specific bot mappings that the version of Forge was built for. Any other option is at user's pearl and may not work. + if (this.mappings.channel().equals("official") && !hasOfficialMappings(mcprepo, mcVersion)) { + var cver = new ComparableVersion(artifact.getVersion()); + var newMappings = StupidHacks.getDefaultMappings(cver).withMCVersion(mcVersion); + if (!newMappings.channel().equals(mappings.channel()) || !newMappings.version().equals(mappings.version())) { + mappings = newMappings; + LOGGER.info("Using Mappings: " + mappings); + } + } + var artifacts = repo.process(artifact, mappings, outputJson); finalize(artifact, mappings, artifacts); } @@ -185,17 +244,41 @@ protected void createMinecraft(Artifact artifact, MCPConfigRepo mcprepo, Map 1) + verStart = MinecraftVersion.from(pts[0].substring(1)); + if (pts[1].length() > 1) + verEnd = MinecraftVersion.from(pts[1].substring(0, pts[1].length() - 1)); + version = "all"; + } + + // Quick check of maven-metadata.xml to get a list of known MCPConfig versions + var maven = mcprepo.getCache().maven(); + var mcpConfigVersions = new HashSet<>(maven.getVersions(MCP.artifact("1.21.11"))); + mcpConfigVersions.remove("1.12.2"); // Force 1.12.2 to not use MCPConfig, instead using the legacy MCP toolchain + var mcpLegacyVersions = new HashSet<>(maven.getVersions(MCPLegacy.artifact("1.12.2"))); + // Used while developing on maven local + //mcpLegacyVersions.addAll(List.of("1.7.2", "1.7.10-pre4", "1.7.10")); + if ("all".equals(version)) { if (outputJson != null) throw new IllegalArgumentException("Output Json does not support bulk operations"); var manifestFile = mcprepo.getLauncherManifestTask().execute(); var manifest = JsonData.launcherManifest(manifestFile); + // The launcher manifest is normally in reverse release order, so don't worry about sorting them. for (var ver : manifest.versions) { - + MinecraftVersion cver; try { - var cver = MinecraftVersion.from(ver.id); - if (cver.compareTo(MIN_OFFICIAL_MAPPINGS) < 0) + cver = MinecraftVersion.from(ver.id); + if (verStart != null && cver.compareTo(verStart) < 0) + continue; + if (verEnd != null && cver.compareTo(verEnd) > 0) + continue; + if (cver.compareTo(MIN_OFFICIAL_MAPPINGS) < 0 && !mcpConfigVersions.contains(ver.id) && !mcpLegacyVersions.contains(ver.id)) continue; } catch (IllegalArgumentException e) { // Invalid/unknown version, so skip. @@ -205,41 +288,49 @@ protected void createMinecraft(Artifact artifact, MCPConfigRepo mcprepo, Map artifacts = null; - if (hasMcp(mcprepo, ver.id)) - artifacts = mcprepo.process(versioned, mappings.withMCVersion(ver.id), outputJson); - else if (mappings.channel().equals("official") && (hasOfficialMappings(mcprepo, ver.id) || !MCPConfigRepo.isObfuscated(ver.id))) - artifacts = mcprepo.processWithoutMcp(versioned, mappings.withMCVersion(ver.id), outputJson); + + var mappings = this.mappings.withMCVersion(ver.id); + // If we're using official mappings, and are on a version that doesn't have official mappings, lets just use SRG names + if (this.mappings.channel().equals("official")) { + if (cver.compareTo(MIN_OFFICIAL_MAPPINGS) < 0) + mappings = Mappings.of("srg").withMCVersion(ver.id); + } + + if (mcpConfigVersions.contains(ver.id)) + artifacts = mcprepo.process(versioned, mappings, outputJson); + else if (mcpLegacyVersions.contains(ver.id)) + artifacts = mcprepo.processLegacy(versioned, mappings, outputJson); + else if (hasOfficialMappings(mcprepo, ver.id) || !MCPConfigRepo.isObfuscated(ver.id)) + artifacts = mcprepo.processWithoutMcp(versioned, mappings, outputJson); else { LOGGER.info("Skipping " + versioned + " no mcp config"); continue; } - finalize(versioned, mappings, artifacts); + LOGGER.push(); + try { + finalize(versioned, mappings, artifacts); + } finally { + LOGGER.pop(); + } } } else { var mcVersion = mcpToMcVersion(version); var mappings = this.mappings.withMCVersion(mcVersion); List artifacts = null; - if (hasMcp(mcprepo, version)) + if (mcpConfigVersions.contains(version)) artifacts = mcprepo.process(artifact, mappings, outputJson); - else if (mappings.channel().equals("official") && (hasOfficialMappings(mcprepo, mcVersion) || !MCPConfigRepo.isObfuscated(mcVersion))) + else if (mcpLegacyVersions.contains(version)) + artifacts = mcprepo.processLegacy(artifact, mappings, outputJson); + else if (hasOfficialMappings(mcprepo, mcVersion) || !MCPConfigRepo.isObfuscated(mcVersion)) artifacts = mcprepo.processWithoutMcp(artifact, mappings, outputJson); else - throw new IllegalStateException("Can not process " + artifact + " as it does not have a MCPConfig ror official mappings"); + throw new IllegalStateException("Can not process " + artifact + " as it does not have a MCPConfig, MCPLegacy, or official mappings"); finalize(artifact, mappings, artifacts); } } - // This is ugly, but there really isn't a good way to check if a file exists. DownlodUtils could probably use a 'exists' that HEAD's and returns false non 404 - private boolean hasMcp(MCPConfigRepo repo, String version) { - if (this.mcpConfigVersions.isEmpty()) { - var versions = repo.getCache().maven().getVersions(MCP.artifact("1.21.11")); - this.mcpConfigVersions.addAll(versions); - } - return this.mcpConfigVersions.contains(version); - } - // No official mappings, we can't do anything private static boolean hasOfficialMappings(MCPConfigRepo repo, String version) { var tasks = repo.getMCTasks(version); @@ -250,17 +341,6 @@ private static boolean hasOfficialMappings(MCPConfigRepo repo, String version) { json.getDownload(MinecraftTasks.MCFile.SERVER_MAPPINGS.key) != null; } - private static String forgeToMcVersion(String version) { - // Save for a few april-fools versions, Minecraft doesn't use _ in their version names. - // So when Forge needs to reference a version of Minecraft that uses - in the name, it replaces - // it with _ - // This could cause issues if we ever support a version with _ in it, but fuck it I don't care right now. - int idx = version.indexOf('-'); - if (idx == -1) - throw new IllegalArgumentException("Invalid Forge version: " + version); - return version.substring(0, idx).replace('_', '-'); - } - public static String mcpToMcVersion(String version) { // MCP names can either be {MCVersion} or {MCVersion}-{Timestamp}, EXA: 1.21.1-20240808.132146 // So lets see if the thing following the last - matches a timestamp @@ -477,7 +557,7 @@ private File facade(Task inputTask, Artifact artifact) { args.add(file.getAbsolutePath()); } - execute("facade ", Constants.FACAD_JAVA_VERSION, tool, args, target); + execute("facade ", Constants.FACADE_JAVA_VERSION, tool, args, target); cache.save(); return target; } @@ -495,25 +575,23 @@ private void execute(String name, int javaVersion, File tool, List args, if (parent != null && !parent.exists()) parent.mkdirs(); - var log = new File(target.getAbsolutePath() + ".log"); var ret = ProcessUtils.runJar(jdk, parent, log, tool, Collections.emptyList(), args); if (ret.exitCode != 0) throw new IllegalStateException("Failed to " + name + " file (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); - } private void updateVariants(Artifact artifact) { var root = new File(this.output, artifact.getFolder()); var inputs = new ArrayList(); for (var file : root.listFiles()) { - if (file.isFile() && file.getName().endsWith(".variants")) { + if (file.isFile() && file.getName().endsWith(".variants")) inputs.add(file); - } } var target = new File(this.output, artifact.withExtension("module").getLocalPath()); var cache = Util.cache(target); + cache.add("codever", "1"); for (var input : inputs) { cache.add(input.getName(), input); } @@ -521,12 +599,18 @@ private void updateVariants(Artifact artifact) { if (Mavenizer.checkCache(target, cache)) return; + var seen = new HashSet(); var module = GradleModule.of(artifact); for (var input : inputs) { try { var data = JsonData.fromJson(input, GradleModule.Variant[].class); - for (var variant : data) - module.variant(variant); + for (var variant : data) { + // Stupid hack to work around a bug in our old system. (MCPMappings was marked as 'primary' so we may run into duplicate variants) + // Variants must have unique attributes + var attributes = JsonData.toJson(variant.attributes); + if (seen.add(attributes)) + module.variant(variant); + } } catch (Throwable t) { throw new RuntimeException("Failed to read artifact variants: %s".formatted(input), t); } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/cache/JDKCache.java b/src/main/java/net/minecraftforge/mcmaven/impl/cache/JDKCache.java index e335caf..56e784d 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/cache/JDKCache.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/cache/JDKCache.java @@ -41,6 +41,14 @@ public File root() { return this.root; } + public File tryGet(int version) { + try { + return get(version); + } catch (Exception e) { + throw new IllegalStateException("Failed to find JDK for version " + version, e); + } + } + // TODO: [MCMavenizer][JDKCache] Make this thread safe. If this method is accessed concurrently, the same JDK could be downloaded more than once. /** * Gets the JDK for the given version. diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/cache/MavenCache.java b/src/main/java/net/minecraftforge/mcmaven/impl/cache/MavenCache.java index 2021731..252b8e7 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/cache/MavenCache.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/cache/MavenCache.java @@ -23,6 +23,8 @@ import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; import java.util.ArrayList; import java.util.List; import java.util.Map; @@ -41,6 +43,7 @@ public sealed class MavenCache permits MinecraftMavenCache { private final File cache; private final String repo; private final List foreignRepositories; + private final boolean isFileRepo; /** * Initializes a new maven cache with the given name, repository, and cache directory. @@ -72,6 +75,7 @@ public MavenCache(String name, String repo, File root, Map forei this.foreignRepositories.add(new MavenCache(n, r, root)); } this.knownHashes = knownHashes; + this.isFileRepo = this.repo.startsWith("file:"); } public final File getFolder() { @@ -176,6 +180,19 @@ public final File downloadVersionMeta(Artifact artifact) { protected File download(boolean changing, String path) throws IOException { var target = new File(cache, path); + // if we're a local file, let short circuit and just return that file + if (this.isFileRepo) { + try { + var uri = new URI(this.repo + path); + var file = new File(uri.getPath()); + if (file.exists()) + return file; + } catch (URISyntaxException e) { + throw new IOException(e); + } + } + + if (target.exists()) { boolean invalidHash = false; diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/MCPMappings.java b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/MCPMappings.java index 0cc698a..1471524 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/MCPMappings.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/MCPMappings.java @@ -4,40 +4,127 @@ */ package net.minecraftforge.mcmaven.impl.mappings; +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + import org.jetbrains.annotations.Nullable; +import net.minecraftforge.mcmaven.impl.cache.MavenCache; +import net.minecraftforge.mcmaven.impl.repo.forge.FG2Userdev; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; import net.minecraftforge.mcmaven.impl.util.Artifact; +import net.minecraftforge.mcmaven.impl.util.Constants; import net.minecraftforge.mcmaven.impl.util.Task; +import net.minecraftforge.mcmaven.impl.util.Util; public class MCPMappings extends Mappings { - public MCPMappings(String channel, @Nullable String version) { - super(channel, version); - if (version == null) - throw new IllegalArgumentException("MCP Mappings can not have a null version"); - } - - @Override - public Task getCsvZip(MCPSide side) { - var key = new Key(Tasks.CSVs, side); - var ret = tasks.get(key); - if (ret != null) - return ret; + private final Map resolved = new IdentityHashMap<>(); + + public MCPMappings(String channel, @Nullable String version) { + super(channel, version); + if (version == null) + throw new IllegalArgumentException("MCP Mappings can not have a null version"); + } + + @Override + public Mappings withMCVersion(String version) { + return this; + } + + @Override + public boolean isPrimary() { + return false; + } + + @Override + public ResolvedMappings withContext(MCPSide side) { + return this.resolved.computeIfAbsent(side, _ -> withContextImpl(side)); + } + + private ResolvedMappings withContextImpl(MCPSide side) { + var csv = downloadCsv(side.getMCP().getCache().maven()); + var root = new File(side.getMCP().getBuildFolder(), "data/mapings"); + var artifact = this.getArtifact(side); + var mappings = side.getTasks().getMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, null); + } + @Override + public ResolvedMappings withContext(FG2Userdev fg2) { + return this.resolved.computeIfAbsent(fg2, _ -> withContextImpl(fg2)); + } + + private ResolvedMappings withContextImpl(FG2Userdev fg2) { + var csv = downloadCsv(fg2.getCache().maven()); + var root = new File(fg2.getBuildFolder(), "data/mapings"); + var artifact = this.getArtifact(fg2); + var mappings = fg2.getMCP().getMappings(); + var extra = fg2.getExtraMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, extra); + } + + @Override + public ResolvedMappings withContext(MCPLegacy legacy) { + return this.resolved.computeIfAbsent(legacy, _ -> withContextImpl(legacy)); + } + + private ResolvedMappings withContextImpl(MCPLegacy legacy) { + var csv = downloadCsv(legacy.getCache().maven()); + var root = new File(legacy.getBuildFolder(), "data/mapings"); + var artifact = this.getArtifact(legacy); + var mappings = legacy.getMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, null); + } + + private Task downloadCsv(MavenCache maven) { // This is simple because it's the old MCP Bot based zip files // So it's just a matter of downloading from Forge's maven. var artifact = Artifact.from("de.oceanlabs.mcp", "mcp_" + this.channel(), this.version(), null, "zip"); - var maven = side.getMCP().getCache().maven(); - - ret = Task.named("srg2names[" + this + ']', - () -> maven.download(artifact) - ); - tasks.put(key, ret); - return ret; + return Task.named("srg2names[" + this + ']', () -> downloadCsv(maven, artifact)); } - @Override - public Mappings withMCVersion(String version) { - return this; + private File downloadCsv(MavenCache maven, Artifact artifact) { + try { + return maven.download(artifact); + } catch (Exception e) { + // They didnt have a suffix and we couldn't find the mapping + if (this.version().indexOf('-') == -1 && e instanceof FileNotFoundException) { + List versions = null; + try { + versions = maven.getVersions(artifact); + } catch (Exception _) { + // Whelp we couldn't find the version list to provide a useful hint, so just fall back to the normal error + return Util.sneak(e); + } + + var candidates = new ArrayList(); + var prefix = this.version() + '-'; + for (var version : versions) { + if (version.startsWith(prefix)) + candidates.add(version); + } + var buf = new StringBuilder(); + buf.append("Could not find mapping file for ").append(this.toString()); + if (candidates.size() == 1) + buf.append(" Did you mean ").append(candidates.getFirst()).append('?'); + else if (!candidates.isEmpty()) { + buf.append(" Did you mean one of these:"); + for (var candidate : candidates) + buf.append("\n\t").append(candidate); + } + buf.append("\n\tYou can try looking at ") + .append(Constants.FORGE_MAVEN).append(artifact.withVersion(null).getFolder()).append("/maven-metadata.xml") + .append(" for full list of possible versions"); + + throw new IllegalArgumentException(buf.toString(), e); + } + return Util.sneak(e); + } } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/Mappings.java b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/Mappings.java index 80db428..81320ba 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/Mappings.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/Mappings.java @@ -5,56 +5,18 @@ package net.minecraftforge.mcmaven.impl.mappings; import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.nio.charset.StandardCharsets; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; -import java.util.zip.ZipOutputStream; -import net.minecraftforge.mcmaven.impl.Mavenizer; -import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPConfigRepo; +import net.minecraftforge.mcmaven.impl.repo.forge.FG2Userdev; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; -import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.Constants; -import net.minecraftforge.mcmaven.impl.util.ProcessUtils; -import net.minecraftforge.mcmaven.impl.util.Task; -import net.minecraftforge.mcmaven.impl.util.Util; -import net.minecraftforge.srgutils.IMappingFile; -import net.minecraftforge.srgutils.IRenamer; -import net.minecraftforge.srgutils.IMappingFile.IField; -import net.minecraftforge.srgutils.IMappingFile.IMethod; -import net.minecraftforge.srgutils.IMappingFile.IParameter; import org.jetbrains.annotations.Nullable; -import de.siegmar.fastcsv.reader.CsvReader; - -public class Mappings { +public abstract class Mappings { public static final String CHANNEL_ATTR = "net.minecraftforge.mappings.channel"; public static final String VERSION_ATTR = "net.minecraftforge.mappings.version"; - protected enum Tasks { - CSVs("srg2names"), - MappedToSrg("mapped2srg"), - MappedToObf("mapped2obf"); - - private final String name; - - private Tasks(String name) { - this.name = name; - } - }; - - protected record Key(Tasks type, MCPSide side) {} - - protected final Map tasks = new HashMap<>(); private final String channel; private final @Nullable String version; @@ -67,42 +29,18 @@ public static Mappings of(String mappingsNotation) { case "parchment": return new ParchmentMappings(version); case "official": - return new Mappings(channel, version); + return new OfficialMappings(channel, version); case "snapshot": case "snapshot_nodoc": case "stable": case "stable_nodoc": return new MCPMappings(channel, version); + case "srg": + return new SRGMappings("srg", version); } throw new IllegalArgumentException("Unsupported Mappings: " + mappingsNotation); } - public record Data(Map names, Map docs) { } - public static Data load(File data) throws IOException { - var names = new HashMap(); - var docs = new HashMap(); - try (var zip = new ZipFile(data)) { - var entries = zip.stream().filter(e -> e.getName().endsWith(".csv")).toList(); - for (var entry : entries) { - try (var reader = CsvReader.builder().ofNamedCsvRecord(new InputStreamReader(zip.getInputStream(entry)))) { - for (var row : reader) { - var header = row.getHeader(); - var obf = header.contains("searge") ? "searge" : "param"; - var searge = row.getField(obf); - names.put(searge, row.getField("name")); - if (header.contains("desc")) { - String desc = row.getField("desc"); - if (!desc.isBlank()) - docs.put(searge, desc); - } - } - } - } - } - - return new Data(names, docs); - } - public Mappings(String channel, @Nullable String version) { this.channel = channel; this.version = version; @@ -125,204 +63,54 @@ public File getFolder(File root) { return new File(root, channel() + '/' + version()); } - public boolean isPrimary() { - // This is the 'primary' mapping and thus what we publish as the root artifacts. - // Not as gradle module metadata only variants. - // Basically the thing that looks like a normal maven artifact - return true; - } + // This is the 'primary' mapping and thus what we publish as the root artifacts. + // Not as gradle module metadata only variants. + // Basically the thing that looks like a normal maven artifact + // This is only true for 'official' mappings + public abstract boolean isPrimary(); + + public abstract Mappings withMCVersion(String version); + + public abstract ResolvedMappings withContext(MCPSide side); + public abstract ResolvedMappings withContext(FG2Userdev fg2); + public abstract ResolvedMappings withContext(MCPLegacy legacy); - public Mappings withMCVersion(String version) { - if (Objects.equals(this.version, version)) - return this; - return new Mappings(channel(), version); - } - public Artifact getArtifact(MCPSide side) { + protected Artifact getArtifact(MCPSide side) { //net.minecraft:mappings_{CHANNEL}:{MCP_VERSION}[-{VERSION}]@zip + var name = "mappings_" + this.channel; var mcpVersion = side.getMCP().getName().getVersion(); var mcVersion = side.getMCP().getConfig().version; var artifactVersion = mcpVersion; if (this.version() != null && !mcVersion.equals(this.version())) artifactVersion += '-' + this.version(); - return Artifact.from(Constants.MC_GROUP, "mappings_" + this.channel, artifactVersion) - .withExtension("zip"); - } - - public Task getCsvZip(MCPSide side) { - var key = new Key(Tasks.CSVs, side); - var ret = tasks.get(key); - if (ret != null) - return ret; - - var mc = side.getMCP().getMinecraftTasks(); - var srg = side.getTasks().getMappings(); - - if (MCPConfigRepo.isObfuscated(mc.getVersion())) { - var client = mc.versionFile(MinecraftTasks.MCFile.CLIENT_MAPPINGS); - var server = mc.versionFile(MinecraftTasks.MCFile.SERVER_MAPPINGS); - ret = Task.named("srg2names[" + this + ']', - Task.deps(srg, client, server), - () -> getMappings(side, srg, client, server) - ); - } else { - // Create an empty srg->mapped zip file when requested. This is needed by old MCPConfig setups until we bump it to a version that doesn't require the concept of mappings at all - ret = Task.named("srg2names[" + this + "][Empty]", () -> makeEmptyCsv(side)); - } - - tasks.put(key, ret); - return ret; - } - - public Task getMapped2Srg(MCPSide side) { - return getTsrg(side, Tasks.MappedToSrg); - } - - public Task getMapped2Obf(MCPSide side) { - return getTsrg(side, Tasks.MappedToObf); - } - - private Task getTsrg(MCPSide side, Tasks type) { - var key = new Key(type, side); - var ret = tasks.get(key); - if (ret != null) - return ret; - - var srg = side.getTasks().getMappings(); - var csv = getCsvZip(side); - ret = Task.named(type.name + '[' + this + ']', - Task.deps(srg, csv), - () -> makeTsrg(side, srg, csv, type == Tasks.MappedToObf) - ); - tasks.put(key, ret); - return ret; + return Artifact.from(Constants.MC_GROUP, name, artifactVersion).withExtension("zip"); } - private File makeEmptyCsv(MCPSide side) { - var root = getFolder(new File(side.getMCP().getBuildFolder(), "data/mapings")); - var output = new File(root, "official.zip"); + protected Artifact getArtifact(FG2Userdev fg2) { + //net.minecraft:mappings_{CHANNEL}:{MC_VERSION}-{legacy|FORGE_VERSION}[-{VERSION}]@zip + var name = "mappings_" + this.channel; + // either the raw mc version, or MC_VERSION-PYTHON_VERSION; + var version = fg2.getMCP().getName().getVersion(); + if (fg2.getExtraMappings() == null) + version += "-legacy"; + else + version += '-' + fg2.getForgeVersion(); + if (this.version() != null && !fg2.getMinecraftVersion().equals(this.version())) + version += '-' + this.version(); - var cache = Util.cache(output); - - if (Mavenizer.checkCache(output, cache)) - return output; - - if (!output.getParentFile().exists()) - output.getParentFile().mkdirs(); - - byte[] header = String.join(",", "searge", "name", "side", "desc").getBytes(StandardCharsets.UTF_8); - - try (var fos = new FileOutputStream(output); - var out = new ZipOutputStream(fos)) { - out.putNextEntry(new ZipEntry("fields.csv")); - out.write(header); - out.closeEntry(); - out.putNextEntry(new ZipEntry("methods.csv")); - out.write(header); - out.closeEntry(); - } catch (IOException e) { - Util.sneak(e); - } - - cache.save(); - return output; + return Artifact.from(Constants.MC_GROUP, name, version).withExtension("zip"); } - private File getMappings(MCPSide side, Task srgMappings, Task clientTask, Task serverTask) { - var tool = side.getMCP().getCache().maven().download(Constants.INSTALLER_TOOLS); - - var root = getFolder(new File(side.getMCP().getBuildFolder(), "data/mapings")); - var output = new File(root, "official.zip"); - var log = new File(root, "official.log"); - - var mappings = srgMappings.execute(); - var client = clientTask.execute(); - var server = serverTask.execute(); - - var cache = Util.cache(output); - cache.add("tool", tool); - cache.add("mappings", mappings); - cache.add("client", client); - cache.add("server", server); - - if (Mavenizer.checkCache(output, cache)) - return output; - - var args = List.of( - "--task", - "MAPPINGS_CSV", - "--srg", - mappings.getAbsolutePath(), - "--client", - client.getAbsolutePath(), - "--server", - server.getAbsolutePath(), - "--output", - output.getAbsolutePath() - ); - - File jdk; - try { - jdk = side.getMCP().getCache().jdks().get(Constants.INSTALLER_TOOLS_JAVA_VERSION); - } catch (Exception e) { - throw new IllegalStateException("Failed to find JDK for version " + Constants.INSTALLER_TOOLS_JAVA_VERSION, e); - } - - var ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, Collections.emptyList(), args); - if (ret.exitCode != 0) - throw new IllegalStateException("Failed to run MCP Step (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); - - cache.save(); - return output; - } - - private File makeTsrg(MCPSide side, Task srgTask, Task csvTask, boolean toObf) { - var root = getFolder(new File(side.getMCP().getBuildFolder(), "data/mapings")); - var output = new File(root, channel() + '-' + version + '-' + (toObf ? "srg" : "obf") + ".tsrg.gz"); - - var srg = srgTask.execute(); - var csv = csvTask.execute(); - - var cache = Util.cache(output) - .add("srg", srg) - .add("csv", csv); - - if (Mavenizer.checkCache(output, cache)) - return output; - - try { - var names = Mappings.load(csv).names(); - - var map = IMappingFile.load(srg); // obf2srg - if (!toObf) - map = map.reverse().chain(map); // srg2obf + obf2srg = srg2srg - - // Now we rename target2mapped - map = map.rename(new IRenamer() { - @Override - public String rename(IField value) { - return names.getOrDefault(value.getMapped(), value.getMapped()); - } - - @Override - public String rename(IMethod value) { - return names.getOrDefault(value.getMapped(), value.getMapped()); - } - - @Override - public String rename(IParameter value) { - return names.getOrDefault(value.getMapped(), value.getMapped()); - } - }); - - // Write in reversed == mapped2target - map.write(output.getAbsoluteFile().toPath(), IMappingFile.Format.TSRG2, true); - } catch (IOException e) { - Util.sneak(e); - } + protected Artifact getArtifact(MCPLegacy legacy) { + //net.minecraft:mappings_{CHANNEL}:{MC_VERSION}-legacy[-{VERSION}]@zip + var name = "mappings_" + this.channel; + // either the raw mc version, or MC_VERSION-PYTHON_VERSION; + var version = legacy.getName().getVersion() + "-legacy"; + if (this.version() != null && !legacy.getMinecraftTasks().getVersion().equals(this.version())) + version += '-' + this.version(); - cache.save(); - return output; + return Artifact.from(Constants.MC_GROUP, name, version).withExtension("zip"); } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/OfficialMappings.java b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/OfficialMappings.java new file mode 100644 index 0000000..5a398dd --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/OfficialMappings.java @@ -0,0 +1,167 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.mappings; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.jetbrains.annotations.Nullable; + +import net.minecraftforge.mcmaven.impl.Mavenizer; +import net.minecraftforge.mcmaven.impl.repo.forge.FG2Userdev; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPConfigRepo; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; +import net.minecraftforge.mcmaven.impl.util.Constants; +import net.minecraftforge.mcmaven.impl.util.ProcessUtils; +import net.minecraftforge.mcmaven.impl.util.Task; +import net.minecraftforge.mcmaven.impl.util.Util; + +class OfficialMappings extends Mappings { + private final Map resolved = new IdentityHashMap<>(); + + public OfficialMappings(String channel, @Nullable String version) { + super(channel, version); + } + + @Override + public boolean isPrimary() { + return true; + } + + @Override + public Mappings withMCVersion(String version) { + if (Objects.equals(version(), version)) + return this; + return new OfficialMappings(channel(), version); + } + + @Override + public ResolvedMappings withContext(MCPSide side) { + return this.resolved.computeIfAbsent(side, _ -> withContextImpl(side)); + } + + private ResolvedMappings withContextImpl(MCPSide side) { + var csv = makeCsv(side); + var root = new File(side.getMCP().getBuildFolder(), "data/mapings"); + var artifact = this.getArtifact(side); + var mappings = side.getTasks().getMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, null); + } + + @Override + public ResolvedMappings withContext(FG2Userdev fg2) { + throw new IllegalStateException("Official mappings does not support Legacy: " + fg2.getName()); + } + + @Override + public ResolvedMappings withContext(MCPLegacy legacy) { + throw new IllegalStateException("Official mappings does not support Legacy: " + legacy.getName()); + } + + private Task makeCsv(MCPSide side) { + var mc = side.getMCP().getMinecraftTasks(); + var srg = side.getTasks().getMappings(); + + // Create an empty srg->mapped zip file when requested. This is needed by old MCPConfig setups until we bump it to a version that doesn't require the concept of mappings at all + if (!MCPConfigRepo.isObfuscated(mc.getVersion())) + return Task.named("srg2names[" + this + "][Empty]", () -> makeEmptyCsv(side)); + + var client = mc.versionFile(MinecraftTasks.MCFile.CLIENT_MAPPINGS); + var server = mc.versionFile(MinecraftTasks.MCFile.SERVER_MAPPINGS); + return Task.named("srg2names[" + this + ']', + Task.deps(srg, client, server), + () -> getMappings(side, srg, client, server) + ); + } + + private File makeEmptyCsv(MCPSide side) { + var root = getFolder(new File(side.getMCP().getBuildFolder(), "data/mapings")); + var output = new File(root, "official.zip"); + + var cache = Util.cache(output); + + if (Mavenizer.checkCache(output, cache)) + return output; + + if (!output.getParentFile().exists()) + output.getParentFile().mkdirs(); + + byte[] header = String.join(",", "searge", "name", "side", "desc").getBytes(StandardCharsets.UTF_8); + + try (var fos = new FileOutputStream(output); + var out = new ZipOutputStream(fos)) { + out.putNextEntry(new ZipEntry("fields.csv")); + out.write(header); + out.closeEntry(); + out.putNextEntry(new ZipEntry("methods.csv")); + out.write(header); + out.closeEntry(); + } catch (IOException e) { + Util.sneak(e); + } + + cache.save(); + return output; + } + + private File getMappings(MCPSide side, Task srgMappings, Task clientTask, Task serverTask) { + var tool = side.getMCP().getCache().maven().download(Constants.INSTALLER_TOOLS); + + var root = getFolder(new File(side.getMCP().getBuildFolder(), "data/mapings")); + var output = new File(root, "official.zip"); + var log = new File(root, "official.log"); + + var mappings = srgMappings.execute(); + var client = clientTask.execute(); + var server = serverTask.execute(); + + var cache = Util.cache(output); + cache.add("tool", tool); + cache.add("mappings", mappings); + cache.add("client", client); + cache.add("server", server); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var args = List.of( + "--task", + "MAPPINGS_CSV", + "--srg", + mappings.getAbsolutePath(), + "--client", + client.getAbsolutePath(), + "--server", + server.getAbsolutePath(), + "--output", + output.getAbsolutePath() + ); + + File jdk; + try { + jdk = side.getMCP().getCache().jdks().get(Constants.INSTALLER_TOOLS_JAVA_VERSION); + } catch (Exception e) { + throw new IllegalStateException("Failed to find JDK for version " + Constants.INSTALLER_TOOLS_JAVA_VERSION, e); + } + + var ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, Collections.emptyList(), args); + if (ret.exitCode != 0) + throw new IllegalStateException("Failed to run MCP Step (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + cache.save(); + return output; + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ParchmentMappings.java b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ParchmentMappings.java index dc8941b..ff7ebc0 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ParchmentMappings.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ParchmentMappings.java @@ -10,6 +10,7 @@ import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; import java.util.ArrayList; +import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Objects; @@ -24,16 +25,18 @@ import net.minecraftforge.mcmaven.impl.Mavenizer; import net.minecraftforge.mcmaven.impl.cache.Cache; import net.minecraftforge.mcmaven.impl.cache.MavenCache; +import net.minecraftforge.mcmaven.impl.repo.forge.FG2Userdev; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCP; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; -import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.Task; import net.minecraftforge.mcmaven.impl.util.Util; import net.minecraftforge.srgutils.IMappingFile; import net.minecraftforge.util.file.FileUtils; public class ParchmentMappings extends Mappings { + private final Map resolved = new IdentityHashMap<>(); private final ParchmentVersion parsedVersion; private Task downloadTask; @@ -63,30 +66,41 @@ public Mappings withMCVersion(String mcVer) { } @Override - public Artifact getArtifact(MCPSide side) { - return this.parsedVersion.getMappingArtifact(side.getMCP().getName().getVersion()); + public ResolvedMappings withContext(MCPSide side) { + return this.resolved.computeIfAbsent(side, _ -> withContextImpl(side)); + } + + private ResolvedMappings withContextImpl(MCPSide side) { + var csv = makeCsv(side); + var root = new File(side.getMCP().getBuildFolder(), "data/mapings"); + var artifact = this.getArtifact(side); + var mappings = side.getTasks().getMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, null); } @Override - public Task getCsvZip(MCPSide side) { - var key = new Key(Tasks.CSVs, side); - var ret = tasks.get(key); - if (ret != null) - return ret; + public ResolvedMappings withContext(FG2Userdev fg2) { + throw new IllegalStateException("Parchment mappings does not support Legacy: " + fg2.getName()); + } + @Override + public ResolvedMappings withContext(MCPLegacy legacy) { + throw new IllegalStateException("Parchment mappings does not support Legacy: " + legacy.getName()); + } + + private Task makeCsv(MCPSide side) { var mc = side.getMCP().getMinecraftTasks(); var srg = side.getTasks().getMappings(); var client = mc.versionFile(MinecraftTasks.MCFile.CLIENT_MAPPINGS); var server = mc.versionFile(MinecraftTasks.MCFile.SERVER_MAPPINGS); var data = downloadTask(side.getMCP()); + var mcpRoot = new File(side.getMCP().getBuildFolder(), "data/mapings"); - ret = Task.named("srg2names[" + this + ']', + return Task.named("srg2names[" + this + ']', Task.deps(Set.of(srg, client, server, data).stream().filter(Objects::nonNull).toList()), - () -> getMappings(side.getMCP(), srg, client, server, data) + () -> getMappings(mcpRoot, srg, client, server, data) ); - tasks.put(key, ret); - return ret; } private Task downloadTask(MCP mcp) { @@ -104,13 +118,13 @@ private File download(Cache cache) { return maven.download(artifact); } - private File getMappings(MCP mcp, Task srgTask, Task clientTask, Task serverTask, Task dataTask) throws IOException { + private File getMappings(File mcpRoot, Task srgTask, Task clientTask, Task serverTask, Task dataTask) throws IOException { var srg = srgTask.execute(); var client = clientTask.execute(); var server = serverTask.execute(); var data = dataTask.execute(); - var root = getFolder(new File(mcp.getBuildFolder(), "data/mapings")); + var root = getFolder(mcpRoot); var output = new File(root, "parchment-" + version() + ".zip"); var cache = Util.cache(output) .add("srg", srg) diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ResolvedMappings.java b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ResolvedMappings.java new file mode 100644 index 0000000..a5218c9 --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/ResolvedMappings.java @@ -0,0 +1,172 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.mappings; + +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +import org.jetbrains.annotations.Nullable; + +import net.minecraftforge.mcmaven.impl.Mavenizer; +import net.minecraftforge.mcmaven.impl.tasks.MCPNames; +import net.minecraftforge.mcmaven.impl.util.Artifact; +import net.minecraftforge.mcmaven.impl.util.Task; +import net.minecraftforge.mcmaven.impl.util.Util; +import net.minecraftforge.srgutils.IMappingFile; +import net.minecraftforge.srgutils.IRenamer; +import net.minecraftforge.srgutils.IMappingFile.IField; +import net.minecraftforge.srgutils.IMappingFile.IMethod; +import net.minecraftforge.srgutils.IMappingFile.IParameter; + +public class ResolvedMappings { + public static final String CHANNEL_ATTR = "net.minecraftforge.mappings.channel"; + public static final String VERSION_ATTR = "net.minecraftforge.mappings.version"; + + protected enum Tasks { + CSVs("srg2names"), + MappedToSrg("mapped2srg"), + MappedToObf("mapped2obf"); + + private final String name; + + private Tasks(String name) { + this.name = name; + } + }; + + protected final Map tasks = new HashMap<>(); + private final String channel; + private final String version; + private final Artifact artifact; + private final Task baseMappings; + private final Task csvZip; + private final Task mappedToSrg; + private final Task mappedToObf; + + public ResolvedMappings( + String channel, String version, + Artifact artifact, File root, + Task baseMappings, Task csvZip, + @Nullable byte[] extra + ) { + this.channel = channel; + this.version = version; + this.artifact = artifact; + this.baseMappings = baseMappings; + this.csvZip = csvZip; + this.mappedToObf = getTsrg(root, Tasks.MappedToObf, null); + this.mappedToSrg = getTsrg(root, Tasks.MappedToSrg, extra); + } + + public String channel() { + return this.channel; + } + + public String version() { + return this.version; + } + + @Override + public String toString() { + return channel() + '-' + version(); + } + + public File getFolder(File root) { + return new File(root, channel() + '/' + version()); + } + + public Artifact getArtifact() { + return this.artifact; + } + + public Task getCsvZip() { + return this.csvZip; + } + + public Task getMapped2Srg() { + return this.mappedToSrg; + } + + public Task getMapped2Obf() { + return this.mappedToObf; + } + + + + + + private Task getTsrg(File root, Tasks type, @Nullable byte[] extra) { + var csv = getCsvZip(); + + return Task.named(type.name + '[' + this + ']', + Task.deps(this.baseMappings, csv), + () -> makeTsrg(root, this.baseMappings, csv, type == Tasks.MappedToObf, extra) + ); + } + + private File makeTsrg(File mcpRoot, Task srgTask, Task csvTask, boolean toObf, @Nullable byte[] extra) { + var root = getFolder(mcpRoot); + var output = new File(root, channel() + '-' + version + '-' + (toObf ? "srg" : "obf") + ".tsrg.gz"); + + var srg = srgTask.execute(); + var csv = csvTask.execute(); + + var cache = Util.cache(output) + .add("srg", srg) + .add("csv", csv); + + if (extra != null) + cache.add("extra", extra); + + if (Mavenizer.checkCache(output, cache)) + return output; + + try { + var names = MCPNames.load(csv).names(); + + var map = IMappingFile.load(srg); // obf2srg + if (!toObf) + map = map.reverse().chain(map); // srg2obf + obf2srg = srg2srg + + // Now we rename target2mapped + map = map.rename(new IRenamer() { + @Override + public String rename(IField value) { + return names.getOrDefault(value.getMapped(), value.getMapped()); + } + + @Override + public String rename(IMethod value) { + return names.getOrDefault(value.getMapped(), value.getMapped()); + } + + @Override + public String rename(IParameter value) { + return names.getOrDefault(value.getMapped(), value.getMapped()); + } + }); + + // target2mapped -> mapped2target + map = map.reverse(); + + // Extra is for FG2 environements for extra 'reobf' mappings. + // So named2srg + if (extra != null) { + var extraMap = IMappingFile.load(new ByteArrayInputStream(extra)); + map = map.merge(extraMap); + } + + map.write(output.getAbsoluteFile().toPath(), IMappingFile.Format.TSRG2, false); + } catch (IOException e) { + Util.sneak(e); + } + + cache.save(); + return output; + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/mappings/SRGMappings.java b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/SRGMappings.java new file mode 100644 index 0000000..b97d4ed --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/mappings/SRGMappings.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.mappings; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.IdentityHashMap; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import org.jetbrains.annotations.Nullable; + +import net.minecraftforge.mcmaven.impl.Mavenizer; +import net.minecraftforge.mcmaven.impl.repo.forge.FG2Userdev; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; +import net.minecraftforge.mcmaven.impl.util.Task; +import net.minecraftforge.mcmaven.impl.util.Util; + +// The raw mappings, typically named SRG for old versions, but unobfed versions its equivelent to official +public class SRGMappings extends Mappings { + private final Map resolved = new IdentityHashMap<>(); + + public SRGMappings(String channel, @Nullable String version) { + super(channel, version); + } + + @Override + public Mappings withMCVersion(String version) { + if (this.version() != null && this.version().equals(version)) + return this; + return new SRGMappings(channel(), version); + } + + @Override + public boolean isPrimary() { + return false; + } + + @Override + public ResolvedMappings withContext(MCPSide side) { + return this.resolved.computeIfAbsent(side, _ -> withContextImpl(side)); + } + + private ResolvedMappings withContextImpl(MCPSide side) { + var root = new File(side.getMCP().getBuildFolder(), "data/mapings"); + var csv = csvTask(root); + var artifact = this.getArtifact(side); + var mappings = side.getTasks().getMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, null); + } + + @Override + public ResolvedMappings withContext(FG2Userdev fg2) { + return this.resolved.computeIfAbsent(fg2, _ -> withContextImpl(fg2)); + } + + public ResolvedMappings withContextImpl(FG2Userdev fg2) { + var root = new File(fg2.getBuildFolder(), "data/mapings"); + var csv = csvTask(root); + var artifact = this.getArtifact(fg2); + var mappings = fg2.getMCP().getMappings(); + var extra = fg2.getExtraMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, extra); + } + + @Override + public ResolvedMappings withContext(MCPLegacy legacy) { + return this.resolved.computeIfAbsent(legacy, _ -> withContextImpl(legacy)); + } + + public ResolvedMappings withContextImpl(MCPLegacy legacy) { + var root = new File(legacy.getBuildFolder(), "data/mapings"); + var csv = csvTask(root); + var artifact = this.getArtifact(legacy); + var mappings = legacy.getMappings(); + return new ResolvedMappings(channel(), version(), artifact, root, mappings, csv, null); + } + + private Task csvTask(File root) { + return Task.named("srg2names[" + this + "][Empty]", () -> makeEmptyCsv(root)); + } + + private File makeEmptyCsv(File root) { + var output = new File(getFolder(root), "srg.zip"); + + var cache = Util.cache(output); + + if (Mavenizer.checkCache(output, cache)) + return output; + + if (!output.getParentFile().exists()) + output.getParentFile().mkdirs(); + + byte[] header = String.join(",", "searge", "name", "side", "desc").getBytes(StandardCharsets.UTF_8); + + try (var fos = new FileOutputStream(output); + var out = new ZipOutputStream(fos)) { + out.putNextEntry(new ZipEntry("fields.csv")); + out.write(header); + out.closeEntry(); + out.putNextEntry(new ZipEntry("methods.csv")); + out.write(header); + out.closeEntry(); + } catch (IOException e) { + Util.sneak(e); + } + + cache.save(); + return output; + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/Repo.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/Repo.java index 3d60429..9bef670 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/Repo.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/Repo.java @@ -8,6 +8,7 @@ import net.minecraftforge.mcmaven.impl.cache.Cache; import net.minecraftforge.mcmaven.impl.data.GradleModule; import net.minecraftforge.mcmaven.impl.mappings.Mappings; +import net.minecraftforge.mcmaven.impl.mappings.ResolvedMappings; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPConfigRepo; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; import net.minecraftforge.mcmaven.impl.util.Artifact; @@ -69,13 +70,13 @@ protected static PendingArtifact pending(String message, Task task, Artifact art protected Supplier sourceVariant(Mappings mappings) { return () -> new GradleModule.Variant[] { GradleModule.Variant.of("sources") - .attribute("org.gradle.status", "release") - .attribute("org.gradle.usage", "java-runtime") - .attribute("org.gradle.category", "documentation") - .attribute("org.gradle.dependency.bundling", "external") - .attribute("org.gradle.docstype", "sources") - .attribute("org.gradle.libraryelements", "jar") - .attribute(Mappings.CHANNEL_ATTR, mappings.channel()) + .attribute("org.gradle.status", "release") + .attribute("org.gradle.usage", "java-runtime") + .attribute("org.gradle.category", "documentation") + .attribute("org.gradle.dependency.bundling", "external") + .attribute("org.gradle.docstype", "sources") + .attribute("org.gradle.libraryelements", "jar") + .attribute(Mappings.CHANNEL_ATTR, mappings.channel()) .attribute(Mappings.VERSION_ATTR, mappings.version()) }; } @@ -112,48 +113,32 @@ protected static Task variantTask(Task parent, Supplier }); } - protected Supplier simpleVariant(String name, Mappings mappings) { + protected Supplier simpleVariant(String name, String mappingChannel, @Nullable String mappingVersion) { return () -> new GradleModule.Variant[] { GradleModule.Variant .of(name) .attribute("org.gradle.status", "release") .attribute("org.gradle.category", "library") .attribute("org.gradle.libraryelements", "jar") - .attribute(Mappings.CHANNEL_ATTR, mappings.channel()) - .attribute(Mappings.VERSION_ATTR, mappings.version()) + .attribute(Mappings.CHANNEL_ATTR, mappingChannel) + .attribute(Mappings.VERSION_ATTR, mappingVersion) }; } protected GradleModule.Variant[] classVariants(Mappings mappings, MCPSide side) { - return classVariants(mappings, side, List.of(), List.of(), List.of()); - } - - // Classes needs a variant for each OS type so that we can have different natives - protected GradleModule.Variant[] classVariants( - Mappings mappings, - MCPSide side, - Collection extraDeps, - Collection extraCompileDeps, - Collection extraRuntimeDeps - ) { var deps = new ArrayList(); deps.addAll(side.getMCLibraries()); deps.addAll(side.getMCPConfigLibraries()); - deps.addAll(extraDeps); - - // ExtraRuntimeDeps is never used.... Not sure where it was meant to go + var jsonTask = side.getMCP().getMinecraftTasks().versionJson; + var json = JsonData.minecraftVersion(jsonTask.execute()); + var java = json.javaVersion != null ? json.javaVersion.majorVersion : null; - return classVariants( - mappings, - side.getMCP().getMinecraftTasks().versionJson, - deps, - extraCompileDeps - ); + return classVariants(mappings, java, deps, List.of()); } protected GradleModule.Variant[] classVariants( Mappings mappings, - Task jsonTask, + @Nullable Integer javaVersion, Collection deps, Collection extraCompileDeps ) { @@ -180,9 +165,6 @@ protected GradleModule.Variant[] classVariants( } } - var json = JsonData.minecraftVersion(jsonTask.execute()); - var java = json.javaVersion != null ? json.javaVersion.majorVersion : null; - Consumer common = v -> { v.attribute("org.gradle.status", "release") .attribute("org.gradle.usage", "java-runtime") @@ -194,8 +176,8 @@ protected GradleModule.Variant[] classVariants( .attribute(Mappings.VERSION_ATTR, mappings.version()) ; - if (java != null) - v.attribute("org.gradle.jvm.version", java); + if (javaVersion != null) + v.attribute("org.gradle.jvm.version", javaVersion); v.deps(all); }; @@ -286,15 +268,15 @@ public PendingArtifact withTask(Task task) { * channel-verson-map2obf.tsrg.gz: gzip compressed tsrg file for mapped names to obf (notch) names * channel-verson-map2srg.tsrg.gz: gzip compressed tsrg file for mapped names to srg (intermediate) names */ - protected List mappingArtifacts(File cache, Mappings mappings, MCPSide side, Map> outputJson) { + protected List mappingArtifacts(File cache, ResolvedMappings mappings, String mcVersion, Map> outputJson) { if (outputJson != null) { outputJson.put("mappings.channel", mappings::channel); outputJson.put("mappings.version", mappings::version); } - var coords = mappings.getArtifact(side); - var csvs = pending("Mappings Zip", mappings.getCsvZip(side), coords, false); - var pom = pending("Mappings POM", simplePom(cache, coords), coords.withExtension("pom"), false); + var coords = mappings.getArtifact(); + var csvs = pending("Mappings Zip", mappings.getCsvZip(), coords, false); + var pom = pending("Mappings POM", simplePom(mappings.getFolder(cache), coords), coords.withExtension("pom"), false); if (outputJson != null) { outputJson.put("mappings.csv.artifact", csvs.artifact()::toString); @@ -302,11 +284,11 @@ protected List mappingArtifacts(File cache, Mappings mappings, } // Just create the zip and pom for unobfuscated versions. - if (!MCPConfigRepo.isObfuscated(side.getMCP().getMinecraftTasks().getVersion())) + if (!MCPConfigRepo.isObfuscated(mcVersion)) return List.of(csvs, pom); - var m2o = pending("Mappings map2obf", mappings.getMapped2Obf(side), coords.withClassifier("map2obf").withExtension("tsrg.gz"), false); - var m2s = pending("Mappings map2srg", mappings.getMapped2Srg(side), coords.withClassifier("map2srg").withExtension("tsrg.gz"), false); + var m2o = pending("Mappings map2obf", mappings.getMapped2Obf(), coords.withClassifier("map2obf").withExtension("tsrg.gz"), false); + var m2s = pending("Mappings map2srg", mappings.getMapped2Srg(), coords.withClassifier("map2srg").withExtension("tsrg.gz"), false); if (outputJson != null) { outputJson.put("mappings.obf.artifact", m2o.artifact()::toString); outputJson.put("mappings.obf.file", m2o.task().filePathSupplier()); diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FG2Userdev.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FG2Userdev.java new file mode 100644 index 0000000..4efe4be --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FG2Userdev.java @@ -0,0 +1,667 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.repo.forge; + +import static net.minecraftforge.mcmaven.impl.Mavenizer.LOGGER; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Predicate; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.jetbrains.annotations.Nullable; + +import net.minecraftforge.mcmaven.impl.Mavenizer; +import net.minecraftforge.mcmaven.impl.cache.Cache; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPLegacy; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks.ArtifactFile; +import net.minecraftforge.mcmaven.impl.util.Artifact; +import net.minecraftforge.mcmaven.impl.util.ComparableVersion; +import net.minecraftforge.mcmaven.impl.util.ContextualPatch; +import net.minecraftforge.mcmaven.impl.util.StupidHacks; +import net.minecraftforge.mcmaven.impl.util.Task; +import net.minecraftforge.mcmaven.impl.util.Util; +import net.minecraftforge.util.data.json.JsonData; +import net.minecraftforge.util.data.json.MinecraftVersion; +import net.minecraftforge.util.data.json.RunConfig; +import net.minecraftforge.util.file.FileUtils; +import net.minecraftforge.util.hash.HashFunction; + +public class FG2Userdev implements ForgeVersionCommon { + private final File build; + private final ForgeRepo forge; + private final Artifact name; + private final FGVersion fgVersion; + private final File data; + private final String dataHash; + private final MinecraftVersion config; + private final String mcVersion; + private final String forgeVersion; + private final @Nullable byte[] extraMappings; + private final MCPLegacy mcp; + private final MCPLegacy.Child mcpChild; + private final Task sourcesTask; + private final Map extracts = new HashMap<>(); + + FG2Userdev(File build, ForgeRepo forge, Artifact name, FGVersion fgVersion) { + this.build = build; + this.forge = forge; + this.name = name; + this.fgVersion = fgVersion; + + this.data = this.forge.getCache().maven().download(name); + if (!this.data.exists()) + throw new IllegalStateException("Failed to download " + name); + + this.dataHash = Util.sneak(() -> HashFunction.sha1().hash(this.data)); + + try (var zip = new ZipFile(data)) { + var entry = zip.getEntry("dev.json"); + if (entry == null) + throw except("Missing dev.json"); + + var cfg_data = zip.getInputStream(entry).readAllBytes(); + this.config = JsonData.minecraftVersion(cfg_data); + + entry = zip.getEntry("merged.srg"); + this.extraMappings = entry == null ? null : zip.getInputStream(entry).readAllBytes(); + } catch (IOException e) { + throw except("Error reading config", e); + } + + this.mcVersion = Util.forgeToMcVersion(this.name.getVersion()); + + // FG 1.x we need to merge the access transformers, FML and Forge patches are seperate, and source/resources are not in a zip + var fg1 = this.fgVersion.ordinal() <= FGVersion.v1_2.ordinal(); + + var legacyMCP = StupidHacks.legacyMcp(new ComparableVersion(this.name.getVersion())); + this.mcp = this.forge.mcpconfig.legacy(this.mcVersion, legacyMCP); + var atFile = fg1 ? mergeAts() : extract("merged_at.cfg"); + // 1.7.2 Forge switched to FG1.2 in 1.7.2-10.12.0.1048, this changed our decompiler to + // a version that bulk sorts all classes. So produces different decomp. + var legacyFG = "1.7.2".equals(this.mcVersion) && this.fgVersion != FGVersion.v1_2 ? this.fgVersion : null; + this.mcpChild = this.mcp.getChild(this, atFile, legacyFG); + + this.forgeVersion = this.name.getVersion().substring(this.name.getVersion().indexOf('-') + 1); + + // FG 1.1 needs to remap between FML and Forge patches + if (this.fgVersion == FGVersion.v1 || this.fgVersion == FGVersion.v1_1) + this.sourcesTask = patchForgeRenamed(); + else if (this.fgVersion == FGVersion.v1_2) + this.sourcesTask = patchForge(); + else + this.sourcesTask = patch(); + } + + public File getBuildFolder() { + return this.build; + } + + public Cache getCache() { + return this.forge.getCache(); + } + + public Artifact getName() { + return this.name; + } + + @Override + public String getMinecraftVersion() { + return this.mcVersion; + } + + public String getForgeVersion() { + return this.forgeVersion; + } + + public FGVersion getGradleVersion() { + return this.fgVersion; + } + + public MCPLegacy getMCP() { + return this.mcp; + } + + public @Nullable byte[] getExtraMappings() { + return this.extraMappings; + } + + public Task getSources() { + return this.sourcesTask; + } + + private RuntimeException except(String message) { + return new IllegalArgumentException("Invalid FG2 Dependency: " + this.name + " - " + message); + } + + private RuntimeException except(String message, Throwable e) { + return new IllegalArgumentException("Invalid FG2 Dependency: " + this.name + " - " + message, e); + } + + @Override + public String getDataHash() { + return this.dataHash; + } + + @Override + public int getJavaTarget() { + return this.mcp.getJavaTarget(); + } + + @Override + public Artifact getMCPArtifact() { + return this.getMCP().getName(); + } + + @Override + public @Nullable List getModules() { + return Collections.emptyList(); + } + + @Override + public List getCompileOnly() { + return Collections.emptyList(); + } + + @Override + public List getRuntimeOnly() { + return Collections.emptyList(); + } + + @Override + public void forAllLibraries(Consumer consumer, Predicate filter) { + + /* + for (var library : this.config.getLibs()) { + var artifact = StupidHacks.fixLegacyForgeDeps(Artifact.from(library.coord)); + if (artifact == null || library.dl == null) + continue; + artifact = artifact.withOS(library.os); + if (filter == null || filter.test(artifact)) { + System.out.println("Dev: " + artifact + " " + artifact.getOs()); + consumer.accept(artifact); + } + } + for (var library : this.getMCP().getMinecraftTasks().getClientLibraries()) { + if (filter == null || filter.test(library.artifact())) { + System.out.println("MC: " + library.artifact() + " " + library.artifact().getOs()); + consumer.accept(library.artifact()); + } + } + */ + var libs = getLibraryFiles(); + for (var lib : libs) { + if (filter == null || filter.test(lib.artifact())) { + consumer.accept(lib.artifact()); + } + } + } + + @Override + public List getLibraries() { + var ret = new ArrayList(); + // We don't support OS specific/classified libraries on these old versions because they were made before + // Mojang supported them. So just get them all via the name like FG2 does + for (var lib : this.config.libraries) + ret.add(Artifact.from(lib.name)); + return ret; + } + + @Override + public List getClasspath() { + var ret = new ArrayList(); + for (var lib : getLibraryFiles()) + ret.add(lib.file()); + return ret; + + } + public List getLibraryFiles() { + var classpath = new ArrayList(); + var seen = new HashMap(); + var cache = this.forge.getCache(); + + // minecraft version.json libs + userdev libs + var mc = this.getMCP().getMinecraftTasks(); + var libs = mc.getClientLibraries(); + for (var lib : libs) { + + // We might have to upgrade a vanilla dependency + var artifact = StupidHacks.fixLegacyForgeDeps(lib.artifact()); + if (artifact == null) + continue; + + if (artifact != lib.artifact()) + classpath.add(new ArtifactFile(artifact, Util.getArtifact(cache, artifact, true))); + else + classpath.add(lib); + + // We just want the group:name and clssifier, in case they have updated the version since we were built + seen.put(artifact.withVersion(null).toString(), artifact); + } + + if (this.config.inheritsFrom == null) { + // If there is no inherits, this is before Mojang added that feature to the launcher + // So we need to try and strip out any libraries that come from the vanilla launcher. + for (var lib : this.config.libraries) { + var artifact = StupidHacks.fixLegacyForgeDeps(Artifact.from(lib.name)); + var unversioned = artifact.withVersion(null).toString(); + if (!seen.containsKey(unversioned)) { + if (artifact != null) + classpath.add(new ArtifactFile(artifact, Util.getArtifact(cache, artifact, true))); + } + } + } else { + for (var lib : this.config.libraries) { + var artifact = StupidHacks.fixLegacyForgeDeps(Artifact.from(lib.name)); + if (artifact != null) + classpath.addFirst(new ArtifactFile(artifact, Util.getArtifact(cache, artifact, true))); // Add our versions before vanilla's in case we upgrade + } + } + + return classpath; + } + + @Override + public MinecraftTasks getMinecraftTasks() { + return this.getMCP().getMinecraftTasks(); + } + + public Task extract(String name) { + var ret = this.extracts.get(name); + if (ret == null) { + ret = Task.named("extract[" + name + ']', () -> extractImpl(name)); + this.extracts.put(name, ret); + } + return ret; + } + + private File extractImpl(String name) { + var target = new File(new File(this.build, "data"), name); + var cache = Util.cache(target) + .addKnown("data", dataHash); + + if (Mavenizer.checkCache(target, cache)) + return target; + + try (var zip = new ZipFile(data)) { + var entry = zip.getEntry(name); + if (entry == null) + throw except("Missing Data: " + name); + + FileUtils.ensureParent(target); + + try (var os = new FileOutputStream(target)) { + zip.getInputStream(entry).transferTo(os); + } + + target.setLastModified(entry.getLastModifiedTime().toMillis()); + + cache.save(); + return target; + } catch (IOException e) { + throw except("Failed to extract `" + name + '`', e); + } + } + + private Task patch() { + var input = inject(); + var patches = StupidHacks.needsPatchFixes(this.name) + ? fixPatches(extract("patches.zip")) + : extract("patches.zip"); + + return Task.named("patch", + Task.deps(input, patches), + () -> this.patch(input.execute(), patches.execute(), "patched") + ); + } + + private Task fixPatches(Task base) { + var output = new File(this.build, "fixed-patches.zip"); + return Task.named("fix-patches", Task.deps(base), () -> + StupidHacks.fixPatches(this.name, base.execute(), output) + ); + } + + private File patch(File input, File patches, String name) { + var output = new File(this.build, name + ".jar"); + var rejects = new File(this.build, name + "-rejects.jar"); + + var cache = Util.cache(output) + .add("input", input) + .add("patches", patches) + .addKnown("fg", this.fgVersion.name()) + ; + + if (Mavenizer.checkCache(output, cache)) + return output; + + FileUtils.ensureParent(output); + if (output.exists()) + output.delete(); + if (rejects.exists()) + rejects.delete(); + + patchUnstable(input, patches, output, rejects); + + cache.save(); + return output; + } + + private void patchUnstable(File input, File patchesArchive, File output, File rejectOutput) { + final var patches = new HashMap(); + + // Gather all of our patches + try (ZipInputStream zin = new ZipInputStream(new FileInputStream(patchesArchive))) { + for (ZipEntry entry; (entry = zin.getNextEntry()) != null;) { + String name = entry.getName(); + if (!name.endsWith(".patch")) + continue; + + var filename = name.substring(0, name.length() - 6); + var patch = ContextualPatch.create(zin) + .setAccessC14N(true) + .setMaxFuzz(2); + + patches.put(filename, patch); + } + } catch (IOException e) { + Util.sneak(e); + } + + Throwable failure = null; + // Apply them + try (var zin = new ZipInputStream(new FileInputStream(input)); + var zout = new ZipOutputStream(new FileOutputStream(output)) + ) { + for (ZipEntry entry; (entry = zin.getNextEntry()) != null; ) { + var name = entry.getName(); + var patch = patches.get(name); + var newEntry = new ZipEntry(name); + newEntry.setTime(entry.getTime()); + zout.putNextEntry(newEntry); + + if (patch == null) { + zin.transferTo(zout); + } else { + var ctx = ContextualPatch.context(zin); + var result = patch.patchSingle(false, ctx); + + if (!result.getStatus().isSuccess()) { + if (failure == null) { + LOGGER.error("Input: " + input.getAbsolutePath()); + LOGGER.error("Patches: " + patchesArchive.getAbsolutePath()); + LOGGER.error("Output: " + output.getAbsolutePath()); + } + + LOGGER.error("Fialed to patch " + name); + for (var hunk : result.getHunks()) + LOGGER.error(" Hunk #" + hunk.getHunkID() + ": " + hunk.getStatus().name()); + + failure = result.getFailure(); + } else { + for (var itr = ctx.getData().iterator(); itr.hasNext();) { + var line = itr.next(); + zout.write(line.getBytes(StandardCharsets.UTF_8)); + if (itr.hasNext()) + zout.write('\n'); + } + } + } + zout.closeEntry(); + } + } catch (IOException e) { + Util.sneak(e); + } + + if (failure != null) + Util.sneak(failure); + } + + private Task inject() { + var input = this.mcpChild.getFinalStep(); + var sources = extract("sources.zip"); + var resources = extract("resources.zip"); + return Task.named("inject", + Task.deps(input, sources, resources), + () -> this.inject(input.execute(), sources.execute(), resources.execute()) + ); + } + + private File inject(File input, File sources, File resources) { + var output = new File(this.build, "injected.jar"); + var cache = Util.cache(output) + .add("input", input) + .add("sources", sources) + .add("resources", resources); + + if (Mavenizer.checkCache(output, cache)) + return output; + + try { + FileUtils.mergeJars(output, false, resources, sources, input); + cache.save(); + return output; + } catch (IOException e) { + return Util.sneak(e); + } + } + + private Task mergeAts() { + var fml = extract("src/main/resources/fml_at.cfg"); + var forge = extract("src/main/resources/forge_at.cfg"); + return Task.named("merge-ats", + Task.deps(fml, forge), () -> this.mergeAts(fml.execute(), forge.execute()) + ); + } + + private File mergeAts(File fml, File forge) { + var output = new File(this.build, "merged-at.cfg"); + var cache = Util.cache(output) + .add("fml", fml) + .add("forge", forge); + + if (Mavenizer.checkCache(output, cache)) + return output; + + try (var out = new FileOutputStream(output)) { + out.write(Files.readAllBytes(fml.toPath())); + out.write("\n# Forge\n".getBytes(StandardCharsets.UTF_8)); + out.write(Files.readAllBytes(forge.toPath())); + cache.save(); + return output; + } catch (IOException e) { + return Util.sneak(e); + } + } + + private Task injectLegacy() { + var input = this.mcpChild.getFinalStep(); + return Task.named("inject", + Task.deps(input), + () -> this.injectLegacy(input.execute()) + ); + } + + private File injectLegacy(File input) { + var output = new File(this.build, "injected.jar"); + var cache = Util.cache(output) + .add("input", input) + .addKnown("data", this.dataHash); + + if (Mavenizer.checkCache(output, cache)) + return output; + + FileUtils.ensureParent(output); + try(var out = new ZipOutputStream(new FileOutputStream(output))) { + var seen = new HashSet(); + try (var zin = new ZipInputStream(new FileInputStream(input))) { + for (ZipEntry entry = null; (entry = zin.getNextEntry()) != null; ) { + seen.add(entry.getName()); + out.putNextEntry(FileUtils.getStableEntry(entry.getName())); + zin.transferTo(out); + out.closeEntry(); + } + } + try (var zin = new ZipInputStream(new FileInputStream(this.data))) { + for (ZipEntry entry = null; (entry = zin.getNextEntry()) != null; ) { + var name = entry.getName(); + if (entry.isDirectory()) + continue; + else if (name.startsWith("src/main/resources/")) + name = name.substring(19); + else if (name.startsWith("src/main/java/")) + name = name.substring(14); + else + continue; + + if (!seen.add(name)) + continue; + + out.putNextEntry(FileUtils.getStableEntry(name)); + zin.transferTo(out); + out.closeEntry(); + } + } + cache.save(); + return output; + } catch (IOException e) { + return Util.sneak(e); + } + } + + private Task patchFml() { + var base = injectLegacy(); + var patches = extract("fmlpatches.zip"); + return Task.named("patch-fml", + Task.deps(base, patches), () -> this.patch(base.execute(), patches.execute(), "patched-fml") + ); + } + + private Task patchForge() { + var base = patchFml(); + var patches = extract("forgepatches.zip"); + return Task.named("patch", + Task.deps(base, patches), () -> this.patch(base.execute(), patches.execute(), "patched") + ); + } + + private Task rename() { + var input = patchFml(); + return Task.named("rename", + Task.deps(input), () -> this.rename(input.execute()) + ); + } + + private File rename(File input) { + var output = new File(this.build, "renamed.jar"); + var cache = Util.cache(output) + .add("input", input) + .addKnown("mappings", this.dataHash); + + if (Mavenizer.checkCache(output, cache)) + return output; + + if (this.fgVersion == FGVersion.v1) + LegacyRenamer.renameFG_1_0(input, this.data, output); + else + LegacyRenamer.rename(input, this.data, output, true); + + cache.save(); + return output; + } + + private Task patchForgeRenamed() { + var base = rename(); + var patches = extract("forgepatches.zip"); + return Task.named("patch", + Task.deps(base, patches), () -> this.patch(base.execute(), patches.execute(), "patched") + ); + } + + public Task getSourcesWithJavadocs() { + var input = this.getSources(); + + // FG v1 applied javadocs before Forge patches, so just return the source + if (this.fgVersion == FGVersion.v1) + return input; + + return Task.named("javadocs", + Task.deps(input), () -> this.javadocs(input.execute()) + ); + } + + private File javadocs(File input) { + var output = new File(this.build, "patched-javadocs.jar"); + var cache = Util.cache(output) + .add("input", input) + .addKnown("mappings", this.dataHash); + + if (Mavenizer.checkCache(output, cache)) + return output; + + LegacyRenamer.rename(input, this.data, output, false); + + cache.save(); + return output; + } + + public Map getRuns() { + var mc = new ComparableVersion(this.mcp.getMinecraftTasks().getVersion()); + var prefix = "cpw.mods."; + if (mc.compareTo(new ComparableVersion("1.8")) >= 0) + prefix = "net.minecraftforge."; + + // Use LinkedHashMap to keep things stable + var serverEnv = new LinkedHashMap(); + serverEnv.put("MCP_TO_SRG", "{mcp_to_srg}"); + serverEnv.put("mainClass", "net.minecraft.launchwrapper.Launch"); + serverEnv.put("MCP_MAPPINGS", "{mcp_mappings}"); + serverEnv.put("FORGE_VERSION", this.getForgeVersion()); + serverEnv.put("FORGE_GROUP", "net.minecraftforge"); + serverEnv.put("MC_VERSION", this.getMinecraftVersion()); + serverEnv.put("tweakClass", prefix + "fml.common.launcher.FMLServerTweaker"); + + var clientEnv = new LinkedHashMap<>(serverEnv); + clientEnv.put("tweakClass", prefix + "fml.common.launcher.FMLTweaker"); + clientEnv.put("assetIndex", "{asset_index}"); + clientEnv.put("assetDirectory", "{assets_root}"); + clientEnv.put("nativesDirectory", "{natives}"); + + var ret = new LinkedHashMap(); + ret.put("client", run(true, clientEnv)); + ret.put("server", run(false, serverEnv)); + return ret; + } + + private static RunConfig run(boolean client, Map env) { + var ret = new RunConfig(); + if (client) { + ret.name = "client"; + ret.main = "net.minecraftforge.legacydev.MainClient"; + } else { + ret.name = "server"; + ret.main = "net.minecraftforge.legacydev.MainServer"; + } + ret.client = client; + ret.env = env; + return ret; + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FGVersion.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FGVersion.java index 7c77d79..0d04dd0 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FGVersion.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/FGVersion.java @@ -18,11 +18,35 @@ * GitHub.com */ public enum FGVersion { + /* + * Gradle 1.8, 'fmlpatches.zip' in SRG names 'forgepatches.zip' in mapped names + * Sources/resources included flat in the userdev jar + * All Mappings/SRG files included in userdev + * Forge and FML ats are seperate files + */ + v1("1.0"), v1_1("1.1"), + /* + * Forge patches moved to SRG names + */ v1_2("1.2"), + /** + * Gradle 2.14, Fernflower closed source with some 'fixes', technically lower then '2.0' because we re-used 2.0-SNAPSHOT... + */ + v2_0_1("2.0.1"), + v2_0_2("2.0.2"), + /** + * Gradle 2.5, fernflower closed sourced 'fixed' https://github.com/LexManos/FernFlowerFixer/ + */ v2 ("2.0"), + /** + * Gradle 2.14, fernflower 2.0-SNAPSHOT + */ v2_1("2.1"), v2_2("2.2"), + /** + * ForgeFlower 1.0.342-SNAPSHOT + */ v2_3("2.3"), /** * Gradle 3.9, MCPConfig v1 @@ -72,11 +96,12 @@ private static void forge(FGVersion fg, String forge) { } static { + forge(v1, "1.6.4-9.11.1.960"); forge(v1_1, "1.7.2-10.12.0.967"); forge(v1_2, "1.7.2-10.12.0.1048"); forge(v2, "1.8-11.14.3.1503"); - //forge(v2_0_1, "1.8-11.14.3.1506"); - //forge(v2_0_2, "1.8-11.14.3.1535"); + forge(v2_0_1, "1.8-11.14.3.1506"); + forge(v2_0_2, "1.8-11.14.3.1535"); forge(v2_1, "1.8.8-11.14.4.1583"); //-1.8.8 forge(v2_2, "1.9.4-12.17.0.1908"); //-1.9.4 forge(v2_3, "1.12-14.21.0.2320"); @@ -94,6 +119,7 @@ private static void forge(FGVersion fg, String forge) { // Special versions that do not match the general ranges of historical versions. // This will probably need updating when we back port new ForgeDev toolchain to older versions. private static final List SPECIAL_CASES = List.of( + special(null, "1.6.4-9.11.1.965", "1.6.5"), special(v3, "1.12.2-14.23.5.2851", "1.12.3") ); @@ -117,7 +143,7 @@ private static SpecialCase special(FGVersion fg, String start, String end) { } for (var entry : FORGE_TO_FG.entrySet()) { - if (entry.getValue().compareTo(ver) <= 0) + if (ver.compareTo(entry.getValue()) >= 0) return entry.getKey(); } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeRepo.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeRepo.java index eb45770..0ef2708 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeRepo.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeRepo.java @@ -5,6 +5,7 @@ package net.minecraftforge.mcmaven.impl.repo.forge; import net.minecraftforge.mcmaven.impl.cache.Cache; +import net.minecraftforge.mcmaven.impl.cache.MavenCache; import net.minecraftforge.mcmaven.impl.data.GradleModule; import net.minecraftforge.mcmaven.impl.mappings.Mappings; import net.minecraftforge.mcmaven.impl.repo.Repo; @@ -17,12 +18,15 @@ import net.minecraftforge.mcmaven.impl.util.Constants; import net.minecraftforge.mcmaven.impl.Mavenizer; import net.minecraftforge.mcmaven.impl.util.POMBuilder; -import net.minecraftforge.mcmaven.impl.util.POMBuilder.Dependencies; import net.minecraftforge.mcmaven.impl.util.POMBuilder.Dependencies.Dependency; +import net.minecraftforge.mcmaven.impl.util.StupidHacks; import net.minecraftforge.mcmaven.impl.util.Task; import net.minecraftforge.mcmaven.impl.util.Util; +import net.minecraftforge.srgutils.IMappingFile; import net.minecraftforge.util.data.json.JsonData; +import net.minecraftforge.util.data.json.RunConfig; import net.minecraftforge.util.file.FileUtils; +import net.minecraftforge.util.hash.HashFunction; import static net.minecraftforge.mcmaven.impl.Mavenizer.LOGGER; @@ -31,19 +35,28 @@ import org.jetbrains.annotations.Nullable; +import java.io.ByteArrayOutputStream; +import java.io.ByteArrayInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.nio.file.Files; +import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipOutputStream; // TODO: [MCMavenizer][ForgeRepo] For now, the ForgeRepo needs to be fully complete with everything it has to do. // later, we can worry about refactoring it so that other repositories such as MCP (clean) and FMLOnly can function. @@ -73,6 +86,25 @@ public ForgeRepo(Cache cache, MCPConfigRepo mcpconfig) { this.globalBuild = new File(cache.root(), "forge/.global"); } + public static boolean isSupported(String version) { + // Python isn't supported yet + if (isPython(version)) + return false; + + var fg = FGVersion.fromForge(version); + if (fg == null) + return false; + + // Early versions of 1.8.8 use a old '2.0' build with funkey fernflower + // The mcp artifact was also updated behind the scenes, so it won't apply correctly + // I need to rebuild those files in order for this to work + // But we have later builds of 1.8.8 that work so im not worried about it for now + if (version.startsWith("1.8.8-") && fg.ordinal() < FGVersion.v2_1.ordinal()) + return false; + + return true; + } + private static boolean isPython(String version) { var ver = new ComparableVersion(version); return ver.compareTo(PYTHON_START) >= 0 && ver.compareTo(PYTHON_END) < 0; @@ -89,20 +121,34 @@ public List process(Artifact artifact, Mappings mappings, Map2.3 artifacts + /// + /// We need to generate the following artifacts: + /// - `net.minecraftforge:forge:{version}` + /// - default: + /// - The default jar contains the recompiled class files, patcher assets + /// - sources: + /// - Source files used to recompile the default jar. + /// - metadata.zip: + /// - Metadata about the version, such as runs.json and version.json. + /// exist. + /// - `net.minecraft:mappings_{CHANNEL}:{MCP_VERSION}[-{VERSION}]@zip` + /// - A zip file containing fields, methods, and params.csv files mapping SRG->MCP names. + private List processV2(String version, Mappings baseMappings, Map> outputJson, FGVersion fgVersion) { + var name = Artifact.from(Constants.FORGE_GROUP, Constants.FORGE_NAME, version); + var userdev = getUserdev(version); + + var build = new File(this.cache.root(), "forge/" + userdev.getFolder()); + var jdks = this.cache.jdks(); + + var dev = new FG2Userdev(build, this, userdev, fgVersion); + var mcVersion = Util.forgeToMcVersion(version); + var srgTask = dev.getMCP().getMappings(); + var srgSources = dev.getSources(); + + var mappings = baseMappings.withContext(dev); + + Task sourcesTask = srgSources; + // v1 and v1.1 have Forge patches in mapped names, and I haven't implemented converting to SRG yet as it would need to run Srg2Source + // So check if we are using the 'correct' mappings, and if not throw and exception + if (fgVersion == FGVersion.v1 || fgVersion == FGVersion.v1_1) { + var required = StupidHacks.getDefaultMappings(new ComparableVersion(version)); + if (!required.channel().equals(mappings.channel()) || !required.version().equals(mappings.version())) + throw new IllegalStateException("Can not setup " + name + " as it requires you to us the explicit mappings: " + required.toString()); + sourcesTask = dev.getSourcesWithJavadocs(); + } else if (!mappings.channel().equals("srg")) { + sourcesTask = new RenameTask(build, userdev.getName(), srgSources, mappings, true, srgTask, mcVersion); + } + var classesTask = new RecompileTask(build, name, jdks, dev.getJavaTarget(), dev::getClasspath, sourcesTask, mappings); + + var mappingCoords = mappings.getArtifact(); + + var mappingArtifacts = mappingArtifacts(build, mappings, mcVersion, outputJson); + + var sources = pending("Sources", sourcesTask, name.withClassifier("sources"), true, sourceVariant(baseMappings)); + var classes = pending("Classes", classesTask, name, false, () -> classVariants(baseMappings, dev, mappingCoords)); + var metadata = pending("Metadata", metadata(build, dev, dev.getRuns()), name.withClassifier("metadata").withExtension("zip"), false, metadataVariant()); + + var pom = pending("Maven POM", pom(mappings.getFolder(build), dev, version, null, mappingCoords), name.withExtension("pom"), false); + + // Gradle only allows downloading artifacts from one repo, so we need to pull in any classifers that we reference + var classifiers = getClassifieres(name, dev.getLibraries(), new HashMap<>()); + + addJsonData(outputJson, dev); + + var ret = new ArrayList(); + ret.addAll(mappingArtifacts); + ret.addAll(List.of(sources, classes, pom, metadata)); + ret.addAll(classifiers.values()); + return ret; + } + + /// This handles UserDev3 artifacts, which are anything created using FG 3->6 /// /// We need to generate the following artifacts: @@ -142,69 +251,48 @@ private static Artifact getUserdev(String forge) { /// - pom: /// - Standard maven pom file that contains all dependency information. // Made this an MD comment to make it easier to read in IDE - Jonathan - private List processV3(String version, Mappings mappings, Map> outputJson) { + private List processV3(String version, Mappings baseMappings, Map> outputJson) { var name = Artifact.from(Constants.FORGE_GROUP, Constants.FORGE_NAME, version); var userdev = getUserdev(version); var build = new File(this.cache.root(), "forge/" + userdev.getFolder()); + var jdks = this.cache.jdks(); var patcher = new Patcher(build, this, userdev); var joined = patcher.getMCP().getSide(MCPSide.JOINED); - var sourcesTask = new RenameTask(build, userdev.getName(), joined, patcher.get(), mappings, true); - var recompile = new RecompileTask(build, name, patcher.getMCP(), patcher::getClasspath, sourcesTask, mappings); + var mcVersion = joined.getMCP().getMinecraftTasks().getVersion(); + var mappings = baseMappings.withContext(joined); + var srgTask = joined.getTasks().getMappings(); + var srgSources = patcher.get(); + + var sourcesTask = mappings.channel().equals("srg") + ? srgSources + : new RenameTask(build, userdev.getName(), srgSources, mappings, true, srgTask, mcVersion); + var recompile = new RecompileTask(build, name, jdks, patcher.getJavaTarget(), patcher::getClasspath, sourcesTask, mappings); var classesTask = new InjectTask(build, this.cache, name, patcher, recompile, mappings); var extraCoords = Artifact.from(Constants.MC_GROUP, Constants.MC_CLIENT + "-extra", patcher.getMCP().getName().getVersion()); // If we are not obfuscated, don't add the csv zip as a extra artifact - var mappingCoords = patcher.isObfuscated() ? mappings.getArtifact(joined) : null; + var mappingCoords = patcher.isObfuscated() ? mappings.getArtifact() : null; - var mappingArtifacts = mappingArtifacts(build, mappings, joined, outputJson); + var mappingArtifacts = mappingArtifacts(build, mappings, mcVersion, outputJson); + var mappingFolder = mappingCoords == null ? build : mappings.getFolder(build); - var sources = pending("Sources", sourcesTask, name.withClassifier("sources"), true, sourceVariant(mappings)); - var classes = pending("Classes", classesTask, name, false, () -> classVariants(mappings, patcher, extraCoords, mappingCoords)); - var metadata = pending("Metadata", metadata(build, patcher), name.withClassifier("metadata").withExtension("zip"), false, metadataVariant()); + var sources = pending("Sources", sourcesTask, name.withClassifier("sources"), true, sourceVariant(baseMappings)); + var classes = pending("Classes", classesTask, name, false, () -> classVariants(baseMappings, patcher, extraCoords, mappingCoords)); + var metadata = pending("Metadata", metadata(build, patcher, patcher.config.runs), name.withClassifier("metadata").withExtension("zip"), false, metadataVariant()); - var pom = pending("Maven POM", pom(build, patcher, version, extraCoords, mappingCoords), name.withExtension("pom"), false); + var pom = pending("Maven POM", pom(mappingFolder, patcher, version, extraCoords, mappingCoords), name.withExtension("pom"), false); var extraOutput = this.mcpconfig.processExtra(Constants.MC_GROUP + ':' + Constants.MC_CLIENT, patcher.getMCP().getName().getVersion()); // Gradle only allows downloading artifacts from one repo, so we need to pull in any classifers that we reference var classifiers = new HashMap(); - for (var parent : patcher.getStack()) { - for (var artifact : parent.getArtifacts()) { - if (!name.getGroup().equals(artifact.getGroup()) - || !name.getName().equals(artifact.getName()) - || !name.getVersion().equals(artifact.getVersion()) - || classifiers.containsKey(artifact) - ) - continue; - // Classifers can not have variants in gradle, so we just need to download them - classifiers.put(artifact, - pending("Classifier-" + artifact.getClassifier(), - Task.named( - "classifier[" + artifact.getClassifier() + '@' + artifact.getExtension() + ']', - () -> this.cache.maven().download(artifact) - ), - artifact, - false - ) - ); - } - } + for (var parent : patcher.getStack()) + getClassifieres(name, parent.getLibraries(), classifiers); - // Add some extra metadata for FG to consume: - if (outputJson != null) { - outputJson.put("mcp.version", patcher.getMCP().getName()::getVersion); - outputJson.put("mcp.artifact", patcher.getMCP().getName()::toString); - outputJson.put("mc.version", patcher.getMCP().getMinecraftTasks()::getVersion); - - var modules = patcher.config.modules; - if (modules == null || modules.isEmpty()) - outputJson.put("patcher.modules", () -> ""); - else - outputJson.put("patcher.modules", () -> String.join(",", modules)); - } + addJsonData(outputJson, patcher); var ret = new ArrayList(); ret.addAll(mappingArtifacts); @@ -214,8 +302,49 @@ private List processV3(String version, Mappings mappings, Map { + // Gradle only allows downloading artifacts from one repo, so we need to pull in any classifers that we reference + private Map getClassifieres(Artifact name, Collection artifacts, Map classifiers) { + for (var artifact : artifacts) { + if (!name.getGroup().equals(artifact.getGroup()) + || !name.getName().equals(artifact.getName()) + || !name.getVersion().equals(artifact.getVersion()) + || classifiers.containsKey(artifact) + ) + continue; + // Classifers can not have variants in gradle, so we just need to download them + classifiers.put(artifact, + pending("Classifier-" + artifact.getClassifier(), + Task.named( + "classifier[" + artifact.getClassifier() + '@' + artifact.getExtension() + ']', + () -> this.cache.maven().download(artifact) + ), + artifact, + false + ) + ); + } + return classifiers; + } + + private void addJsonData(@Nullable Map> outputJson, ForgeVersionCommon info) { + // Add some extra metadata for FG to consume: + if (outputJson == null) + return; + + var mcp = info.getMCPArtifact(); + outputJson.put("mcp.version", mcp::getVersion); + outputJson.put("mcp.artifact", mcp::toString); + outputJson.put("mc.version", info::getMinecraftVersion); + + var modules = info.getModules(); + if (modules == null || modules.isEmpty()) + outputJson.put("patcher.modules", () -> ""); + else + outputJson.put("patcher.modules", () -> String.join(",", modules)); + } + + private static Task metadata(File build, ForgeVersionCommon forge, Map runs) { + return Task.named("metadata[forge]", Task.deps(forge.getMinecraftTasks().versionJson), () -> { var output = new File(build, "metadata.zip"); // metadata @@ -224,17 +353,19 @@ private static Task metadata(File build, Patcher patcher) { // metadata/launcher var launcherDir = new File(metadataDir, "launcher"); - var runsJsonStr = JsonData.toJson(patcher.config.runs); + var runsJsonStr = JsonData.toJson(runs); // metadata/minecraft var minecraftDir = new File(metadataDir, "minecraft"); - var versionJson = patcher.getMCP().getMinecraftTasks().versionJson.execute(); + var versionJson = forge.getMinecraftTasks().versionJson.execute(); var cache = Util.cache(output) - .add(versionJson) - .add(versionProperties) - .add("data", patcher.getDataHash()) + .add("json", versionJson) + .add("properties", versionProperties) + .add("runs", runsJsonStr) + .addKnown("data", forge.getDataHash()) .addKnown("version", "1"); + if (Mavenizer.checkCache(output, cache)) return output; @@ -262,7 +393,6 @@ private static Task metadata(File build, Patcher patcher) { new File(minecraftDir, "version.json").toPath(), StandardCopyOption.REPLACE_EXISTING ); - cache.add(versionProperties); // metadata.zip FileUtils.makeZip(metadataDir, output); @@ -275,11 +405,12 @@ private static Task metadata(File build, Patcher patcher) { }); } - private static Task pom(File build, Patcher patcher, String version, @Nullable Artifact clientExtra, @Nullable Artifact mappings) { + private static Task pom(File build, ForgeVersionCommon forge, String version, @Nullable Artifact clientExtra, @Nullable Artifact mappings) { return Task.named("pom[forge]", () -> { var output = new File(build, "forge.pom"); + var cache = Util.cache(output) - .addKnown("data", patcher.getDataHash()) + .addKnown("data", forge.getDataHash()) .addKnown("code-version", "1"); if (clientExtra != null) @@ -291,16 +422,23 @@ private static Task pom(File build, Patcher patcher, String version, @Nullable A if (Mavenizer.checkCache(output, cache)) return output; - var builder = new POMBuilder("net.minecraftforge", "forge", version).preferGradleModule().dependencies(dependencies -> { + var builder = new POMBuilder(Constants.FORGE_GROUP, Constants.FORGE_NAME, version).preferGradleModule().dependencies(dependencies -> { if (clientExtra != null) dependencies.add(clientExtra); if (mappings != null) dependencies.add(mappings); - patcher.forAllLibraries(dependencies::add, Artifact::hasNoOs); + forge.forAllLibraries(dependencies::add, Artifact::hasNoOs); + + // Following https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope + // PROVIDED == 'compileOnly' + for (var descriptor : forge.getCompileOnly()) + dependencies.add(Artifact.from(descriptor), Dependency.Scope.PROVIDED); - addExtraRuntimeToPom(patcher, dependencies); + // RUNTIME = 'runtimeOnly' + for (var descriptor : forge.getRuntimeOnly()) + dependencies.add(Artifact.from(descriptor), Dependency.Scope.RUNTIME); }); FileUtils.ensureParent(output); @@ -316,47 +454,144 @@ private static Task pom(File build, Patcher patcher, String version, @Nullable A } @SuppressWarnings("deprecation") - private static void addExtraRuntimeToPom(Patcher patcher, Dependencies deps) { - if (patcher.config.extraDependencies == null) - return; + protected GradleModule.Variant[] classVariants(Mappings mappings, ForgeVersionCommon patcher, Artifact... extraDeps) { + var libs = new ArrayList<>(Arrays.asList(extraDeps)); + patcher.forAllLibraries(libs::add); - // Following https://maven.apache.org/guides/introduction/introduction-to-dependency-mechanism.html#dependency-scope - // PROVIDED == 'compileOnly' - if (patcher.config.extraDependencies.compileOnly != null) { - for (var descriptor : patcher.config.extraDependencies.compileOnly) { - deps.add(Artifact.from(descriptor), Dependency.Scope.PROVIDED); - } - } + var extraCompile = new ArrayList(); + for (var descriptor : patcher.getCompileOnly()) + extraCompile.add(Artifact.from(descriptor)); + var extraRuntime = new ArrayList(); + for (var descriptor : patcher.getRuntimeOnly()) + extraRuntime.add(Artifact.from(descriptor)); - // RUNTIME = 'runtimeOnly' - if (patcher.config.extraDependencies.runtimeOnly != null) { - for (var descriptor : patcher.config.extraDependencies.runtimeOnly) { - deps.add(Artifact.from(descriptor), Dependency.Scope.RUNTIME); - } - } + + return super.classVariants(mappings, patcher.getMinecraftTasks().getJavaVersion(), libs, extraCompile); } - @SuppressWarnings("deprecation") - protected GradleModule.Variant[] classVariants(Mappings mappings, Patcher patcher, Artifact... extraDeps) { - var extra = new ArrayList<>(Arrays.asList(extraDeps)); - extra.addAll(patcher.getArtifacts()); - var extraCompile = new ArrayList(); - var extraRuntime = new ArrayList(); - if (patcher.config.extraDependencies != null) { - if (patcher.config.extraDependencies.compileOnly != null) { - for (var descriptor : patcher.config.extraDependencies.compileOnly) { - extraCompile.add(Artifact.from(descriptor)); + // Old code that I think may be useful for older versions, so I don't want to delete quite yet, delete when all legacy versions are supported + public static class Info { + public static final Map MAPPINGS = new LinkedHashMap<>(); + public static final Map CONF = new LinkedHashMap<>(); + + private static String hashEntries(ZipFile zip, List entries) { + Collections.sort(entries); + var bos = new ByteArrayOutputStream(); + for (var name : entries) { + var entry = zip.getEntry(name); + try { + var stream = zip.getInputStream(entry); + stream.transferTo(bos); + } catch (IOException e) { + throw new RuntimeException(e); } } - - if (patcher.config.extraDependencies.runtimeOnly != null) { - for (var descriptor : patcher.config.extraDependencies.runtimeOnly) { - extraRuntime.add(Artifact.from(descriptor)); + return HashFunction.sha1().hash(bos.toByteArray()); + } + private static void zip(ZipFile zip, List entries, String suffix, String version, String hash) { + Collections.sort(entries); + var output = new File(version + '-' + suffix + '-' + hash + ".zip"); + try (var out = new ZipOutputStream(new FileOutputStream(output))) { + for (var ent : entries) { + var entry = zip.getEntry(ent); + var name = entry.getName().replace("conf/", ""); + if (name.isEmpty() || entry.isDirectory()) + continue; + byte[] data = zip.getInputStream(entry).readAllBytes(); + + switch (name) { + case "packaged.srg": + name = "joined.srg"; + var map = IMappingFile.load(new ByteArrayInputStream(data)); + var tmp = Path.of("Z:/test/_test/joined.srg"); + map.write(tmp, IMappingFile.Format.SRG, false); + data = Files.readAllBytes(tmp); + break; + case "packaged.exc": + name = "joined.exc"; + break; + case "Start.java": + name = "patches/Start.java"; + continue; + //break; + } + if (name.endsWith(".csv")) + name = name.substring(name.indexOf('/') + 1); + if (name.startsWith("minecraft_ff/")) + name = "patches/minecraft_merged_ff/" + name.substring(13); + + var newEntry = new ZipEntry(name); + newEntry.setTime(entry.getTime()); + out.putNextEntry(newEntry); + out.write(data); + out.closeEntry(); } + } catch (IOException e) { + throw new RuntimeException(e); } } + private static List gatherVariants(MavenCache cache, String version, @Nullable FGVersion fg) { + var userdev = getUserdev(version); + if (fg == null) + userdev = userdev.withClassifier("src").withExtension("zip"); + var mappingsFiles = new ArrayList(); + var confFiles = new ArrayList(); + + var file = cache.download(userdev); + try (var zip = new ZipFile(file)) { + for (var itr = zip.entries().asIterator(); itr.hasNext(); ) { + var entry = itr.next(); + var ename = entry.getName(); + var filename = ename.lastIndexOf('/') == -1 ? ename : ename.substring(ename.lastIndexOf('/') + 1); + + if ("fields.csv".equals(filename) + || "methods.csv".equals(filename) + || "params.csv".equals(filename) + ) { + mappingsFiles.add(ename); + } else if (ename.startsWith("conf/") + || ename.startsWith("forge/conf/") + || ename.startsWith("forge/fml/conf/") + ) { + confFiles.add(ename); + } + } + + if (confFiles.isEmpty()) + throw new IllegalStateException("Missing conf files: " + version); + if (mappingsFiles.isEmpty()) + throw new IllegalStateException("Missing mapping files: " + version); + + var hash = hashEntries(zip, mappingsFiles); + if (!MAPPINGS.containsKey(hash)) { + System.out.println("New Mappings: " + hash + " " + version); + MAPPINGS.put(hash, version); + zip(zip, mappingsFiles, "mappings", version, hash); + } + hash = hashEntries(zip, confFiles); + if (!CONF.containsKey(hash)) { + System.out.println("New Conf: " + hash + " " + version); + CONF.put(hash, version); + zip(zip, confFiles, "conf", version, hash); + } - return super.classVariants(mappings, patcher.getMCPSide(), extra, extraCompile, extraRuntime); + } catch (IOException e) { + throw new IllegalStateException("Error reading config " + file.getAbsolutePath(), e); + } + return List.of(); + } + public static void finish() { + if (!MAPPINGS.isEmpty()) { + System.out.println("Mappings: "); + for (var entry : MAPPINGS.entrySet()) + System.out.println("\t" + entry.getKey() + ' ' + entry.getValue()); + } + if (!CONF.isEmpty()) { + System.out.println("Conf: "); + for (var entry : CONF.entrySet()) + System.out.println("\t" + entry.getKey() + ' ' + entry.getValue()); + } + } } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeVersionCommon.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeVersionCommon.java new file mode 100644 index 0000000..407d187 --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/ForgeVersionCommon.java @@ -0,0 +1,36 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.repo.forge; + +import java.io.File; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Predicate; + +import org.jetbrains.annotations.Nullable; + +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; +import net.minecraftforge.mcmaven.impl.util.Artifact; + +public interface ForgeVersionCommon { + String getDataHash(); + + int getJavaTarget(); + List getClasspath(); + + Artifact getMCPArtifact(); + String getMinecraftVersion(); + @Nullable List getModules(); + + default void forAllLibraries(Consumer consumer) { + this.forAllLibraries(consumer, null); + } + void forAllLibraries(Consumer consumer, Predicate filter); + List getLibraries(); + List getCompileOnly(); + List getRuntimeOnly(); + + MinecraftTasks getMinecraftTasks(); +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/InjectTask.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/InjectTask.java index da8e216..3f117a6 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/InjectTask.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/InjectTask.java @@ -6,7 +6,7 @@ import net.minecraftforge.mcmaven.impl.Mavenizer; import net.minecraftforge.mcmaven.impl.cache.Cache; -import net.minecraftforge.mcmaven.impl.mappings.Mappings; +import net.minecraftforge.mcmaven.impl.mappings.ResolvedMappings; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.util.file.FileUtils; import net.minecraftforge.mcmaven.impl.util.Task; @@ -24,10 +24,10 @@ public final class InjectTask implements Task { private final Artifact name; private final Cache cache; private final Patcher patcher; - private final Mappings mappings; + private final ResolvedMappings mappings; private final Task task; - InjectTask(File build, Cache cache, Artifact name, Patcher patcher, Task input, Mappings mappings) { + InjectTask(File build, Cache cache, Artifact name, Patcher patcher, Task input, ResolvedMappings mappings) { this.build = mappings.getFolder(build); this.name = name; this.cache = cache; diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/LegacyRenamer.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/LegacyRenamer.java new file mode 100644 index 0000000..d94a673 --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/LegacyRenamer.java @@ -0,0 +1,160 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.repo.forge; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import net.minecraftforge.mcmaven.impl.tasks.MCPNames; +import net.minecraftforge.mcmaven.impl.tasks.MCPNames.JavadocAdder; +import net.minecraftforge.mcmaven.impl.util.NewLineDetector; +import net.minecraftforge.mcmaven.impl.util.Util; +import net.minecraftforge.util.file.FileUtils; + +/* + * Older versions require renaming between FML and Forge patches, + * and include custom comments for injecting javadocs as a second step. + * So this implements that. Basically just copy pasted from FG 1.0 + */ +class LegacyRenamer { + //private static final Pattern SRG_FINDER = Pattern.compile("func_[0-9]+_[a-zA-Z_]+|field_[0-9]+_[a-zA-Z_]+|p_[\\w]+_\\d+_"); + private static final Pattern METHOD = Pattern.compile("^((?: {4})+|\\t+)(?:[\\w$.\\[\\]]+ )+(func_[0-9]+_[a-zA-Z_]+)\\("); + private static final Pattern FIELD = Pattern.compile("^((?: {4})+|\\t+)(?:[\\w$.\\[\\]]+ )+(field_[0-9]+_[a-zA-Z_]+) *(?:=|;)"); + + private static final Pattern METHOD_FG1 = Pattern.compile("^( {4}|\\t)(?:[\\w$.\\[\\]]+ )*(func_[0-9]+_[a-zA-Z_]+)\\("); + private static final Pattern FIELD_FG1 = Pattern.compile("^( {4}|\\t)(?:[\\w$.\\[\\]]+ )*(field_[0-9]+_[a-zA-Z_]+) *(?:=|;)"); + + public static void renameFG_1_0(File input, File mappings, File output) { + rename(input, mappings, output, false, true); + } + + public static void rename(File input, File mappings, File output, boolean javadocMarkers) { + rename(input, mappings, output, javadocMarkers, false); + } + + private static void rename(File input, File mappings, File output, boolean javadocMarkers, boolean fg_1_0) { + try { + var names = MCPNames.load(mappings); + FileUtils.ensureParent(output); + try (var zin = new ZipInputStream(new FileInputStream(input)); + var zout = new ZipOutputStream(new FileOutputStream(output))) { + for (ZipEntry entry; (entry = zin.getNextEntry()) != null; ) { + var newEntry = new ZipEntry(entry.getName()); + newEntry.setTime(entry.getTime()); + zout.putNextEntry(newEntry); + + if (!entry.getName().endsWith(".java")) { + zin.transferTo(zout); + } else { + if (entry.getName().endsWith("ComponentVillageStartPiece.java")) + System.currentTimeMillis(); + var lines = NewLineDetector.readLines(zin); + if (fg_1_0) + lines = renameFG_1_0(names, lines); + else + lines = rename(names, lines, javadocMarkers); + for (var itr = lines.iterator(); itr.hasNext();) { + var line = itr.next(); + zout.write(line.getBytes(StandardCharsets.UTF_8)); + if (itr.hasNext()) + zout.write('\n'); + } + } + + zout.closeEntry(); + } + } + } catch (IOException e) { + Util.sneak(e); + } + } + + private static List rename(MCPNames names, List lines, boolean javadocMarkers) { + var newLines = new ArrayList(lines.size()); + Matcher matcher = null; + for (var line : lines) { + if (line.trim().startsWith("// JAVADOC ")) { + var idx = line.indexOf('$'); + if (idx != -1 && idx < line.length() - 2) { + var name = line.substring(idx + 2); + var docs = names.docs().get(name); + if (docs != null && !docs.isEmpty()) + line = JavadocAdder.buildJavadoc(line.substring(line.indexOf('/')), docs, true); + } + } else if ((matcher = METHOD.matcher(line)).find()) { + var name = matcher.group(2); + var docs = names.docs().get(name); + + if (!name.equals(names.names().getOrDefault(name, name)) && docs != null && !docs.isEmpty()) { + String formatted; + if (javadocMarkers) + formatted = matcher.group(1) + "// JAVADOC METHOD $$ " + name; + else + formatted = JavadocAdder.buildJavadoc(matcher.group(1), docs, true); + + MCPNames.insertAboveAnnotations(newLines, formatted); + } + } else if ((matcher = FIELD.matcher(line)).find()) { + var name = matcher.group(2); + var docs = names.docs().get(name); + if (!name.equals(names.names().getOrDefault(name, name)) && docs != null && !docs.isEmpty()) { + String formatted; + if (javadocMarkers) + formatted = matcher.group(1) + "// JAVADOC FIELD $$ " + name; + else + formatted = JavadocAdder.buildJavadoc(matcher.group(1), docs, false); + + MCPNames.insertAboveAnnotations(newLines, formatted); + } + } + newLines.add(names.replaceInLine(line, null)); + } + return newLines; + } + + + // FG 1.0 didn't use the itnermediate javadoc markers + // And didnt take annotations into account when inserting javadocs + private static List renameFG_1_0(MCPNames names, List lines) { + var newLines = new ArrayList(lines.size()); + Matcher matcher = null; + String lastLine = null; + for (var line : lines) { + if ((matcher = METHOD_FG1.matcher(line)).find()) { + var name = matcher.group(2); + var docs = names.docs().get(name); + + if (docs != null && !docs.isEmpty()) { + if (lastLine != null && !lastLine.isEmpty() && !lastLine.endsWith("{")) + newLines.add(""); + String formatted = JavadocAdder.buildJavadoc(matcher.group(1), docs, true); + newLines.add(formatted); + } + } else if ((matcher = FIELD_FG1.matcher(line)).find()) { + var name = matcher.group(2); + var docs = names.docs().get(name); + if (docs != null && !docs.isEmpty()) { + if (lastLine != null && !lastLine.isEmpty() && !lastLine.endsWith("{")) + newLines.add(""); + String formatted = JavadocAdder.buildJavadoc(matcher.group(1), docs, docs.length() >= 70); + newLines.add(formatted); + } + } + newLines.add(names.replaceInLine(line, null)); + lastLine = line; + } + return newLines; + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/Patcher.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/Patcher.java index 0421c5d..d5ce32e 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/Patcher.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/forge/Patcher.java @@ -6,7 +6,6 @@ import java.io.File; import java.io.FileInputStream; -import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; import java.nio.charset.StandardCharsets; @@ -40,6 +39,7 @@ import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCP; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPConfigRepo; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.Constants; import net.minecraftforge.util.data.json.JsonData; @@ -63,7 +63,7 @@ * These files may or may not be in SRG names, depending on the patcher configuration. * But the point is this creates source code. */ -public class Patcher implements Supplier { +public class Patcher implements Supplier, ForgeVersionCommon { private final File build; private final ForgeRepo forge; private final Artifact name; @@ -246,14 +246,16 @@ public String getDataHash() { return this.dataHash; } - public void forAllLibraries(Consumer consumer) { - this.forAllLibraries(consumer, null); + @Override + public int getJavaTarget() { + return this.getMCP().getConfig().java_target; } + @Override public void forAllLibraries(Consumer consumer, Predicate filter) { this.forAllLibrariesInternal(consumer, filter, this.getMCPSide().getMCLibraries()); this.forAllLibrariesInternal(consumer, filter, this.getMCPSide().getMCPConfigLibraries()); - this.forAllLibrariesInternal(consumer, filter, this.getArtifacts()); + this.forAllLibrariesInternal(consumer, filter, this.getLibraries()); } // to avoid duplicate code @@ -264,7 +266,8 @@ private void forAllLibrariesInternal(Consumer consumer, @Nulla } } - public List getArtifacts() { + @Override + public List getLibraries() { // TODO MOVE ALL THIS LOGIC TO SOME SORT OF "ARTIFACT LIST GENERATOR" IN ARTIFACT.JAVA var artifacts = new ArrayList() /*{ private final Set distincts = new HashSet<>(); @@ -303,12 +306,14 @@ boolean is(Artifact artifact) { for (var lib : this.config.libraries) { var artifact = Artifact.from(lib); artifact = StupidHacks.fixLegacyForgeDeps(artifact); - artifacts.add(artifact); + if (artifact != null) + artifacts.add(artifact); } return artifacts; } + @Override public List getClasspath() { var classpath = new ArrayList(); @@ -320,37 +325,16 @@ public List getClasspath() { var cache = this.forge.getCache(); for (var lib : this.getMCP().getConfig().getLibraries(MCPSide.JOINED)) { - classpath.add(getArtifact(cache, lib)); + classpath.add(Util.getArtifact(cache, lib)); } for (var lib : this.config.libraries) { - classpath.add(getArtifact(cache, lib)); + classpath.add(Util.getArtifact(cache, lib)); } return classpath; } - private static final File getArtifact(Cache cache, String coords) { - var artifact = Artifact.from(coords); - // Some libraries are on Minecraft's maven. Such as launchwrapper. - // Rather then configure Forge's server to proxy Mojang's I add this check. - if ("net.minecraft".equals(artifact.getGroup())) - return cache.minecraft().download(artifact); - try { - return cache.maven().download(artifact); - } catch (Exception e) { - // If its 404 on Forge's maven, try Mojang's - if (e.getCause() instanceof FileNotFoundException) { - try { - return cache.minecraft().download(artifact); - } catch (Exception e2) { - e.addSuppressed(e2); - } - } - return Util.sneak(e); - } - } - /** @return The final unnamed sources */ public Task get() { return this.last; @@ -389,7 +373,7 @@ private File extractJoinedFiles(String filename, List files) { var output = new File(this.build, filename); var cache = Util.cache(output); - cache.add("data", this.data); + cache.addKnown("data", this.dataHash); if (Mavenizer.checkCache(output, cache)) return output; @@ -435,7 +419,7 @@ private File extractSingleTask(String key, String value) { var target = new File(this.build, "data/" + key + '/' + filename); var cache = Util.cache(target); - cache.add("data", this.data); + cache.addKnown("data", this.dataHash); if (Mavenizer.checkCache(target, cache)) return target; @@ -566,7 +550,7 @@ private File postProcess(Task inputTask, PatcherConfig.V2.DataFunction data, Fil var tool = maven.download(toolA); var cache = Util.cache(output); - cache.add("data", this.data); + cache.addKnown("data", this.dataHash); cache.add("tool", tool); cache.add("input", input); cache.add("jvm-args", data.getJvmArgs().stream().collect(Collectors.joining(" "))); @@ -623,7 +607,7 @@ private File patch(Task inputTask, File output, File rejects) { var cache = Util.cache(output); cache.add("input", input); - cache.add("data", this.data); + cache.addKnown("data", this.dataHash); if (Mavenizer.checkCache(output, cache)) return output; @@ -798,4 +782,40 @@ record Info(File file, Artifact artifact, Predicate filter, Function getModules() { + return this.config.modules; + } + + @SuppressWarnings("deprecation") + @Override + public List getCompileOnly() { + if (this.config.extraDependencies == null || this.config.extraDependencies.compileOnly == null) + return Collections.emptyList(); + return this.config.extraDependencies.compileOnly; + } + + @SuppressWarnings("deprecation") + @Override + public List getRuntimeOnly() { + if (this.config.extraDependencies == null || this.config.extraDependencies.runtimeOnly == null) + return Collections.emptyList(); + return this.config.extraDependencies.runtimeOnly; + } + + @Override + public MinecraftTasks getMinecraftTasks() { + return this.getMCP().getMinecraftTasks(); + } } \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPConfigRepo.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPConfigRepo.java index 5d192ff..e0be027 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPConfigRepo.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPConfigRepo.java @@ -6,12 +6,14 @@ import net.minecraftforge.mcmaven.impl.Mavenizer; import net.minecraftforge.mcmaven.impl.repo.Repo; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks.ArtifactFile; import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks.MCFile; import net.minecraftforge.mcmaven.impl.tasks.RecompileTask; import net.minecraftforge.mcmaven.impl.tasks.RenameTask; import net.minecraftforge.mcmaven.impl.cache.Cache; import net.minecraftforge.mcmaven.impl.data.GradleModule; import net.minecraftforge.mcmaven.impl.mappings.Mappings; +import net.minecraftforge.mcmaven.impl.mappings.ResolvedMappings; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.ComparableVersion; import net.minecraftforge.mcmaven.impl.util.Constants; @@ -24,6 +26,9 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; + +import org.jetbrains.annotations.Nullable; + import java.io.File; import java.io.FileOutputStream; import java.io.FileWriter; @@ -69,6 +74,8 @@ // TODO [MCMavenizer][Documentation] Document public final class MCPConfigRepo extends Repo { private final Map versions = new HashMap<>(); + private record LegacyKey(Artifact artifact, @Nullable String python) {} + private final Map legacy = new HashMap<>(); private final Map mcTasks = new HashMap<>(); private final Task downloadLauncherManifest = Task.named("downloadLauncherManifest", this::downloadLauncherManifest); @@ -91,6 +98,21 @@ private MCP download(Artifact artifact) { return new MCP(this, artifact); } + public MCPLegacy legacy(String mcVersion) { + return legacy(mcVersion, null); + } + + // Variants used for Forge artifacts + public MCPLegacy legacy(String mcVersion, @Nullable String python) { + var artifact = MCPLegacy.artifact(mcVersion); + var key = new LegacyKey(artifact, python); + return this.legacy.computeIfAbsent(key, this::downloadLegacy); + } + + private MCPLegacy downloadLegacy(LegacyKey key) { + return new MCPLegacy(this, key.artifact(), key.python()); + } + public MinecraftTasks getMCTasks(String version) { return this.mcTasks.computeIfAbsent(version, _ -> new MinecraftTasks(this.cache, version, this.downloadLauncherManifest)); } @@ -140,7 +162,7 @@ private void validate(Artifact artifact) { } @Override - public List process(Artifact artifact, Mappings mappings, Map> outputJson) { + public List process(Artifact artifact, Mappings baseMappings, Map> outputJson) { validate(artifact); var version = artifact.getVersion(); @@ -154,6 +176,7 @@ public List process(Artifact artifact, Mappings mappings, Map process(Artifact artifact, Mappings mappings, Map classVariants(mappings, mcpSide)), + pom.withVariants(() -> classVariants(baseMappings, mcpSide)), metadata ); } - var mappingArtifacts = mappingArtifacts(build, mappings, mcpSide, outputJson); + var mappingArtifacts = mappingArtifacts(build, mappings, mcVersion, outputJson); if (isMappings) return mappingArtifacts; return switch (mappings.channel()) { - case "notch" -> List.of(pending("Classes", mcpTasks.getRawJar(), name.withClassifier("raw"), false, simpleVariant("obf-notch", new Mappings("notch", null)))); - case "srg", "searge" -> List.of(pending("Classes", mcpTasks.getSrgJar(), name.withClassifier("srg"), false, simpleVariant("obf-searge", new Mappings("searge", null)))); + case "notch" -> List.of(pending("Classes", mcpTasks.getRawJar(), name.withClassifier("raw"), false, simpleVariant("obf-notch", "notch", null))); + case "srg", "searge" -> List.of(pending("Classes", mcpTasks.getSrgJar(), name.withClassifier("srg"), false, simpleVariant("obf-searge", "searge", null))); default -> { var pending = new ArrayList(); - - var sourcesTask = new RenameTask(build, name.getName(), mcpSide, mcpSide.getSources(), mappings, true); - var recompile = new RecompileTask(build, name, mcpSide.getMCP(), mcpSide::getClasspath, sourcesTask, mappings); + var srgTask = mcpSide.getTasks().getMappings(); + var jdks = this.getCache().jdks(); + var javaTarget = mcpSide.getMCP().getConfig().java_target; + var srgSources = mcpSide.getSources(); + + var sourcesTask = mappings.channel().equals("srg") + ? srgSources + : new RenameTask(build, name.getName(), srgSources, mappings, true, srgTask, mcVersion); + var recompile = new RecompileTask(build, name, jdks, javaTarget, mcpSide::getClasspath, sourcesTask, mappings); var classesTask = mergeExtra(build, side, recompile, mcpSide.getTasks().getExtra(), mappings); - var sources = pending("Sources", sourcesTask, name.withClassifier("sources"), true, sourceVariant(mappings)); - var classes = pending("Classes", classesTask, name, false, () -> classVariants(mappings, mcpSide)); + var sources = pending("Sources", sourcesTask, name.withClassifier("sources"), true, sourceVariant(baseMappings)); + var classes = pending("Classes", classesTask, name, false, () -> classVariants(baseMappings, mcpSide)); pending.addAll(List.of( sources, classes, metadata, pom @@ -232,7 +263,7 @@ public List processWithoutMcp(Artifact artifact, Mappings mappi //net.minecraft:mappings_{CHANNEL}:{MCP_VERSION}[-{VERSION}]@zip var mapCoords = Artifact.from(Constants.MC_GROUP, "mappings_official", artifact.getVersion()).withExtension("zip"); - var mapPom = pending("Mappings POM", simplePom(cache, mapCoords), mapCoords.withExtension("pom"), false); + var mapPom = pending("Mappings POM", simplePom(mappings.getFolder(cache), mapCoords), mapCoords.withExtension("pom"), false); var m2o = pending("Mappings map2obf", tasks.mergeMappings(), mapCoords.withClassifier("map2obf").withExtension("tsrg.gz"), false); if (outputJson != null) { @@ -255,6 +286,56 @@ public List processWithoutMcp(Artifact artifact, Mappings mappi } } + public List processLegacy(Artifact artifact, Mappings baseMappings, Map> outputJson) { + if (!Constants.MC_GROUP.equals(artifact.getGroup())) + throw new IllegalArgumentException("MCPConfigRepo cannot process modules that aren't for group net.minecraft"); + + var side = artifact.getName(); + var version = artifact.getVersion(); + var name = Artifact.from("net.minecraft", side, version); + switch (side) { + //case "client": // Eventually add sided variants + //case "server": + case "joined": + break; + default: + throw new IllegalArgumentException("MCPConfigRepo does not support artifact: " + artifact); + } + + var jdks = this.cache.jdks(); + var mcp = this.legacy(version); + var tasks = mcp.getMinecraftTasks(); + var mcVersion = tasks.getVersion(); + var build = mcp.getBuildFolder(); + var srgTask = mcp.getMappings(); + var srgSources = mcp.getChild().getFinalStep(); + + var mappings = baseMappings.withContext(mcp); + var sourcesTask = mappings.channel().equals("srg") + ? srgSources + : new RenameTask(build, mcp.getName().getName(), srgSources, mappings, true, srgTask, mcVersion); + var javaTarget = mcp.getJavaTarget(); + var classesTask = new RecompileTask(build, name, jdks, javaTarget, mcp::getClasspath, sourcesTask, mappings); + + var mappingArtifacts = mappingArtifacts(build, mappings, mcVersion, outputJson); + + var sources = pending("Sources", sourcesTask, name.withClassifier("sources"), true, sourceVariant(baseMappings)); + var classes = pending("Classes", classesTask, name, false, () -> { + var libs = tasks.getClientLibraries().stream().map(ArtifactFile::artifact).toList(); + return classVariants(baseMappings, javaTarget, libs, List.of()); + }); + var metadata = pending("Metadata", metadata(build, artifact.getName(), tasks), name.withClassifier("metadata").withExtension("zip"), false, metadataVariant()); + var pom = pending("Maven POM", tasks.joinedPom(), name.withExtension("pom"), false); + + if (outputJson != null) + outputJson.put("mc.version", tasks::getVersion); + + var ret = new ArrayList(); + ret.addAll(mappingArtifacts); + ret.addAll(List.of(sources, classes, pom, metadata)); + return ret; + } + public List processExtra(String module, String version) { if (!module.startsWith("net.minecraft:")) throw new IllegalArgumentException("MCPConfigRepo cannot process modules that aren't for group net.minecraft"); @@ -277,7 +358,7 @@ public List processExtra(String module, String version) { } // TODO [MCMavenizer][client-extra] Band-aid fix for merging for clean! Remove later. - private static Task mergeExtra(File build, String side, Task recompiled, Task extra, Mappings mappings) { + private static Task mergeExtra(File build, String side, Task recompiled, Task extra, ResolvedMappings mappings) { return Task.named("mergeExtra[" + side + "][" + mappings + ']', Task.deps(extra, recompiled), () -> { var output = new File(mappings.getFolder(build), "recompiled-extra.jar"); var recompiledF = recompiled.execute(); @@ -395,7 +476,7 @@ private GradleModule.Variant[] classVariantsClient(Mappings mappings, MinecraftT var json = JsonData.minecraftVersion(tasks.versionJson.execute()); for (var lib : json.getLibs()) deps.add(Artifact.from(lib.coord).withOS(lib.os)); - return classVariants(mappings, tasks.versionJson, deps, List.of()); + return classVariants(mappings, tasks.getJavaVersion(), deps, List.of()); } private GradleModule.Variant[] classVariantsServer(Mappings mappings, MinecraftTasks tasks) { @@ -412,6 +493,6 @@ private GradleModule.Variant[] classVariantsServer(Mappings mappings, MinecraftT } catch (IOException e) { return Util.sneak(e); } - return classVariants(mappings, tasks.versionJson, deps, List.of()); + return classVariants(mappings, tasks.getJavaVersion(), deps, List.of()); } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPLegacy.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPLegacy.java new file mode 100644 index 0000000..69fba2a --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPLegacy.java @@ -0,0 +1,1277 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.repo.mcpconfig; + +import java.io.BufferedReader; +import java.io.BufferedWriter; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.FileWriter; +import java.io.IOException; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.zip.ZipEntry; +import java.util.zip.ZipFile; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.jetbrains.annotations.Nullable; + +import com.google.gson.reflect.TypeToken; + +import net.minecraftforge.mcmaven.impl.Mavenizer; +import net.minecraftforge.mcmaven.impl.cache.Cache; +import net.minecraftforge.mcmaven.impl.cache.JDKCache; +import net.minecraftforge.mcmaven.impl.cache.MavenCache; +import net.minecraftforge.mcmaven.impl.repo.forge.FG2Userdev; +import net.minecraftforge.mcmaven.impl.repo.forge.FGVersion; +import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MinecraftTasks.MCFile; +import net.minecraftforge.mcmaven.impl.util.Artifact; +import net.minecraftforge.mcmaven.impl.util.ComparableVersion; +import net.minecraftforge.mcmaven.impl.util.Constants; +import net.minecraftforge.mcmaven.impl.util.NewLineDetector; +import net.minecraftforge.mcmaven.impl.util.ProcessUtils; +import net.minecraftforge.mcmaven.impl.util.StupidHacks; +import net.minecraftforge.mcmaven.impl.util.Task; +import net.minecraftforge.mcmaven.impl.util.Util; +import net.minecraftforge.srgutils.IMappingFile; +import net.minecraftforge.util.data.json.JsonData; +import net.minecraftforge.util.download.DownloadUtils; +import net.minecraftforge.util.file.FileUtils; +import net.minecraftforge.util.hash.HashFunction; + +public class MCPLegacy { + public static Artifact artifact(String version) { + return Artifact.from("de.oceanlabs.mcp", "mcp", version, "srg", "zip"); + } + + private final MCPConfigRepo repo; + private final Artifact name; + private final boolean isPython; + private final String mcVersion; + private final ComparableVersion mcVersionComp; + private final File data; + private final String dataHash; + private final File build; + private final MinecraftTasks mcTasks; + + private final Map children = new HashMap<>(); + private final Map extracts = new HashMap<>(); + private final Task mappings; + private final Task exceptorJson; + private final Task exceptorConfig; + private final Task statics; + private final Task listLibraries; + + private Child child; + + public MCPLegacy(MCPConfigRepo repo, Artifact name, @Nullable String python) { + this.repo = repo; + this.isPython = python != null; + this.mcVersion = name.getVersion(); + this.mcVersionComp = new ComparableVersion(this.mcVersion); + + if (python != null) { + this.name = name.withVersion(name.getVersion() + '-' + python.replace(".zip", "")).withClassifier(null); + this.data = new File(this.repo.getCache().root(), "mcp/legacy/" + python); + if (!this.data.exists()) { + if (!DownloadUtils.tryDownloadFile(this.data, Constants.MCP_DOWNLOADS + python)) { + if (this.data.exists()) + this.data.delete(); + throw new IllegalStateException("Failed to download legacy MCP " + Constants.MCP_DOWNLOADS + python); + } + } + } else { + this.name = name; + var fixed = StupidHacks.fixMCPArtifact(name); + this.data = this.repo.getCache().maven().download(fixed); + if (!this.data.exists()) + throw new IllegalStateException("Failed to download " + fixed); + } + + this.dataHash = Util.sneak(() -> HashFunction.sha1().hash(this.data)); + + this.build = new File(this.repo.getCache().root(), "mcp/" + this.name.getFolder()); + this.mcTasks = this.repo.getMCTasks(this.mcVersion); // Legacy stuff, there is no variant so use the full version + + // TODO: [Mavenizer] Maybe write a task to convert the old python to newer formated artifacts? + var prefix = this.isPython ? "conf/" : ""; + this.mappings = extract(prefix + "joined.srg"); + this.exceptorJson = extract(prefix + "exceptor.json"); + this.exceptorConfig = extract(prefix + "joined.exc"); + + if (this.mcVersionComp.compareTo(MC_1_8) < 0) + this.statics = null; + else + this.statics = extract(this.isPython ? "conf/STATIC_METHODS.txt" : "static_methods.txt"); // may not exist + this.listLibraries = listLibraries(); + } + + public Artifact getName() { + return this.name; + } + + public MinecraftTasks getMinecraftTasks() { + return this.mcTasks; + } + + public Cache getCache() { + return this.repo.getCache(); + } + + public File getBuildFolder() { + return this.build; + } + + public File getData() { + return this.data; + } + + public String getDataHash() { + return this.dataHash; + } + + public Task getMappings() { + return this.mappings; + } + + public int getJavaTarget() { + // The vanilla launcher uses Java 8 for all legacy versions + // And the MCP archives have variant patches that deal with the decompile differences + // Also, modern macs simply do not have java 5 & 6 distros. So we have to use java 8 + // My bulk testing works fine, I do not set the -target version when recompiling, Not + // sure if I care to do that. + var comp = new ComparableVersion(this.mcVersion); + if (comp.compareTo(MC_1_6_1) < 0) + return 5; + if (comp.compareTo(MC_1_12) < 0) + return 6; + return 8; + } + + public List getClasspath() { + var classpath = new ArrayList(); + for (var lib : this.getMinecraftTasks().getClientLibraries()) + classpath.add(lib.file()); + + // They added annotations in 1.7.10 so we need null annotations + var comp = new ComparableVersion(this.mcVersion); + if (comp.compareTo(MC_1_7_10) >= 0) { + var artifact = Artifact.from("com.google.code.findbugs:jsr305:1.3.9"); + classpath.add(this.getCache().maven().download(artifact)); + } + + return classpath; + } + + public Child getChild() { + if (this.child == null) + this.child = new Child(this.build, null, null); + return this.child; + } + + public Child getChild(FG2Userdev forge, Task atTask, @Nullable FGVersion legacyFG) { + var accessTransformer = atTask.execute(); + var hash = Util.hash(HashFunction.sha1(), accessTransformer); + + var key = forge.getName().getName(); + if (legacyFG != null) + key += '-' + legacyFG.toString(); + key += '/' + hash; + + var ret = this.children.get(key); + if (ret == null) { + var base = new File(this.build, key); + ret = new Child(base, accessTransformer, legacyFG); + this.children.put(key, ret); + } + return ret; + } + + private String prefix() { + return '[' + this.name.getVersion() + ']'; + } + + private Task extract(String name) { + var ret = this.extracts.get(name); + if (ret == null) { + ret = Task.named(prefix() + "extract[" + name + ']', () -> extractImpl(name)); + this.extracts.put(name, ret); + } + return ret; + } + + private File extractImpl(String name) { + var target = new File(this.build, name); + var cache = Util.cache(target) + .addKnown("data", dataHash); + + if (Mavenizer.checkCache(target, cache)) + return target; + + FileUtils.ensureParent(target); + + try (var zip = new ZipFile(data)) { + var entry = zip.getEntry(name); + if (entry == null) + throw except("Missing Data: " + name); + + try (var os = new FileOutputStream(target)) { + zip.getInputStream(entry).transferTo(os); + } + + target.setLastModified(entry.getLastModifiedTime().toMillis()); + + cache.save(); + return target; + } catch (IOException e) { + throw except("Failed to extract `" + name + '`', e); + } + } + + private RuntimeException except(String message) { + return new IllegalArgumentException("Invalid MCP Dependency: " + this.name + " - " + message); + } + private RuntimeException except(String message, Throwable t) { + return new IllegalArgumentException("Invalid MCP Dependency: " + this.name + " - " + message, t); + } + + private Task listLibraries() { + var json = this.getMinecraftTasks().versionJson; + var output = new File(this.build, "libraries.txt"); + var maven = this.repo.getCache().minecraft(); + return Task.named(prefix() + "list-libraries", + Task.deps(json), () -> MCPTaskFactory.listLibraries(json, output, maven) + ); + } + + private Task exceptorConfig() { + var base = this.exceptorConfig; + if (this.statics == null) + return base; + var statics = this.statics; + var mappings = getMappings(); + return Task.named(prefix() + "exceptor-config", + Task.deps(base, statics, mappings), () -> + exceptorConfig(base.execute(), statics.execute(), mappings.execute()) + ); + } + + private File exceptorConfig(File base, File statics, File mappings) { + var output = new File(this.build, "exceptor.exc"); + var cache = Util.cache(output) + .add("base", base) + .add("statics", statics) + .add("mappings", mappings); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var excMap = new HashMap(); + try (var reader = new BufferedReader(new FileReader(base, StandardCharsets.UTF_8))) { + for (String line; (line = reader.readLine()) != null; ) { + if (line.startsWith("#")) + excMap.put(line, null); + else { + var pts = line.split("=", 2); + excMap.put(pts[0], pts[1]); + } + } + } catch (IOException e) { + return Util.sneak(e); + } + + var staticsSet = new HashSet(); + try (var reader = new BufferedReader(new FileReader(statics, StandardCharsets.UTF_8))) { + for (String line; (line = reader.readLine()) != null; ) + staticsSet.add(line); + } catch (IOException e) { + return Util.sneak(e); + } + + // Generate default exc lines from srg + try { + var map = IMappingFile.load(mappings); + for (var cls : map.getClasses()) { + for (var mtd : cls.getMethods()) { + var name = mtd.getMapped(); + if (!name.startsWith("func_")) + continue; + + var prefix = "p_" + name.split("_", 3)[1]; + var args = new ArrayList(); + int idx = staticsSet.contains(name) ? 0 : 1; // Static methods don't have 'this' + + var chrs = mtd.getDescriptor().toCharArray(); + for (int x = 1; x < chrs.length; x++) { + var c = chrs[x]; + if (c == ')') + break; + + // its prefix + bytecode index, so need to double increment for doubles/longs + args.add(prefix + "_" + idx++ + "_"); + switch (c) { + case '[': // Arrays + while (x < chrs.length && chrs[x] == '[') + x++; + if (chrs[x] == 'L') { + while (x < chrs.length && chrs[x] != ';') + x++; + } + break; + case 'D': // double + case 'J': // long + idx++; + break; + case 'L': // object + while (x < chrs.length && chrs[x] != ';') + x++; + } + } + + if (!args.isEmpty()) { + var key = cls.getMapped() + "." + name + mtd.getMappedDescriptor(); + var info = excMap.get(key); + if (info == null) + info = ""; + else { + var offset = info.indexOf('|'); + if (offset != -1) + info = info.substring(0, offset); + } + excMap.put(key, info + '|' + String.join(",", args)); + } + } + } + } catch (IOException e) { + return Util.sneak(e); + } + + FileUtils.ensureParent(output); + try (var out = new BufferedWriter(new FileWriter(output, StandardCharsets.UTF_8))) { + var keys = new ArrayList<>(excMap.keySet()); + Collections.sort(keys); + + for (var key : keys) { + out.write(key); + var value = excMap.get(key); + if (value != null) { + out.write('='); + out.write(value); + } + out.write('\n'); + } + } catch (IOException e) { + return Util.sneak(e); + } + + cache.save(); + return output; + } + + private Task rename() { + var merged = merge(); + var mappings = this.getMappings(); + return Task.named(prefix() + "rename", + Task.deps(merged, mappings), () -> this.rename(merged.execute(), mappings.execute()) + ); + } + + // We used to use SpecialSource, but im going to try with renamer, it should be fine... + private File rename(File input, File mappings) { + var tool = this.repo.getCache().maven().download(Constants.RENAMER); + var output = new File(this.build, "joined-renamed.jar"); + var log = new File(this.build, "joined-renamed.log"); + var cache = Util.cache(output) + .add("tool", tool) + .add("input", input) + .add("mappings", mappings); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var args = List.of( + "--input", input.getAbsolutePath(), + "--output", output.getAbsolutePath(), + "--map", mappings.getAbsolutePath(), + "--disable-abstract-param" // this is taken care of by the cleanup task + ); + + var jdk = this.repo.getCache().jdks().tryGet(Constants.RENAMER_JAVA_VERSION); + var ret = ProcessUtils.runJar(jdk, this.build, log, tool, Collections.emptyList(), args); + if (ret.exitCode != 0) + throw new IllegalStateException("Failed to run Renamer (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + cache.save(); + return output; + } + + private Task merge() { + var client = this.getMinecraftTasks().versionFile(MCFile.CLIENT_JAR); + var server = splitServer(); + return Task.named(prefix() + "merge", + Task.deps(client, server), () -> this.merge(client, server) + ); + } + + private File merge(Task clientTask, Task serverTask) { + var tool = this.repo.getCache().maven().download(Constants.LEGACY_MERGETOOL); + var client = clientTask.execute(); + var server = serverTask.execute(); + var output = new File(this.build, "joined.jar"); + var log = new File(this.build, "joined.log"); + var cache = Util.cache(output) + .add("tool", tool) + .add("client", client) + .add("server", server); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var args = List.of( + "--merge", + "--client", client.getAbsolutePath(), + "--server", server.getAbsolutePath(), + "--output", output.getAbsolutePath(), + "--inject", + "--ann", this.mcVersion, // Pick based on MC version + //"--ann", "NMF", // net.minecraftforge.fml.relauncher + "--keep-data" + ); + + var jdk = this.repo.getCache().jdks().tryGet(Constants.LEGACY_MERGETOOL_JAVA_VERSION); + var ret = ProcessUtils.runJar(jdk, this.build, log, tool, Collections.emptyList(), args); + if (ret.exitCode != 0) + throw new IllegalStateException("Failed to run MergeTool (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + cache.save(); + return output; + } + + private Task splitServer() { + var input = this.getMinecraftTasks().extractServer(); + return Task.named(prefix() + "splitServer", + Task.deps(input), () -> this.splitServer(input.execute()) + ); + } + + private static final String[] LEGACY_SPLIT_SERVER = new String[] { + "org/bouncycastle/", + "org/apache/", + "com/google/", + "com/mojang/authlib/", + "com/mojang/util/", + "gnu/trove/", + "io/netty/", + "javax/annotation/", + "argo/", + "it/" + }; + private static final String LEGACY_SPLIT_SERVER_HASH = HashFunction.sha1().hash(String.join("\0", LEGACY_SPLIT_SERVER)); + /// Typically we would want to split the server based on the whitelist from the mapping file + /// But I haven't written the unit tests to verify that all the old versions are correct with that + /// And FG2 just uses wildcards. + /// So i'm being lazy and just copying the whildcards + private File splitServer(File input) { + var output = new File(this.build, "server-split.jar"); + var cache = Util.cache(output) + .add("input", input) + .addKnown("filter", LEGACY_SPLIT_SERVER_HASH); + + if (Mavenizer.checkCache(output, cache)) + return output; + + FileUtils.ensureParent(output); + + try (var zin = new ZipInputStream(new FileInputStream(input)); + var zout = new ZipOutputStream(new FileOutputStream(output))) { + for (ZipEntry entry; (entry = zin.getNextEntry()) != null; ) { + boolean filtered = false; + for (var filter : LEGACY_SPLIT_SERVER) { + if (entry.getName().startsWith(filter)) { + filtered = true; + break; + } + } + if (filtered) + continue; + + var newEntry = new ZipEntry(entry.getName()); + newEntry.setSize(entry.getSize()); + newEntry.setTime(entry.getTime()); + zout.putNextEntry(newEntry); + zin.transferTo(zout); + zout.closeEntry(); + } + } catch (IOException e) { + return Util.sneak(e); + } + + cache.save(); + return output; + } + + public class Child { + private final MavenCache maven; + private final JDKCache jdks; + private final File build; + private final File accessTransformer; + private final @Nullable FGVersion legacyFG; + private final String prefix; + private final MCPCfg cfg; + private final Task inject; + + public Child(File build, File accessTransformer, @Nullable FGVersion legacyFG) { + this.maven = MCPLegacy.this.repo.getCache().maven(); + this.jdks = MCPLegacy.this.repo.getCache().jdks(); + this.build = build; + this.accessTransformer = accessTransformer; + this.legacyFG = legacyFG; + this.prefix = MCPLegacy.this.prefix(); + this.cfg = MCPCfg.get(MCPLegacy.this.mcVersionComp, legacyFG); + this.inject = mcpInject(); + } + + public Task getFinalStep() { + return this.inject; + } + + private Task mcpInject() { + var input = cleanup(); + return Task.named(prefix + "mcp-inject", + Task.deps(input), () -> mcpInject(input.execute()) + ); + } + + private File mcpInject(File input) { + var output = new File(this.build, "mcp-inject.jar"); + var cache = Util.cache(output) + .add("input", input) + .addKnown("mcp", MCPLegacy.this.dataHash); + + if (Mavenizer.checkCache(output, cache)) + return output; + + FileUtils.ensureParent(output); + if (output.exists()) + output.delete(); + + try (var zout = new ZipOutputStream(new FileOutputStream(output))) { + + var packages = new HashSet(); + // Copy everything from one jar to the other, gathering java packages + try (var zin = new ZipInputStream(new FileInputStream(input))) { + for (ZipEntry entry; (entry = zin.getNextEntry()) != null; ) { + var name = entry.getName(); + + var newEntry = new ZipEntry(name); + newEntry.setTime(entry.getTime()); + zout.putNextEntry(newEntry); + zin.transferTo(zout); + zout.closeEntry(); + + if (name.endsWith(".java")) { + var idx = name.lastIndexOf('/'); + packages.add(name.substring(0, idx)); + } + } + } + + var prefix = MCPLegacy.this.isPython ? "conf/patches/inject/" : "patches/inject/"; + try (var zin = new ZipInputStream(new FileInputStream(MCPLegacy.this.data))) { + for (ZipEntry entry; (entry = zin.getNextEntry()) != null; ) { + if ((prefix + "package-info-template.java").equals(entry.getName())) { + var template = new String(zin.readAllBytes(), StandardCharsets.UTF_8); + for (var pkg : packages) { + var name = pkg + "/package-info.java"; + zout.putNextEntry(new ZipEntry(FileUtils.getStableEntry(name))); + zout.write(template.replaceAll("\\{PACKAGE\\}", pkg.replace('/', '.')).getBytes(StandardCharsets.UTF_8)); + zout.closeEntry(); + } + } else if (entry.getName().startsWith(prefix + "common/")) { + if (entry.isDirectory()) + continue; + var name = entry.getName().substring(22); + zout.putNextEntry(new ZipEntry(FileUtils.getStableEntry(name))); + zin.transferTo(zout); + zout.closeEntry(); + } + } + } + } catch (IOException e) { + return Util.sneak(e); + } + + cache.save(); + return output; + } + + private Task cleanup() { + var input = this.decompile(); + return Task.named(prefix + "cleanup", + Task.deps(input), () -> cleanup(input.execute()) + ); + } + + private File cleanup(File input) { + var cleanup = this.cfg.cleanup; + var tool = maven.download(cleanup.artifact); + var output = new File(this.build, "cleaned.jar"); + var log = new File(this.build, "cleaned.log"); + var cache = Util.cache(output) + .add("tool", tool) + .add("input", input) + .add("args", String.join(", ", cleanup.args)) + .addKnown("mcp", MCPLegacy.this.dataHash); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var args = new ArrayList(List.of( + "--input", input.getAbsolutePath(), + "--output", output.getAbsolutePath(), + "--patches", MCPLegacy.this.data.getAbsolutePath(), + "--patches-prefix", (MCPLegacy.this.isPython ? "conf/" : "") + "patches/minecraft_merged_ff/" + )); + /* + if (MCPLegacy.this.child != this) // We're on a Forge version, so pull out the side annotations and inject it later + args.add("--filter-fml"); + */ + args.addAll(cleanup.args); + + var jdk = jdks.tryGet(Constants.MCPCLEANUP_JAVA_VERSION); + var ret = ProcessUtils.runJar(jdk, this.build, log, tool, List.of(), args); + if (ret.exitCode != 0) + throw new IllegalStateException("Failed to run MCPCleanup (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + cache.save(); + return output; + } + + private Task writeSorts() { + if (this.cfg.decompiler.sorts == null) + return null; + return Task.named(prefix + "sort", () -> writeSortsImpl()); + } + + private File writeSortsImpl() { + var output = new File(this.build, "sorts.cfg"); + var sorts = new StringBuilder(); + for (var sort : this.cfg.decompiler.sorts) { + sorts.append("-sort=") + .append(sort) + .append('\n'); + } + var cache = Util.cache(output) + .add("sorts", sorts.toString()); + + if (Mavenizer.checkCache(output, cache)) + return output; + + FileUtils.ensureParent(output); + try { + Files.writeString(output.toPath(), sorts); + } catch (IOException e) { + return Util.sneak(e); + } + + cache.save(); + return output; + } + + private Task decompile() { + var input = this.exceptor(); + var libraries = MCPLegacy.this.listLibraries; + var sorts = this.writeSorts(); + return Task.named(prefix + "decompile", + Task.deps(input, libraries, sorts), () -> decompile(input.execute(), libraries.execute(), sorts == null ? null : sorts.execute()) + ); + } + + private File decompile(File input, File libraries, File sorts) { + var output = new File(this.build, "decompiled.jar"); + var log = new File(this.build, "decompiled.log"); + + var decompiler = this.cfg.decompiler; + + var tool = maven.download(decompiler.artifact); + + int java_version = MCPLegacy.this.getJavaTarget(); + // Mac OSX doesn't have Java below 6, so we must use 8. + if (java_version < 8) + java_version = 8; + + var cache = Util.cache(output) + .add("input", input) + .add("tool", tool) + .add("args", String.join(", ", decompiler.args)) + .addKnown("java", Integer.toString(java_version)); + + if (sorts != null) + cache.add("sorts", sorts); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var jvm = List.of("-Xmx3G"); // This is what FG2.3 has, older versions have 512M, we're on modern machines 3G should be fine + + var temp = output; + List run; + if (decompiler.legacy) { + // Older versions don't support directly outputting to a single jar file, they require a directory and will output to dir\{input_file_name} + var dir = new File(this.build, "decompile-legacy-temp"); + if (!dir.exists()) + dir.mkdir(); + temp = new File(dir, input.getName()); + + run = new ArrayList(decompiler.args.size()); + if (sorts != null) + run.addAll(List.of("-cfg", sorts.getAbsolutePath())); + + for (var arg : decompiler.args) { + if ("{libraries}".equals(arg)) { + // This is a version that doens't support the -cfg arg + // So we have to specify all the libraries via command line + // But this could easily explode to more then the max command size + // So we need to move all libraries into a common directory and use relative names + var workDir = log.getParentFile(); + var libsDir = new File(workDir, "decompile-libs"); + try { + var libs = Files.readAllLines(libraries.toPath()); + if (!libsDir.exists()) + libsDir.mkdirs(); + for (var lib : libs) { + if (lib.startsWith("-e=")) { + var source = new File(lib.substring(3)); + var target = new File(libsDir, source.getName()); + if (!target.exists()) // This doesn't check hashes, but IDGF + Files.copy(source.toPath(), target.toPath()); + run.add("-e=decompile-libs/" + target.getName()); + } else { + run.add(lib); // Shouldn't happen, but just in case + } + } + } catch (IOException e) { + return Util.sneak(e); + } + } else { + run.add(arg); + } + } + run.add(input.getAbsolutePath()); + run.add(dir.getAbsolutePath()); + } else { + run = new ArrayList(decompiler.args); + if (sorts != null) + run.addAll(List.of("-cfg", sorts.getAbsolutePath())); + run.addAll(List.of( + "-cfg", libraries.getAbsolutePath(), + input.getAbsolutePath(), + output.getAbsolutePath() + )); + } + + if (temp.exists()) + temp.delete(); + + var jdk = jdks.tryGet(java_version); + //jdk = new File("C:\\Program Files\\java\\jdk1.7.0_80"); + var ret = StupidHacks.runDecompiler(jdk, log, tool, jvm, run); + if (ret.exitCode != 0 || !temp.exists()) + throw new IllegalStateException("Failed to run decompiler (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + // Move the decompiled code to the output file if we're using legacy code + if (temp != output) { + try { + Files.move(temp.toPath(), output.toPath(), StandardCopyOption.REPLACE_EXISTING); + } catch (IOException e) { + return Util.sneak(e); + } + } + + cache.save(); + return output; + } + + private Task exceptor() { + var input = this.modifyAccess(); + var config = MCPLegacy.this.exceptorConfig(); + var json = this.cfg.exceptor.json ? this.modifyExceptorAccess(input) : null; + return Task.named(prefix + "exceptor", + Task.deps(input, config, json), () -> this.exceptor(input.execute(), config.execute(), json) + ); + } + + // We used to use SpecialSource, but im going to try with renamer, it should be fine... + private File exceptor(File input, File config, Task jsonTask) { + var except = this.cfg.exceptor; + var json = jsonTask == null ? null : jsonTask.execute(); + var tool = maven.download(Constants.MCINJECTOR); + var output = new File(this.build, "joined-mci.jar"); + var log = new File(this.build, "joined-mci.log"); + var internalLog = new File(this.build, "joined-mci-internal.log"); + var customArgs = new ArrayList(); + if (except.generate) + customArgs.add("--generateParams"); + if (except.markers) + customArgs.add("--applyMarkers"); + if (except.lvt) + customArgs.addAll(List.of("--lvt", "LVT")); + + var cache = Util.cache(output) + .add("tool", tool) + .add("input", input) + .add("config", config) + .add("args", String.join(", ", customArgs)); + if (json != null) + cache.add("json", json); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var args = new ArrayList<>(List.of( + "--jarIn", input.getAbsolutePath(), + "--jarOut", output.getAbsolutePath(), + "--mapIn", config.getAbsolutePath(), + "--log", internalLog.getAbsolutePath() // Annoying but we have to have two logs to actually have the logs be written + )); + args.addAll(customArgs); + if (json != null) + args.addAll(List.of("--jsonIn", json.getAbsolutePath())); + + var jdk = jdks.tryGet(Constants.MCINJECTOR_JAVA_VERSION); + var ret = ProcessUtils.runJar(jdk, this.build, log, tool, Collections.emptyList(), args); + if (ret.exitCode != 0) + throw new IllegalStateException("Failed to run MCInjector (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + internalLog.delete(); // this is a duplicate file, everything is in the main log + + cache.save(); + return output; + } + + private Task modifyExceptorAccess(Task inputTask) { + var config = MCPLegacy.this.exceptorJson; + return Task.named("modifyExceptorAccess", + Task.deps(inputTask, config), () -> this.modifyExceptorAccess(inputTask.execute(), config.execute()) + ); + } + + private File modifyExceptorAccess(File input, File config) { + var output = new File(this.build, "exceptor-ated.json"); + var cache = Util.cache(output) + .add("input", input) + .add("config", config); + + if (this.accessTransformer != null) + cache.add("at", this.accessTransformer); + + if (Mavenizer.checkCache(output, cache)) + return output; + + try { + var struct = JsonData.fromJson(config, MCI_CONFIG); + + // Fix the inner class access that we modify + if (this.accessTransformer != null) { + for (var line : Files.readAllLines(this.accessTransformer.toPath(), StandardCharsets.UTF_8)) { + var idx = line.indexOf('#'); + if (idx != -1) + line = line.substring(0, idx); + line = line.trim().replace('.', '/'); + if (line.isEmpty()) + continue; + + var pts = line.split(" "); + idx = pts[1].indexOf('$'); + if (pts.length != 2 || idx == -1) + continue; + + var access = pts[0]; + var cls = pts[1]; + + var parent = pts[1].substring(0, idx); + for (var cfg : new MCInjectorConfig[] { struct.get(parent), struct.get(pts[1]) }) { + if (cfg == null || cfg.innerClasses == null) + continue; + for (var inner : cfg.innerClasses) { + if (cls.equals(inner.inner_class)) + inner.fixAccess(access); + } + } + } + } + + // remove any inner classes that doesn't exist.. Not sure why we do this but old code is old + try (var zip = new ZipFile(input)) { + for (var itr = struct.entrySet().iterator(); itr.hasNext(); ) { + var entry = itr.next(); + var outerName = entry.getKey(); + var cfg = entry.getValue(); + + if (zip.getEntry(outerName + ".class") == null) { + itr.remove(); + continue; + } + + if (cfg.innerClasses == null) + continue; + + for (var inner = cfg.innerClasses.iterator(); inner.hasNext(); ) { + var innerName = inner.next().inner_class; + if (zip.getEntry(innerName + ".class") == null) + inner.remove(); + } + } + } + + JsonData.toJson(struct, output); + cache.save(); + return output; + } catch (IOException e) { + return Util.sneak(e); + } + } + + private Task modifyAccess() { + var renamed = MCPLegacy.this.rename(); + if (this.accessTransformer == null) + return renamed; + + // Obfed access transformers, we need to remap them + var config = MCPLegacy.this.mcVersionComp.compareTo(MC_1_6_4) <= 0 + ? renameAts() + : null; + + return Task.named(prefix + "modifyAccess", + Task.deps(renamed, config), () -> this.modifyAccess(renamed.execute(), config == null ? null : config.execute()) + ); + } + + private File modifyAccess(File input, @Nullable File config) { + var tool = maven.download(Constants.ACCESS_TRANSFORMER); + var output = new File(this.build, "modify-access.jar"); + var log = new File(this.build, "modify-access.log"); + if (config == null) + config = this.accessTransformer; + + var cache = Util.cache(output) + .add("tool", tool) + .add("input", input) + .add("config", config); + + if (Mavenizer.checkCache(output, cache)) + return output; + + var args = List.of( + "--inJar", input.getAbsolutePath(), + "--outJar", output.getAbsolutePath(), + "--atfile", config.getAbsolutePath(), + "--ignore-invalid" // Needed for some old Forge versions with invalid lines + ); + + var jdk = jdks.tryGet(Constants.ACCESS_TRANSFORMER_JAVA_VERSION); + var ret = ProcessUtils.runJar(jdk, this.build, log, tool, Collections.emptyList(), args); + if (ret.exitCode != 0) + throw new IllegalStateException("Failed to run Access Transformer (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); + + cache.save(); + return output; + } + + private Task renameAts() { + var mappings = MCPLegacy.this.getMappings(); + + return Task.named(prefix + "renameAts", + Task.deps(mappings), () -> this.renameAts(mappings.execute()) + ); + } + + // Obfusciated access transformers for 1.6.4 and below + // Uses an old format that AccessTransformer doesn't support, so I transform it here, fairly easy to do + // https://github.com/MinecraftForge/FML/blob/f1b3381e61fac1a0ae90f521223c6bc613eb4888/common/cpw/mods/fml/common/asm/transformers/AccessTransformer.java#L120 + // + // If we reworked the process to apply ATs before renaming, then all that really needs to be done is to replace the first . with a space + // But This is simple enough to do and should work + private File renameAts(File mappings) { + var output = new File(this.build, "renamed-at.cfg"); + var cache = Util.cache(output) + .add("input", this.accessTransformer) + .add("mappings", mappings); + + if (Mavenizer.checkCache(output, cache)) + return output; + + FileUtils.ensureParent(output); + try (var out = new FileOutputStream(output); + var inf = new FileInputStream(this.accessTransformer) + ) { + var map = IMappingFile.load(mappings); + var reader = new NewLineDetector(new InputStreamReader(inf)); + + String line; + while ((line = reader.readLine()) != null) { + String comment = null; + int idx = line.indexOf('#'); + if (idx != -1) { + comment = line.substring(idx); + line = idx == 0 ? "" : line.substring(0, idx); + } + + line = line.trim(); + if (!line.isEmpty()) { + idx = line.indexOf(' '); + if (idx == -1) { // Must have both access and traget + Mavenizer.LOGGER.debug("Invalid access transformer line: " + line); + continue; + } + + var target = line.substring(idx + 1); + var transformation = line.substring(0, idx + 1); + idx = target.indexOf('.'); + + // Class target + if (idx == -1) { + line = transformation + map.remapClass(target); + } else { + var cls = target.substring(0, idx); + target = target.substring(idx + 1); + var mcls = map.getClass(cls); + + // There are no mappings for this class, so assume its not obfed + if (mcls != null) { + idx = target.indexOf('('); + + if (idx == -1) { // Field + line = transformation + map.remapClass(cls) + ' ' + mcls.remapField(target); + } else { // Method + var name = target.substring(0, idx); + var desc = target.substring(idx); + line = transformation + map.remapClass(cls) + ' ' + mcls.remapMethod(name, desc) + map.remapDescriptor(desc); + } + } + } + } + + + out.write(line.getBytes(StandardCharsets.UTF_8)); + if (comment != null) { + if (!line.isEmpty()) + out.write(' '); + out.write(comment.getBytes(StandardCharsets.UTF_8)); + } + out.write('\n'); + } + } catch (IOException e) { + return Util.sneak(e); + } + + cache.save(); + return output; + } + } + + private static final TypeToken> MCI_CONFIG = new TypeToken<>() {}; + public record MCInjectorConfig(@Nullable EnclosingMethod enclosingMethod, @Nullable ArrayList innerClasses) { + public record EnclosingMethod(String desc, String name, String owner) { } + public static class InnerClass { + public @Nullable String access; + public String inner_class; + public String inner_name; + public String outer_class; + public @Nullable String start; + + private static final int ACC_DEFAULT = 0x0000; + private static final int ACC_PUBLIC = 0x0001; + private static final int ACC_PRIVATE = 0x0002; + private static final int ACC_PROTECTED = 0x0004; + private static final int ACC_FINAL = 0x0010; + + void fixAccess(String wanted) { + int access = this.access == null ? 0 : Integer.parseInt(this.access, 16); + int ret = access & ~7; + int t = 0; + + if (wanted.startsWith("public")) + t = ACC_PUBLIC; + else if (wanted.startsWith("private")) + t = ACC_PRIVATE; + else if (wanted.startsWith("protected")) + t = ACC_PROTECTED; + + ret |= switch (access & 7) { + case ACC_PRIVATE -> t; + case ACC_DEFAULT -> (t != ACC_PRIVATE ? t : ACC_DEFAULT); + case ACC_PROTECTED -> (t != ACC_PRIVATE && t != ACC_DEFAULT ? t : ACC_PROTECTED); + case ACC_PUBLIC -> ACC_PUBLIC; + default -> access & 7; + }; + + if (wanted.endsWith("-f")) + ret &= ~ACC_FINAL; + else if (wanted.endsWith("+f")) + ret |= ACC_FINAL; + this.access = ret == 0 ? null : Integer.toHexString(ret); + } + } + } + + // The main difference between FG versions was the decompiler we invoked/embeded + private static final ComparableVersion MC_1_6_1 = new ComparableVersion("1.6.1"); + private static final ComparableVersion MC_1_6_4 = new ComparableVersion("1.6.4"); + private static final ComparableVersion MC_1_7_2 = new ComparableVersion("1.7.2"); + private static final ComparableVersion MC_1_7_10 = new ComparableVersion("1.7.10"); + private static final ComparableVersion MC_1_8 = new ComparableVersion("1.8"); + private static final ComparableVersion MC_1_8_8 = new ComparableVersion("1.8.8"); + private static final ComparableVersion MC_1_8_9 = new ComparableVersion("1.8.9"); + private static final ComparableVersion MC_1_9 = new ComparableVersion("1.9"); + private static final ComparableVersion MC_1_9_4 = new ComparableVersion("1.9.4"); + private static final ComparableVersion MC_1_12 = new ComparableVersion("1.12"); + + private record Exceptor(boolean json, boolean markers, boolean lvt, boolean generate) { + private static Exceptor get(ComparableVersion mc) { + var json = true; + var markers = true; + var lvt = false; + var generate = true; + + if (mc.compareTo(MC_1_6_4) <= 0) + json = false; + + if (mc.compareTo(MC_1_8_8) >= 0) { + json = false; + lvt = true; + } + + if (mc.compareTo(MC_1_9) >= 0) + markers = false; + if (mc.compareTo(MC_1_7_2) <= 0) + generate = false; + return new Exceptor(json, markers, lvt, generate); + } + } + private record Cleanup(Artifact artifact, List args) { + private static Cleanup get(ComparableVersion mc) { + var args = List.of(); + if (mc.compareTo(MC_1_8_9) >= 0) { + args = List.of(); + } else if (mc.compareTo(MC_1_8_8) >= 0) { + args = List.of("--fix-generic-params");//, "--fernflower", "--enum-args=false", "--enum-constructors=false"); + } else if (mc.compareTo(MC_1_8) >= 0) { + args = List.of("--fix-generic-params", "--fml", "--fernflower", + // These patches are enum sythetic arg fixes which cleaup does in a generic way + "--ignore-patch", "net/minecraft/network/EnumConnectionState.java", + "--ignore-patch", "net/minecraft/util/EnumChatFormatting.java", + "--ignore-patch", "net/minecraft/util/EnumParticleTypes.java" + ); + } else if (mc.compareTo(MC_1_7_10) >= 0) { + args = List.of("--fix-generic-params", "--fml", "--fernflower"); + } else if (mc.compareTo(MC_1_7_2) >= 0) { + args = List.of("--mode", "1.7.2"); + } else { + args = List.of("--mode", "1.6.4"); + } + return new Cleanup(Constants.MCPCLEANUP, args); + } + } + private record Decompiler(Artifact artifact, boolean legacy, List args, List sorts) { + + private static final List DECOMPILER_ARGS = List.of( + "-din=1", // DECOMPILE_INNER + "-dgs=1", // DECOMPILE_GENERIC_SIGNATURES + "-asc=1", // ASCII_STRING_CHARACTERS + "-iec=1", // INCLUDE_ENTIRE_CLASSPATH + "-rsy=1", // REMOVE_SYNTHETIC + "-rbr=1", // REMOVE_BRIDGE + "-lit=0", // LITERALS_AS_IS + // UNIT_TEST_MODE = 0 + "-mpm=0", // MAX_PROCESSING_METHOD + //"-jvn=1", // Use JAD Renamer, FG3 has an 'advanced' renamer which renames abstract names, but we can do that in MCPCleanup + "--var-renamer", "mcp", // This is the 'Advanced' renamer from FG3 + "--disable-buffer-casts" // Disables the java9 fixes for Buffer methods, it should be fine because we recompile with java 8. + ); + private static final List DECOMPILER_ARGS_18 = List.of( + "-din=1", // DECOMPILE_INNER + "-rbr=0", // REMOVE_BRIDGE + "-dgs=1", // DECOMPILE_GENERIC_SIGNATURES + "-asc=1", // ASCII_STRING_CHARACTERS + "-log=WARN" + ); + /* + private static final List DECOMPILER_ARGS_188 = List.of( + "-din=1", // DECOMPILE_INNER + "-rbr=1", // REMOVE_BRIDGE + "-dgs=1", // DECOMPILE_GENERIC_SIGNATURES + "-asc=1", // ASCII_STRING_CHARACTERS + "-rsy=1", // REMOVE_SYNTHETIC + "-iec=1", // INCLUDE_ENTIRE_CLASSPATH + "-jvn=1", // Use JAD Renamer + "-log=WARN", + "{libraries}" // Will be expanded to a list of all libraries using -e= + ); + */ + + private static String sort(String... args) { + return String.join(",", args); + } + private static Decompiler get(ComparableVersion mc, @Nullable FGVersion legacyFG) { + if (mc.compareTo(MC_1_12) >= 0) + return new Decompiler(Constants.FERNFLOWER_FG_2_3, false, DECOMPILER_ARGS, null); + else if (mc.compareTo(MC_1_8_8) >= 0) + return new Decompiler(Constants.FERNFLOWER_FG_2_2, false, DECOMPILER_ARGS, null); + else if (mc.compareTo(MC_1_8) >= 0) + return new Decompiler(Constants.FERNFLOWER_FG_2_0_LEGACY, true, DECOMPILER_ARGS_18, null); + else if (mc.compareTo(MC_1_7_2) >= 0 && legacyFG == null) // Forge 1.7.2 uses FG 1.1 and 1.2 so we need this check + return new Decompiler(Constants.FERNFLOWER_FG_2_0_LEGACY, true, DECOMPILER_ARGS_18, null); + else { + // Old Fernflower uses HashSet to iterate over inner classes + // Which the iteration order has changed a few times between java versions + // So unfortunately, we have to hardcode some specifc sorting to make patches apply + var sorts = switch (mc.toString()) { + case "1.7.2" -> List.of( + sort("net/minecraft/client/gui/inventory/GuiContainerCreative", "CreativeSlot", "ContainerCreative"), + sort("net/minecraft/client/renderer/texture/Stitcher", "Slot", "Holder"), + sort("net/minecraft/client/settings/GameSettings", "SwitchOptions", "Options"), + sort("net/minecraft/enchantment/EnchantmentHelper", "IModifier", "ModifierDamage", "ModifierLiving", "HurtIterator", "DamageIterator"), + sort("net/minecraft/entity/player/EntityPlayer", "EnumStatus", "EnumChatVisibility"), + sort("net/minecraft/world/biome/BiomeGenBase", "SpawnListEntry", "Height", "TempCategory"), + sort("net/minecraft/world/gen/structure/ComponentScatteredFeaturePieces", "Feature", "JunglePyramid", "SwampHut", "DesertPyramid"), + sort("net/minecraft/world/gen/structure/StructureMineshaftPieces", "Cross", "Room", "Corridor", "Stairs"), + sort("net/minecraft/world/gen/structure/StructureStrongholdPieces", "Stairs", "Straight", "Library", "PortalRoom", "ChestCorridor", "RoomCrossing", "StairsStraight", "Stairs2", "Prison", "LeftTurn", "RightTurn", "Stones", "Stronghold", "Stronghold$Door", "Crossing", "Corridor", "SwitchDoor", "PieceWeight"), + sort("net/minecraft/world/gen/structure/StructureVillagePieces", + "Well", "Village", "Hall", "House1", "Church", "House4Garden", "Path", + "House2", "Start", "House3", "WoodHut", "Field1", "Field2", "Road", "Torch", "PieceWeight"), + sort("net/minecraft/world/storage/MapData", "MapCoord", "MapInfo") + ); + default -> null; + }; + return new Decompiler(Constants.FERNFLOWER_FG_1_0, true, DECOMPILER_ARGS_18, sorts); + } + } + } + private record MCPCfg( + Exceptor exceptor, + Cleanup cleanup, + Decompiler decompiler + ) { + private static MCPCfg get(ComparableVersion mc, @Nullable FGVersion legacyFG) { + var except = Exceptor.get(mc); + var cleanup = Cleanup.get(mc); + var decomp = Decompiler.get(mc, legacyFG); + return new MCPCfg(except, cleanup, decomp); + } + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPTaskFactory.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPTaskFactory.java index 0eeea26..7f6e8a2 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPTaskFactory.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MCPTaskFactory.java @@ -20,7 +20,6 @@ import java.util.Set; import java.util.TreeSet; import java.util.function.BiPredicate; -import java.util.function.ToIntFunction; import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.util.jar.JarInputStream; @@ -39,6 +38,7 @@ import io.codechicken.diffpatch.util.archiver.ArchiveFormat; import net.minecraftforge.mcmaven.impl.Mavenizer; import net.minecraftforge.mcmaven.impl.cache.MavenCache; +import net.minecraftforge.mcmaven.impl.cache.MinecraftMavenCache; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.Constants; import net.minecraftforge.util.data.json.JsonData; @@ -609,11 +609,11 @@ private Task listLibraries(String name, Map step) { var json = this.side.getMCP().getMinecraftTasks().versionJson; return Task.named(name, Task.deps(json), - () -> listLibraries(json, output) + () -> listLibraries(json, output, this.side.getMCP().getCache().minecraft()) ); } - private File listLibraries(Task jsonTask, File output) { + public static File listLibraries(Task jsonTask, File output, MinecraftMavenCache minecraft) { var jsonF = jsonTask.execute(); var json = JsonData.minecraftVersion(jsonF); @@ -641,7 +641,6 @@ private File listLibraries(Task jsonTask, File output) { return output; var buf = new StringBuilder(20_000); - var minecraft = this.side.getMCP().getCache().minecraft(); for (var lib : libs) { // Sometimes natives don't have a main download if (lib.dl == null) @@ -869,31 +868,12 @@ private File execute(String name, List jvmArgs, List runAr throw new IllegalStateException("Failed to find JDK for version " + java_version, e); } - ToIntFunction logHandler = null; - if (isDecompile) { - jvm = Mavenizer.fillDecompileJvmArgs(jvm, true, true); - logHandler = MCPTaskFactory::parseDecompileLog; - } + ProcessUtils.Result ret; + if (isDecompile) + ret = StupidHacks.runDecompiler(jdk, log, tool, jvm, run); + else + ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, jvm, run); - var ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, jvm, run, logHandler); - if (isDecompile) { - if (ret.exitCode == NOT_ENOUGH_MEMORY) { - LOGGER.error("Failed to create JVM with Not Enough Memory issue, Modern minecraft requires atleast 4GB to decompile. Run it on a system with more ram."); - } else if (ret.exitCode == INVALID_INITAL_HEAP) { - LOGGER.error("Attempted to run decompile with JVM args: " + jvm + " resulted in Invalid Inital and Max heap settings."); - LOGGER.error("This is typically caused by you having a environement variable setting the global memory options, remove or set those variables to values higher then 4GB."); - } - if (ret.exitCode == OUT_OF_MEMORY || ret.exitCode == INVALID_INITAL_HEAP) { - var newJvm = Mavenizer.fillDecompileJvmArgs(resolveArgs(cache, tasks, jvmArgs), false, false); - if (!newJvm.equals(jvm)) { - LOGGER.error("First decompile failed with OutOfMemory using JVM Args: " + jvm); - LOGGER.error("Attempting again with: " + newJvm); - ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, newJvm, run, logHandler); - if (ret.exitCode == OUT_OF_MEMORY) - LOGGER.error("Ran out of memory again, you can specify more manually using the --decompile-memory Mavenizer argument"); - } - } - } if (ret.exitCode != 0) throw new IllegalStateException("Failed to run MCP Step (exit code " + ret.exitCode + "), See log: " + log.getAbsolutePath()); @@ -946,39 +926,4 @@ private List resolveArgs(HashStore cache, Map tasks, List< } return ret; } - - private static final int OUT_OF_MEMORY = -1001; - private static final int FAILED_DECOMPILE = -1002; - private static final int INVALID_INITAL_HEAP = -1003; - private static final int NOT_ENOUGH_MEMORY = -1004; - // Yes this is slow as fuck, but this is only run during a decompile run which is already slow, - // This is to check if Fernflower is broken and returning success when it really failed. - // It also eagerly exits the process when something fails so as to not waste time. - private static int parseDecompileLog(String line) { - if (line.startsWith("Initial heap size set to a larger value than the maximum heap size")) - return INVALID_INITAL_HEAP; - if (line.startsWith("Could not reserve enough space for object heap")) - return NOT_ENOUGH_MEMORY; - if (line.startsWith("java.lang.OutOfMemoryError:")) - return OUT_OF_MEMORY; - if (line.startsWith("Exception in thread") && line.contains("java.lang.OutOfMemoryError")) - return OUT_OF_MEMORY; - if (line.contains("ERROR:")) { - // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + cl.qualifiedName + " couldn't be written."; - // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + classWrapper.getClassStruct().qualifiedName + " couldn't be written."; - // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + node.classStruct.qualifiedName + " couldn't be written."; - if (line.endsWith(" couldn't be written.")) - return FAILED_DECOMPILE; - // DecompilerContext.getLogger().logError("Class " + cl.qualifiedName + " couldn't be processed.", t); - if (line.endsWith(" couldn't be processed.")) - return FAILED_DECOMPILE; - // DecompilerContext.getLogger().logError("Class " + cl.qualifiedName + " couldn't be fully decompiled.", t); - if (line.endsWith(" couldn't be fully decompiled.")) - return FAILED_DECOMPILE; - // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + classStruct.qualifiedName + " couldn't be decompiled."; - if (line.endsWith(" couldn't be decompiled.")) - return FAILED_DECOMPILE; - } - return 0; - } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MinecraftTasks.java b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MinecraftTasks.java index e8d3dec..9874ecb 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MinecraftTasks.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/repo/mcpconfig/MinecraftTasks.java @@ -20,6 +20,8 @@ import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.TransformerException; +import org.jetbrains.annotations.Nullable; + import net.minecraftforge.mcmaven.impl.Mavenizer; import net.minecraftforge.mcmaven.impl.cache.Cache; import net.minecraftforge.util.data.json.JsonData; @@ -49,6 +51,7 @@ public class MinecraftTasks { private Task renameClient; private Task renameServer; private Task clientPom; + private Task joinedPom; private Task serverPom; private Task extractServer; @@ -261,12 +264,18 @@ private File renameJar(String name, Task jarTask, Task mapTask) { public Task clientPom() { if (this.clientPom == null) - this.clientPom = Task.named("pom[" + this.version + "][client]", Task.deps(this.versionJson), this::clientPomImpl); + this.clientPom = Task.named("pom[" + this.version + "][client]", Task.deps(this.versionJson), () -> clientPomImpl("client")); return this.clientPom; } - private File clientPomImpl() { - var output = new File(this.cacheRoot, "client.pom"); + public Task joinedPom() { + if (this.joinedPom == null) + this.joinedPom = Task.named("pom[" + this.version + "][joined]", Task.deps(this.versionJson), () -> clientPomImpl("joined")); + return this.joinedPom; + } + + private File clientPomImpl(String name) { + var output = new File(this.cacheRoot, name + "pom"); var json = this.versionJson.execute(); var cache = Util.cache(output) @@ -277,7 +286,7 @@ private File clientPomImpl() { var meta = JsonData.minecraftVersion(json); - var builder = new POMBuilder("net.minecraft", "client", version) + var builder = new POMBuilder("net.minecraft", name, version) .preferGradleModule() .dependencies(deps -> { for (var lib : meta.libraries) @@ -285,13 +294,7 @@ private File clientPomImpl() { }); - FileUtils.ensureParent(output); - try (var os = new FileOutputStream(output)) { - os.write(builder.build().getBytes(StandardCharsets.UTF_8)); - } catch (IOException | ParserConfigurationException | TransformerException e) { - Util.sneak(e); - } - + write(builder, output); cache.save(); return output; } @@ -303,7 +306,7 @@ public Task serverPom() { } private File serverPomImpl() { - var output = new File(this.cacheRoot, "client.pom"); + var output = new File(this.cacheRoot, "server.pom"); var jarFile = versionFile(MCFile.SERVER_JAR).execute(); var cache = Util.cache(output) @@ -316,16 +319,18 @@ private File serverPomImpl() { var builder = new POMBuilder("net.minecraft", "server", version) .preferGradleModule() .dependencies(deps -> libs.forEach(deps::add)); + write(builder, output); + cache.save(); + return output; + } + private static void write(POMBuilder pom, File output) { FileUtils.ensureParent(output); try (var os = new FileOutputStream(output)) { - os.write(builder.build().getBytes(StandardCharsets.UTF_8)); + os.write(pom.build().getBytes(StandardCharsets.UTF_8)); } catch (IOException | ParserConfigurationException | TransformerException e) { Util.sneak(e); } - - cache.save(); - return output; } public Task extractServer() { @@ -378,7 +383,8 @@ public List getClientLibraries() { if (clientLibraries == null) { var ret = new ArrayList(); var json = JsonData.minecraftVersion(this.versionJson.execute()); - for (var lib : json.getLibs()) { + var libs = json.getLibs(); + for (var lib : libs) { //Natives don't have main download if (lib.dl == null) continue; @@ -431,4 +437,10 @@ private List getServerLibrariesBundle() { } return ret; } + + public @Nullable Integer getJavaVersion() { + var jsonTask = this.versionJson; + var json = JsonData.minecraftVersion(jsonTask.execute()); + return json.javaVersion != null ? json.javaVersion.majorVersion : null; + } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/tasks/MCPNames.java b/src/main/java/net/minecraftforge/mcmaven/impl/tasks/MCPNames.java index da9f6a2..5971151 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/tasks/MCPNames.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/tasks/MCPNames.java @@ -36,7 +36,7 @@ // TODO [Mavenizer][MCPNames] This is also in ForgeDev! Consolidate this! // TODO [Mavenizer][MCPNames] GARBAGE GARBAGE GARBAGE, CLEAN UP OR RE-IMPLEMENT -record MCPNames(String hash, Map names, Map docs) { +public record MCPNames(String hash, Map names, Map docs) { // We use \n here because we want to normalize line endings no matter the operating system we are running on. private static final String LINE_SEPERATOR = "\n"; //System.lineSeparator(); //@formatter:off @@ -59,11 +59,17 @@ private record Data(Map names, Map docs) { } private static Data loadData(File data) throws IOException { var names = new HashMap(); var docs = new HashMap(); + var candidates = Set.of("methods.csv", "fields.csv", "params.csv"); try (var zip = new ZipFile(data)) { var entries = zip.entries(); while (entries.hasMoreElements()) { var entry = entries.nextElement(); - if (!entry.getName().endsWith(".csv")) continue; + var filename = entry.getName(); + var idx = filename.lastIndexOf('/'); + if (idx != -1) + filename = filename.substring(idx + 1); + if (!candidates.contains(filename)) + continue; try (var reader = CsvReader.builder().ofNamedCsvRecord(new InputStreamReader(zip.getInputStream(entry)))) { for (var row : reader) { @@ -86,7 +92,7 @@ private static Data loadData(File data) throws IOException { return new Data(names, docs); } - static MCPNames load(File data) throws IOException { + public static MCPNames load(File data) throws IOException { var loaded = loadData(data); return new MCPNames(HashFunction.sha1().hash(data), loaded.names, loaded.docs); } @@ -264,7 +270,7 @@ private boolean injectJavadoc(List lines, String line, String _package, } /** Inserts the given javadoc line into the list of lines before any annotations */ - private static void insertAboveAnnotations(List list, String line) { + public static void insertAboveAnnotations(List list, String line) { int back = 0; while (list.get(list.size() - 1 - back).trim().startsWith("@")) back++; @@ -288,7 +294,7 @@ private String getMapped(String srg, @Nullable Set blacklist) { return ret; } - private String replaceInLine(String line, @Nullable Set blacklist) { + public String replaceInLine(String line, @Nullable Set blacklist) { var buf = new StringBuffer(); var matcher = SRG_FINDER.matcher(line); while (matcher.find()) { @@ -299,7 +305,7 @@ private String replaceInLine(String line, @Nullable Set blacklist) { return buf.toString(); } - private interface JavadocAdder { + public static final class JavadocAdder { /** * Converts a raw javadoc string into a nicely formatted, indented, and wrapped string. * @@ -309,7 +315,7 @@ private interface JavadocAdder { * doc * @return A fully formatted javadoc comment string complete with comment characters and newlines. */ - static String buildJavadoc(String indent, String javadoc, boolean multiline) { + public static String buildJavadoc(String indent, String javadoc, boolean multiline) { var builder = new StringBuilder(); // split and wrap. diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RecompileTask.java b/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RecompileTask.java index f3f43b7..1468cef 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RecompileTask.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RecompileTask.java @@ -5,8 +5,8 @@ package net.minecraftforge.mcmaven.impl.tasks; import net.minecraftforge.mcmaven.impl.Mavenizer; -import net.minecraftforge.mcmaven.impl.mappings.Mappings; -import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCP; +import net.minecraftforge.mcmaven.impl.cache.JDKCache; +import net.minecraftforge.mcmaven.impl.mappings.ResolvedMappings; import net.minecraftforge.mcmaven.impl.util.Artifact; import net.minecraftforge.mcmaven.impl.util.ProcessUtils; import net.minecraftforge.mcmaven.impl.util.Task; @@ -24,15 +24,18 @@ public final class RecompileTask implements Task { private final File build; private final Artifact name; - private final MCP mcp; + private final JDKCache jdks; + private final int javaTarget; private final Supplier> classpath; - private final Mappings mappings; + private final ResolvedMappings mappings; private final Task task; - public RecompileTask(File build, Artifact name, MCP mcp, Supplier> classpath, Task sources, Mappings mappings) { + public RecompileTask(File build, Artifact name, + JDKCache jdks, int javaTarget, Supplier> classpath, Task sources, ResolvedMappings mappings) { this.build = mappings.getFolder(build); this.name = name; - this.mcp = mcp; + this.jdks = jdks; + this.javaTarget = javaTarget; this.classpath = classpath; this.mappings = mappings; this.task = this.recompileSources(sources); @@ -63,23 +66,27 @@ protected Task recompileSources(Task input) { private File recompileSourcesImpl(Task inputTask, File output) { var cache = Util.cache(output); - var javaTarget = this.mcp.getConfig().java_target; var sourcesJar = inputTask.execute(); cache.add("sources", sourcesJar); cache.addKnown("debug", "true"); + if (this.javaTarget < 8) + cache.addKnown("java_target", Integer.toString(this.javaTarget)); if (Mavenizer.checkCache(output, cache)) return output; + // OSX doesn't have java < 8, so we need to grab 8 and set the --target argument + int jdkTarget = javaTarget < 8 ? 8 : javaTarget; + File jdk; try { - jdk = this.mcp.getCache().jdks().get(javaTarget); + jdk = this.jdks.get(jdkTarget); } catch (Exception e) { - throw new IllegalStateException("JDK not found: " + javaTarget, e); + throw new IllegalStateException("JDK not found: " + jdkTarget, e); } - ProcessUtils.recompileJar(jdk, this.classpath.get(), sourcesJar, output, true); + ProcessUtils.recompileJar(jdk, this.classpath.get(), sourcesJar, output, true, javaTarget); cache.save(); return output; diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RenameTask.java b/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RenameTask.java index e4894ed..22a69fb 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RenameTask.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/tasks/RenameTask.java @@ -5,8 +5,7 @@ package net.minecraftforge.mcmaven.impl.tasks; import net.minecraftforge.mcmaven.impl.Mavenizer; -import net.minecraftforge.mcmaven.impl.mappings.Mappings; -import net.minecraftforge.mcmaven.impl.repo.mcpconfig.MCPSide; +import net.minecraftforge.mcmaven.impl.mappings.ResolvedMappings; import net.minecraftforge.util.file.FileUtils; import net.minecraftforge.util.hash.HashFunction; import net.minecraftforge.mcmaven.impl.util.StupidHacks; @@ -31,9 +30,10 @@ */ public final class RenameTask implements Task { private final String name; - private final MCPSide side; private final boolean javadocs; + private final Task srgTask; private final Task task; + private final boolean legacy; /** * Creates a new renamer for the given patcher. @@ -43,11 +43,12 @@ public final class RenameTask implements Task { * @param sources The task that creates the unnamed sources * @param javadocs Wither to inject javadocs and rename lambda parameters, false is used for ForgeDev as we remap to SRG patches when making userdev */ - public RenameTask(File build, String name, MCPSide side, Task sources, Mappings mappings, boolean javadocs) { + public RenameTask(File build, String name, Task sources, ResolvedMappings mappings, boolean javadocs, Task srgTask, String mcVersion) { this.name = name; - this.side = side; this.javadocs = javadocs; + this.srgTask = srgTask; this.task = this.remapSources(sources, mappings.getFolder(build), mappings); + this.legacy = StupidHacks.isLegacyRenamer(mcVersion); } @Override @@ -65,11 +66,10 @@ public String name() { return this.task.name(); } - private Task remapSources(Task input, File outputDir, Mappings provider) { + private Task remapSources(Task input, File outputDir, ResolvedMappings provider) { var output = new File(outputDir, !this.javadocs ? "remapped.jar" : "remapped-javadoc.jar"); - var mappings = provider.getCsvZip(side); - var srg = this.javadocs ? side.getTasks().getMappings() : null; - var legacy = StupidHacks.isLegacyRenamer(side.getMCP().getMinecraftTasks().getVersion()); + var mappings = provider.getCsvZip(); + var srg = this.javadocs ? this.srgTask : null; return Task.named("remap[" + this.name + "][" + provider + ']' + (!this.javadocs ? "" : "[javadoc]"), Task.deps(input, mappings, srg), () -> remapSourcesImpl(input, mappings, output, srg, legacy) diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/util/Constants.java b/src/main/java/net/minecraftforge/mcmaven/impl/util/Constants.java index 15f6ebe..6a6d33e 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/util/Constants.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/util/Constants.java @@ -14,6 +14,22 @@ public final class Constants { public static final String FORGE_NAME = "forge"; public static final String FORGE_ARTIFACT = FORGE_GROUP + ':' + FORGE_NAME; public static final String FORGE_MAVEN = "https://maven.minecraftforge.net/"; + public static final String MCP_DOWNLOADS = FORGE_FILES + "mcp/"; + + // Legacy FG2 Decompilers + // This was https://files.minecraftforge.net/fernflower_temporary.zip!/fernflower.jar but that has issues with running on anything higher then java 6 + // Java 6 doesn't exist for OSX, so i've had to implement patches to fix those decompile differences. + // See https://github.com/MinecraftForge/ForgeFlower/tree/legacy-1.0 + public static final Artifact FERNFLOWER_FG_1_0 = Artifact.from("net.minecraftforge:forgeflower:0.1.0.0"); + // This was https://files.minecraftforge.net/fernflower-fix-1.0.zip!/fernflower.jar but I extracted it to make it easier to download + // This is the old obfusicated fernflower decompiler, with all of my fixes from: https://github.com/LexManos/FernFlowerFixer/ + public static final Artifact FERNFLOWER_FG_2_0_LEGACY = Artifact.from("net.minecraftforge:fernflower:0.2.0.0"); + // 2.0-SNAPSHOT used for MC 1.8.8 + public static final Artifact FERNFLOWER_FG_2_0_188 = Artifact.from("net.minecraftforge:fernflower:2.0.9"); + // 2.0-SNAPSHOT used for MC 1.9.4 + public static final Artifact FERNFLOWER_FG_2_0_194 = Artifact.from("net.minecraftforge:fernflower:2.0.17"); + public static final Artifact FERNFLOWER_FG_2_2 = Artifact.from("net.minecraftforge:forgeflower:0.2.2.0"); + public static final Artifact FERNFLOWER_FG_2_3 = Artifact.from("net.minecraftforge:forgeflower:0.2.3.1"); // TODO Other toolchains such as FMLOnly (not required, but would be useful so we have the framework to use other toolchains) public static final String FMLONLY_NAME = "fmlonly"; @@ -25,7 +41,7 @@ public final class Constants { public static final String LAUNCHER_MANIFEST = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; public static final String MOJANG_MAVEN = "https://libraries.minecraft.net/"; - public static final Artifact ACCESS_TRANSFORMER = Artifact.from("net.minecraftforge:accesstransformers:8.2.14:fatjar"); + public static final Artifact ACCESS_TRANSFORMER = Artifact.from("net.minecraftforge:accesstransformers:8.2.17:fatjar"); public static final int ACCESS_TRANSFORMER_JAVA_VERSION = 8; public static final Artifact SIDE_STRIPPER = Artifact.from("net.minecraftforge:mergetool:1.2.5:fatjar"); @@ -41,5 +57,14 @@ public final class Constants { public static final int STUBIFY_JAVA_VERSION = 25; public static final Artifact FACADE = Artifact.from("net.minecraftforge:facade:1.0.2"); - public static final int FACAD_JAVA_VERSION = 25; + public static final int FACADE_JAVA_VERSION = 25; + + public static final Artifact MCINJECTOR = Artifact.from("de.oceanlabs.mcp:mcinjector:3.4.5:fatjar"); + public static final int MCINJECTOR_JAVA_VERSION = 8; + + public static final Artifact MCPCLEANUP = Artifact.from("net.minecraftforge:mcpcleanup:2.4.5:fatjar"); + public static final int MCPCLEANUP_JAVA_VERSION = 8;; + + public static final Artifact LEGACY_MERGETOOL = Artifact.from("net.minecraftforge:mergetool:0.2.3.4:fatjar"); + public static final int LEGACY_MERGETOOL_JAVA_VERSION = 8; } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/util/ContextualPatch.java b/src/main/java/net/minecraftforge/mcmaven/impl/util/ContextualPatch.java new file mode 100644 index 0000000..e45c6f1 --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/util/ContextualPatch.java @@ -0,0 +1,680 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * Contributor(s): + * + * The Original Software is NetBeans. The Initial Developer of the Original + * Software is Sun Microsystems, Inc. Portions Copyright 1997-2007 Sun + * Microsystems, Inc. All Rights Reserved. + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + */ +package net.minecraftforge.mcmaven.impl.util; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * A stripped down version of https://github.com/cloudbees/diff4j/blob/master/src/main/java/com/cloudbees/diff/ContextualPatch.java + * With support for access based fuzzing and some helpers + */ +public final class ContextualPatch { + @SuppressWarnings("unchecked") + private static R sneak(Throwable t) throws E { + throw (E) t; + } + + // first seen in mercurial diffs: characters after the second @@ - ignore them + private final Pattern unifiedRangePattern = Pattern.compile("@@ -(\\d+)(,\\d+)? \\+(\\d+)(,\\d+)? @@(\\s.*)?"); + private final Pattern normalChangeRangePattern = Pattern.compile("(\\d+),(\\d+)c(\\d+),(\\d+)"); + private final Pattern normalAddRangePattern = Pattern.compile("(\\d+)a(\\d+),(\\d+)"); + private final Pattern normalDeleteRangePattern = Pattern.compile("(\\d+),(\\d+)d(\\d+)"); + private final Pattern binaryHeaderPattern = Pattern.compile("MIME: (.*?); encoding: (.*?); length: (-?\\d+?)"); + + private final List patches = new ArrayList(); + + private int maxFuzz = 0; + private boolean c14nWhitespace = false; + private boolean c14nAccess = false; + + private BufferedReader patchReader; + private String patchLine; + private boolean patchLineRead = true; + private int lastPatchedLine; // the last line that was successfuly patched + + public static SingleFileContext context(String data) { + return new SingleFileContext(data); + } + + public static SingleFileContext context(InputStream data) throws IOException { + return new SingleFileContext(data); + } + + public static ContextualPatch create(String patchString) { + try { + return new ContextualPatch(new BufferedReader(new StringReader(patchString))); + } catch (Throwable e) { + return sneak(e); + } + } + public static ContextualPatch create(InputStream input) throws IOException { + try { + return new ContextualPatch(new InputStreamReader(input, StandardCharsets.UTF_8)); + } catch (PatchException e) { + return sneak(e); + } + } + + private ContextualPatch(Reader reader) throws IOException, PatchException { + patchReader = new BufferedReader(reader); + for (SinglePatch patch; (patch = getNextPatch()) != null; ) + patches.add(patch); + } + + public ContextualPatch setMaxFuzz(int maxFuzz) { + this.maxFuzz = maxFuzz; + return this; + } + + public ContextualPatch setWhitespaceC14N(boolean canonicalize) { + this.c14nWhitespace = canonicalize; + return this; + } + + public ContextualPatch setAccessC14N(boolean canonicalize) { + this.c14nAccess = canonicalize; + return this; + } + + public PatchReport patchSingle(boolean dryRun, IContextProvider context) { + if (this.patches.size() != 1) + throw new IllegalStateException("Unexpected multi-patch file"); + var patch = this.patches.getFirst(); + try { + return applyPatch(patch, dryRun, context); + } catch (Exception e) { + return new PatchReport(patch.targetPath, patch.binary, PatchStatus.Failure, e, new ArrayList()); + } + } + + /** + * @param dryRun true if the method should not make any modifications to files, false otherwise + * @return List of patch reports. Never null, may be empty. + */ + public List patch(boolean dryRun, IContextProvider context) { + List report = new ArrayList(); + for (SinglePatch patch : patches) { + try { + report.add(applyPatch(patch, dryRun, context)); + //report.add(new PatchReport(patch.targetFile, computeBackup(patch.targetFile), patch.binary, PatchStatus.Patched, null)); + } catch (Exception e) { + report.add(new PatchReport(patch.targetPath, patch.binary, PatchStatus.Failure, e, new ArrayList())); + } + } + return report; + } + + private PatchReport applyPatch(SinglePatch patch, boolean dryRun, IContextProvider context) { + lastPatchedLine = 1; + List ret = new ArrayList(); + + List target = context.getData(patch.targetPath); + + if (target != null && !patch.binary) { + try { + if (patchCreatesNewFileThatAlreadyExists(patch, target)) { //Check if the patch doesn't need to be applied... + for (int x = 0; x < patch.hunks.length; x++) + ret.add(new HunkReport(PatchStatus.Skipped, null, 0, 0, x)); + return new PatchReport(patch.targetPath, patch.binary, PatchStatus.Skipped, null, ret); + } + } catch (Exception e) { + // something went wrong, try doing normal patch + } + } else if (target == null) { + target = new ArrayList(); + } + + if (patch.mode == Mode.DELETE) { + target = new ArrayList(); + } else { + if (!patch.binary) { + int x = 0; + for (Hunk hunk : patch.hunks) { + x++; + try { + ret.add(applyHunk(target, hunk, x)); + } catch (Exception e) { + ret.add(new HunkReport(PatchStatus.Failure, e, 0, 0, x, hunk)); + } + } + } + } + + if (!dryRun) + context.setData(patch.targetPath, target); + + for (HunkReport hunk : ret) { + if (hunk.getStatus() == PatchStatus.Failure) + return new PatchReport(patch.targetPath, patch.binary, PatchStatus.Failure, hunk.getFailure(), ret); + } + return new PatchReport(patch.targetPath, patch.binary, PatchStatus.Patched, null, ret); + } + + private boolean patchCreatesNewFileThatAlreadyExists(SinglePatch patch, List originalFile) throws PatchException { + if (patch.hunks.length != 1) + return false; + Hunk hunk = patch.hunks[0]; + if (hunk.baseStart != 0 || hunk.baseCount != 0 || hunk.modifiedStart != 1 || hunk.modifiedCount != originalFile.size()) + return false; + + List target = new ArrayList(hunk.modifiedCount); + applyHunk(target, hunk, 0); + return target.equals(originalFile); + } + + private HunkReport applyHunk(List target, Hunk hunk, int hunkID) throws PatchException { + int idx = -1; + int fuzz = 0; + for (; idx == -1 && fuzz <= this.maxFuzz; fuzz++) { + idx = findHunkIndex(target, hunk, fuzz, hunkID); + if (idx != -1) + break; + } + if (idx == -1) + throw new PatchException("Cannot find hunk target"); + return applyHunk(target, hunk, idx, false, fuzz, hunkID); + } + + private int findHunkIndex(List target, Hunk hunk, int fuzz, int hunkID) throws PatchException { + int idx = hunk.modifiedStart; // first guess from the hunk range specification + if (idx >= lastPatchedLine && applyHunk(target, hunk, idx, true, fuzz, hunkID).getStatus().isSuccess()) + return idx; + + // try to search for the context + for (int i = idx - 1; i >= lastPatchedLine; i--) { + if (applyHunk(target, hunk, i, true, fuzz, hunkID).getStatus().isSuccess()) + return i; + } + for (int i = idx + 1; i < target.size(); i++) { + if (applyHunk(target, hunk, i, true, fuzz, hunkID).getStatus().isSuccess()) + return i; + } + return -1; + } + + /** + * @return true if the application succeeded + */ + private HunkReport applyHunk(List target, Hunk hunk, int idx, boolean dryRun, int fuzz, int hunkID) throws PatchException { + int startIdx = idx; + idx--; // indices in the target list are 0-based + int hunkIdx = -1; + for (String hunkLine : hunk.lines) { + hunkIdx++; + boolean isAddition = isAdditionLine(hunkLine); + if (!isAddition) { + if (idx >= target.size()) { + if (dryRun) + return new HunkReport(PatchStatus.Failure, null, idx, fuzz, hunkID); + throw new PatchException("Unapplicable hunk #" + hunkID + " @@ " + startIdx); + } + var clean = target.get(idx); + boolean match = similar(clean, hunkLine.substring(1), hunkLine.charAt(0)); + if (!match && fuzz != 0 && !isRemovalLine(hunkLine)) + match = (hunkIdx < fuzz || hunkIdx >= hunk.lines.size() - fuzz ? true : match); + if (!match) { + if (dryRun) + return new HunkReport(PatchStatus.Failure, null, idx, fuzz, hunkID); + throw new PatchException("Unapplicable hunk #" + hunkID + " @@ " + startIdx); + } + } + if (dryRun) { + if (isAddition) + idx--; + } else { + if (isAddition) { + target.add(idx, hunkLine.substring(1)); + } else if (isRemovalLine(hunkLine)) { + target.remove(idx); + idx--; + } + } + idx++; + } + idx++; // indices in the target list are 0-based + lastPatchedLine = idx; + return new HunkReport((fuzz != 0 ? PatchStatus.Fuzzed : PatchStatus.Patched), null, startIdx, fuzz, hunkID); + } + + private boolean isAdditionLine(String hunkLine) { + return hunkLine.charAt(0) == '+'; + } + + private boolean isRemovalLine(String hunkLine) { + return hunkLine.charAt(0) == '-'; + } + + private SinglePatch getNextPatch() throws IOException, PatchException { + SinglePatch patch = new SinglePatch(); + for (; ; ) { + String line = readPatchLine(); + if (line == null) + return null; + + if (line.startsWith("Index:")) { + patch.targetPath = line.substring(6).trim(); + } else if (line.startsWith("MIME: application/octet-stream;")) { + unreadPatchLine(); + readBinaryPatchContent(patch); + break; + } else if (line.startsWith("--- ")) { + unreadPatchLine(); + readPatchContent(patch); + break; + } else if (line.startsWith("*** ")) { + throw new IllegalArgumentException("Invalid patch format, Context Format not supported"); + } else if (isNormalDiffRange(line)) { + throw new IllegalArgumentException("Invalid patch format, Normal Format not supported"); + } + } + return patch; + } + + private boolean isNormalDiffRange(String line) { + return normalAddRangePattern.matcher(line).matches() + || normalChangeRangePattern.matcher(line).matches() + || normalDeleteRangePattern.matcher(line).matches(); + } + + /** + * Reads binary diff hunk. + */ + private void readBinaryPatchContent(SinglePatch patch) throws PatchException, IOException { + List hunks = new ArrayList(); + Hunk hunk = new Hunk(0, 0, 0, 0, new ArrayList<>()); + for (; ; ) { + String line = readPatchLine(); + if (line == null || line.startsWith("Index:") || line.length() == 0) { + unreadPatchLine(); + break; + } + if (patch.binary) { + hunk.lines.add(line); + } else { + Matcher m = binaryHeaderPattern.matcher(line); + if (m.matches()) { + patch.binary = true; + int length = Integer.parseInt(m.group(3)); + if (length == -1) + break; + hunks.add(hunk); + } + } + } + patch.hunks = hunks.toArray(new Hunk[hunks.size()]); + } + + /** + * Reads unified diff hunks. + */ + private void readPatchContent(SinglePatch patch) throws IOException, PatchException { + String base = readPatchLine(); + if (base == null || !base.startsWith("--- ")) + throw new PatchException("Invalid unified diff header: " + base); + String modified = readPatchLine(); + if (modified == null || !modified.startsWith("+++ ")) + throw new PatchException("Invalid unified diff header: " + modified); + if (patch.targetPath == null) + computeTargetPath(base, modified, patch); + + List hunks = new ArrayList(); + Hunk hunk = null; + + for (; ; ) { + String line = readPatchLine(); + if (line == null || line.length() == 0 || line.startsWith("Index:")) { + unreadPatchLine(); + break; + } + char c = line.charAt(0); + if (c == '@') { + hunk = parseRange(line); + hunks.add(hunk); + } else if (c == ' ' || c == '+' || c == '-') { + hunk.lines.add(line); + } else if (line.equals(Hunk.ENDING_NEWLINE)) { + patch.noEndingNewline = true; + } else { + // first seen in mercurial diffs: be optimistic, this is probably the end of this patch + unreadPatchLine(); + break; + } + } + patch.hunks = hunks.toArray(new Hunk[hunks.size()]); + } + + private void computeTargetPath(String base, String modified, SinglePatch patch) { + base = base.substring("+++ ".length()); + modified = modified.substring("--- ".length()); + // first seen in mercurial diffs: base and modified paths are different: base starts with "a/" and modified starts with "b/" + if ((base.equals("/dev/null") || base.startsWith("a/")) && (modified.equals("/dev/null") || modified.startsWith("b/"))) { + if (base.startsWith("a/")) + base = base.substring(2); + if (modified.startsWith("b/")) + modified = modified.substring(2); + } + base = untilTab(base).trim(); + if (base.equals("/dev/null")) { + // "/dev/null" in base indicates a new file + patch.targetPath = untilTab(modified).trim(); + patch.mode = Mode.ADD; + } else { + patch.targetPath = base; + patch.mode = modified.equals("/dev/null") ? Mode.DELETE : Mode.CHANGE; + } + } + + private String untilTab(String base) { + int pathEndIdx = base.indexOf('\t'); + if (pathEndIdx > 0) + base = base.substring(0, pathEndIdx); + return base; + } + + private Hunk parseRange(String range) throws PatchException { + Matcher m = unifiedRangePattern.matcher(range); + if (!m.matches()) + throw new PatchException("Invalid unified diff range: " + range); + + int baseStart = Integer.parseInt(m.group(1)); + int baseCount = m.group(2) != null ? Integer.parseInt(m.group(2).substring(1)) : 1; + int modifiedStart = Integer.parseInt(m.group(3)); + int modifiedCount = m.group(4) != null ? Integer.parseInt(m.group(4).substring(1)) : 1; + return new Hunk(baseStart, baseCount, modifiedStart, modifiedCount, new ArrayList<>()); + } + + private String readPatchLine() throws IOException { + if (patchLineRead) + patchLine = patchReader.readLine(); + else + patchLineRead = true; + return patchLine; + } + + private void unreadPatchLine() { + patchLineRead = false; + } + + private static class SinglePatch { + //String targetIndex; + String targetPath; + Hunk[] hunks; + //boolean targetMustExist = true; // == false if the patch contains one hunk with just additions ('+' lines) + @SuppressWarnings("unused") + boolean noEndingNewline; // resulting file should not end with a newline + boolean binary; // binary patches contain one encoded Hunk + Mode mode; + } + + enum Mode { + /** + * Update to existing file + */ + CHANGE, + /** + * Adding a new file + */ + ADD, + /** + * Deleting an existing file + */ + DELETE + } + + public static enum PatchStatus { + Patched(true), + Missing(false), + Failure(false), + Skipped(true), + Fuzzed(true); + + private boolean success; + + PatchStatus(boolean success) { + this.success = success; + } + + public boolean isSuccess() { + return success; + } + } + + public static final class PatchReport { + private String target; + private boolean binary; + private PatchStatus status; + private Throwable failure; + private List hunks; + + PatchReport(String target, boolean binary, PatchStatus status, Throwable failure, List hunks) { + this.target = target; + this.binary = binary; + this.status = status; + this.failure = failure; + this.hunks = hunks; + } + + public String getTarget() { + return target; + } + + public boolean isBinary() { + return binary; + } + + public PatchStatus getStatus() { + return status; + } + + public Throwable getFailure() { + return failure; + } + + public List getHunks() { + return hunks; + } + } + + public static interface IContextProvider { + public List getData(String target); + + public void setData(String target, List data); + } + + public static class HunkReport { + private PatchStatus status; + private Throwable failure; + private int index; + private int fuzz; + private int hunkID; + public Hunk hunk; + + public HunkReport(PatchStatus status, Throwable failure, int index, int fuzz, int hunkID) { + this.status = status; + this.failure = failure; + this.index = index; + this.fuzz = fuzz; + this.hunkID = hunkID; + } + + public HunkReport(PatchStatus status, Throwable failure, int index, int fuzz, int hunkID, Hunk hunk) { + this(status, failure, index, fuzz, hunkID); + this.hunk = hunk; + } + + public PatchStatus getStatus() { + return status; + } + + public Throwable getFailure() { + return failure; + } + + public int getIndex() { + return index; + } + + public int getFuzz() { + return fuzz; + } + + public int getHunkID() { + return hunkID; + } + } + + private boolean similar(String target, String hunk, char lineType) { + if (c14nAccess) { + if (c14nWhitespace) { + target = target.replaceAll("[\t| ]+", " "); + hunk = hunk.replaceAll("[\t| ]+", " "); + } + String[] t = target.split(" "); + String[] h = hunk.split(" "); + + //don't check length, changing any modifier to default (removing it) will change length + int targetIndex = 0; + int hunkIndex = 0; + while (targetIndex < t.length && hunkIndex < h.length) { + boolean isTargetAccess = isAccess(t[targetIndex]); + boolean isHunkAccess = isAccess(h[hunkIndex]); + if (isTargetAccess || isHunkAccess) { + //Skip access modifiers + if (isTargetAccess) + targetIndex++; + if (isHunkAccess) + hunkIndex++; + continue; + } + String hunkPart = h[hunkIndex]; + String targetPart = t[targetIndex]; + boolean labels = isLabel(targetPart) && isLabel(hunkPart); + if (!labels && !targetPart.equals(hunkPart)) + return false; + hunkIndex++; + targetIndex++; + } + return h.length == hunkIndex && t.length == targetIndex; + } + + if (c14nWhitespace) + return target.replaceAll("[\t| ]+", " ").equals(hunk.replaceAll("[\t| ]+", " ")); + else + return target.equals(hunk); + } + + private boolean isAccess(String data) { + return data.equalsIgnoreCase("public") || + data.equalsIgnoreCase("private") || + data.equalsIgnoreCase("protected") || + data.equalsIgnoreCase("final"); + } + + private boolean isLabel(String data) //Damn FernFlower + { + return data.startsWith("label"); + } + + /** + * The patch is invalid or cannot be applied on the specified file. + * + * @author Maros Sandor + */ + @SuppressWarnings("serial") + public static final class PatchException extends Exception { + public PatchException(String msg) { + super(msg); + } + } + + /** + * One unidiff or context hunk. + * + * @author Maros Sandor + */ + public record Hunk( + int baseStart, + int baseCount, + int modifiedStart, + int modifiedCount, + List lines + ) { + public static final String ENDING_NEWLINE = "\\ No newline at end of file"; + } + + public static class SingleFileContext implements ContextualPatch.IContextProvider { + private List data; + + private SingleFileContext(String base) { + data = NewLineDetector.readLines(base); + } + + private SingleFileContext(InputStream base) throws IOException { + data = NewLineDetector.readLines(base); + } + + @Override + public List getData(String target) { + List out = new ArrayList(data.size() + 5); + out.addAll(data); + return out; + } + + @Override + public void setData(String target, List data) { + this.data = data; + } + + public List getData() { + return this.data; + } + } +} diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/util/NewLineDetector.java b/src/main/java/net/minecraftforge/mcmaven/impl/util/NewLineDetector.java new file mode 100644 index 0000000..1f42f3e --- /dev/null +++ b/src/main/java/net/minecraftforge/mcmaven/impl/util/NewLineDetector.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) Forge Development LLC and contributors + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.mcmaven.impl.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.Reader; + +import java.io.BufferedReader; +import java.io.StringReader; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +// The normal BufferedReader doesn't have a way to determine between a +// file with a newline at the end and a file with no newline at the end. +// So we detect that and add an extra empty result from 'readLine' +public class NewLineDetector { + public static List readLines(String text) { + try { + return readLines(new StringReader(text)); + } catch (IOException e) { // Should never happen as we're reading from a string + return Util.sneak(e); + } + } + public static List readLines(InputStream input) throws IOException { + return readLines(new InputStreamReader(input, StandardCharsets.UTF_8)); + } + private static List readLines(Reader input) throws IOException { + var reader = new NewLineDetector(input); + List lines = new ArrayList(); + for (String line; (line = reader.readLine()) != null; ) + lines.add(line); + return lines; + } + + private final BufferedReader buffered; + private final Detector detector; + + public NewLineDetector(Reader reader) { + this.detector = new Detector(reader); + this.buffered = new BufferedReader(this.detector); + } + + public String readLine() throws IOException { + String ret = this.buffered.readLine(); + if (ret == null && this.detector.lastWasNewline) { + this.detector.lastWasNewline = false; + return ""; + } + return ret; + } + + private static class Detector extends Reader { + private final Reader wrapped; + private boolean lastWasNewline = false; + + public Detector(Reader wrapped) { + this.wrapped = wrapped; + } + + @Override + public int read(char[] cbuf, int off, int len) throws IOException { + int ret = this.wrapped.read(cbuf, off, len); + if (ret > 0) { + int c = cbuf[off + ret - 1]; + lastWasNewline = c == '\n'; + } + return ret; + } + + @Override + public void close() throws IOException { + this.wrapped.close(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/util/ProcessUtils.java b/src/main/java/net/minecraftforge/mcmaven/impl/util/ProcessUtils.java index 688486a..a19e5ab 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/util/ProcessUtils.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/util/ProcessUtils.java @@ -269,7 +269,7 @@ public static Result runJar(File javaHome, File workDir, File logFile, File tool } } - public static File recompileJar(File javaHome, List classpath, File sourcesJar, File outputJar, boolean enableDebug) { + public static File recompileJar(File javaHome, List classpath, File sourcesJar, File outputJar, boolean enableDebug, int javaTarget) { // classpath arg var classpathString = makeClasspathString(classpath); @@ -284,7 +284,7 @@ public static File recompileJar(File javaHome, List classpath, File source var sourcesOutput = new File(temp, "sources").getAbsoluteFile(); try { // Ensure the output directory exists - FileUtils.ensureParent(sourcesOutput); + FileUtils.ensure(sourcesOutput); try (ZipInputStream zin = new ZipInputStream(new FileInputStream(sourcesJar))) { ZipEntry entry; @@ -325,14 +325,24 @@ public static File recompileJar(File javaHome, List classpath, File source var outputClasses = new File(temp, "classes"); FileUtils.ensure(outputClasses); - var args = new ArrayList<>(List.of( - "-nowarn", + var args = new ArrayList(); + args.add("-nowarn"); + + if (enableDebug) + args.add("-g"); + + if (javaTarget < 8) { + args.addAll(List.of( + "-source", "1." + javaTarget, + "-target", "1." + javaTarget + )); + } + + args.addAll(List.of( "-d " + wrap(outputClasses.getAbsolutePath().replace('\\', '/')), "-classpath " + wrap(classpathString), sourcePath.toString() )); - if (enableDebug) - args.add("-g"); var process = ProcessUtils.runJavac(javaHome, temp, new File(outputJar.getAbsolutePath() + ".log"), args, sourcesJar); if (process.exitCode != 0) { diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/util/StupidHacks.java b/src/main/java/net/minecraftforge/mcmaven/impl/util/StupidHacks.java index a012229..05f7a50 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/util/StupidHacks.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/util/StupidHacks.java @@ -4,9 +4,27 @@ */ package net.minecraftforge.mcmaven.impl.util; +import static net.minecraftforge.mcmaven.impl.Mavenizer.LOGGER; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.function.ToIntFunction; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +import org.jetbrains.annotations.Nullable; +import net.minecraftforge.mcmaven.impl.Mavenizer; +import net.minecraftforge.mcmaven.impl.mappings.Mappings; import net.minecraftforge.srgutils.MinecraftVersion; +import net.minecraftforge.util.file.FileUtils; /* * Here be stupid hacks that help us support old versions, @@ -16,7 +34,7 @@ public class StupidHacks { public static Artifact fixLegacyTools(Artifact artifact) { - // Some MCPConfig versions reference a merge tool that has an invlaid 'BUKKIT' side. + // Some MCPConfig versions reference a merge tool that has an invalid 'BUKKIT' side. // This was fixed in 0.2.3.3 https://github.com/MinecraftForge/MergeTool/commit/e68e1b06ba87c68bc1a5c922395286b53a17dddf if ("net.minecraftforge".equals(artifact.getGroup()) && "mergetool".equals(artifact.getName()) && "0.2.3.2".equals(artifact.getVersion())) return artifact.withVersion("0.2.3.3"); @@ -32,10 +50,20 @@ public static Artifact fixLegacyTools(Artifact artifact) { // They moved then from root to modules "org.scala-lang:scala-swing_2.11:1.0.1", "org.scala-lang.modules:scala-swing_2.11:1.0.1", "org.scala-lang:scala-xml_2.11:1.0.2", "org.scala-lang.modules:scala-xml_2.11:1.0.2", - "org.scala-lang:scala-parser-combinators_2.11:1.0.1", "org.scala-lang.modules:scala-parser-combinators_2.11:1.0.1" + "org.scala-lang:scala-parser-combinators_2.11:1.0.1", "org.scala-lang.modules:scala-parser-combinators_2.11:1.0.1", + // They renamed the artifact + //"tv.twitch:twitch-external-platform:4.5", "tv.twitch:twitch-platform:4.5", + // Bumped transitivly somewhere in 1.7.10-pre4, and relied on the bumped version + "com.google.guava:guava:15.0", "com.google.guava:guava:16.0" ); + // these are artifacts that Mojang referenced in the past, but now are deleted. + //private static Set DELETED_ARTIFACTS = Set.of( + // "tv.twitch:twitch-external-platform" + //); - public static Artifact fixLegacyForgeDeps(Artifact artifact) { + public static @Nullable Artifact fixLegacyForgeDeps(Artifact artifact) { + //if (DELETED_ARTIFACTS.contains(artifact.withVersion(null).toString())) + // return null; // scala did some releases overwriting their own files, so we host them ourselves because of the hashes, but during dev it should be fine to just use the official var newCoords = FORGE_FIXES.get(artifact.toString()); if (newCoords != null) @@ -54,4 +82,192 @@ public static boolean isLegacyRenamer(String version) { } + private static final int OUT_OF_MEMORY = -1001; + private static final int FAILED_DECOMPILE = -1002; + private static final int INVALID_INITAL_HEAP = -1003; + private static final int NOT_ENOUGH_MEMORY = -1004; + // Yes this is slow as fuck, but this is only run during a decompile run which is already slow, + // This is to check if Fernflower is broken and returning success when it really failed. + // It also eagerly exits the process when something fails so as to not waste time. + private static int parseDecompileLog(String line) { + if (line.startsWith("Initial heap size set to a larger value than the maximum heap size")) + return INVALID_INITAL_HEAP; + if (line.startsWith("Could not reserve enough space for object heap")) + return NOT_ENOUGH_MEMORY; + if (line.startsWith("java.lang.OutOfMemoryError:")) + return OUT_OF_MEMORY; + if (line.startsWith("Exception in thread") && line.contains("java.lang.OutOfMemoryError")) + return OUT_OF_MEMORY; + if (line.contains("ERROR:")) { + // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + cl.qualifiedName + " couldn't be written."; + // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + classWrapper.getClassStruct().qualifiedName + " couldn't be written."; + // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + node.classStruct.qualifiedName + " couldn't be written."; + if (line.endsWith(" couldn't be written.")) + return FAILED_DECOMPILE; + // DecompilerContext.getLogger().logError("Class " + cl.qualifiedName + " couldn't be processed.", t); + if (line.endsWith(" couldn't be processed.")) + return FAILED_DECOMPILE; + // DecompilerContext.getLogger().logError("Class " + cl.qualifiedName + " couldn't be fully decompiled.", t); + if (line.endsWith(" couldn't be fully decompiled.")) + return FAILED_DECOMPILE; + // String message = "Method " + mt.getName() + " " + mt.getDescriptor() + " in class " + classStruct.qualifiedName + " couldn't be decompiled."; + if (line.endsWith(" couldn't be decompiled.")) + return FAILED_DECOMPILE; + } + return 0; + } + + public static ProcessUtils.Result runDecompiler(File jdk, File log, File tool, List defaultJvm, List run) { + ToIntFunction logHandler = StupidHacks::parseDecompileLog; + var jvm = Mavenizer.fillDecompileJvmArgs(defaultJvm, true, true); + + var ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, jvm, run, logHandler); + if (ret.exitCode == NOT_ENOUGH_MEMORY) { + LOGGER.error("Failed to create JVM with Not Enough Memory issue, Modern minecraft requires atleast 4GB to decompile. Run it on a system with more ram."); + } else if (ret.exitCode == INVALID_INITAL_HEAP) { + LOGGER.error("Attempted to run decompile with JVM args: " + jvm + " resulted in Invalid Inital and Max heap settings."); + LOGGER.error("This is typically caused by you having a environement variable setting the global memory options, remove or set those variables to values higher then 4GB."); + } + if (ret.exitCode == OUT_OF_MEMORY || ret.exitCode == INVALID_INITAL_HEAP) { + var newJvm = Mavenizer.fillDecompileJvmArgs(defaultJvm, false, false); + if (!newJvm.equals(jvm)) { + LOGGER.error("First decompile failed with OutOfMemory using JVM Args: " + jvm); + LOGGER.error("Attempting again with: " + newJvm); + ret = ProcessUtils.runJar(jdk, log.getParentFile(), log, tool, newJvm, run, logHandler); + if (ret.exitCode == OUT_OF_MEMORY) + LOGGER.error("Ran out of memory again, you can specify more manually using the --decompile-memory Mavenizer argument"); + } + } + return ret; + } + + // Forge patches sometimes arn't in full SRG names so we need to pick known 'good' mappings + // So I scanned Forge's git history and found each mapping change. + private record ForgeMappings(ComparableVersion start, String mappings) {} + private static final List FORGE_MAPPINGS = new ArrayList(); + private static void mappings(String forge, String mappings) { + FORGE_MAPPINGS.add(new ForgeMappings(new ComparableVersion(forge), mappings)); + } + static { + // FG 1.x, these were packed in the userdev. Manually uploaded + mappings("1.6.3-9.11.0.873", "snapshot:20130918-1.6.3"); + mappings("1.7.2-10.12.0.967", "snapshot:20131226-1.7.2"); + mappings("1.7.2-10.12.0.1024", "snapshot:20140205-1.7.2"); + mappings("1.7.10_pre4-10.12.2.1137", "snapshot:20140624-1.7.10-pre4"); + + // FG2, normal published mappings + mappings("1.7.2-10.12.0.1024", "snapshot:20140205-1.7.2"); + mappings("1.8-11.14.3.1503", "snapshot:20141130-1.8"); + mappings("1.8.8-11.14.4.1575", "snapshot:20151122-1.8"); + mappings("1.8.9-11.15.0.1656", "stable:20-1.8.8"); + mappings("1.9-12.16.0.1766", "snapshot:20160312-1.9"); + mappings("1.9.4-12.17.0.1908", "snapshot:20160518-1.9.4"); + mappings("1.10.2-12.18.2.2125", "snapshot:20161111-1.10.2"); + mappings("1.11-13.19.1.2198", "snapshot:20161220-1.11"); + mappings("1.12-14.21.0.2334", "snapshot:20170617-1.12"); + mappings("1.12-14.21.0.2352", "snapshot:20170624-1.12"); + mappings("1.12.2-14.23.0.2502", "snapshot:20171003-1.12"); + mappings("1.13", "snapshot:20180921-1.13"); + mappings("1.14.2-26.0.0", "snapshot:20190526-1.13.2"); + mappings("1.14.2-26.0.6", "snapshot:20190608-1.14.2"); + mappings("1.14.2-26.0.47", "snapshot:20190621-1.14.2"); + mappings("1.14.3-27.0.60", "snapshot:20190719-1.14.3"); + mappings("1.14.4", "official"); + } + public static Mappings getDefaultMappings(ComparableVersion forgeVersion) { + String version = null; + for (var map : FORGE_MAPPINGS) { + if (forgeVersion.compareTo(map.start) < 0) + break; + version = map.mappings(); + } + if (version == null) + throw new IllegalStateException("Could not determine default mappings for " + forgeVersion); + return Mappings.of(version); + } + + // List of 'bad' forge builds that are just broke for some reason and we explicitly don't want to support + public static final Set BLACKLISTED_FORGE_BUILDS = Set.of( + // First 1.8 build, missing FML source files in userdev. No way to make this work + "1.8-11.14.0.1237-1.8", + // Broken access transformer, fixed in 1268 + "1.8-11.14.0.1267-1.8", + // Has some fucked up patches with weird generics, I have no idea how they came into existance, they dont match github... + // This is the very first 1.8.8 build, and the next one works fine, so I'm chalking it up to Gradle Snapshot voodoo + "1.8.8-11.14.4.1575-1.8.8", + // These are early builds of 1.8.8 which use FG 2.0 snapshots, and an old Fernflower + // With a long since deleted mcp zip/patch set + "1.8.8-11.14.4.1576-1.8.8", + "1.8.8-11.14.4.1579-1.8.8", + "1.8.8-11.14.4.1580-1.8.8", + "1.8.8-11.14.4.1581-1.8.8", + "1.8.8-11.14.4.1582-1.8.8", + // Has a reference to scala.actors.threadpool.Arrays which is part of org.scala-lang:scala-actors + // Which we don't ship, but instead is a transitive of org.scala-lang:scala-actors-migration_2.11:1.1.0 + // I could add the library to this version, but i'd rather just skip it + "1.10.2-12.18.2.2103" + ); + + private static final ComparableVersion MC_1_12 = new ComparableVersion("1.12"); + private static final ComparableVersion MC_1_12_FIXED_SRG = new ComparableVersion("1.12-14.21.0.2340"); + public static String legacyMcp(ComparableVersion forge) { + // net/minecraft/command/AdvancementCommand$ActionType$Mode -> AdvancementCommand$Mode + if (forge.compareTo(MC_1_12) >= 0 && forge.compareTo(MC_1_12_FIXED_SRG) < 0) + return "mcp940.zip"; + return null; + } + + // Has a invalid `/cp/MethodsReturnNonnullByDefault.java` patch file. + private static final ComparableVersion NON_NULL_PATCH_START = new ComparableVersion("1.12.2-14.23.5.2839"); + private static final ComparableVersion NON_NULL_PATCH_END = new ComparableVersion("1.12.2-14.23.5.2844"); + public static boolean needsPatchFixes(Artifact name) { + if (!Constants.FORGE_GROUP.equals(name.getGroup()) || !Constants.FORGE_NAME.equals(name.getName())) + return false; + var current = new ComparableVersion(name.getVersion()); + if (current.compareTo(NON_NULL_PATCH_START) >= 0 && current.compareTo(NON_NULL_PATCH_END) <= 0) + return true; + return false; + } + + public static File fixPatches(Artifact artifact, File input, File output) { + var cache = Util.cache(output) + .add("input", output); + + if (Mavenizer.checkCache(output, cache)) + return output; + + // Has a invalid `/cp/MethodsReturnNonnullByDefault.java` patch file. + // Guess it was created by old FG bug, so we can just kill it + FileUtils.ensureParent(output); + try (var zin = new ZipInputStream(new FileInputStream(input)); + var zout = new ZipOutputStream(new FileOutputStream(output))) { + + for (ZipEntry entry; (entry = zin.getNextEntry()) != null; ) { + var name = entry.getName(); + // Good night sweet prince + if ("cp/MethodsReturnNonnullByDefault.java.patch".equals(name)) + continue; + + var newEntry = new ZipEntry(name); + newEntry.setTime(entry.getTime()); + zout.putNextEntry(newEntry); + zin.transferTo(zout); + zout.closeEntry(); + } + } catch (IOException e) { + Util.sneak(e); + } + + return output; + } + + public static Artifact fixMCPArtifact(Artifact artifact) { + // There is an artifact created in 2014 that doesn't follow the + // same format as the other mcp artifacts, and has bad parameter names + // And rather then update an existing file on the maven that may be used by whatever + // I made a new fixed file + if ("1.7.10".equals(artifact.getVersion())) + return artifact.withVersion("1.7.10-fixed"); + return artifact; + } } diff --git a/src/main/java/net/minecraftforge/mcmaven/impl/util/Task.java b/src/main/java/net/minecraftforge/mcmaven/impl/util/Task.java index 529a690..4c95463 100644 --- a/src/main/java/net/minecraftforge/mcmaven/impl/util/Task.java +++ b/src/main/java/net/minecraftforge/mcmaven/impl/util/Task.java @@ -8,12 +8,14 @@ import java.io.File; import java.time.Duration; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Objects; import java.util.SequencedCollection; import java.util.concurrent.Callable; import java.util.function.Supplier; +import java.util.stream.Collectors; import java.util.stream.StreamSupport; /** Represents a task that can be executed. Tasks in this tool will always provide a file. */ @@ -47,7 +49,7 @@ public interface Task { * Useful for our output json file. */ default Supplier filePathSupplier() { - return () -> this.execute().getAbsolutePath(); + return () -> this.execute().getAbsolutePath(); } static Task named(String name, Callable supplier) { @@ -82,6 +84,23 @@ private static Supplier cast(Object obj) { }; } + static class DependencyException extends RuntimeException { + private static final long serialVersionUID = 1L; + private List deps = new ArrayList<>(); + private final Task erroring; + + public DependencyException(Task erroring, Throwable cause, Task self) { + super("Failed to execute task `%s` which is required by task `%s`".formatted(erroring.name(), self.name()), cause); + this.erroring = erroring; + deps.add(self); + } + + @Override + public String getMessage() { + return "Failed to execute task `%s` which is required by task `%s`".formatted(this.erroring.name(), this.deps.stream().map(Task::name).collect(Collectors.joining("->"))); + } + } + final class Simple implements Task { private final String name; private final SequencedCollection> deps; @@ -97,9 +116,9 @@ private Simple(String name, SequencedCollection files) { logger.getInfo().println(); } } + + public static String forgeToMcVersion(String version) { + // Save for a few april-fools versions, Minecraft doesn't use _ in their version names. + // So when Forge needs to reference a version of Minecraft that uses - in the name, it replaces + // it with _ + // This could cause issues if we ever support a version with _ in it, but fuck it I don't care right now. + int idx = version.indexOf('-'); + if (idx == -1) + throw new IllegalArgumentException("Invalid Forge version: " + version); + return version.substring(0, idx).replace('_', '-'); + } + + public static final File getArtifact(Cache cache, String coords) { + return getArtifact(cache, Artifact.from(coords)); + } + public static final File getArtifact(Cache cache, Artifact artifact, boolean quiet) { + if (!quiet) + return getArtifact(cache, artifact); + + // This is ugly, but I dont feel like re-working the maven system to not print the stack when doing a fallback + boolean old = Mavenizer.cacheMiss; + Mavenizer.cacheMiss = true; + try { + return getArtifact(cache, artifact); + } finally { + Mavenizer.cacheMiss = old; + } + } + private static final File getArtifact(Cache cache, Artifact artifact) { + // Some libraries are on Minecraft's maven. Such as launchwrapper. + // Rather then configure Forge's server to proxy Mojang's I add this check. + if ("net.minecraft".equals(artifact.getGroup())) + return cache.minecraft().download(artifact); + try { + return cache.maven().download(artifact); + } catch (Exception e) { + // If its 404 on Forge's maven, try Mojang's + if (e instanceof FileNotFoundException) { + try { + return cache.minecraft().download(artifact); + } catch (Exception e2) { + e.addSuppressed(e2); + } + } + return Util.sneak(e); + } + } }