diff options
Diffstat (limited to 'src/main/java/net/fabricmc/loom/util')
29 files changed, 2720 insertions, 46 deletions
diff --git a/src/main/java/net/fabricmc/loom/util/Checksum.java b/src/main/java/net/fabricmc/loom/util/Checksum.java index e3607d4c..ba9cd2e6 100644 --- a/src/main/java/net/fabricmc/loom/util/Checksum.java +++ b/src/main/java/net/fabricmc/loom/util/Checksum.java @@ -27,6 +27,7 @@ package net.fabricmc.loom.util; import java.io.File; import java.io.IOException; import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; import com.google.common.hash.HashCode; import com.google.common.hash.Hashing; @@ -44,8 +45,9 @@ public class Checksum { try { HashCode hash = Files.asByteSource(file).hash(Hashing.sha1()); - log.debug("Checksum check: '" + hash.toString() + "' == '" + checksum + "'?"); - return hash.toString().equals(checksum); + String hashString = hash.toString(); + log.debug("Checksum check: '" + hashString + "' == '" + checksum + "'?"); + return hashString.equals(checksum); } catch (IOException e) { e.printStackTrace(); } @@ -70,4 +72,9 @@ public class Checksum { throw new UncheckedIOException("Failed to get file hash of " + file, e); } } + + public static byte[] sha256(String string) { + HashCode hash = Hashing.sha256().hashString(string, StandardCharsets.UTF_8); + return hash.asBytes(); + } } diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index dae98a87..8a68b0fb 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -34,6 +34,7 @@ import net.fabricmc.loom.configuration.RemappedConfigurationEntry; import net.fabricmc.loom.configuration.RemappedConfigurationEntry.PublishingMode; public class Constants { + public static final String PLUGIN_ID = "dev.architectury.loom"; public static final String LIBRARIES_BASE = "https://libraries.minecraft.net/"; public static final String RESOURCES_BASE = "https://resources.download.minecraft.net/"; public static final String VERSION_MANIFESTS = "https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"; @@ -72,6 +73,30 @@ public class Constants { public static final String MAPPINGS_FINAL = "mappingsFinal"; public static final String LOADER_DEPENDENCIES = "loaderLibraries"; public static final String LOOM_DEVELOPMENT_DEPENDENCIES = "loomDevelopmentDependencies"; + public static final String SRG = "srg"; + public static final String MCP_CONFIG = "mcp"; + public static final String FORGE = "forge"; + public static final String FORGE_USERDEV = "forgeUserdev"; + public static final String FORGE_INSTALLER = "forgeInstaller"; + public static final String FORGE_UNIVERSAL = "forgeUniversal"; + /** + * Forge's own dependencies. Not intended to be used by users, + * {@link #FORGE_RUNTIME_LIBRARY forgeRuntimeLibrary} is for that instead. + */ + public static final String FORGE_DEPENDENCIES = "forgeDependencies"; + public static final String FORGE_NAMED = "forgeNamed"; + /** + * "Extra" runtime dependencies on Forge. Contains the Minecraft resources + * and {@linkplain Dependencies#FORGE_RUNTIME the Architectury Loom runtime}. + */ + public static final String FORGE_EXTRA = "forgeExtra"; + /** + * The configuration used to create the Forge runtime classpath file list. + * Users can also directly add files to this config. + * + * @see net.fabricmc.loom.configuration.providers.forge.ForgeUserdevProvider + */ + public static final String FORGE_RUNTIME_LIBRARY = "forgeRuntimeLibrary"; public static final String MAPPING_CONSTANTS = "mappingsConstants"; public static final String UNPICK_CLASSPATH = "unpick"; /** @@ -94,6 +119,9 @@ public class Constants { public static final String DEV_LAUNCH_INJECTOR = "net.fabricmc:dev-launch-injector:"; public static final String TERMINAL_CONSOLE_APPENDER = "net.minecrell:terminalconsoleappender:"; public static final String JETBRAINS_ANNOTATIONS = "org.jetbrains:annotations:"; + public static final String JAVAX_ANNOTATIONS = "com.google.code.findbugs:jsr305:"; // I hate that I have to add these. + public static final String FORGE_RUNTIME = "dev.architectury:architectury-loom-runtime:"; + public static final String ACCESS_TRANSFORMERS = "net.minecraftforge:accesstransformers:"; private Dependencies() { } @@ -106,6 +134,10 @@ public class Constants { public static final String DEV_LAUNCH_INJECTOR = "0.2.1+build.8"; public static final String TERMINAL_CONSOLE_APPENDER = "1.2.0"; public static final String JETBRAINS_ANNOTATIONS = "22.0.0"; + public static final String JAVAX_ANNOTATIONS = "3.0.2"; + public static final String FORGE_RUNTIME = "1.1.3"; + public static final String ACCESS_TRANSFORMERS = "3.0.1"; + public static final String ACCESS_TRANSFORMERS_NEW = "8.0.5"; private Versions() { } @@ -131,10 +163,18 @@ public class Constants { } public static final class TaskGroup { - public static final String FABRIC = "fabric"; + public static final String FABRIC = "loom"; public static final String IDE = "ide"; private TaskGroup() { } } + + public static final class Forge { + public static final String LAUNCH_TESTING = "net.minecraftforge.userdev.LaunchTesting"; + public static final String ACCESS_TRANSFORMER_PATH = "META-INF/accesstransformer.cfg"; + + private Forge() { + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/DependencyDownloader.java b/src/main/java/net/fabricmc/loom/util/DependencyDownloader.java new file mode 100644 index 00000000..329d88bf --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/DependencyDownloader.java @@ -0,0 +1,97 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.io.File; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.Dependency; +import org.gradle.api.artifacts.ModuleDependency; +import org.gradle.api.file.FileCollection; + +/** + * Simplified dependency downloading. + * + * @author Juuz + */ +public final class DependencyDownloader { + /** + * Resolves a dependency as well as its transitive dependencies into a {@link FileCollection}. + * + * @param project the project needing these files + * @param dependencyNotation the dependency notation + * @return the resolved files + */ + public static FileCollection download(Project project, String dependencyNotation) { + return download(project, dependencyNotation, true, false); + } + + public static FileCollection download(Project project, String dependencyNotation, boolean transitive, boolean resolve) { + Dependency dependency = project.getDependencies().create(dependencyNotation); + + if (dependency instanceof ModuleDependency) { + ((ModuleDependency) dependency).setTransitive(transitive); + } + + Configuration config = project.getConfigurations().detachedConfiguration(dependency); + config.setTransitive(transitive); + FileCollection files = config.fileCollection(dep -> true); + + if (resolve) { + files = project.files(files.getFiles()); + } + + return files; + } + + private static Set<File> resolve(Configuration configuration, boolean transitive) { + Configuration copy = configuration.copy(); + copy.setTransitive(transitive); + Set<File> files = new LinkedHashSet<>(copy.resolve()); + + for (Configuration extendsForm : configuration.getExtendsFrom()) { + files.addAll(resolve(extendsForm, transitive)); + } + + return files; + } + + /** + * Resolves a configuration and its superconfigurations. + * + * <p>Note that unlike resolving a {@linkplain Configuration#copyRecursive() recursive copy} of the configuration, + * this method overrides the transitivity of all superconfigurations as well. + * + * @param configuration the configuration to resolve + * @param transitive true if transitive dependencies should be included, false otherwise + * @return a mutable set containing the resolved files of the configuration + */ + public static Set<File> resolveFiles(Configuration configuration, boolean transitive) { + return resolve(configuration, transitive); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/DownloadUtil.java b/src/main/java/net/fabricmc/loom/util/DownloadUtil.java index fcc79aa7..56ccf8d3 100644 --- a/src/main/java/net/fabricmc/loom/util/DownloadUtil.java +++ b/src/main/java/net/fabricmc/loom/util/DownloadUtil.java @@ -48,8 +48,8 @@ public class DownloadUtil { * @param logger The logger to print everything to, typically from {@link Project#getLogger()} * @throws IOException If an exception occurs during the process */ - public static void downloadIfChanged(URL from, File to, Logger logger) throws IOException { - downloadIfChanged(from, to, logger, false); + public static boolean downloadIfChanged(URL from, File to, Logger logger) throws IOException { + return downloadIfChanged(from, to, logger, false); } /** @@ -61,7 +61,7 @@ public class DownloadUtil { * @param quiet Whether to only print warnings (when <code>true</code>) or everything * @throws IOException If an exception occurs during the process */ - public static void downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException { + public static boolean downloadIfChanged(URL from, File to, Logger logger, boolean quiet) throws IOException { HttpURLConnection connection = (HttpURLConnection) from.openConnection(); if (LoomGradlePlugin.refreshDeps) { @@ -102,7 +102,7 @@ public class DownloadUtil { logger.info("'{}' Not Modified, skipping.", to); } - return; //What we've got is already fine + return false; //What we've got is already fine } long contentLength = connection.getContentLengthLong(); @@ -140,6 +140,8 @@ public class DownloadUtil { saveETag(to, eTag, logger); } + + return true; } /** @@ -228,5 +230,7 @@ public class DownloadUtil { if (etagFile.exists()) { etagFile.delete(); } + + HashedDownloadUtil.delete(file); } } diff --git a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java index 377bb8cb..05f0642f 100644 --- a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java +++ b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java @@ -78,6 +78,8 @@ public final class FileSystemUtil { return new Delegate(FileSystems.newFileSystem(jarUri, create ? jfsArgsCreate : jfsArgsEmpty), true); } catch (FileSystemAlreadyExistsException e) { return new Delegate(FileSystems.getFileSystem(jarUri), false); + } catch (IOException e) { + throw new IOException("Could not create JAR file system for " + uri + " (create: " + create + ")", e); } } } diff --git a/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java b/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java index 3ca404e2..a9b77ab4 100644 --- a/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java +++ b/src/main/java/net/fabricmc/loom/util/HashedDownloadUtil.java @@ -43,20 +43,48 @@ import org.gradle.api.logging.Logger; import net.fabricmc.loom.LoomGradlePlugin; public class HashedDownloadUtil { + public static boolean requiresDownload(File to, String expectedHash, Logger logger) { + if (LoomGradlePlugin.refreshDeps) { + return true; + } + + if (to.exists()) { + String sha1 = getSha1(to, logger); + + // The hash in the sha1 file matches + return !expectedHash.equals(sha1); + } + + return true; + } + public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet) throws IOException { - downloadIfInvalid(from, to, expectedHash, logger, quiet, () -> { }); + downloadIfInvalid(from, to, expectedHash, logger, quiet, true); + } + + public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict) throws IOException { + downloadIfInvalid(from, to, expectedHash, logger, quiet, strict, () -> { }); } - public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, Runnable startDownload) throws IOException { + public static void downloadIfInvalid(URL from, File to, String expectedHash, Logger logger, boolean quiet, boolean strict, Runnable startDownload) throws IOException { if (LoomGradlePlugin.refreshDeps) { delete(to); } - String sha1 = getSha1(to, logger); - - if (expectedHash.equals(sha1)) { - // The hash in the sha1 file matches - return; + if (to.exists()) { + if (strict) { + if (Checksum.equals(to, expectedHash)) { + // The hash matches the target file + return; + } + } else { + String sha1 = getSha1(to, logger); + + if (expectedHash.equals(sha1)) { + // The hash in the sha1 file matches + return; + } + } } startDownload.run(); diff --git a/src/main/java/net/fabricmc/loom/util/LfWriter.java b/src/main/java/net/fabricmc/loom/util/LfWriter.java new file mode 100644 index 00000000..cbd6540f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/LfWriter.java @@ -0,0 +1,43 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.Writer; + +/** + * A {@link BufferedWriter} that writes {@code \n} (LF) instead of {@link System#lineSeparator()}. + */ +public class LfWriter extends BufferedWriter { + public LfWriter(Writer out) { + super(out); + } + + @Override + public void newLine() throws IOException { + write('\n'); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/LoggerFilter.java b/src/main/java/net/fabricmc/loom/util/LoggerFilter.java new file mode 100644 index 00000000..1f9f9b96 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/LoggerFilter.java @@ -0,0 +1,49 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.io.PrintStream; + +import org.jetbrains.annotations.NotNull; + +public class LoggerFilter { + public static void replaceSystemOut() { + try { + PrintStream previous = System.out; + System.setOut(new PrintStream(previous) { + @Override + public PrintStream printf(@NotNull String format, Object... args) { + if (format.equals("unknown invokedynamic bsm: %s/%s%s (tag=%d iif=%b)%n")) { + return this; + } + + return super.printf(format, args); + } + }); + } catch (SecurityException ignored) { + // Failed to replace logger filter, just ignore + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/MappingsProviderVerbose.java b/src/main/java/net/fabricmc/loom/util/MappingsProviderVerbose.java new file mode 100644 index 00000000..9ddb07ef --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/MappingsProviderVerbose.java @@ -0,0 +1,115 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.io.IOException; +import java.io.StringWriter; +import java.io.UncheckedIOException; +import java.lang.reflect.Field; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Set; + +import dev.architectury.tinyremapper.IMappingProvider; +import dev.architectury.tinyremapper.TinyRemapper; + +import net.fabricmc.mappingio.adapter.RegularAsFlatMappingVisitor; +import net.fabricmc.mappingio.format.Tiny2Writer; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +public class MappingsProviderVerbose { + public static void saveFile(TinyRemapper providers) throws IOException { + try { + Field field = TinyRemapper.class.getDeclaredField("mappingProviders"); + field.setAccessible(true); + Set<IMappingProvider> mappingProviders = (Set<IMappingProvider>) field.get(providers); + saveFile(mappingProviders); + } catch (IllegalAccessException | NoSuchFieldException e) { + e.printStackTrace(); + } + } + + public static void saveFile(Iterable<IMappingProvider> providers) throws IOException { + MemoryMappingTree tree = new MemoryMappingTree(); + tree.setSrcNamespace("from"); + tree.setDstNamespaces(new ArrayList<>(Collections.singletonList("to"))); + RegularAsFlatMappingVisitor flatVisitor = new RegularAsFlatMappingVisitor(tree); + + for (IMappingProvider provider : providers) { + provider.load(new IMappingProvider.MappingAcceptor() { + @Override + public void acceptClass(String from, String to) { + try { + flatVisitor.visitClass(from, to); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void acceptMethod(IMappingProvider.Member from, String to) { + try { + flatVisitor.visitMethod(from.owner, from.name, from.desc, to); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void acceptMethodArg(IMappingProvider.Member from, int lvIndex, String to) { + try { + flatVisitor.visitMethodArg(from.owner, from.name, from.desc, lvIndex, lvIndex, "", to); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + + @Override + public void acceptMethodVar(IMappingProvider.Member from, int i, int i1, int i2, String s) { + // NO-OP + } + + @Override + public void acceptField(IMappingProvider.Member from, String to) { + try { + flatVisitor.visitField(from.owner, from.name, from.desc, to); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + } + + Path check = Files.createTempFile("CHECK", null); + StringWriter stringWriter = new StringWriter(); + Tiny2Writer tiny2Writer = new Tiny2Writer(stringWriter, false); + tree.accept(tiny2Writer); + Files.writeString(check, stringWriter.toString(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + System.out.println("Saved debug check mappings to " + check); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/ModPlatform.java b/src/main/java/net/fabricmc/loom/util/ModPlatform.java new file mode 100644 index 00000000..0f7ce897 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ModPlatform.java @@ -0,0 +1,50 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.util.Locale; + +import org.gradle.api.GradleException; +import org.gradle.api.Project; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.LoomGradleExtensionAPI; + +public enum ModPlatform { + FABRIC, + FORGE; + + public static void assertPlatform(Project project, ModPlatform platform) { + assertPlatform(LoomGradleExtension.get(project), platform); + } + + public static void assertPlatform(LoomGradleExtensionAPI extension, ModPlatform platform) { + if (extension.getPlatform().get() != platform) { + String msg = "Loom is not running on %s.%nYou can switch to it by adding 'loom.platform = %s' to your gradle.properties"; + String name = platform.name().toLowerCase(Locale.ROOT); + throw new GradleException(String.format(msg, name, name)); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/PropertyUtil.java b/src/main/java/net/fabricmc/loom/util/PropertyUtil.java new file mode 100644 index 00000000..98e85b68 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/PropertyUtil.java @@ -0,0 +1,45 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import org.gradle.api.provider.HasConfigurableValue; +import org.gradle.api.provider.Provider; + +/** + * Working with properties. + */ +public final class PropertyUtil { + /** + * Gets a property and finalizes its value. + * + * @param property the property + * @param <T> the value type + * @return the current property value + */ + public static <T, P extends Provider<T> & HasConfigurableValue> T getAndFinalize(P property) { + property.finalizeValue(); + return property.get(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java index f37211be..7873c78f 100644 --- a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java +++ b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java @@ -39,7 +39,6 @@ import org.cadixdev.mercury.remapper.MercuryRemapper; import org.gradle.api.Project; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.RemappedConfigurationEntry; import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; import net.fabricmc.loom.util.gradle.ProgressLoggerHelper; @@ -48,18 +47,29 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree; public class SourceRemapper { private final Project project; - private final boolean toNamed; + private String from; + private String to; private final List<Consumer<ProgressLoggerHelper>> remapTasks = new ArrayList<>(); private Mercury mercury; - public SourceRemapper(Project project, boolean toNamed) { + public SourceRemapper(Project project, boolean named) { + this(project, named ? intermediary(project) : "named", !named ? intermediary(project) : "named"); + } + + public static String intermediary(Project project) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + return extension.isForge() ? "srg" : "intermediary"; + } + + public SourceRemapper(Project project, String from, String to) { this.project = project; - this.toNamed = toNamed; + this.from = from; + this.to = to; } - public static void remapSources(Project project, File input, File output, boolean named, boolean reproducibleFileOrder, boolean preserveFileTimestamps) { - SourceRemapper sourceRemapper = new SourceRemapper(project, named); + public static void remapSources(Project project, File input, File output, String from, String to, boolean reproducibleFileOrder, boolean preserveFileTimestamps) { + SourceRemapper sourceRemapper = new SourceRemapper(project, from, to); sourceRemapper.scheduleRemapSources(input, output, reproducibleFileOrder, preserveFileTimestamps); sourceRemapper.remapAll(); } @@ -161,18 +171,27 @@ public class SourceRemapper { LoomGradleExtension extension = LoomGradleExtension.get(project); MappingsProviderImpl mappingsProvider = extension.getMappingsProvider(); - MappingSet mappings = extension.getOrCreateSrcMappingCache(toNamed ? 1 : 0, () -> { + String intermediary = extension.isForge() ? "srg" : "intermediary"; + int id = -1; + + if (from.equals(intermediary) && to.equals("named")) { + id = 1; + } else if (to.equals(intermediary) && from.equals("named")) { + id = 0; + } + + MappingSet mappings = extension.getOrCreateSrcMappingCache(id, () -> { try { - MemoryMappingTree m = mappingsProvider.getMappings(); - project.getLogger().info(":loading " + (toNamed ? "intermediary -> named" : "named -> intermediary") + " source mappings"); - return new TinyMappingsReader(m, toNamed ? MappingsNamespace.INTERMEDIARY.toString() : MappingsNamespace.NAMED.toString(), toNamed ? MappingsNamespace.NAMED.toString() : MappingsNamespace.INTERMEDIARY.toString()).read(); + MemoryMappingTree m = (from.equals("srg") || to.equals("srg")) && extension.shouldGenerateSrgTiny() ? mappingsProvider.getMappingsWithSrg() : mappingsProvider.getMappings(); + project.getLogger().info(":loading " + from + " -> " + to + " source mappings"); + return new TinyMappingsReader(m, from, to).read(); } catch (Exception e) { throw new RuntimeException(e); } }); - Mercury mercury = extension.getOrCreateSrcMercuryCache(toNamed ? 1 : 0, () -> { - Mercury m = createMercuryWithClassPath(project, toNamed); + Mercury mercury = extension.getOrCreateSrcMercuryCache(id, () -> { + Mercury m = createMercuryWithClassPath(project, to.equals("named")); for (File file : extension.getUnmappedModCollection()) { Path path = file.toPath(); @@ -185,6 +204,16 @@ public class SourceRemapper { m.getClassPath().add(extension.getMinecraftMappedProvider().getMappedJar().toPath()); m.getClassPath().add(extension.getMinecraftMappedProvider().getIntermediaryJar().toPath()); + if (extension.isForge()) { + m.getClassPath().add(extension.getMinecraftMappedProvider().getSrgJar().toPath()); + + if (extension.isForgeAndNotOfficial()) { + m.getClassPath().add(extension.getMinecraftMappedProvider().getForgeMappedJar().toPath()); + m.getClassPath().add(extension.getMinecraftMappedProvider().getForgeIntermediaryJar().toPath()); + m.getClassPath().add(extension.getMinecraftMappedProvider().getForgeSrgJar().toPath()); + } + } + Set<File> files = project.getConfigurations() .detachedConfiguration(project.getDependencies().create(Constants.Dependencies.JETBRAINS_ANNOTATIONS + Constants.Dependencies.Versions.JETBRAINS_ANNOTATIONS)) .resolve(); diff --git a/src/main/java/net/fabricmc/loom/util/ThreadingUtils.java b/src/main/java/net/fabricmc/loom/util/ThreadingUtils.java new file mode 100644 index 00000000..d6d06f79 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ThreadingUtils.java @@ -0,0 +1,192 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Collectors; + +import com.google.common.base.Stopwatch; + +public class ThreadingUtils { + public static <T> void run(T[] values, UnsafeConsumer<T> action) { + run(Arrays.stream(values) + .<UnsafeRunnable>map(t -> () -> action.accept(t)) + .collect(Collectors.toList())); + } + + public static <T> void run(Collection<T> values, UnsafeConsumer<T> action) { + run(values.stream() + .<UnsafeRunnable>map(t -> () -> action.accept(t)) + .collect(Collectors.toList())); + } + + public static void run(UnsafeRunnable... jobs) { + run(Arrays.asList(jobs)); + } + + public static void run(Collection<UnsafeRunnable> jobs) { + try { + ExecutorService service = Executors.newFixedThreadPool(Math.max(1, Math.min(jobs.size(), Runtime.getRuntime().availableProcessors()))); + List<Future<?>> futures = new LinkedList<>(); + + for (UnsafeRunnable runnable : jobs) { + futures.add(service.submit(() -> { + try { + runnable.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + })); + } + + for (Future<?> future : futures) { + future.get(); + } + + service.shutdownNow(); + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public static <T, R> List<R> get(Collection<T> values, Function<T, R> action) { + return get(values.stream() + .<UnsafeCallable<R>>map(t -> () -> action.apply(t)) + .collect(Collectors.toList())); + } + + @SafeVarargs + public static <T> List<T> get(UnsafeCallable<T>... jobs) { + return get(Arrays.asList(jobs)); + } + + public static <T> List<T> get(Collection<UnsafeCallable<T>> jobs) { + try { + ExecutorService service = Executors.newFixedThreadPool(Math.max(1, Math.min(jobs.size(), Runtime.getRuntime().availableProcessors()))); + List<Future<T>> futures = new LinkedList<>(); + List<T> result = new ArrayList<>(); + + for (UnsafeCallable<T> runnable : jobs) { + futures.add(service.submit(() -> { + try { + return runnable.call(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + })); + } + + for (Future<T> future : futures) { + result.add(future.get()); + } + + service.shutdownNow(); + return result; + } catch (InterruptedException | ExecutionException e) { + throw new RuntimeException(e); + } + } + + public interface UnsafeRunnable { + void run() throws Throwable; + } + + public interface UnsafeCallable<T> { + T call() throws Throwable; + } + + public interface UnsafeConsumer<T> { + void accept(T value) throws Throwable; + } + + public static TaskCompleter taskCompleter() { + return new TaskCompleter(); + } + + public static class TaskCompleter implements Function<Throwable, Void> { + Stopwatch stopwatch = Stopwatch.createUnstarted(); + List<CompletableFuture<?>> tasks = new ArrayList<>(); + ExecutorService service = Executors.newFixedThreadPool(Math.max(1, Runtime.getRuntime().availableProcessors())); + List<UnsafeConsumer<Stopwatch>> completionListener = new ArrayList<>(); + + public TaskCompleter add(UnsafeRunnable job) { + if (!stopwatch.isRunning()) { + stopwatch.start(); + } + + tasks.add(CompletableFuture.runAsync(() -> { + try { + job.run(); + } catch (Throwable throwable) { + throw new RuntimeException(throwable); + } + }, service).exceptionally(this)); + + return this; + } + + public TaskCompleter onComplete(UnsafeConsumer<Stopwatch> consumer) { + completionListener.add(consumer); + return this; + } + + public void complete() { + try { + CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0])).exceptionally(this).get(); + service.shutdownNow(); + + if (stopwatch.isRunning()) { + stopwatch.stop(); + } + } catch (Throwable e) { + throw new RuntimeException(e); + } finally { + try { + for (UnsafeConsumer<Stopwatch> consumer : completionListener) { + consumer.accept(stopwatch); + } + } catch (Throwable e) { + e.printStackTrace(); + } + } + } + + @Override + public Void apply(Throwable throwable) { + throwable.printStackTrace(); + return null; + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java index d534f972..1dc42878 100644 --- a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java @@ -27,25 +27,31 @@ package net.fabricmc.loom.util; import java.io.File; import java.io.IOException; import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.function.Consumer; import java.util.regex.Pattern; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; +import dev.architectury.tinyremapper.IMappingProvider; +import dev.architectury.tinyremapper.TinyRemapper; +import org.apache.commons.lang3.mutable.Mutable; +import org.apache.commons.lang3.mutable.MutableObject; +import org.apache.commons.lang3.tuple.Triple; import org.gradle.api.Project; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.mappingio.tree.MappingTree; import net.fabricmc.mappingio.tree.MemoryMappingTree; -import net.fabricmc.tinyremapper.IMappingProvider; -import net.fabricmc.tinyremapper.TinyRemapper; /** * Contains shortcuts to create tiny remappers using the mappings accessibly to the project. */ public final class TinyRemapperHelper { - private static final Map<String, String> JSR_TO_JETBRAINS = new ImmutableMap.Builder<String, String>() + public static final Map<String, String> JSR_TO_JETBRAINS = new ImmutableMap.Builder<String, String>() .put("javax/annotation/Nullable", "org/jetbrains/annotations/Nullable") .put("javax/annotation/Nonnull", "org/jetbrains/annotations/NotNull") .put("javax/annotation/concurrent/Immutable", "org/jetbrains/annotations/Unmodifiable") @@ -65,31 +71,62 @@ public final class TinyRemapperHelper { public static TinyRemapper getTinyRemapper(Project project, String fromM, String toM, boolean fixRecords, Consumer<TinyRemapper.Builder> builderConsumer) throws IOException { LoomGradleExtension extension = LoomGradleExtension.get(project); - MemoryMappingTree mappingTree = extension.getMappingsProvider().getMappings(); - if (fixRecords && !mappingTree.getSrcNamespace().equals(fromM)) { - throw new IllegalStateException("Mappings src namespace must match remap src namespace"); - } + TinyRemapper remapper = _getTinyRemapper(project, fixRecords, builderConsumer).getLeft(); + remapper.replaceMappings(ImmutableSet.of( + TinyRemapperHelper.create((fromM.equals("srg") || toM.equals("srg")) && extension.isForge() ? extension.getMappingsProvider().getMappingsWithSrg() : extension.getMappingsProvider().getMappings(), fromM, toM, true), + out -> TinyRemapperHelper.JSR_TO_JETBRAINS.forEach(out::acceptClass) + )); + return remapper; + } - int intermediaryNsId = mappingTree.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString()); + public static Triple<TinyRemapper, Mutable<MemoryMappingTree>, List<TinyRemapper.ApplyVisitorProvider>> _getTinyRemapper(Project project, boolean fixRecords, Consumer<TinyRemapper.Builder> builderConsumer) throws IOException { + LoomGradleExtension extension = LoomGradleExtension.get(project); + Mutable<MemoryMappingTree> mappings = new MutableObject<>(); + List<TinyRemapper.ApplyVisitorProvider> postApply = new ArrayList<>(); TinyRemapper.Builder builder = TinyRemapper.newRemapper() - .withMappings(create(mappingTree, fromM, toM, true)) - .withMappings(out -> JSR_TO_JETBRAINS.forEach(out::acceptClass)) .renameInvalidLocals(true) - .rebuildSourceFilenames(true) - .invalidLvNamePattern(MC_LV_PATTERN) - .inferNameFromSameLvIndex(true) - .extraPreApplyVisitor((cls, next) -> { - if (fixRecords && !cls.isRecord() && "java/lang/Record".equals(cls.getSuperName())) { - return new RecordComponentFixVisitor(next, mappingTree, intermediaryNsId); - } + .logUnknownInvokeDynamic(false) + .ignoreConflicts(extension.isForge()) + .cacheMappings(true) + .threads(Runtime.getRuntime().availableProcessors()) + .logger(project.getLogger()::lifecycle) + .rebuildSourceFilenames(true); + + if (extension.isForge()) { + /* FORGE: Required for classes like aej$OptionalNamedTag (1.16.4) which are added by Forge patches. + * They won't get remapped to their proper packages, so IllegalAccessErrors will happen without ._. + */ + builder.fixPackageAccess(true); + } - return next; - }); + builder.invalidLvNamePattern(MC_LV_PATTERN); + builder.inferNameFromSameLvIndex(true); + builder.extraPreApplyVisitor((cls, next) -> { + if (fixRecords && !cls.isRecord() && "java/lang/Record".equals(cls.getSuperName()) && mappings.getValue() != null) { + return new RecordComponentFixVisitor(next, mappings.getValue(), mappings.getValue().getNamespaceId(MappingsNamespace.INTERMEDIARY.toString())); + } + + return next; + }); + builder.extraPostApplyVisitor((trClass, classVisitor) -> { + for (TinyRemapper.ApplyVisitorProvider provider : postApply) { + classVisitor = provider.insertApplyVisitor(trClass, classVisitor); + } + + return classVisitor; + }); builderConsumer.accept(builder); - return builder.build(); + return Triple.of(builder.build(), mappings, postApply); + } + + public static Triple<TinyRemapper, Mutable<MemoryMappingTree>, List<TinyRemapper.ApplyVisitorProvider>> getTinyRemapper(Project project, boolean fixRecords, Consumer<TinyRemapper.Builder> builderConsumer) throws IOException { + Triple<TinyRemapper, Mutable<MemoryMappingTree>, List<TinyRemapper.ApplyVisitorProvider>> remapper = _getTinyRemapper(project, fixRecords, builderConsumer); + remapper.getLeft().readClassPath(getMinecraftDependencies(project)); + remapper.getLeft().prepareClasses(); + return remapper; } public static Path[] getMinecraftDependencies(Project project) { diff --git a/src/main/java/net/fabricmc/loom/util/aw2at/Aw2At.java b/src/main/java/net/fabricmc/loom/util/aw2at/Aw2At.java new file mode 100644 index 00000000..06fd7e86 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/aw2at/Aw2At.java @@ -0,0 +1,118 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.aw2at; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; + +import org.cadixdev.at.AccessChange; +import org.cadixdev.at.AccessTransform; +import org.cadixdev.at.AccessTransformSet; +import org.cadixdev.at.ModifierChange; +import org.cadixdev.bombe.type.signature.MethodSignature; +import org.gradle.api.Project; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; + +import net.fabricmc.accesswidener.AccessWidenerReader; +import net.fabricmc.accesswidener.AccessWidenerVisitor; +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.task.RemapJarTask; + +/** + * Converts AW files to AT files. + * + * @author Juuz + */ +public final class Aw2At { + public static void setup(Project project, RemapJarTask remapJar) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + + if (extension.getAccessWidenerPath().isPresent()) { + // Find the relative AW file name + String awName = null; + Path awPath = extension.getAccessWidenerPath().get().getAsFile().toPath(); + SourceSetContainer sourceSets = project.getExtensions().getByType(SourceSetContainer.class); + SourceSet main = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME); + boolean found = false; + + for (File srcDir : main.getResources().getSrcDirs()) { + Path srcDirPath = srcDir.toPath().toAbsolutePath(); + + if (awPath.startsWith(srcDirPath)) { + awName = srcDirPath.relativize(awPath).toString().replace(File.separator, "/"); + found = true; + break; + } + } + + if (!found) { + awName = awPath.getFileName().toString(); + } + + remapJar.getAtAccessWideners().add(awName); + } + + remapJar.getAtAccessWideners().addAll(extension.getForge().getExtraAccessWideners()); + } + + public static AccessTransformSet toAccessTransformSet(BufferedReader reader) throws IOException { + AccessTransformSet atSet = AccessTransformSet.create(); + + new AccessWidenerReader(new AccessWidenerVisitor() { + @Override + public void visitClass(String name, AccessWidenerReader.AccessType access, boolean transitive) { + atSet.getOrCreateClass(name).merge(toAt(access)); + } + + @Override + public void visitMethod(String owner, String name, String descriptor, AccessWidenerReader.AccessType access, boolean transitive) { + atSet.getOrCreateClass(owner).mergeMethod(MethodSignature.of(name, descriptor), toAt(access)); + } + + @Override + public void visitField(String owner, String name, String descriptor, AccessWidenerReader.AccessType access, boolean transitive) { + atSet.getOrCreateClass(owner).mergeField(name, toAt(access)); + } + }).read(reader); + + return atSet; + } + + private static AccessTransform toAt(AccessWidenerReader.AccessType access) { + return switch (access) { + // FIXME: This behaviour doesn't match what the actual AW does for methods. + // - accessible makes the method final if it was private + // - extendable makes the method protected if it was (package-)private + // Neither of these can be achieved with Forge ATs without using bytecode analysis. + // However, this might be good enough for us. (The effects only apply in prod.) + case ACCESSIBLE -> AccessTransform.of(AccessChange.PUBLIC); + case EXTENDABLE -> AccessTransform.of(AccessChange.PUBLIC, ModifierChange.REMOVE); + case MUTABLE -> AccessTransform.of(ModifierChange.REMOVE); + }; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java b/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java new file mode 100644 index 00000000..389dbbef --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/CollectionUtil.java @@ -0,0 +1,75 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.function; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import java.util.function.Predicate; + +/** + * Stream-like utilities for working with collections. + * + * @author Juuz + */ +public final class CollectionUtil { + /** + * Finds the first element matching the predicate. + * + * @param collection the collection to be searched + * @param filter the predicate to be matched + * @param <E> the element type + * @return the first matching element, or empty if none match + */ + public static <E> Optional<E> find(Iterable<? extends E> collection, Predicate<? super E> filter) { + for (E e : collection) { + if (filter.test(e)) { + return Optional.of(e); + } + } + + return Optional.empty(); + } + + /** + * Transforms the collection with a function. + * + * @param collection the source collection + * @param transform the transformation function + * @param <A> the source type + * @param <B> the target type + * @return a mutable list with the transformed entries + */ + public static <A, B> List<B> map(Iterable<? extends A> collection, Function<? super A, ? extends B> transform) { + ArrayList<B> result = new ArrayList<>(); + + for (A a : collection) { + result.add(transform.apply(a)); + } + + return result; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/function/FsPathConsumer.java b/src/main/java/net/fabricmc/loom/util/function/FsPathConsumer.java new file mode 100644 index 00000000..536721c7 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/FsPathConsumer.java @@ -0,0 +1,39 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.function; + +import java.io.IOException; +import java.nio.file.FileSystem; +import java.nio.file.Path; + +/** + * Consumes two file systems and corresponding path objects. + * + * @author Juuz + */ +@FunctionalInterface +public interface FsPathConsumer { + void accept(FileSystem sourceFs, FileSystem targetFs, Path sourcePath, Path targetPath) throws IOException; +} diff --git a/src/main/java/net/fabricmc/loom/util/function/IoConsumer.java b/src/main/java/net/fabricmc/loom/util/function/IoConsumer.java new file mode 100644 index 00000000..379d4293 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/IoConsumer.java @@ -0,0 +1,38 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.function; + +import java.io.IOException; + +/** + * Like Consumer, but can throw IOException. + * + * @param <A> the result type + * @author Juuz + */ +@FunctionalInterface +public interface IoConsumer<A> { + void accept(A a) throws IOException; +} diff --git a/src/main/java/net/fabricmc/loom/util/function/LazyBool.java b/src/main/java/net/fabricmc/loom/util/function/LazyBool.java new file mode 100644 index 00000000..7d2933a4 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/function/LazyBool.java @@ -0,0 +1,52 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.function; + +import java.util.Objects; +import java.util.function.BooleanSupplier; + +/** + * A lazily computed boolean value. + * + * @author Juuz + */ +public final class LazyBool implements BooleanSupplier { + private BooleanSupplier supplier; + private Boolean value; + + public LazyBool(BooleanSupplier supplier) { + this.supplier = Objects.requireNonNull(supplier, "supplier"); + } + + @Override + public boolean getAsBoolean() { + if (value == null) { + value = supplier.getAsBoolean(); + supplier = null; // Release the supplier + } + + return value; + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/AtRemapper.java b/src/main/java/net/fabricmc/loom/util/srg/AtRemapper.java new file mode 100644 index 00000000..8e1d3008 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/AtRemapper.java @@ -0,0 +1,140 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.IOException; +import java.io.StringReader; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.function.UnaryOperator; + +import com.google.common.collect.ImmutableMap; +import org.gradle.api.logging.Logger; + +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.function.CollectionUtil; +import net.fabricmc.mappingio.tree.MappingTree; + +/** + * Remaps AT classes from SRG to Yarn. + * + * @author Juuz + */ +public final class AtRemapper { + public static void remap(Logger logger, Path jar, MappingTree mappings) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + jar.toUri()), ImmutableMap.of("create", false))) { + Path atPath = fs.getPath(Constants.Forge.ACCESS_TRANSFORMER_PATH); + + if (Files.exists(atPath)) { + String atContent = Files.readString(atPath); + + String[] lines = atContent.split("\n"); + List<String> output = new ArrayList<>(lines.length); + + for (int i = 0; i < lines.length; i++) { + String line = lines[i].trim(); + + if (line.startsWith("#") || line.isBlank()) { + output.add(i, line); + continue; + } + + String[] parts = line.split("\\s+"); + + if (parts.length < 2) { + logger.warn("Invalid AT Line: " + line); + output.add(i, line); + continue; + } + + String name = parts[1].replace('.', '/'); + parts[1] = CollectionUtil.find( + mappings.getClasses(), + def -> def.getName("srg").equals(name) + ).map(def -> def.getName("named")).orElse(name).replace('/', '.'); + + if (parts.length >= 3) { + if (parts[2].contains("(")) { + parts[2] = parts[2].substring(0, parts[2].indexOf('(')) + remapDescriptor(parts[2].substring(parts[2].indexOf('(')), s -> { + return CollectionUtil.find( + mappings.getClasses(), + def -> def.getName("srg").equals(s) + ).map(def -> def.getName("named")).orElse(s); + }); + } + } + + output.add(i, String.join(" ", parts)); + } + + Files.write(atPath, String.join("\n", output).getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE); + } + } + } + + private static String remapDescriptor(String original, UnaryOperator<String> classMappings) { + try { + StringReader reader = new StringReader(original); + StringBuilder result = new StringBuilder(); + boolean insideClassName = false; + StringBuilder className = new StringBuilder(); + + while (true) { + int c = reader.read(); + + if (c == -1) { + break; + } + + if ((char) c == ';') { + insideClassName = false; + result.append(classMappings.apply(className.toString())); + } + + if (insideClassName) { + className.append((char) c); + } else { + result.append((char) c); + } + + if (!insideClassName && (char) c == 'L') { + insideClassName = true; + className.setLength(0); + } + } + + return result.toString(); + } catch (IOException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/CoreModClassRemapper.java b/src/main/java/net/fabricmc/loom/util/srg/CoreModClassRemapper.java new file mode 100644 index 00000000..535bcb21 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/CoreModClassRemapper.java @@ -0,0 +1,115 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.google.common.collect.ImmutableMap; +import com.google.gson.Gson; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.gradle.api.logging.Logger; + +import net.fabricmc.loom.util.function.CollectionUtil; +import net.fabricmc.mappingio.tree.MappingTree; + +/** + * Remaps coremod class names from SRG to Yarn. + * + * @author Juuz + */ +public final class CoreModClassRemapper { + private static final Pattern CLASS_NAME_PATTERN = Pattern.compile("^(.*')((?:com\\.mojang\\.|net\\.minecraft\\.)[A-Za-z0-9.-_$]+)('.*)$"); + + public static void remapJar(Path jar, MappingTree mappings, Logger logger) throws IOException { + try (FileSystem fs = FileSystems.newFileSystem(URI.create("jar:" + jar.toUri()), ImmutableMap.of("create", false))) { + Path coremodsJsonPath = fs.getPath("META-INF", "coremods.json"); + + if (Files.notExists(coremodsJsonPath)) { + logger.info(":no coremods in " + jar.getFileName()); + return; + } + + JsonObject coremodsJson; + + try (Reader reader = Files.newBufferedReader(coremodsJsonPath)) { + coremodsJson = new Gson().fromJson(reader, JsonObject.class); + } + + for (Map.Entry<String, JsonElement> nameFileEntry : coremodsJson.entrySet()) { + String file = nameFileEntry.getValue().getAsString(); + Path js = fs.getPath(file); + + if (Files.exists(js)) { + logger.info(":remapping coremod '" + file + "'"); + remap(js, mappings); + } else { + logger.warn("Coremod '" + file + "' listed in coremods.json but not found"); + } + } + } + } + + public static void remap(Path js, MappingTree mappings) throws IOException { + List<String> lines = Files.readAllLines(js); + List<String> output = new ArrayList<>(lines); + + for (int i = 0; i < lines.size(); i++) { + String line = lines.get(i); + Matcher matcher = CLASS_NAME_PATTERN.matcher(line); + + if (matcher.matches()) { + String className = matcher.group(2).replace('.', '/'); + String remapped = CollectionUtil.find(mappings.getClasses(), def -> def.getName("srg").equals(className)) + .map(def -> def.getName("named")) + .orElse(className); + + if (!className.equals(remapped)) { + output.set(i, matcher.group(1) + remapped.replace('/', '.') + matcher.group(3)); + } + } + } + + if (!lines.equals(output)) { + try (Writer writer = Files.newBufferedWriter(js, StandardCharsets.UTF_8, StandardOpenOption.WRITE)) { + writer.write(String.join("\n", output)); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/InnerClassRemapper.java b/src/main/java/net/fabricmc/loom/util/srg/InnerClassRemapper.java new file mode 100644 index 00000000..e769c2f4 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/InnerClassRemapper.java @@ -0,0 +1,101 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.stream.Collectors; + +import com.google.common.collect.BiMap; +import com.google.common.collect.HashBiMap; +import dev.architectury.tinyremapper.IMappingProvider; + +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.mappingio.tree.MappingTree; + +public class InnerClassRemapper { + public static IMappingProvider of(Set<String> fromClassNames, MappingTree mappingsWithSrg, String from, String to) throws IOException { + return sink -> { + remapInnerClass(fromClassNames, mappingsWithSrg, from, to, sink::acceptClass); + }; + } + + public static Set<String> readClassNames(Path jar) { + Set<String> set = new HashSet<>(); + + try (FileSystemUtil.Delegate system = FileSystemUtil.getJarFileSystem(jar, false)) { + Iterator<Path> iterator = Files.walk(system.get().getPath("/")).iterator(); + + while (iterator.hasNext()) { + Path path = iterator.next(); + String name = path.toString(); + if (name.startsWith("/")) name = name.substring(1); + + if (!Files.isDirectory(path) && name.contains("$") && name.endsWith(".class")) { + String className = name.substring(0, name.length() - 6); + set.add(className); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + + return set; + } + + private static void remapInnerClass(Set<String> classNames, MappingTree mappingsWithSrg, String from, String to, BiConsumer<String, String> action) { + BiMap<String, String> availableClasses = HashBiMap.create(mappingsWithSrg.getClasses().stream() + .collect(Collectors.groupingBy(classDef -> classDef.getName(from), + Collectors.<MappingTree.ClassMapping, String>reducing( + null, + classDef -> classDef.getName(to), + (first, last) -> last + )) + )); + + for (String className : classNames) { + if (!availableClasses.containsKey(className)) { + String parentName = className.substring(0, className.indexOf('$')); + String childName = className.substring(className.indexOf('$') + 1); + String remappedParentName = availableClasses.getOrDefault(parentName, parentName); + String remappedName = remappedParentName + "$" + childName; + + if (!className.equals(remappedName)) { + if (availableClasses.containsValue(remappedName)) { + // https://github.com/MinecraftForge/MinecraftForge/blob/b027a92dd287d6810a9fdae4d4b1e1432d7dc9cc/patches/minecraft/net/minecraft/Util.java.patch#L8 + action.accept(className, remappedName + "_UNBREAK"); + } else { + action.accept(className, remappedName); + } + } + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/MCPReader.java b/src/main/java/net/fabricmc/loom/util/srg/MCPReader.java new file mode 100644 index 00000000..96382bb5 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/MCPReader.java @@ -0,0 +1,363 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.opencsv.CSVReader; +import com.opencsv.exceptions.CsvValidationException; +import org.apache.commons.io.IOUtils; +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.lorenz.io.srg.tsrg.TSrgReader; +import org.cadixdev.lorenz.model.ClassMapping; +import org.cadixdev.lorenz.model.FieldMapping; +import org.cadixdev.lorenz.model.InnerClassMapping; +import org.cadixdev.lorenz.model.MethodMapping; +import org.cadixdev.lorenz.model.TopLevelClassMapping; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; +import net.fabricmc.stitch.commands.tinyv2.TinyClass; +import net.fabricmc.stitch.commands.tinyv2.TinyField; +import net.fabricmc.stitch.commands.tinyv2.TinyFile; +import net.fabricmc.stitch.commands.tinyv2.TinyMethod; +import net.fabricmc.stitch.commands.tinyv2.TinyMethodParameter; +import net.fabricmc.stitch.commands.tinyv2.TinyV2Reader; + +public class MCPReader { + private final Path intermediaryTinyPath; + private final Path srgTsrgPath; + + public MCPReader(Path intermediaryTinyPath, Path srgTsrgPath) { + this.intermediaryTinyPath = intermediaryTinyPath; + this.srgTsrgPath = srgTsrgPath; + } + + public TinyFile read(Path mcpJar) throws IOException { + Map<MemberToken, String> srgTokens = readSrg(); + TinyFile intermediaryTiny = TinyV2Reader.read(intermediaryTinyPath); + Map<String, String> intermediaryToMCPMap = createIntermediaryToMCPMap(intermediaryTiny, srgTokens); + Map<String, String[]> intermediaryToDocsMap = new HashMap<>(); + Map<String, Map<Integer, String>> intermediaryToParamsMap = new HashMap<>(); + + try { + injectMcp(mcpJar, intermediaryToMCPMap, intermediaryToDocsMap, intermediaryToParamsMap); + } catch (CsvValidationException e) { + throw new RuntimeException(e); + } + + mergeTokensIntoIntermediary(intermediaryTiny, intermediaryToMCPMap, intermediaryToDocsMap, intermediaryToParamsMap); + return intermediaryTiny; + } + + private Map<String, String> createIntermediaryToMCPMap(TinyFile tiny, Map<MemberToken, String> officialToMCP) { + Map<String, String> map = new HashMap<>(); + + for (TinyClass tinyClass : tiny.getClassEntries()) { + String classObf = tinyClass.getMapping().get(0); + String classIntermediary = tinyClass.getMapping().get(1); + MemberToken classTokenObf = MemberToken.ofClass(classObf); + + if (officialToMCP.containsKey(classTokenObf)) { + map.put(classIntermediary, officialToMCP.get(classTokenObf)); + } + + for (TinyField tinyField : tinyClass.getFields()) { + String fieldObf = tinyField.getMapping().get(0); + String fieldIntermediary = tinyField.getMapping().get(1); + MemberToken fieldTokenObf = MemberToken.ofField(classTokenObf, fieldObf); + + if (officialToMCP.containsKey(fieldTokenObf)) { + map.put(fieldIntermediary, officialToMCP.get(fieldTokenObf)); + } + } + + for (TinyMethod tinyMethod : tinyClass.getMethods()) { + String methodObf = tinyMethod.getMapping().get(0); + String methodIntermediary = tinyMethod.getMapping().get(1); + MemberToken methodTokenObf = MemberToken.ofMethod(classTokenObf, methodObf, tinyMethod.getMethodDescriptorInFirstNamespace()); + + if (officialToMCP.containsKey(methodTokenObf)) { + map.put(methodIntermediary, officialToMCP.get(methodTokenObf)); + } + } + } + + return map; + } + + private void mergeTokensIntoIntermediary(TinyFile tiny, Map<String, String> intermediaryToMCPMap, Map<String, String[]> intermediaryToDocsMap, Map<String, Map<Integer, String>> intermediaryToParamsMap) { + stripTinyWithParametersAndLocal(tiny); + + // We will be adding the "named" namespace with MCP + tiny.getHeader().getNamespaces().add("named"); + + for (TinyClass tinyClass : tiny.getClassEntries()) { + String classIntermediary = tinyClass.getMapping().get(1); + tinyClass.getMapping().add(intermediaryToMCPMap.getOrDefault(classIntermediary, classIntermediary)); + + for (TinyField tinyField : tinyClass.getFields()) { + String fieldIntermediary = tinyField.getMapping().get(1); + String[] docs = intermediaryToDocsMap.get(fieldIntermediary); + tinyField.getMapping().add(intermediaryToMCPMap.getOrDefault(fieldIntermediary, fieldIntermediary)); + + if (docs != null) { + tinyField.getComments().clear(); + tinyField.getComments().addAll(Arrays.asList(docs)); + } + } + + for (TinyMethod tinyMethod : tinyClass.getMethods()) { + String methodIntermediary = tinyMethod.getMapping().get(1); + String[] docs = intermediaryToDocsMap.get(methodIntermediary); + tinyMethod.getMapping().add(intermediaryToMCPMap.getOrDefault(methodIntermediary, methodIntermediary)); + + if (docs != null) { + tinyMethod.getComments().clear(); + tinyMethod.getComments().addAll(Arrays.asList(docs)); + } + + Map<Integer, String> params = intermediaryToParamsMap.get(methodIntermediary); + + if (params != null) { + for (Map.Entry<Integer, String> entry : params.entrySet()) { + int lvIndex = entry.getKey(); + String paramName = entry.getValue(); + + ArrayList<String> mappings = new ArrayList<>(); + mappings.add(""); + mappings.add(""); + mappings.add(paramName); + tinyMethod.getParameters().add(new TinyMethodParameter(lvIndex, mappings, new ArrayList<>())); + } + } + } + } + } + + private void stripTinyWithParametersAndLocal(TinyFile tiny) { + for (TinyClass tinyClass : tiny.getClassEntries()) { + for (TinyMethod tinyMethod : tinyClass.getMethods()) { + tinyMethod.getParameters().clear(); + tinyMethod.getLocalVariables().clear(); + } + } + } + + private Map<MemberToken, String> readSrg() throws IOException { + Map<MemberToken, String> tokens = new HashMap<>(); + + try (BufferedReader reader = Files.newBufferedReader(srgTsrgPath, StandardCharsets.UTF_8)) { + String content = IOUtils.toString(reader); + + if (content.startsWith("tsrg2")) { + readTsrg2(tokens, content); + } else { + MappingSet mappingSet = new TSrgReader(new StringReader(content)).read(); + + for (TopLevelClassMapping classMapping : mappingSet.getTopLevelClassMappings()) { + appendClass(tokens, classMapping); + } + } + } + + return tokens; + } + + private void readTsrg2(Map<MemberToken, String> tokens, String content) throws IOException { + MemoryMappingTree tree = new MemoryMappingTree(); + MappingReader.read(new StringReader(content), tree); + int obfIndex = tree.getNamespaceId("obf"); + int srgIndex = tree.getNamespaceId("srg"); + + for (MappingTree.ClassMapping classDef : tree.getClasses()) { + MemberToken ofClass = MemberToken.ofClass(classDef.getName(obfIndex)); + tokens.put(ofClass, classDef.getName(srgIndex)); + + for (MappingTree.FieldMapping fieldDef : classDef.getFields()) { + tokens.put(MemberToken.ofField(ofClass, fieldDef.getName(obfIndex)), fieldDef.getName(srgIndex)); + } + + for (MappingTree.MethodMapping methodDef : classDef.getMethods()) { + tokens.put(MemberToken.ofMethod(ofClass, methodDef.getName(obfIndex), methodDef.getDesc(obfIndex)), + methodDef.getName(srgIndex)); + } + } + } + + private void injectMcp(Path mcpJar, Map<String, String> intermediaryToSrgMap, Map<String, String[]> intermediaryToDocsMap, Map<String, Map<Integer, String>> intermediaryToParamsMap) + throws IOException, CsvValidationException { + Map<String, List<String>> srgToIntermediary = inverseMap(intermediaryToSrgMap); + Map<String, List<String>> simpleSrgToIntermediary = new HashMap<>(); + Pattern methodPattern = Pattern.compile("(func_\\d*)_.*"); + + for (Map.Entry<String, List<String>> entry : srgToIntermediary.entrySet()) { + Matcher matcher = methodPattern.matcher(entry.getKey()); + + if (matcher.matches()) { + simpleSrgToIntermediary.put(matcher.group(1), entry.getValue()); + } + } + + try (FileSystem fs = FileSystems.newFileSystem(mcpJar, (ClassLoader) null)) { + Path fields = fs.getPath("fields.csv"); + Path methods = fs.getPath("methods.csv"); + Path params = fs.getPath("params.csv"); + Pattern paramsPattern = Pattern.compile("p_[^\\d]*(\\d+)_(\\d)+_?"); + + try (CSVReader reader = new CSVReader(Files.newBufferedReader(fields, StandardCharsets.UTF_8))) { + reader.readNext(); + String[] line; + + while ((line = reader.readNext()) != null) { + List<String> intermediaryField = srgToIntermediary.get(line[0]); + String[] docs = line[3].split("\n"); + + if (intermediaryField != null) { + for (String s : intermediaryField) { + intermediaryToSrgMap.put(s, line[1]); + + if (!line[3].trim().isEmpty() && docs.length > 0) { + intermediaryToDocsMap.put(s, docs); + } + } + } + } + } + + try (CSVReader reader = new CSVReader(Files.newBufferedReader(methods, StandardCharsets.UTF_8))) { + reader.readNext(); + String[] line; + + while ((line = reader.readNext()) != null) { + List<String> intermediaryMethod = srgToIntermediary.get(line[0]); + String[] docs = line[3].split("\n"); + + if (intermediaryMethod != null) { + for (String s : intermediaryMethod) { + intermediaryToSrgMap.put(s, line[1]); + + if (!line[3].trim().isEmpty() && docs.length > 0) { + intermediaryToDocsMap.put(s, docs); + } + } + } + } + } + + if (Files.exists(params)) { + try (CSVReader reader = new CSVReader(Files.newBufferedReader(params, StandardCharsets.UTF_8))) { + reader.readNext(); + String[] line; + + while ((line = reader.readNext()) != null) { + Matcher param = paramsPattern.matcher(line[0]); + + if (param.matches()) { + String named = line[1]; + String srgMethodStartWith = "func_" + param.group(1); + int lvIndex = Integer.parseInt(param.group(2)); + List<String> intermediaryMethod = simpleSrgToIntermediary.get(srgMethodStartWith); + + if (intermediaryMethod != null) { + for (String s : intermediaryMethod) { + intermediaryToParamsMap.computeIfAbsent(s, s1 -> new HashMap<>()).put(lvIndex, named); + } + } + } + } + } + } + } + } + + private Map<String, List<String>> inverseMap(Map<String, String> intermediaryToMCPMap) { + Map<String, List<String>> map = new HashMap<>(); + + for (Map.Entry<String, String> token : intermediaryToMCPMap.entrySet()) { + map.computeIfAbsent(token.getValue(), s -> new ArrayList<>()).add(token.getKey()); + } + + return map; + } + + private void appendClass(Map<MemberToken, String> tokens, ClassMapping<?, ?> classMapping) { + MemberToken ofClass = MemberToken.ofClass(classMapping.getFullObfuscatedName()); + tokens.put(ofClass, classMapping.getFullDeobfuscatedName()); + + for (FieldMapping fieldMapping : classMapping.getFieldMappings()) { + tokens.put(MemberToken.ofField(ofClass, fieldMapping.getObfuscatedName()), fieldMapping.getDeobfuscatedName()); + } + + for (MethodMapping methodMapping : classMapping.getMethodMappings()) { + tokens.put(MemberToken.ofMethod(ofClass, methodMapping.getObfuscatedName(), methodMapping.getObfuscatedDescriptor()), methodMapping.getDeobfuscatedName()); + } + + for (InnerClassMapping mapping : classMapping.getInnerClassMappings()) { + appendClass(tokens, mapping); + } + } + + private record MemberToken( + TokenType type, + @Nullable MCPReader.MemberToken owner, + String name, + @Nullable String descriptor + ) { + static MemberToken ofClass(String name) { + return new MemberToken(TokenType.CLASS, null, name, null); + } + + static MemberToken ofField(MemberToken owner, String name) { + return new MemberToken(TokenType.FIELD, owner, name, null); + } + + static MemberToken ofMethod(MemberToken owner, String name, String descriptor) { + return new MemberToken(TokenType.METHOD, owner, name, descriptor); + } + } + + private enum TokenType { + CLASS, + METHOD, + FIELD + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/MappingException.java b/src/main/java/net/fabricmc/loom/util/srg/MappingException.java new file mode 100644 index 00000000..5bf2a63b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/MappingException.java @@ -0,0 +1,36 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +/** + * An exception that occurs when processing obfuscation mappings. + * + * @author Juuz + */ +public class MappingException extends RuntimeException { + public MappingException(String message) { + super(message); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SpecialSourceExecutor.java b/src/main/java/net/fabricmc/loom/util/srg/SpecialSourceExecutor.java new file mode 100644 index 00000000..59392e94 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/SpecialSourceExecutor.java @@ -0,0 +1,148 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.File; +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.List; +import java.util.Set; +import java.util.stream.Collectors; + +import com.google.common.base.Stopwatch; +import org.apache.commons.io.output.NullOutputStream; +import org.gradle.api.Project; +import org.gradle.api.logging.LogLevel; +import org.gradle.api.logging.configuration.ShowStacktrace; + +import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.configuration.providers.forge.McpConfigProvider.RemapAction; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.ThreadingUtils; + +public class SpecialSourceExecutor { + private static String trimLeadingSlash(String string) { + if (string.startsWith(File.separator)) { + return string.substring(File.separator.length()); + } else if (string.startsWith("/")) { + return string.substring(1); + } + + return string; + } + + public static Path produceSrgJar(RemapAction remapAction, Project project, String side, Set<File> mcLibs, Path officialJar, Path mappings) + throws Exception { + Set<String> filter = Files.readAllLines(mappings, StandardCharsets.UTF_8).stream() + .filter(s -> !s.startsWith("\t")) + .map(s -> s.split(" ")[0] + ".class") + .collect(Collectors.toSet()); + LoomGradleExtension extension = LoomGradleExtension.get(project.getProject()); + Path stripped = extension.getFiles().getProjectBuildCache().toPath().resolve(officialJar.getFileName().toString().substring(0, officialJar.getFileName().toString().length() - 4) + "-filtered.jar"); + Files.deleteIfExists(stripped); + + Stopwatch stopwatch = Stopwatch.createStarted(); + + try (FileSystemUtil.Delegate output = FileSystemUtil.getJarFileSystem(stripped, true)) { + try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(officialJar, false)) { + ThreadingUtils.TaskCompleter completer = ThreadingUtils.taskCompleter(); + + for (Path path : (Iterable<? extends Path>) Files.walk(fs.get().getPath("/"))::iterator) { + String trimLeadingSlash = trimLeadingSlash(path.toString()); + if (!trimLeadingSlash.endsWith(".class")) continue; + boolean has = filter.contains(trimLeadingSlash); + String s = trimLeadingSlash; + + while (s.contains("$") && !has) { + s = s.substring(0, s.lastIndexOf("$")) + ".class"; + has = filter.contains(s); + } + + if (!has) continue; + Path to = output.get().getPath(trimLeadingSlash); + Path parent = to.getParent(); + if (parent != null) Files.createDirectories(parent); + + completer.add(() -> { + Files.copy(path, to, StandardCopyOption.COPY_ATTRIBUTES); + }); + } + + completer.complete(); + } + } finally { + project.getLogger().info("Copied class files in " + stopwatch.stop()); + } + + Path output = extension.getFiles().getProjectBuildCache().toPath().resolve(officialJar.getFileName().toString().substring(0, officialJar.getFileName().toString().length() - 4) + "-srg-output.jar"); + Files.deleteIfExists(output); + stopwatch = Stopwatch.createStarted(); + + List<String> args = remapAction.getArgs(stripped, output, mappings, project.files(mcLibs)); + + project.getLogger().lifecycle(":remapping minecraft (" + remapAction + ", " + side + ", official -> mojang)"); + + Path workingDir = tmpDir(); + + project.javaexec(spec -> { + spec.setArgs(args); + spec.setClasspath(remapAction.getClasspath()); + spec.workingDir(workingDir.toFile()); + spec.getMainClass().set(remapAction.getMainClass()); + + // if running with INFO or DEBUG logging + if (project.getGradle().getStartParameter().getShowStacktrace() != ShowStacktrace.INTERNAL_EXCEPTIONS + || project.getGradle().getStartParameter().getLogLevel().compareTo(LogLevel.LIFECYCLE) < 0) { + spec.setStandardOutput(System.out); + spec.setErrorOutput(System.err); + } else { + spec.setStandardOutput(NullOutputStream.NULL_OUTPUT_STREAM); + spec.setErrorOutput(NullOutputStream.NULL_OUTPUT_STREAM); + } + }).rethrowFailure().assertNormalExitValue(); + + project.getLogger().lifecycle(":remapped minecraft (" + remapAction + ", " + side + ", official -> mojang) in " + stopwatch.stop()); + + Files.deleteIfExists(stripped); + + Path tmp = tmpFile(); + Files.deleteIfExists(tmp); + Files.copy(output, tmp); + + Files.deleteIfExists(output); + return tmp; + } + + private static Path tmpFile() throws IOException { + return Files.createTempFile(null, null); + } + + private static Path tmpDir() throws IOException { + return Files.createTempDirectory(null); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java b/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java new file mode 100644 index 00000000..ede2dbad --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgMerger.java @@ -0,0 +1,406 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2020-2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.StringReader; +import java.io.UncheckedIOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.google.common.base.MoreObjects; +import com.google.common.base.Stopwatch; +import org.apache.commons.io.IOUtils; +import org.gradle.api.logging.Logger; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.loom.util.function.CollectionUtil; +import net.fabricmc.mappingio.FlatMappingVisitor; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.adapter.RegularAsFlatMappingVisitor; +import net.fabricmc.mappingio.format.Tiny2Writer; +import net.fabricmc.mappingio.format.TsrgReader; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MappingTreeView; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +/** + * Utilities for merging SRG mappings. + * + * @author Juuz + */ +public final class SrgMerger { + private final Logger logger; + private final Map<String, List<MappingTreeView.MemberMappingView>> addRegardlessSrg = new HashMap<>(); + private final MemoryMappingTree srg; + private final MemoryMappingTree src; + private final MemoryMappingTree output; + private final FlatMappingVisitor flatOutput; + private final List<Runnable> postProcesses = new ArrayList<>(); + private final boolean lenient; + private final Set<String> methodSrgNames = new HashSet<>(); + + public SrgMerger(Logger logger, Path srg, @Nullable Supplier<Path> mojmap, Path tiny, boolean lenient) throws IOException { + this.logger = logger; + this.srg = readSrg(srg, mojmap); + this.src = new MemoryMappingTree(); + this.output = new MemoryMappingTree(); + this.flatOutput = new RegularAsFlatMappingVisitor(output); + this.lenient = lenient; + + MappingReader.read(tiny, this.src); + + if (!"official".equals(this.src.getSrcNamespace())) { + throw new MappingException("Mapping file " + tiny + " does not have the 'official' namespace as the default!"); + } + + this.output.visitNamespaces(this.src.getSrcNamespace(), Stream.concat(Stream.of("srg"), this.src.getDstNamespaces().stream()).collect(Collectors.toList())); + } + + public MemoryMappingTree merge() throws IOException { + for (MappingTree.ClassMapping klass : this.srg.getClasses()) { + classToTiny(klass); + } + + try { + for (Runnable process : postProcesses) { + process.run(); + } + } catch (UncheckedIOException e) { + throw e.getCause(); + } + + return output; + } + + /** + * Merges SRG mappings with a tiny mappings tree through the obf names. + * + * @param srg the SRG file in .tsrg format + * @param mojmap the path to the mojmap file used for generating mojmap+srg names, may be null + * @param tiny the tiny file + * @param out the output file, will be in tiny v2 + * @param lenient whether to ignore missing tiny mapping + * @throws IOException if an IO error occurs while reading or writing the mappings + * @throws MappingException if the input tiny tree's default namespace is not 'official' + * or if an element mentioned in the SRG file does not have tiny mappings + */ + public static void mergeSrg(Logger logger, @Nullable Supplier<Path> mojmap, Path srg, Path tiny, Path out, boolean lenient) + throws IOException, MappingException { + Stopwatch stopwatch = Stopwatch.createStarted(); + MemoryMappingTree tree = mergeSrg(logger, mojmap, srg, tiny, lenient); + + try (Tiny2Writer writer = new Tiny2Writer(Files.newBufferedWriter(out), false)) { + tree.accept(writer); + } catch (IOException e) { + e.printStackTrace(); + } + + logger.info(":merged srg mappings in " + stopwatch.stop()); + } + + /** + * Merges SRG mappings with a tiny mappings tree through the obf names. + * + * @param srg the SRG file in .tsrg format + * @param mojmap the path to the mojmap file used for generating mojmap+srg names, may be null + * @param tiny the tiny file + * @param lenient whether to ignore missing tiny mapping + * @return the created mapping tree + * @throws IOException if an IO error occurs while reading or writing the mappings + * @throws MappingException if the input tiny tree's default namespace is not 'official' + * or if an element mentioned in the SRG file does not have tiny mappings + */ + public static MemoryMappingTree mergeSrg(Logger logger, @Nullable Supplier<Path> mojmap, Path srg, Path tiny, boolean lenient) + throws IOException, MappingException { + return new SrgMerger(logger, srg, mojmap, tiny, lenient).merge(); + } + + private MemoryMappingTree readSrg(Path srg, @Nullable Supplier<Path> mojmap) throws IOException { + try (BufferedReader reader = Files.newBufferedReader(srg)) { + String content = IOUtils.toString(reader); + + if (content.startsWith("tsrg2") && mojmap != null) { + addRegardlessSrgs(mojmap); + } + + MemoryMappingTree tsrg = new MemoryMappingTree(); + TsrgReader.read(new StringReader(content), tsrg); + return tsrg; + } + } + + private void addRegardlessSrgs(Supplier<Path> mojmap) throws IOException { + MemoryMappingTree mojmapTree = readTsrg2ToTinyTree(mojmap.get()); + + for (MappingTree.ClassMapping classDef : mojmapTree.getClasses()) { + for (MappingTree.MethodMapping methodDef : classDef.getMethods()) { + String name = methodDef.getSrcName(); + + if (name.indexOf('<') != 0 && name.equals(methodDef.getDstName(0))) { + addRegardlessSrg.computeIfAbsent(classDef.getSrcName(), $ -> new ArrayList<>()).add(methodDef); + } + } + + for (MappingTree.FieldMapping fieldDef : classDef.getFields()) { + if (fieldDef.getSrcName().equals(fieldDef.getDstName(0))) { + addRegardlessSrg.computeIfAbsent(classDef.getSrcName(), $ -> new ArrayList<>()).add(fieldDef); + } + } + } + } + + private static MemoryMappingTree readTsrg2ToTinyTree(Path path) throws IOException { + MemoryMappingTree tree = new MemoryMappingTree(); + + try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + MappingReader.read(reader, tree); + } + + return tree; + } + + private void classToTiny(MappingTree.ClassMapping klass) throws IOException { + String obf = klass.getSrcName(); + String srg = klass.getDstName(0); + MappingTree.ClassMapping classDef = this.src.getClass(obf); + + if (classDef == null) { + if (lenient) { + return; + } else { + throw new MappingException("Missing class: " + obf + " (srg: " + srg + ")"); + } + } + + List<String> classNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? srg : classDef.getName(namespace) + ); + + flatOutput.visitClass(obf, classNames.toArray(new String[0])); + + if (classDef.getComment() != null) { + flatOutput.visitClassComment(obf, classDef.getComment()); + } + + for (MappingTree.MethodMapping method : klass.getMethods()) { + MappingTree.MethodMapping def = CollectionUtil.find( + classDef.getMethods(), + m -> m.getName("official").equals(method.getSrcName()) && m.getDesc("official").equals(method.getSrcDesc()) + ).orElse(null); + + String methodSrgName = method.getDstName(0); + + if (def == null) { + if (lenient) { + // TODO Big Hack! + // We are checking if there are methods with the same srg name that are already added, if it is, then we would not fill in these names + // This is especially troublesome with methods annotated with @DontObfuscate (e.g. m_129629_) + // with environments like yarn where methods with the same srg name may not inherit the same names due to parameter mappings and inheritance + // This requires further testing! + postProcesses.add(() -> { + if (!methodSrgNames.contains(methodSrgName)) { + List<String> methodNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? methodSrgName : method.getSrcName() + ); + + try { + flatOutput.visitMethod(obf, method.getSrcName(), method.getSrcDesc(), methodNames.toArray(new String[0])); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + }); + } else { + throw new MappingException("Missing method: " + method.getSrcName() + " (srg: " + methodSrgName + ")"); + } + + continue; + } + + methodToTiny(obf, method, methodSrgName, def); + + if (methodSrgName.startsWith("func_") || methodSrgName.startsWith("m_")) { + methodSrgNames.add(methodSrgName); + } + } + + postProcesses.add(() -> { + // TODO: This second iteration seems a bit wasteful. + // Is it possible to just iterate this and leave SRG out? + for (MappingTree.MethodMapping def : classDef.getMethods()) { + // If obf = some other name: some special name that srg might not contain. + // This includes constructors and overridden JDK methods. + if (!def.getSrcName().equals(def.getDstName(0))) { + continue; + } + + MappingTree.MethodMapping method = CollectionUtil.find( + klass.getMethods(), + m -> m.getSrcName().equals(def.getName("official")) && m.getSrcDesc().equals(def.getDesc("official")) + ).orElse(null); + + if (method == null) { + try { + methodToTiny(obf, null, def.getSrcName(), def); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } + } + }); + + for (MappingTree.FieldMapping field : klass.getFields()) { + MappingTree.FieldMapping def = CollectionUtil.find( + classDef.getFields(), + f -> f.getName("official").equals(field.getSrcName()) + ).orElse(nullOrThrow(() -> new MappingException("Missing field: " + field.getSrcName() + " (srg: " + field.getDstName(0) + ")"))); + + if (def == null) { + if (tryMatchRegardlessSrgsField(obf, field)) { + List<String> fieldNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? field.getDstName(0) : field.getSrcName() + ); + + flatOutput.visitField(obf, field.getSrcName(), field.getSrcDesc(), fieldNames.toArray(new String[0])); + } + + continue; + } + + List<String> fieldNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? field.getDstName(0) : def.getName(namespace) + ); + + flatOutput.visitField(obf, def.getName("official"), def.getDesc("official"), fieldNames.toArray(new String[0])); + + if (def.getComment() != null) { + flatOutput.visitFieldComment(obf, def.getName("official"), def.getDesc("official"), def.getComment()); + } + } + } + + private void methodToTiny(String obfClassName, @Nullable MappingTree.MethodMapping srgMethod, @Nullable String srgMethodName, MappingTree.MethodMapping actualMethod) + throws IOException { + if (srgMethod != null && srgMethodName != null) { + srgMethodName = srgMethod.getDstName(0); + } + + String finalSrgMethodName = srgMethodName; + List<String> methodNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? finalSrgMethodName : actualMethod.getName(namespace) + ); + + flatOutput.visitMethod(obfClassName, actualMethod.getName("official"), actualMethod.getDesc("official"), methodNames.toArray(new String[0])); + + if (actualMethod.getComment() != null) { + flatOutput.visitMethodComment(obfClassName, actualMethod.getName("official"), actualMethod.getDesc("official"), actualMethod.getComment()); + } + + for (MappingTree.MethodArgMapping arg : actualMethod.getArgs()) { + MappingTree.MethodArgMapping srgArg = srgMethod != null ? srgMethod.getArg(arg.getArgPosition(), arg.getLvIndex(), arg.getName("official")) : null; + String srgName = srgArg != null ? srgArg.getDstName(0) : null; + List<String> argNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? srgName : arg.getName(namespace) + ); + + flatOutput.visitMethodArg(obfClassName, actualMethod.getName("official"), actualMethod.getDesc("official"), arg.getArgPosition(), arg.getLvIndex(), arg.getName("official"), argNames.toArray(new String[0])); + + if (arg.getComment() != null) { + flatOutput.visitMethodArgComment(obfClassName, actualMethod.getName("official"), actualMethod.getDesc("official"), arg.getArgPosition(), arg.getLvIndex(), arg.getName("official"), arg.getComment()); + } + } + + for (MappingTree.MethodVarMapping var : actualMethod.getVars()) { + MappingTree.MethodVarMapping srgVar = srgMethod != null ? srgMethod.getVar(var.getLvtRowIndex(), var.getLvIndex(), var.getStartOpIdx(), var.getName("official")) : null; + String srgName = srgVar != null ? srgVar.getDstName(0) : null; + List<String> varNames = CollectionUtil.map( + output.getDstNamespaces(), + namespace -> "srg".equals(namespace) ? srgName : var.getName(namespace) + ); + + flatOutput.visitMethodVar(obfClassName, actualMethod.getName("official"), actualMethod.getDesc("official"), var.getLvtRowIndex(), var.getLvIndex(), var.getStartOpIdx(), var.getName("official"), varNames.toArray(new String[0])); + + if (var.getComment() != null) { + flatOutput.visitMethodVarComment(obfClassName, actualMethod.getName("official"), actualMethod.getDesc("official"), var.getLvtRowIndex(), var.getLvIndex(), var.getStartOpIdx(), var.getName("official"), var.getComment()); + } + } + } + + private boolean tryMatchRegardlessSrgsMethod(String obf, MappingTree.MethodMapping method) { + List<MappingTreeView.MemberMappingView> mutableDescriptoredList = addRegardlessSrg.get(obf); + + if (!method.getDstName(0).equals(method.getSrcName())) { + for (MappingTreeView.MemberMappingView descriptored : MoreObjects.firstNonNull(mutableDescriptoredList, Collections.<MappingTreeView.MemberMappingView>emptyList())) { + if (descriptored instanceof MappingTree.MethodMapping && descriptored.getSrcName().equals(method.getSrcName()) && descriptored.getSrcDesc().equals(method.getSrcDesc())) { + return true; + } + } + } + + return false; + } + + private boolean tryMatchRegardlessSrgsField(String obf, MappingTree.FieldMapping field) { + List<MappingTreeView.MemberMappingView> mutableDescriptoredList = addRegardlessSrg.get(obf); + + if (!field.getDstName(0).equals(field.getSrcName())) { + for (MappingTreeView.MemberMappingView descriptored : MoreObjects.firstNonNull(mutableDescriptoredList, Collections.<MappingTreeView.MemberMappingView>emptyList())) { + if (descriptored instanceof MappingTree.FieldMapping && descriptored.getSrcName().equals(field.getSrcName())) { + return true; + } + } + } + + return false; + } + + @Nullable + private <T, X extends Exception> T nullOrThrow(Supplier<X> exception) throws X { + if (lenient) { + return null; + } else { + throw exception.get(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java new file mode 100644 index 00000000..456b8b1b --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/SrgNamedWriter.java @@ -0,0 +1,47 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.cadixdev.lorenz.io.srg.SrgWriter; +import org.gradle.api.logging.Logger; + +import net.fabricmc.lorenztiny.TinyMappingsReader; +import net.fabricmc.mappingio.tree.MappingTree; + +public class SrgNamedWriter { + public static void writeTo(Logger logger, Path srgFile, MappingTree mappings, String from, String to) throws IOException { + Files.deleteIfExists(srgFile); + + try (SrgWriter writer = new SrgWriter(Files.newBufferedWriter(srgFile))) { + try (TinyMappingsReader reader = new TinyMappingsReader(mappings, from, to)) { + writer.write(reader.read()); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/Tsrg2Utils.java b/src/main/java/net/fabricmc/loom/util/srg/Tsrg2Utils.java new file mode 100644 index 00000000..4ed3a884 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/Tsrg2Utils.java @@ -0,0 +1,144 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.io.IOException; +import java.io.Writer; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; + +import org.cadixdev.lorenz.MappingSet; +import org.cadixdev.lorenz.io.srg.tsrg.TSrgWriter; +import org.cadixdev.lorenz.model.ClassMapping; +import org.cadixdev.lorenz.model.MethodMapping; + +import net.fabricmc.mappingio.MappingVisitor; +import net.fabricmc.mappingio.MappingWriter; +import net.fabricmc.mappingio.adapter.ForwardingMappingVisitor; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +public class Tsrg2Utils { + public static void writeTsrg(Consumer<MappingVisitor> visitorConsumer, String dstNamespace, boolean applyParameterMappings, Writer writer) + throws IOException { + MappingSet set; + + try (MappingsIO2LorenzWriter lorenzWriter = new MappingsIO2LorenzWriter(dstNamespace, applyParameterMappings)) { + visitorConsumer.accept(lorenzWriter); + set = lorenzWriter.read(); + } + + try (TSrgWriter w = new TSrgWriter(writer)) { + w.write(set); + } + } + + // TODO Move this elsewhere + public abstract static class MappingsIO2Others extends ForwardingMappingVisitor implements MappingWriter { + public MappingsIO2Others() { + super(new MemoryMappingTree()); + } + + public MappingTree tree() { + return (MappingTree) next; + } + + @Override + public void close() throws IOException { + MappingTree tree = tree(); + List<String> names = new ArrayList<>(); + + for (MappingTree.ClassMapping aClass : tree.getClasses()) { + names.add(aClass.getSrcName()); + } + + for (String name : names) { + tree.removeClass(name); + } + } + } + + public static class MappingsIO2LorenzWriter extends MappingsIO2Others { + private final Object dstNamespaceUnresolved; + private int dstNamespace; + private boolean applyParameterMappings; + + public MappingsIO2LorenzWriter(int dstNamespace, boolean applyParameterMappings) { + this.dstNamespaceUnresolved = dstNamespace; + this.applyParameterMappings = applyParameterMappings; + } + + public MappingsIO2LorenzWriter(String dstNamespace, boolean applyParameterMappings) { + this.dstNamespaceUnresolved = dstNamespace; + this.applyParameterMappings = applyParameterMappings; + } + + @Override + public void visitNamespaces(String srcNamespace, List<String> dstNamespaces) throws IOException { + super.visitNamespaces(srcNamespace, dstNamespaces); + this.dstNamespace = dstNamespaceUnresolved instanceof Integer ? (Integer) dstNamespaceUnresolved : dstNamespaces.indexOf((String) dstNamespaceUnresolved); + } + + public MappingSet read() throws IOException { + return this.read(MappingSet.create()); + } + + public MappingSet read(final MappingSet mappings) throws IOException { + MappingTree tree = tree(); + + for (MappingTree.ClassMapping aClass : tree.getClasses()) { + ClassMapping<?, ?> lClass = mappings.getOrCreateClassMapping(aClass.getSrcName()) + .setDeobfuscatedName(aClass.getDstName(dstNamespace)); + + for (MappingTree.FieldMapping aField : aClass.getFields()) { + String srcDesc = aField.getSrcDesc(); + + if (srcDesc == null || srcDesc.isEmpty()) { + lClass.getOrCreateFieldMapping(aField.getSrcName()) + .setDeobfuscatedName(aField.getDstName(dstNamespace)); + } else { + lClass.getOrCreateFieldMapping(aField.getSrcName(), srcDesc) + .setDeobfuscatedName(aField.getDstName(dstNamespace)); + } + } + + for (MappingTree.MethodMapping aMethod : aClass.getMethods()) { + MethodMapping lMethod = lClass.getOrCreateMethodMapping(aMethod.getSrcName(), aMethod.getSrcDesc()) + .setDeobfuscatedName(aMethod.getDstName(dstNamespace)); + + if (applyParameterMappings) { + for (MappingTree.MethodArgMapping aArg : aMethod.getArgs()) { + lMethod.getOrCreateParameterMapping(aArg.getLvIndex()) + .setDeobfuscatedName(aArg.getDstName(dstNamespace)); + } + } + } + } + + return mappings; + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/srg/Tsrg2Writer.java b/src/main/java/net/fabricmc/loom/util/srg/Tsrg2Writer.java new file mode 100644 index 00000000..7463e447 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/srg/Tsrg2Writer.java @@ -0,0 +1,114 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2021 FabricMC + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package net.fabricmc.loom.util.srg; + +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MappingTreeView; + +public class Tsrg2Writer { + public Tsrg2Writer() { + } + + public static String serialize(MappingTree tree) { + List<String> namespaces = Stream.concat(Stream.of(tree.getSrcNamespace()), tree.getDstNamespaces().stream()).collect(Collectors.toList()); + StringBuilder builder = new StringBuilder(); + writeHeader(namespaces, builder); + + for (MappingTree.ClassMapping classMapping : tree.getClasses()) { + writeClass(namespaces, classMapping, builder); + } + + return builder.toString(); + } + + private static void writeClass(List<String> namespaces, MappingTree.ClassMapping def, StringBuilder builder) { + writeMapped(null, namespaces, def, builder); + + for (MappingTree.MethodMapping method : def.getMethods()) { + writeMethod(namespaces, method, builder); + } + + for (MappingTree.FieldMapping field : def.getFields()) { + writeMapped('\t', namespaces, field, builder); + } + } + + private static void writeMethod(List<String> namespaces, MappingTree.MethodMapping def, StringBuilder builder) { + writeMapped('\t', namespaces, def, builder); + + for (MappingTree.MethodArgMapping arg : def.getArgs()) { + builder.append("\t\t").append(arg.getLvIndex()); + writeMapped(' ', namespaces, arg, builder); + } + } + + private static void writeField(List<String> namespaces, MappingTree.FieldMapping def, StringBuilder builder) { + writeMapped('\t', namespaces, def, builder); + } + + private static void writeMapped(Character first, List<String> namespaces, MappingTreeView.ElementMappingView mapped, StringBuilder builder) { + String[] names = namespaces.stream().map(mapped::getName).toArray(String[]::new); + + for (int i = 0; i < names.length; ++i) { + String name = names[i]; + + if (i == 0) { + if (first != null) { + builder.append(first); + } + } else { + builder.append(' '); + } + + builder.append(name); + + if (i == 0 && mapped instanceof MappingTreeView.MemberMappingView) { + String descriptor = ((MappingTreeView.MemberMappingView) mapped).getSrcDesc(); + + if (descriptor != null && !descriptor.isEmpty()) { + builder.append(' '); + builder.append(descriptor); + } + } + } + + builder.append('\n'); + } + + private static void writeHeader(List<String> namespaces, StringBuilder builder) { + builder.append("tsrg2"); + + for (String namespace : namespaces) { + builder.append(' '); + builder.append(namespace); + } + + builder.append('\n'); + } +} |