From e2439b7f57a82c365d4726d068b68ea2eb606f78 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Mon, 11 Oct 2021 13:47:16 +0100 Subject: [PATCH] Rewrite GenSources including full support for CFR. (#511) * Rewrite CFR decompiler interface. Support javadoc * CFR line numbers and fixes. * Cleanup and fix * Use WorkerExecutor to fork, massively cleans up the fernflower code, but does remove the fancy multithreaded logging. * Use IPC to get logging back from the decompilers. * Cleanup UnpickJarTask, fix leak in IPCServer * Used published CFR build * Handle older windows versions that do not support AF_UNIX. * Fixes and basic unit test * Improve memory handling of genSources * Stop decompile worker JVM --- build.gradle | 2 +- .../decompilers/DecompilationMetadata.java | 4 +- .../assets/MinecraftAssetsProvider.java | 12 +- .../decompilers/DecompilerConfiguration.java | 6 +- .../loom/decompilers/LineNumberRemapper.java | 7 +- .../cfr/CFRObfuscationMapping.java | 230 +++++++++++++++++ .../loom/decompilers/cfr/CFRSinkFactory.java | 151 +++++++++++ .../decompilers/cfr/FabricCFRDecompiler.java | 233 ----------------- .../decompilers/cfr/LoomCFRDecompiler.java | 148 +++++++++++ .../AbstractFernFlowerDecompiler.java | 163 ------------ .../fernflower/AbstractForkedFFExecutor.java | 107 -------- .../FabricFernFlowerDecompiler.java | 39 ++- ...dFFExecutor.java => FernflowerLogger.java} | 64 +++-- .../fernflower/ForkingJavaExec.java | 70 ----- .../fernflower/ThreadIDFFLogger.java | 127 --------- .../loom/task/GenerateSourcesTask.java | 244 +++++++++++++++--- .../net/fabricmc/loom/task/LoomTasks.java | 8 +- .../net/fabricmc/loom/task/UnpickJarTask.java | 77 +++--- .../fabricmc/loom/util/IOStringConsumer.java | 31 +++ .../fabricmc/loom/util/OperatingSystem.java | 16 ++ .../fabricmc/loom/util/SourceRemapper.java | 6 +- ...sLogger.java => ProgressLoggerHelper.java} | 18 +- .../ThreadedProgressLoggerConsumer.java | 95 +++++++ .../gradle/ThreadedSimpleProgressLogger.java | 36 +++ .../WorkerDaemonClientsManagerHelper.java | 77 ++++++ .../net/fabricmc/loom/util/ipc/IPCClient.java | 67 +++++ .../net/fabricmc/loom/util/ipc/IPCServer.java | 88 +++++++ .../test/integration/DecompileTest.groovy | 3 +- .../fabricmc/loom/test/unit/IPCTest.groovy | 65 +++++ .../resources/projects/decompile/build.gradle | 6 +- 30 files changed, 1355 insertions(+), 845 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java delete mode 100644 src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java delete mode 100644 src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java delete mode 100644 src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractForkedFFExecutor.java rename src/main/java/net/fabricmc/loom/decompilers/fernflower/{FabricForkedFFExecutor.java => FernflowerLogger.java} (53%) delete mode 100644 src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java delete mode 100644 src/main/java/net/fabricmc/loom/decompilers/fernflower/ThreadIDFFLogger.java create mode 100644 src/main/java/net/fabricmc/loom/util/IOStringConsumer.java rename src/main/java/net/fabricmc/loom/util/gradle/{ProgressLogger.java => ProgressLoggerHelper.java} (92%) create mode 100644 src/main/java/net/fabricmc/loom/util/gradle/ThreadedProgressLoggerConsumer.java create mode 100644 src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java create mode 100644 src/main/java/net/fabricmc/loom/util/gradle/WorkerDaemonClientsManagerHelper.java create mode 100644 src/main/java/net/fabricmc/loom/util/ipc/IPCClient.java create mode 100644 src/main/java/net/fabricmc/loom/util/ipc/IPCServer.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/IPCTest.groovy diff --git a/build.gradle b/build.gradle index 3eeff4c..78b3d18 100644 --- a/build.gradle +++ b/build.gradle @@ -88,7 +88,7 @@ dependencies { // decompilers implementation ('net.fabricmc:fabric-fernflower:1.4.1') - implementation ('org.benf:cfr:0.151') + implementation ('net.fabricmc:cfr:0.0.8') // source code remapping implementation ('net.fabricmc:mercury:0.2.4') diff --git a/src/main/java/net/fabricmc/loom/api/decompilers/DecompilationMetadata.java b/src/main/java/net/fabricmc/loom/api/decompilers/DecompilationMetadata.java index 472254e..a4f6dcf 100644 --- a/src/main/java/net/fabricmc/loom/api/decompilers/DecompilationMetadata.java +++ b/src/main/java/net/fabricmc/loom/api/decompilers/DecompilationMetadata.java @@ -27,5 +27,7 @@ package net.fabricmc.loom.api.decompilers; import java.nio.file.Path; import java.util.Collection; -public record DecompilationMetadata(int numberOfThreads, Path javaDocs, Collection libraries) { +import net.fabricmc.loom.util.IOStringConsumer; + +public record DecompilationMetadata(int numberOfThreads, Path javaDocs, Collection libraries, IOStringConsumer logger) { } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java index ec91627..87f41e0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/assets/MinecraftAssetsProvider.java @@ -45,7 +45,7 @@ import net.fabricmc.loom.configuration.providers.MinecraftProviderImpl; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.util.MirrorUtil; import net.fabricmc.loom.util.HashedDownloadUtil; -import net.fabricmc.loom.util.gradle.ProgressLogger; +import net.fabricmc.loom.util.gradle.ProgressLoggerHelper; public class MinecraftAssetsProvider { public static void provide(MinecraftProviderImpl minecraftProvider, Project project) throws IOException { @@ -78,7 +78,7 @@ public class MinecraftAssetsProvider { HashedDownloadUtil.downloadIfInvalid(new URL(assetIndex.url()), assetsInfo, assetIndex.sha1(), project.getLogger(), false); } - Deque loggers = new ConcurrentLinkedDeque<>(); + Deque loggers = new ConcurrentLinkedDeque<>(); ExecutorService executor = Executors.newFixedThreadPool(Math.min(10, Math.max(Runtime.getRuntime().availableProcessors() / 2, 1))); AssetIndex index; @@ -114,15 +114,15 @@ public class MinecraftAssetsProvider { project.getLogger().debug("validating asset " + assetName[0]); - final ProgressLogger[] progressLogger = new ProgressLogger[1]; + final ProgressLoggerHelper[] progressLogger = new ProgressLoggerHelper[1]; try { HashedDownloadUtil.downloadIfInvalid(new URL(MirrorUtil.getResourcesBase(project) + sha1.substring(0, 2) + "/" + sha1), file, sha1, project.getLogger(), true, () -> { - ProgressLogger logger = loggers.pollFirst(); + ProgressLoggerHelper logger = loggers.pollFirst(); if (logger == null) { //Create a new logger if we need one - progressLogger[0] = ProgressLogger.getProgressFactory(project, MinecraftAssetsProvider.class.getName()); + progressLogger[0] = ProgressLoggerHelper.getProgressFactory(project, MinecraftAssetsProvider.class.getName()); progressLogger[0].start("Downloading assets...", "assets"); } else { // use a free logger if we can @@ -157,6 +157,6 @@ public class MinecraftAssetsProvider { throw new RuntimeException(e); } - loggers.forEach(ProgressLogger::completed); + loggers.forEach(ProgressLoggerHelper::completed); } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java b/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java index bd51c4e..0c58e5c 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java +++ b/src/main/java/net/fabricmc/loom/decompilers/DecompilerConfiguration.java @@ -27,7 +27,7 @@ package net.fabricmc.loom.decompilers; import org.gradle.api.Project; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.decompilers.cfr.FabricCFRDecompiler; +import net.fabricmc.loom.decompilers.cfr.LoomCFRDecompiler; import net.fabricmc.loom.decompilers.fernflower.FabricFernFlowerDecompiler; public final class DecompilerConfiguration { @@ -36,7 +36,7 @@ public final class DecompilerConfiguration { public static void setup(Project project) { LoomGradleExtension extension = LoomGradleExtension.get(project); - extension.getGameDecompilers().add(new FabricFernFlowerDecompiler(project)); - extension.getGameDecompilers().add(new FabricCFRDecompiler(project)); + extension.getGameDecompilers().add(new FabricFernFlowerDecompiler()); + extension.getGameDecompilers().add(new LoomCFRDecompiler()); } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java index 7e50030..307a340 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java +++ b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java @@ -47,10 +47,9 @@ import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import net.fabricmc.loom.util.Constants; -import net.fabricmc.loom.util.gradle.ProgressLogger; +import net.fabricmc.loom.util.IOStringConsumer; /** - * TODO, Move to stitch. * Created by covers1624 on 18/02/19. */ public class LineNumberRemapper { @@ -88,7 +87,7 @@ public class LineNumberRemapper { } } - public void process(ProgressLogger logger, Path input, Path output) throws IOException { + public void process(IOStringConsumer logger, Path input, Path output) throws IOException { Files.walkFileTree(input, new SimpleFileVisitor<>() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { @@ -110,7 +109,7 @@ public class LineNumberRemapper { String idx = rel.substring(0, rel.length() - 6); if (logger != null) { - logger.progress("Remapping " + idx); + logger.accept("Remapping " + idx); } int dollarPos = idx.indexOf('$'); //This makes the assumption that only Java classes are to be remapped. diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java new file mode 100644 index 0000000..83afc21 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRObfuscationMapping.java @@ -0,0 +1,230 @@ +/* + * 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.decompilers.cfr; + +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.benf.cfr.reader.bytecode.analysis.types.JavaRefTypeInstance; +import org.benf.cfr.reader.bytecode.analysis.types.JavaTypeInstance; +import org.benf.cfr.reader.bytecode.analysis.types.MethodPrototype; +import org.benf.cfr.reader.entities.AccessFlag; +import org.benf.cfr.reader.entities.ClassFile; +import org.benf.cfr.reader.entities.ClassFileField; +import org.benf.cfr.reader.entities.Field; +import org.benf.cfr.reader.mapping.NullMapping; +import org.benf.cfr.reader.util.output.DelegatingDumper; +import org.benf.cfr.reader.util.output.Dumper; + +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.mappingio.MappingReader; +import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; +import net.fabricmc.mappingio.tree.MappingTree; +import net.fabricmc.mappingio.tree.MemoryMappingTree; + +public class CFRObfuscationMapping extends NullMapping { + private final MappingTree mappingTree; + + public CFRObfuscationMapping(Path mappings) { + mappingTree = readMappings(mappings); + } + + @Override + public Dumper wrap(Dumper d) { + return new JavadocProvidingDumper(d); + } + + private static MappingTree readMappings(Path input) { + try (BufferedReader reader = Files.newBufferedReader(input)) { + MemoryMappingTree mappingTree = new MemoryMappingTree(); + MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString()); + MappingReader.read(reader, nsSwitch); + + return mappingTree; + } catch (IOException e) { + throw new RuntimeException("Failed to read mappings", e); + } + } + + private class JavadocProvidingDumper extends DelegatingDumper { + JavadocProvidingDumper(Dumper delegate) { + super(delegate); + } + + @Override + public Dumper dumpClassDoc(JavaTypeInstance owner) { + MappingTree.ClassMapping mapping = getClassMapping(owner); + + if (mapping == null) { + return this; + } + + List recordComponentDocs = new LinkedList<>(); + + if (isRecord(owner)) { + ClassFile classFile = ((JavaRefTypeInstance) owner).getClassFile(); + + for (ClassFileField field : classFile.getFields()) { + if (field.getField().testAccessFlag(AccessFlag.ACC_STATIC)) { + continue; + } + + MappingTree.FieldMapping fieldMapping = mapping.getField(field.getFieldName(), field.getField().getDescriptor()); + + if (fieldMapping == null) { + continue; + } + + String comment = fieldMapping.getComment(); + + if (comment != null) { + recordComponentDocs.add(String.format("@param %s %s", fieldMapping.getSrcName(), comment)); + } + } + } + + String comment = mapping.getComment(); + + if (comment != null || !recordComponentDocs.isEmpty()) { + print("/**").newln(); + + if (comment != null) { + for (String line : comment.split("\\R")) { + print(" * ").print(line).newln(); + } + + if (!recordComponentDocs.isEmpty()) { + print(" * ").newln(); + } + } + + for (String componentDoc : recordComponentDocs) { + print(" * ").print(componentDoc).newln(); + } + + print(" */").newln(); + } + + return this; + } + + @Override + public Dumper dumpMethodDoc(MethodPrototype method) { + MappingTree.ClassMapping classMapping = getClassMapping(method.getOwner()); + + if (classMapping == null) { + return this; + } + + List lines = new ArrayList<>(); + MappingTree.MethodMapping mapping = classMapping.getMethod(method.getName(), method.getOriginalDescriptor()); + + if (mapping != null) { + String comment = mapping.getComment(); + + if (comment != null) { + lines.addAll(Arrays.asList(comment.split("\\R"))); + } + + for (MappingTree.MethodArgMapping arg : mapping.getArgs()) { + String argComment = arg.getComment(); + + if (argComment != null) { + lines.addAll(Arrays.asList(("@param " + arg.getSrcName() + " " + argComment).split("\\R"))); + } + } + } + + if (!lines.isEmpty()) { + print("/**").newln(); + + for (String line : lines) { + print(" * ").print(line).newln(); + } + + print(" */").newln(); + } + + return this; + } + + @Override + public Dumper dumpFieldDoc(Field field, JavaTypeInstance owner) { + // None static fields in records are handled in the class javadoc. + if (isRecord(owner) && !isStatic(field)) { + return null; + } + + MappingTree.ClassMapping classMapping = getClassMapping(owner); + + if (classMapping == null) { + return null; + } + + MappingTree.FieldMapping fieldMapping = classMapping.getField(field.getFieldName(), field.getDescriptor()); + dumpComment(fieldMapping.getComment()); + + return this; + } + + private MappingTree.ClassMapping getClassMapping(JavaTypeInstance type) { + String qualifiedName = type.getRawName().replace('.', '/'); + return mappingTree.getClass(qualifiedName); + } + + private boolean isRecord(JavaTypeInstance javaTypeInstance) { + if (javaTypeInstance instanceof JavaRefTypeInstance) { + ClassFile classFile = ((JavaRefTypeInstance) javaTypeInstance).getClassFile(); + return classFile.getClassSignature().getSuperClass().getRawName().equals("java.lang.Record"); + } + + return false; + } + + private boolean isStatic(Field field) { + return field.testAccessFlag(AccessFlag.ACC_STATIC); + } + + private void dumpComment(String comment) { + if (comment == null || comment.isBlank()) { + return; + } + + print("/**").newln(); + + for (String line : comment.split("\n")) { + print(" * ").print(line).newln(); + } + + print(" */").newln(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java new file mode 100644 index 0000000..bdc5a28 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/CFRSinkFactory.java @@ -0,0 +1,151 @@ +/* + * 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.decompilers.cfr; + +import java.io.IOException; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.NavigableMap; +import java.util.Set; +import java.util.TreeMap; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; + +import com.google.common.base.Charsets; +import org.benf.cfr.reader.api.OutputSinkFactory; +import org.benf.cfr.reader.api.SinkReturns; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.util.IOStringConsumer; + +public class CFRSinkFactory implements OutputSinkFactory { + private static final Logger ERROR_LOGGER = LoggerFactory.getLogger(CFRSinkFactory.class); + + private final JarOutputStream outputStream; + private final IOStringConsumer logger; + private final Set addedDirectories = new HashSet<>(); + private final Map> lineMap = new TreeMap<>(); + + public CFRSinkFactory(JarOutputStream outputStream, IOStringConsumer logger) { + this.outputStream = outputStream; + this.logger = logger; + } + + @Override + public List getSupportedSinks(SinkType sinkType, Collection available) { + return switch (sinkType) { + case JAVA -> Collections.singletonList(SinkClass.DECOMPILED); + case LINENUMBER -> Collections.singletonList(SinkClass.LINE_NUMBER_MAPPING); + default -> Collections.emptyList(); + }; + } + + @Override + public Sink getSink(SinkType sinkType, SinkClass sinkClass) { + return switch (sinkType) { + case JAVA -> (Sink) decompiledSink(); + case LINENUMBER -> (Sink) lineNumberMappingSink(); + case EXCEPTION -> (e) -> ERROR_LOGGER.error((String) e); + default -> null; + }; + } + + private Sink decompiledSink() { + return sinkable -> { + String filename = sinkable.getPackageName().replace('.', '/'); + if (!filename.isEmpty()) filename += "/"; + filename += sinkable.getClassName() + ".java"; + + byte[] data = sinkable.getJava().getBytes(Charsets.UTF_8); + + writeToJar(filename, data); + }; + } + + private Sink lineNumberMappingSink() { + return sinkable -> { + final String className = sinkable.getClassName(); + final NavigableMap classFileMappings = sinkable.getClassFileMappings(); + final NavigableMap mappings = sinkable.getMappings(); + + if (classFileMappings == null || mappings == null) return; + + for (Map.Entry entry : mappings.entrySet()) { + // New line number + Integer dstLineNumber = entry.getValue(); + + // Line mapping in the original jar + Integer srcLineNumber = classFileMappings.get(entry.getKey()); + + if (srcLineNumber == null || dstLineNumber == null) continue; + + lineMap.computeIfAbsent(className, (c) -> new TreeMap<>()).put(srcLineNumber, dstLineNumber); + } + }; + } + + private synchronized void writeToJar(String filename, byte[] data) { + String[] path = filename.split("/"); + String pathPart = ""; + + for (int i = 0; i < path.length - 1; i++) { + pathPart += path[i] + "/"; + + if (addedDirectories.add(pathPart)) { + JarEntry entry = new JarEntry(pathPart); + entry.setTime(new Date().getTime()); + + try { + outputStream.putNextEntry(entry); + outputStream.closeEntry(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + JarEntry entry = new JarEntry(filename); + entry.setTime(new Date().getTime()); + entry.setSize(data.length); + + try { + logger.accept("Writing: " + filename); + outputStream.putNextEntry(entry); + outputStream.write(data); + outputStream.closeEntry(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public Map> getLineMap() { + return Collections.unmodifiableMap(lineMap); + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java deleted file mode 100644 index 7237a49..0000000 --- a/src/main/java/net/fabricmc/loom/decompilers/cfr/FabricCFRDecompiler.java +++ /dev/null @@ -1,233 +0,0 @@ -/* - * 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.decompilers.cfr; - -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -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.jar.Attributes; -import java.util.jar.JarEntry; -import java.util.jar.JarOutputStream; -import java.util.jar.Manifest; -import java.util.stream.Collectors; -import java.util.zip.ZipEntry; -import java.util.zip.ZipFile; - -import com.google.common.base.Charsets; -import com.google.common.collect.ImmutableMap; -import org.benf.cfr.reader.api.CfrDriver; -import org.benf.cfr.reader.api.ClassFileSource; -import org.benf.cfr.reader.api.OutputSinkFactory; -import org.benf.cfr.reader.api.SinkReturns; -import org.benf.cfr.reader.bytecode.analysis.parse.utils.Pair; -import org.gradle.api.Project; -import org.gradle.api.internal.project.ProjectInternal; -import org.gradle.internal.logging.progress.ProgressLogger; -import org.gradle.internal.logging.progress.ProgressLoggerFactory; -import org.gradle.internal.service.ServiceRegistry; - -import net.fabricmc.loom.api.decompilers.DecompilationMetadata; -import net.fabricmc.loom.api.decompilers.LoomDecompiler; - -public class FabricCFRDecompiler implements LoomDecompiler { - private final Project project; - - public FabricCFRDecompiler(Project project) { - this.project = project; - } - - @Override - public String name() { - return "ExperimentalCfr"; - } - - @Override - public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { - project.getLogger().warn("!!!! The CFR decompiler support is currently incomplete, line numbers will not match up and there will be no javadocs in the generated source."); - - // Setups the multi threaded logger, the thread id is used as the key to the ProgressLogger's - ServiceRegistry registry = ((ProjectInternal) project).getServices(); - ProgressLoggerFactory factory = registry.get(ProgressLoggerFactory.class); - ProgressLogger progressGroup = factory.newOperation(getClass()).setDescription("Decompile"); - - Map loggerMap = new ConcurrentHashMap<>(); - Function createLogger = (threadId) -> { - ProgressLogger pl = factory.newOperation(getClass(), progressGroup); - pl.setDescription("decompile worker"); - pl.started(); - return pl; - }; - - progressGroup.started(); - - Manifest manifest = new Manifest(); - manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); - Set addedDirectories = new HashSet<>(); - - try (OutputStream fos = Files.newOutputStream(sourcesDestination); JarOutputStream jos = new JarOutputStream(fos, manifest); ZipFile inputZip = new ZipFile(compiledJar.toFile())) { - CfrDriver driver = new CfrDriver.Builder() - .withOptions(ImmutableMap.of( - "renameillegalidents", "true", - "trackbytecodeloc", "true" - )) - .withClassFileSource(new ClassFileSource() { - @Override - public void informAnalysisRelativePathDetail(String usePath, String classFilePath) { - } - - @Override - public Collection addJar(String jarPath) { - return null; - } - - @Override - public String getPossiblyRenamedPath(String path) { - return path; - } - - @Override - public Pair getClassFileContent(String path) throws IOException { - ZipEntry zipEntry = inputZip.getEntry(path); - - if (zipEntry == null) { - throw new FileNotFoundException(path); - } - - try (InputStream inputStream = inputZip.getInputStream(zipEntry)) { - return Pair.make(inputStream.readAllBytes(), path); - } - } - }) - .withOutputSink(new OutputSinkFactory() { - @Override - public List getSupportedSinks(SinkType sinkType, Collection available) { - return switch (sinkType) { - case PROGRESS -> Collections.singletonList(SinkClass.STRING); - case JAVA -> Collections.singletonList(SinkClass.DECOMPILED); - default -> Collections.emptyList(); - }; - } - - @SuppressWarnings("unchecked") - @Override - public Sink getSink(SinkType sinkType, SinkClass sinkClass) { - return switch (sinkType) { - case PROGRESS -> (p) -> project.getLogger().debug((String) p); - case JAVA -> (Sink) decompiledSink(jos, addedDirectories); - case EXCEPTION -> (e) -> project.getLogger().error((String) e); - default -> null; - }; - } - }) - .build(); - - List classes = Collections.list(inputZip.entries()).stream() - .map(ZipEntry::getName) - .filter(input -> input.endsWith(".class")) - .collect(Collectors.toList()); - - ExecutorService executorService = Executors.newFixedThreadPool(metaData.numberOfThreads()); - List> futures = new LinkedList<>(); - - for (String clazz : classes) { - futures.add(executorService.submit(() -> { - loggerMap.computeIfAbsent(Thread.currentThread().getId(), createLogger).progress(clazz); - driver.analyse(Collections.singletonList(clazz)); - })); - } - - for (Future future : futures) { - future.get(); - } - } catch (IOException | InterruptedException | ExecutionException e) { - throw new RuntimeException("Failed to decompile", e); - } finally { - loggerMap.forEach((threadId, progressLogger) -> progressLogger.completed()); - } - } - - private static OutputSinkFactory.Sink decompiledSink(JarOutputStream jos, Set addedDirectories) { - return decompiled -> { - String filename = decompiled.getPackageName().replace('.', '/'); - if (!filename.isEmpty()) filename += "/"; - filename += decompiled.getClassName() + ".java"; - - byte[] data = decompiled.getJava().getBytes(Charsets.UTF_8); - - writeToJar(filename, data, jos, addedDirectories); - }; - } - - // TODO move to task queue? - private static synchronized void writeToJar(String filename, byte[] data, JarOutputStream jos, Set addedDirectories) { - String[] path = filename.split("/"); - String pathPart = ""; - - for (int i = 0; i < path.length - 1; i++) { - pathPart += path[i] + "/"; - - if (addedDirectories.add(pathPart)) { - JarEntry entry = new JarEntry(pathPart); - entry.setTime(new Date().getTime()); - - try { - jos.putNextEntry(entry); - jos.closeEntry(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } - } - - JarEntry entry = new JarEntry(filename); - entry.setTime(new Date().getTime()); - entry.setSize(data.length); - - try { - jos.putNextEntry(entry); - jos.write(data); - jos.closeEntry(); - } catch (IOException e) { - throw new RuntimeException(e); - } - } -} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.java new file mode 100644 index 0000000..bbfb0be --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cfr/LoomCFRDecompiler.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.decompilers.cfr; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Collections; +import java.util.Map; +import java.util.jar.Attributes; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +import org.benf.cfr.reader.Driver; +import org.benf.cfr.reader.state.ClassFileSourceImpl; +import org.benf.cfr.reader.state.DCCommonState; +import org.benf.cfr.reader.util.AnalysisType; +import org.benf.cfr.reader.util.getopt.Options; +import org.benf.cfr.reader.util.getopt.OptionsImpl; +import org.benf.cfr.reader.util.output.SinkDumperFactory; + +import net.fabricmc.loom.api.decompilers.DecompilationMetadata; +import net.fabricmc.loom.api.decompilers.LoomDecompiler; +import net.fabricmc.loom.decompilers.LineNumberRemapper; + +public class LoomCFRDecompiler implements LoomDecompiler { + private static final Map DECOMPILE_OPTIONS = Map.of( + "renameillegalidents", "true", + "trackbytecodeloc", "true", + "comments", "false" + ); + + @Override + public String name() { + return "Cfr"; + } + + @Override + public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { + final String path = compiledJar.toAbsolutePath().toString(); + final Options options = OptionsImpl.getFactory().create(DECOMPILE_OPTIONS); + + ClassFileSourceImpl classFileSource = new ClassFileSourceImpl(options); + + for (Path library : metaData.libraries()) { + classFileSource.addJarContent(library.toAbsolutePath().toString(), AnalysisType.JAR); + } + + classFileSource.informAnalysisRelativePathDetail(null, null); + + DCCommonState state = new DCCommonState(options, classFileSource); + + if (metaData.javaDocs() != null) { + state = new DCCommonState(state, new CFRObfuscationMapping(metaData.javaDocs())); + } + + final Manifest manifest = new Manifest(); + manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0"); + + Map> lineMap; + + try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(sourcesDestination), manifest)) { + CFRSinkFactory cfrSinkFactory = new CFRSinkFactory(outputStream, metaData.logger()); + SinkDumperFactory dumperFactory = new SinkDumperFactory(cfrSinkFactory, options); + + Driver.doJar(state, path, AnalysisType.JAR, dumperFactory); + + lineMap = cfrSinkFactory.getLineMap(); + } catch (IOException e) { + throw new UncheckedIOException("Failed to decompile", e); + } + + writeLineMap(linemapDestination, lineMap); + } + + private void writeLineMap(Path output, Map> lineMap) { + try (Writer writer = Files.newBufferedWriter(output, StandardCharsets.UTF_8)) { + for (Map.Entry> classEntry : lineMap.entrySet()) { + final String name = classEntry.getKey().replace(".", "/"); + + final Map mapping = classEntry.getValue(); + + int maxLine = 0; + int maxLineDest = 0; + StringBuilder builder = new StringBuilder(); + + for (Map.Entry mappingEntry : mapping.entrySet()) { + final int src = mappingEntry.getKey(); + final int dst = mappingEntry.getValue(); + + maxLine = Math.max(maxLine, src); + maxLineDest = Math.max(maxLineDest, dst); + + builder.append("\t").append(src).append("\t").append(dst).append("\n"); + } + + writer.write("%s\t%d\t%d\n".formatted(name, maxLine, maxLineDest)); + writer.write(builder.toString()); + writer.write("\n"); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to write line map", e); + } + } + + // A test main class to make it quicker/easier to debug with minimal jars + public static void main(String[] args) throws IOException { + LoomCFRDecompiler decompiler = new LoomCFRDecompiler(); + + Path lineMap = Paths.get("linemap.txt"); + + decompiler.decompile(Paths.get("input.jar"), + Paths.get("output-sources.jar"), + lineMap, + new DecompilationMetadata(4, null, Collections.emptyList(), null) + ); + + LineNumberRemapper lineNumberRemapper = new LineNumberRemapper(); + lineNumberRemapper.readMappings(lineMap.toFile()); + lineNumberRemapper.process(null, Paths.get("input.jar"), Paths.get("output.jar")); + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java deleted file mode 100644 index f059ed3..0000000 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractFernFlowerDecompiler.java +++ /dev/null @@ -1,163 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2019-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.decompilers.fernflower; - -import static java.text.MessageFormat.format; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Stack; -import java.util.function.Supplier; - -import org.gradle.api.Project; -import org.gradle.api.internal.project.ProjectInternal; -import org.gradle.api.logging.LogLevel; -import org.gradle.internal.logging.progress.ProgressLogger; -import org.gradle.internal.logging.progress.ProgressLoggerFactory; -import org.gradle.internal.service.ServiceRegistry; -import org.gradle.process.ExecResult; -import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; - -import net.fabricmc.loom.api.decompilers.DecompilationMetadata; -import net.fabricmc.loom.api.decompilers.LoomDecompiler; -import net.fabricmc.loom.util.ConsumingOutputStream; -import net.fabricmc.loom.util.OperatingSystem; - -public abstract class AbstractFernFlowerDecompiler implements LoomDecompiler { - private final Project project; - - protected AbstractFernFlowerDecompiler(Project project) { - this.project = project; - } - - public abstract Class fernFlowerExecutor(); - - @Override - public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { - if (!OperatingSystem.is64Bit()) { - throw new UnsupportedOperationException("FernFlower decompiler requires a 64bit JVM to run due to the memory requirements"); - } - - project.getLogging().captureStandardOutput(LogLevel.LIFECYCLE); - - Map options = new HashMap<>() {{ - put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1"); - put(IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1"); - put(IFernflowerPreferences.REMOVE_SYNTHETIC, "1"); - put(IFernflowerPreferences.LOG_LEVEL, "trace"); - put(IFernflowerPreferences.THREADS, metaData.numberOfThreads()); - put(IFernflowerPreferences.INDENT_STRING, "\t"); - }}; - - List args = new ArrayList<>(); - - options.forEach((k, v) -> args.add(format("-{0}={1}", k, v))); - args.add(absolutePathOf(compiledJar)); - args.add("-o=" + absolutePathOf(sourcesDestination)); - args.add("-l=" + absolutePathOf(linemapDestination)); - args.add("-m=" + absolutePathOf(metaData.javaDocs())); - - // TODO, Decompiler breaks on jemalloc, J9 module-info.class? - for (Path library : metaData.libraries()) { - args.add("-e=" + absolutePathOf(library)); - } - - ServiceRegistry registry = ((ProjectInternal) project).getServices(); - ProgressLoggerFactory factory = registry.get(ProgressLoggerFactory.class); - ProgressLogger progressGroup = factory.newOperation(getClass()).setDescription("Decompile"); - Supplier loggerFactory = () -> { - ProgressLogger pl = factory.newOperation(getClass(), progressGroup); - pl.setDescription("decompile worker"); - pl.started(); - return pl; - }; - Stack freeLoggers = new Stack<>(); - Map inUseLoggers = new HashMap<>(); - - progressGroup.started(); - ExecResult result = ForkingJavaExec.javaexec( - project, - spec -> { - spec.getMainClass().set(fernFlowerExecutor().getName()); - spec.jvmArgs("-Xms200m", "-Xmx3G"); - spec.setArgs(args); - spec.setErrorOutput(new ConsumingOutputStream(line -> { - if (line.startsWith("Inconsistent inner class entries")) { - // Suppress this - return; - } - - System.err.println(line); - })); - spec.setStandardOutput(new ConsumingOutputStream(line -> { - if (line.startsWith("Listening for transport") || !line.contains("::")) { - System.out.println(line); - return; - } - - int sepIdx = line.indexOf("::"); - String id = line.substring(0, sepIdx).trim(); - String data = line.substring(sepIdx + 2).trim(); - - ProgressLogger logger = inUseLoggers.get(id); - - String[] segs = data.split(" "); - - if (segs[0].equals("waiting")) { - if (logger != null) { - logger.progress("Idle.."); - inUseLoggers.remove(id); - freeLoggers.push(logger); - } - } else { - if (logger == null) { - if (!freeLoggers.isEmpty()) { - logger = freeLoggers.pop(); - } else { - logger = loggerFactory.get(); - } - - inUseLoggers.put(id, logger); - } - - logger.progress(data); - } - })); - }); - inUseLoggers.values().forEach(ProgressLogger::completed); - freeLoggers.forEach(ProgressLogger::completed); - progressGroup.completed(); - - result.rethrowFailure(); - result.assertNormalExitValue(); - } - - private static String absolutePathOf(Path path) { - return path.toAbsolutePath().toString(); - } -} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractForkedFFExecutor.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractForkedFFExecutor.java deleted file mode 100644 index 2435233..0000000 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/AbstractForkedFFExecutor.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2019-2020 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.decompilers.fernflower; - -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Objects; - -/** - * Entry point for Forked FernFlower task. - * Takes one parameter, a single file, each line is treated as command line input. - * Forces one input file. - * Forces one output file using '-o=/path/to/output' - * Created by covers1624 on 11/02/19. - *

Extending classes MUST have a standard "public static void main(args)". - * They may then call AbstractForkedFFExecutor#decompile for it to use the overridden AbstractForkedFFExecutor#runFF - *

- */ -public abstract class AbstractForkedFFExecutor { - public static void decompile(String[] args, AbstractForkedFFExecutor ffExecutor) { - Map options = new HashMap<>(); - File input = null; - File output = null; - File lineMap = null; - File mappings = null; - List libraries = new ArrayList<>(); - - boolean isOption = true; - - for (String arg : args) { - if (isOption && arg.length() > 5 && arg.charAt(0) == '-' && arg.charAt(4) == '=') { - String value = arg.substring(5); - - if ("true".equalsIgnoreCase(value)) { - value = "1"; - } else if ("false".equalsIgnoreCase(value)) { - value = "0"; - } - - options.put(arg.substring(1, 4), value); - } else { - isOption = false; - - if (arg.startsWith("-e=")) { - libraries.add(new File(arg.substring(3))); - } else if (arg.startsWith("-o=")) { - if (output != null) { - throw new RuntimeException("Unable to set more than one output."); - } - - output = new File(arg.substring(3)); - } else if (arg.startsWith("-l=")) { - if (lineMap != null) { - throw new RuntimeException("Unable to set more than one lineMap file."); - } - - lineMap = new File(arg.substring(3)); - } else if (arg.startsWith("-m=")) { - if (mappings != null) { - throw new RuntimeException("Unable to use more than one mappings file."); - } - - mappings = new File(arg.substring(3)); - } else { - if (input != null) { - throw new RuntimeException("Unable to set more than one input."); - } - - input = new File(arg); - } - } - } - - Objects.requireNonNull(input, "Input not set."); - Objects.requireNonNull(output, "Output not set."); - Objects.requireNonNull(mappings, "Mappings not set."); - - ffExecutor.runFF(options, libraries, input, output, lineMap, mappings); - } - - public abstract void runFF(Map options, List libraries, File input, File output, File lineMap, File mappings); -} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java index b366377..c300944 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java +++ b/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricFernFlowerDecompiler.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2018-2020 FabricMC + * Copyright (c) 2019-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 @@ -24,20 +24,43 @@ package net.fabricmc.loom.decompilers.fernflower; -import org.gradle.api.Project; +import java.nio.file.Path; +import java.util.Map; -public class FabricFernFlowerDecompiler extends AbstractFernFlowerDecompiler { - public FabricFernFlowerDecompiler(Project project) { - super(project); - } +import org.jetbrains.java.decompiler.main.Fernflower; +import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences; +import org.jetbrains.java.decompiler.main.extern.IResultSaver; +import net.fabricmc.fernflower.api.IFabricJavadocProvider; +import net.fabricmc.loom.api.decompilers.DecompilationMetadata; +import net.fabricmc.loom.api.decompilers.LoomDecompiler; + +public final class FabricFernFlowerDecompiler implements LoomDecompiler { @Override public String name() { return "FabricFlower"; // Or something else? } @Override - public Class fernFlowerExecutor() { - return FabricForkedFFExecutor.class; + public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) { + Map options = Map.of( + IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1", + IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1", + IFernflowerPreferences.REMOVE_SYNTHETIC, "1", + IFernflowerPreferences.LOG_LEVEL, "trace", + IFernflowerPreferences.THREADS, String.valueOf(metaData.numberOfThreads()), + IFernflowerPreferences.INDENT_STRING, "\t", + IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(metaData.javaDocs().toFile()) + ); + + IResultSaver saver = new ThreadSafeResultSaver(sourcesDestination::toFile, linemapDestination::toFile); + Fernflower ff = new Fernflower(FernFlowerUtils::getBytecode, saver, options, new FernflowerLogger(metaData.logger())); + + for (Path library : metaData.libraries()) { + ff.addLibrary(library.toFile()); + } + + ff.addSource(compiledJar.toFile()); + ff.decompileContext(); } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricForkedFFExecutor.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java similarity index 53% rename from src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricForkedFFExecutor.java rename to src/main/java/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java index e88c808..a98060e 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/FabricForkedFFExecutor.java +++ b/src/main/java/net/fabricmc/loom/decompilers/fernflower/FernflowerLogger.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2018-2020 FabricMC + * 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 @@ -24,34 +24,60 @@ package net.fabricmc.loom.decompilers.fernflower; -import java.io.File; -import java.util.List; -import java.util.Map; +import java.io.IOException; -import org.jetbrains.java.decompiler.main.Fernflower; import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; -import org.jetbrains.java.decompiler.main.extern.IResultSaver; -import net.fabricmc.fernflower.api.IFabricJavadocProvider; +import net.fabricmc.loom.util.IOStringConsumer; -public class FabricForkedFFExecutor extends AbstractForkedFFExecutor { - public static void main(String[] args) { - AbstractForkedFFExecutor.decompile(args, new FabricForkedFFExecutor()); +public class FernflowerLogger extends IFernflowerLogger { + private final IOStringConsumer logger; + + public FernflowerLogger(IOStringConsumer logger) { + this.logger = logger; } @Override - public void runFF(Map options, List libraries, File input, File output, File lineMap, File mappings) { - options.put(IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(mappings)); + public void writeMessage(String message, Severity severity) { + if (message.contains("Inconsistent inner class entries for")) return; + System.err.println(message); + } - IResultSaver saver = new ThreadSafeResultSaver(() -> output, () -> lineMap); - IFernflowerLogger logger = new ThreadIDFFLogger(); - Fernflower ff = new Fernflower(FernFlowerUtils::getBytecode, saver, options, logger); + @Override + public void writeMessage(String message, Severity severity, Throwable t) { + writeMessage(message, severity); + } - for (File library : libraries) { - ff.addLibrary(library); + private void write(String data) { + try { + logger.accept(data); + } catch (IOException e) { + throw new RuntimeException("Failed to log", e); } + } - ff.addSource(input); - ff.decompileContext(); + @Override + public void startReadingClass(String className) { + write("Decompiling " + className); + } + + @Override + public void startClass(String className) { + write("Decompiling " + className); + } + + @Override + public void startWriteClass(String className) { + // Nope + } + + @Override + public void startMethod(String methodName) { + // Nope + } + + @Override + public void endMethod() { + // Nope } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java deleted file mode 100644 index bbcd80d..0000000 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ForkingJavaExec.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2016-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.decompilers.fernflower; - -import java.net.URL; -import java.net.URLClassLoader; - -import org.gradle.api.Action; -import org.gradle.api.Project; -import org.gradle.api.artifacts.ConfigurationContainer; -import org.gradle.api.artifacts.dsl.DependencyHandler; -import org.gradle.api.file.FileCollection; -import org.gradle.process.ExecResult; -import org.gradle.process.JavaExecSpec; - -/** - * Simple utility class for a Task that wishes to execute a java process - * with the classpath of the gradle plugin plus groovy. - * - *

Created by covers1624 on 11/02/19. - */ -public class ForkingJavaExec { - public static ExecResult javaexec(Project project, Action action) { - return project.javaexec(spec -> { - spec.classpath(getClasspath(project)); - action.execute(spec); - }); - } - - private static Object getClasspath(Project project) { - if (System.getProperty("fabric.loom.test") != null) { - return getTestClasspath(); - } - - return getRuntimeClasspath(project.getRootProject().getPlugins().hasPlugin("fabric-loom") ? project.getRootProject() : project); - } - - private static FileCollection getRuntimeClasspath(Project project) { - ConfigurationContainer configurations = project.getBuildscript().getConfigurations(); - DependencyHandler handler = project.getDependencies(); - return configurations.getByName("classpath") - .plus(configurations.detachedConfiguration(handler.localGroovy())); - } - - private static URL[] getTestClasspath() { - return ((URLClassLoader) ForkingJavaExec.class.getClassLoader()).getURLs(); - } -} diff --git a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ThreadIDFFLogger.java b/src/main/java/net/fabricmc/loom/decompilers/fernflower/ThreadIDFFLogger.java deleted file mode 100644 index a7b617b..0000000 --- a/src/main/java/net/fabricmc/loom/decompilers/fernflower/ThreadIDFFLogger.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * This file is part of fabric-loom, licensed under the MIT License (MIT). - * - * Copyright (c) 2019-2020 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.decompilers.fernflower; - -import java.io.PrintStream; -import java.text.MessageFormat; -import java.util.Stack; - -import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger; - -/** - * This logger simply prints what each thread is doing - * to the console in a machine parsable way. - * - *

Created by covers1624 on 11/02/19. - */ -public class ThreadIDFFLogger extends IFernflowerLogger { - public final PrintStream stdOut; - public final PrintStream stdErr; - - private final ThreadLocal> workingClass = ThreadLocal.withInitial(Stack::new); - private final ThreadLocal> line = ThreadLocal.withInitial(Stack::new); - - public ThreadIDFFLogger() { - this(System.err, System.out); - } - - public ThreadIDFFLogger(PrintStream stdOut, PrintStream stdErr) { - this.stdOut = stdOut; - this.stdErr = stdErr; - } - - @Override - public void writeMessage(String message, Severity severity) { - System.err.println(message); - } - - @Override - public void writeMessage(String message, Severity severity, Throwable t) { - System.err.println(message); - t.printStackTrace(System.err); - } - - private void print() { - Thread thread = Thread.currentThread(); - long id = thread.getId(); - - if (line.get().isEmpty()) { - System.out.println(MessageFormat.format("{0} :: waiting", id)); - return; - } - - String line = this.line.get().peek(); - System.out.println(MessageFormat.format("{0} :: {1}", id, line).trim()); - } - - @Override - public void startReadingClass(String className) { - workingClass.get().push(className); - line.get().push("Decompiling " + className); - print(); - } - - @Override - public void startClass(String className) { - workingClass.get().push(className); - line.get().push("Decompiling " + className); - print(); - } - - @Override - public void startMethod(String methodName) { - // No need to print out methods - } - - @Override - public void endMethod() { - } - - @Override - public void endClass() { - line.get().pop(); - workingClass.get().pop(); - print(); - } - - @Override - public void startWriteClass(String className) { - line.get().push("Writing " + className); - print(); - } - - @Override - public void endWriteClass() { - line.get().pop(); - print(); - } - - @Override - public void endReadingClass() { - line.get().pop(); - workingClass.get().pop(); - print(); - } -} diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index a19746b..8c6dca2 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -26,18 +26,33 @@ package net.fabricmc.loom.task; import java.io.File; import java.io.IOException; +import java.io.UncheckedIOException; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.Collection; import java.util.List; import java.util.Locale; +import java.util.Objects; +import java.util.UUID; import java.util.stream.Collectors; import javax.inject.Inject; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; import org.gradle.api.tasks.InputFile; import org.gradle.api.tasks.TaskAction; +import org.gradle.workers.WorkAction; +import org.gradle.workers.WorkParameters; +import org.gradle.workers.WorkQueue; +import org.gradle.workers.WorkerExecutor; +import org.gradle.workers.internal.WorkerDaemonClientsManager; +import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.api.decompilers.DecompilationMetadata; @@ -47,58 +62,216 @@ import net.fabricmc.loom.configuration.accesswidener.TransitiveAccessWidenerMapp import net.fabricmc.loom.configuration.providers.mappings.MappingsProviderImpl; import net.fabricmc.loom.decompilers.LineNumberRemapper; import net.fabricmc.loom.util.Constants; -import net.fabricmc.loom.util.gradle.ProgressLogger; +import net.fabricmc.loom.util.IOStringConsumer; +import net.fabricmc.loom.util.OperatingSystem; +import net.fabricmc.loom.util.gradle.ThreadedProgressLoggerConsumer; +import net.fabricmc.loom.util.gradle.ThreadedSimpleProgressLogger; +import net.fabricmc.loom.util.gradle.WorkerDaemonClientsManagerHelper; +import net.fabricmc.loom.util.ipc.IPCClient; +import net.fabricmc.loom.util.ipc.IPCServer; import net.fabricmc.stitch.util.StitchUtil; -public class GenerateSourcesTask extends AbstractLoomTask { +public abstract class GenerateSourcesTask extends AbstractLoomTask { public final LoomDecompiler decompiler; - private File inputJar; + @InputFile + public abstract RegularFileProperty getInputJar(); + + /** + * Max memory for forked JVM in megabytes. + */ + @Input + public abstract Property getMaxMemory(); + + @Inject + public abstract WorkerExecutor getWorkerExecutor(); + + @Inject + public abstract WorkerDaemonClientsManager getWorkerDaemonClientsManager(); @Inject public GenerateSourcesTask(LoomDecompiler decompiler) { this.decompiler = decompiler; + Objects.requireNonNull(getDecompilerConstructor(this.decompiler.getClass().getCanonicalName()), + "%s must have a no args constructor".formatted(this.decompiler.getClass().getCanonicalName())); + getOutputs().upToDateWhen((o) -> false); + getMaxMemory().convention(4096L).finalizeValueOnRead(); } @TaskAction - public void doTask() throws Throwable { - int threads = Runtime.getRuntime().availableProcessors(); - Collection libraries = getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES).getFiles() - .stream().map(File::toPath).collect(Collectors.toSet()); + public void run() throws IOException { + if (!OperatingSystem.is64Bit()) { + throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements."); + } - DecompilationMetadata metadata = new DecompilationMetadata(threads, getMappings(), libraries); - Path runtimeJar = getExtension().getMappingsProvider().mappedProvider.getMappedJar().toPath(); - Path sourcesDestination = getMappedJarFileWithSuffix("-sources.jar").toPath(); - Path linemap = getMappedJarFileWithSuffix("-sources.lmap").toPath(); - decompiler.decompile(inputJar.toPath(), sourcesDestination, linemap, metadata); + if (!OperatingSystem.isUnixDomainSocketsSupported()) { + getProject().getLogger().warn("Decompile worker logging disabled as Unix Domain Sockets is not supported on your operating system."); - if (Files.exists(linemap)) { - Path linemappedJarDestination = getMappedJarFileWithSuffix("-linemapped.jar").toPath(); + doWork(null); + return; + } - // Line map the actually jar used to run the game, not the one used to decompile - remapLineNumbers(runtimeJar, linemap, linemappedJarDestination); + // Set up the IPC path to get the log output back from the forked JVM + final Path ipcPath = Files.createTempFile("loom", "ipc"); + Files.deleteIfExists(ipcPath); - Files.copy(linemappedJarDestination, runtimeJar, StandardCopyOption.REPLACE_EXISTING); - Files.delete(linemappedJarDestination); + try (ThreadedProgressLoggerConsumer loggerConsumer = new ThreadedProgressLoggerConsumer(getProject(), decompiler.name(), "Decompiling minecraft sources"); + IPCServer logReceiver = new IPCServer(ipcPath, loggerConsumer)) { + doWork(ipcPath); + } catch (InterruptedException e) { + throw new RuntimeException("Failed to shutdown log receiver", e); + } finally { + Files.deleteIfExists(ipcPath); } } - private void remapLineNumbers(Path oldCompiledJar, Path linemap, Path linemappedJarDestination) throws IOException { - getProject().getLogger().info(":adjusting line numbers"); - LineNumberRemapper remapper = new LineNumberRemapper(); - remapper.readMappings(linemap.toFile()); + private void doWork(@Nullable Path ipcPath) { + final String jvmMarkerValue = UUID.randomUUID().toString(); + final WorkQueue workQueue = createWorkQueue(jvmMarkerValue); - ProgressLogger progressLogger = ProgressLogger.getProgressFactory(getProject(), getClass().getName()); - progressLogger.start("Adjusting line numbers", "linemap"); + workQueue.submit(DecompileAction.class, params -> { + params.getDecompilerClass().set(decompiler.getClass().getCanonicalName()); - try (StitchUtil.FileSystemDelegate inFs = StitchUtil.getJarFileSystem(oldCompiledJar.toFile(), true); - StitchUtil.FileSystemDelegate outFs = StitchUtil.getJarFileSystem(linemappedJarDestination.toFile(), true)) { - remapper.process(progressLogger, inFs.get().getPath("/"), outFs.get().getPath("/")); + params.getInputJar().set(getInputJar()); + params.getRuntimeJar().set(getExtension().getMappingsProvider().mappedProvider.getMappedJar()); + params.getSourcesDestinationJar().set(getMappedJarFileWithSuffix("-sources.jar")); + params.getLinemap().set(getMappedJarFileWithSuffix("-sources.lmap")); + params.getLinemapJar().set(getMappedJarFileWithSuffix("-linemapped.jar")); + params.getMappings().set(getMappings().toFile()); + + if (ipcPath != null) { + params.getIPCPath().set(ipcPath.toFile()); + } + + params.getClassPath().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES)); + }); + + workQueue.await(); + + if (useProcessIsolation()) { + boolean stopped = WorkerDaemonClientsManagerHelper.stopIdleJVM(getWorkerDaemonClientsManager(), jvmMarkerValue); + + if (!stopped) { + throw new RuntimeException("Failed to stop decompile worker JVM"); + } + } + } + + private WorkQueue createWorkQueue(String jvmMarkerValue) { + if (!useProcessIsolation()) { + return getWorkerExecutor().noIsolation(); } - progressLogger.completed(); + return getWorkerExecutor().processIsolation(spec -> { + spec.forkOptions(forkOptions -> { + forkOptions.setMaxHeapSize("%dm".formatted(getMaxMemory().get())); + forkOptions.systemProperty(WorkerDaemonClientsManagerHelper.MARKER_PROP, jvmMarkerValue); + }); + }); + } + + private boolean useProcessIsolation() { + // Useful if you want to debug the decompiler, make sure you run gradle with enough memory. + return !Boolean.getBoolean("fabric.loom.genSources.debug"); + } + + public interface DecompileParams extends WorkParameters { + Property getDecompilerClass(); + + RegularFileProperty getInputJar(); + RegularFileProperty getRuntimeJar(); + RegularFileProperty getSourcesDestinationJar(); + RegularFileProperty getLinemap(); + RegularFileProperty getLinemapJar(); + RegularFileProperty getMappings(); + + RegularFileProperty getIPCPath(); + + ConfigurableFileCollection getClassPath(); + } + + public abstract static class DecompileAction implements WorkAction { + @Override + public void execute() { + if (!getParameters().getIPCPath().isPresent() || !OperatingSystem.isUnixDomainSocketsSupported()) { + // Does not support unix domain sockets, print to sout. + doDecompile(System.out::println); + return; + } + + final Path ipcPath = getParameters().getIPCPath().get().getAsFile().toPath(); + + try (IPCClient ipcClient = new IPCClient(ipcPath)) { + doDecompile(new ThreadedSimpleProgressLogger(ipcClient)); + } catch (Exception e) { + throw new RuntimeException("Failed to setup IPC Client", e); + } + } + + private void doDecompile(IOStringConsumer logger) { + final Path inputJar = getParameters().getInputJar().get().getAsFile().toPath(); + final Path sourcesDestinationJar = getParameters().getSourcesDestinationJar().get().getAsFile().toPath(); + final Path linemap = getParameters().getLinemap().get().getAsFile().toPath(); + final Path linemapJar = getParameters().getLinemapJar().get().getAsFile().toPath(); + final Path runtimeJar = getParameters().getRuntimeJar().get().getAsFile().toPath(); + + final LoomDecompiler decompiler; + + try { + decompiler = getDecompilerConstructor(getParameters().getDecompilerClass().get()).newInstance(); + } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { + throw new RuntimeException("Failed to create decompiler", e); + } + + DecompilationMetadata metadata = new DecompilationMetadata( + Runtime.getRuntime().availableProcessors(), + getParameters().getMappings().get().getAsFile().toPath(), + getLibraries(), + logger + ); + + decompiler.decompile( + inputJar, + sourcesDestinationJar, + linemap, + metadata + ); + + // Close the decompile loggers + try { + metadata.logger().accept(ThreadedProgressLoggerConsumer.CLOSE_LOGGERS); + } catch (IOException e) { + throw new UncheckedIOException("Failed to close loggers", e); + } + + if (Files.exists(linemap)) { + try { + // Line map the actually jar used to run the game, not the one used to decompile + remapLineNumbers(metadata.logger(), runtimeJar, linemap, linemapJar); + + Files.copy(linemapJar, runtimeJar, StandardCopyOption.REPLACE_EXISTING); + Files.delete(linemapJar); + } catch (IOException e) { + throw new UncheckedIOException("Failed to remap line numbers", e); + } + } + } + + private void remapLineNumbers(IOStringConsumer logger, Path oldCompiledJar, Path linemap, Path linemappedJarDestination) throws IOException { + LineNumberRemapper remapper = new LineNumberRemapper(); + remapper.readMappings(linemap.toFile()); + + try (StitchUtil.FileSystemDelegate inFs = StitchUtil.getJarFileSystem(oldCompiledJar.toFile(), true); + StitchUtil.FileSystemDelegate outFs = StitchUtil.getJarFileSystem(linemappedJarDestination.toFile(), true)) { + remapper.process(logger, inFs.get().getPath("/"), outFs.get().getPath("/")); + } + } + + private Collection getLibraries() { + return getParameters().getClassPath().getFiles().stream().map(File::toPath).collect(Collectors.toSet()); + } } private File getMappedJarFileWithSuffix(String suffix) { @@ -140,13 +313,12 @@ public class GenerateSourcesTask extends AbstractLoomTask { return baseMappings; } - @InputFile - public File getInputJar() { - return inputJar; - } - - public GenerateSourcesTask setInputJar(File inputJar) { - this.inputJar = inputJar; - return this; + private static Constructor getDecompilerConstructor(String clazz) { + try { + //noinspection unchecked + return (Constructor) Class.forName(clazz).getConstructor(); + } catch (NoSuchMethodException | ClassNotFoundException e) { + return null; + } } } diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index c37fcbb..5561adb 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -128,9 +128,9 @@ public final class LoomTasks { File outputJar = mappingsProvider.mappedProvider.getUnpickedJar(); tasks.register("unpickJar", UnpickJarTask.class, unpickJarTask -> { - unpickJarTask.setUnpickDefinition(mappingsProvider.getUnpickDefinitionsFile()); - unpickJarTask.setInputJar(mappingsProvider.mappedProvider.getMappedJar()); - unpickJarTask.setOutputJar(outputJar); + unpickJarTask.getUnpickDefinitions().set(mappingsProvider.getUnpickDefinitionsFile()); + unpickJarTask.getInputJar().set(mappingsProvider.mappedProvider.getMappedJar()); + unpickJarTask.getOutputJar().set(outputJar); }); inputJar = outputJar; @@ -142,7 +142,7 @@ public final class LoomTasks { String taskName = decompiler instanceof FabricFernFlowerDecompiler ? "genSources" : "genSourcesWith" + decompiler.name(); // decompiler will be passed to the constructor of GenerateSourcesTask GenerateSourcesTask generateSourcesTask = tasks.register(taskName, GenerateSourcesTask.class, decompiler).get(); - generateSourcesTask.setInputJar(inputJar); + generateSourcesTask.getInputJar().set(inputJar); if (mappingsProvider.hasUnpickDefinitions()) { generateSourcesTask.dependsOn(tasks.getByName("unpickJar")); diff --git a/src/main/java/net/fabricmc/loom/task/UnpickJarTask.java b/src/main/java/net/fabricmc/loom/task/UnpickJarTask.java index 93c7174..e750cb1 100644 --- a/src/main/java/net/fabricmc/loom/task/UnpickJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/UnpickJarTask.java @@ -29,7 +29,12 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; +import javax.inject.Inject; + +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.RegularFileProperty; import org.gradle.api.tasks.InputFile; +import org.gradle.api.tasks.InputFiles; import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.JavaExec; import org.gradle.api.tasks.OutputFile; @@ -39,26 +44,43 @@ import net.fabricmc.loom.configuration.providers.LaunchProvider; import net.fabricmc.loom.extension.LoomFiles; import net.fabricmc.loom.util.Constants; -public class UnpickJarTask extends JavaExec { - File inputJar; - File unpickDefinition; +public abstract class UnpickJarTask extends JavaExec { + @InputFile + public abstract RegularFileProperty getInputJar(); - File outputJar; + @InputFile + public abstract RegularFileProperty getUnpickDefinitions(); + @InputFiles + // Only 1 file, but it comes from a configuration + public abstract ConfigurableFileCollection getConstantJar(); + + @InputFiles + public abstract ConfigurableFileCollection getUnpickClasspath(); + + @OutputFile + public abstract RegularFileProperty getOutputJar(); + + @Inject public UnpickJarTask() { - getOutputs().upToDateWhen(e -> false); classpath(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH)); getMainClass().set("daomephsta.unpick.cli.Main"); + + getConstantJar().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MAPPING_CONSTANTS)); + getUnpickClasspath().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES)); } @Override public void exec() { - fileArg(getInputJar(), getOutputJar(), getUnpickDefinition()); - fileArg(getConstantJar()); + fileArg(getInputJar().get().getAsFile(), getOutputJar().get().getAsFile(), getUnpickDefinitions().get().getAsFile()); + fileArg(getConstantJar().getSingleFile()); // Classpath fileArg(getExtension().getMinecraftMappedProvider().getMappedJar()); - fileArg(getMinecraftDependencies()); + + for (File file : getUnpickClasspath()) { + fileArg(file); + } writeUnpickLogConfig(); systemProperty("java.util.logging.config.file", getDirectories().getUnpickLoggingConfigFile().getAbsolutePath()); @@ -75,45 +97,6 @@ public class UnpickJarTask extends JavaExec { } } - private File[] getMinecraftDependencies() { - return getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_DEPENDENCIES) - .resolve().toArray(new File[0]); - } - - private File getConstantJar() { - return getProject().getConfigurations().getByName(Constants.Configurations.MAPPING_CONSTANTS).getSingleFile(); - } - - @InputFile - public File getInputJar() { - return inputJar; - } - - public UnpickJarTask setInputJar(File inputJar) { - this.inputJar = inputJar; - return this; - } - - @InputFile - public File getUnpickDefinition() { - return unpickDefinition; - } - - public UnpickJarTask setUnpickDefinition(File unpickDefinition) { - this.unpickDefinition = unpickDefinition; - return this; - } - - @OutputFile - public File getOutputJar() { - return outputJar; - } - - public UnpickJarTask setOutputJar(File outputJar) { - this.outputJar = outputJar; - return this; - } - private void fileArg(File... files) { for (File file : files) { args(file.getAbsolutePath()); diff --git a/src/main/java/net/fabricmc/loom/util/IOStringConsumer.java b/src/main/java/net/fabricmc/loom/util/IOStringConsumer.java new file mode 100644 index 0000000..2459b3a --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/IOStringConsumer.java @@ -0,0 +1,31 @@ +/* + * 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; + +public interface IOStringConsumer { + void accept(String data) throws IOException; +} diff --git a/src/main/java/net/fabricmc/loom/util/OperatingSystem.java b/src/main/java/net/fabricmc/loom/util/OperatingSystem.java index 38e2eb4..9df41ed 100644 --- a/src/main/java/net/fabricmc/loom/util/OperatingSystem.java +++ b/src/main/java/net/fabricmc/loom/util/OperatingSystem.java @@ -24,6 +24,11 @@ package net.fabricmc.loom.util; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.StandardProtocolFamily; +import java.nio.channels.ServerSocketChannel; + public class OperatingSystem { public static String getOS() { String osName = System.getProperty("os.name").toLowerCase(); @@ -63,4 +68,15 @@ public class OperatingSystem { // CI seems to be set by most popular CI services return System.getenv("CI") != null; } + + // Requires Unix, or Windows 10 17063 or later. See: https://devblogs.microsoft.com/commandline/af_unix-comes-to-windows/ + public static boolean isUnixDomainSocketsSupported() { + try (ServerSocketChannel serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) { + return true; + } catch (UnsupportedOperationException e) { + return false; + } catch (IOException e) { + throw new UncheckedIOException(e); + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java index ec1ce8a..776fa6e 100644 --- a/src/main/java/net/fabricmc/loom/util/SourceRemapper.java +++ b/src/main/java/net/fabricmc/loom/util/SourceRemapper.java @@ -43,7 +43,7 @@ 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.ProgressLogger; +import net.fabricmc.loom.util.gradle.ProgressLoggerHelper; import net.fabricmc.lorenztiny.TinyMappingsReader; import net.fabricmc.mappingio.tree.MemoryMappingTree; import net.fabricmc.stitch.util.StitchUtil; @@ -51,7 +51,7 @@ import net.fabricmc.stitch.util.StitchUtil; public class SourceRemapper { private final Project project; private final boolean toNamed; - private final List> remapTasks = new ArrayList<>(); + private final List> remapTasks = new ArrayList<>(); private Mercury mercury; @@ -90,7 +90,7 @@ public class SourceRemapper { project.getLogger().lifecycle(":remapping sources"); - ProgressLogger progressLogger = ProgressLogger.getProgressFactory(project, SourceRemapper.class.getName()); + ProgressLoggerHelper progressLogger = ProgressLoggerHelper.getProgressFactory(project, SourceRemapper.class.getName()); progressLogger.start("Remapping dependency sources", "sources"); remapTasks.forEach(consumer -> consumer.accept(progressLogger)); diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ProgressLogger.java b/src/main/java/net/fabricmc/loom/util/gradle/ProgressLoggerHelper.java similarity index 92% rename from src/main/java/net/fabricmc/loom/util/gradle/ProgressLogger.java rename to src/main/java/net/fabricmc/loom/util/gradle/ProgressLoggerHelper.java index 2b3d180..d2fb1a4 100644 --- a/src/main/java/net/fabricmc/loom/util/gradle/ProgressLogger.java +++ b/src/main/java/net/fabricmc/loom/util/gradle/ProgressLoggerHelper.java @@ -32,11 +32,11 @@ import org.gradle.api.Project; /** * Wrapper to ProgressLogger internal API. */ -public class ProgressLogger { +public class ProgressLoggerHelper { private final Object logger; private final Method getDescription, setDescription, getShortDescription, setShortDescription, getLoggingHeader, setLoggingHeader, start, started, startedArg, progress, completed, completedArg; - private ProgressLogger(Object logger) { + private ProgressLoggerHelper(Object logger) { this.logger = logger; this.getDescription = getMethod("getDescription"); this.setDescription = getMethod("setDescription", String.class); @@ -102,17 +102,17 @@ public class ProgressLogger { * @param category The logger category * @return In any case a progress logger */ - public static ProgressLogger getProgressFactory(Project project, String category) { + public static ProgressLoggerHelper getProgressFactory(Project project, String category) { try { Method getServices = project.getClass().getMethod("getServices"); Object serviceFactory = getServices.invoke(project); Method get = serviceFactory.getClass().getMethod("get", Class.class); Object progressLoggerFactory = get.invoke(serviceFactory, getFactoryClass()); Method newOperation = progressLoggerFactory.getClass().getMethod("newOperation", String.class); - return new ProgressLogger(newOperation.invoke(progressLoggerFactory, category)); + return new ProgressLoggerHelper(newOperation.invoke(progressLoggerFactory, category)); } catch (Exception e) { project.getLogger().error("Unable to get progress logger. Download progress will not be displayed."); - return new ProgressLogger(null); + return new ProgressLoggerHelper(null); } } @@ -132,7 +132,7 @@ public class ProgressLogger { * * @param description The description. */ - public ProgressLogger setDescription(String description) { + public ProgressLoggerHelper setDescription(String description) { invoke(setDescription, description); return this; } @@ -153,7 +153,7 @@ public class ProgressLogger { * * @param description The short description. */ - public ProgressLogger setShortDescription(String description) { + public ProgressLoggerHelper setShortDescription(String description) { invoke(setShortDescription, description); return this; } @@ -176,7 +176,7 @@ public class ProgressLogger { * * @param header The header. May be empty or null. */ - public ProgressLogger setLoggingHeader(String header) { + public ProgressLoggerHelper setLoggingHeader(String header) { invoke(setLoggingHeader, header); return this; } @@ -186,7 +186,7 @@ public class ProgressLogger { * * @return this logger instance */ - public ProgressLogger start(String description, String shortDescription) { + public ProgressLoggerHelper start(String description, String shortDescription) { invoke(start, description, shortDescription); return this; } diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ThreadedProgressLoggerConsumer.java b/src/main/java/net/fabricmc/loom/util/gradle/ThreadedProgressLoggerConsumer.java new file mode 100644 index 0000000..94f49ee --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/gradle/ThreadedProgressLoggerConsumer.java @@ -0,0 +1,95 @@ +/* + * 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.gradle; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Consumer; + +import org.gradle.api.Project; +import org.gradle.api.internal.project.ProjectInternal; +import org.gradle.internal.logging.progress.ProgressLogger; +import org.gradle.internal.logging.progress.ProgressLoggerFactory; + +public class ThreadedProgressLoggerConsumer implements Consumer, AutoCloseable { + public static final String CLOSE_LOGGERS = "LOOM_CLOSE_LOGGERS"; + + private final Project project; + private final String name; + private final String desc; + + private final ProgressLoggerFactory progressLoggerFactory; + private final ProgressLogger progressGroup; + private final Map loggers = Collections.synchronizedMap(new HashMap<>()); + + public ThreadedProgressLoggerConsumer(Project project, String name, String desc) { + this.project = project; + this.name = name; + this.desc = desc; + + this.progressLoggerFactory = ((ProjectInternal) project).getServices().get(ProgressLoggerFactory.class); + this.progressGroup = this.progressLoggerFactory.newOperation(name).setDescription(desc); + progressGroup.started(); + } + + @Override + public void accept(String line) { + if (!line.contains("::")) { + project.getLogger().debug("Malformed threaded IPC log message: " + line); + return; + } + + int idx = line.indexOf("::"); + String id = line.substring(0, idx).trim(); + String data = line.substring(idx + 2).trim(); + + if (data.equals(CLOSE_LOGGERS)) { + resetLoggers(); + return; + } + + loggers.computeIfAbsent(id, this::createLogger).progress(data); + } + + private ProgressLogger createLogger(String id) { + ProgressLogger progressLogger = progressLoggerFactory.newOperation(getClass(), progressGroup); + progressLogger.setDescription(desc); + progressLogger.started(); + return progressLogger; + } + + private void resetLoggers() { + loggers.values().forEach(ProgressLogger::completed); + loggers.clear(); + } + + @Override + public void close() { + resetLoggers(); + + progressGroup.completed(); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java b/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java new file mode 100644 index 0000000..605568a --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/gradle/ThreadedSimpleProgressLogger.java @@ -0,0 +1,36 @@ +/* + * 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.gradle; + +import java.io.IOException; + +import net.fabricmc.loom.util.IOStringConsumer; + +public record ThreadedSimpleProgressLogger(IOStringConsumer parent) implements IOStringConsumer { + @Override + public void accept(String data) throws IOException { + parent.accept("%d::%s".formatted(Thread.currentThread().getId(), data)); + } +} diff --git a/src/main/java/net/fabricmc/loom/util/gradle/WorkerDaemonClientsManagerHelper.java b/src/main/java/net/fabricmc/loom/util/gradle/WorkerDaemonClientsManagerHelper.java new file mode 100644 index 0000000..b5d1f7f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/gradle/WorkerDaemonClientsManagerHelper.java @@ -0,0 +1,77 @@ +/* + * 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.gradle; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.gradle.api.Transformer; +import org.gradle.workers.internal.DaemonForkOptions; +import org.gradle.workers.internal.WorkerDaemonClientsManager; + +public class WorkerDaemonClientsManagerHelper { + public static final String MARKER_PROP = "fabric.loom.decompile.worker"; + + public static boolean stopIdleJVM(WorkerDaemonClientsManager manager, String jvmMarkerValue) { + AtomicBoolean stopped = new AtomicBoolean(false); + + /* Transformer, List> */ + Transformer, List> transformer = workerDaemonClients -> { + for (Object /* WorkerDaemonClient */ client : workerDaemonClients) { + DaemonForkOptions forkOptions = getForkOptions(client); + Map systemProperties = forkOptions.getJavaForkOptions().getSystemProperties(); + + if (systemProperties == null || !jvmMarkerValue.equals(systemProperties.get(MARKER_PROP))) { + // Not the JVM we are looking for + continue; + } + + stopped.set(true); + return Collections.singletonList(client); + } + + return Collections.emptyList(); + }; + + //noinspection unchecked + manager.selectIdleClientsToStop((Transformer) transformer); + + return stopped.get(); + } + + private static DaemonForkOptions getForkOptions(Object /* WorkerDaemonClient */ client) { + try { + Method getForkOptionsMethod = client.getClass().getDeclaredMethod("getForkOptions"); + getForkOptionsMethod.setAccessible(true); + return (DaemonForkOptions) getForkOptionsMethod.invoke(client); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/ipc/IPCClient.java b/src/main/java/net/fabricmc/loom/util/ipc/IPCClient.java new file mode 100644 index 0000000..3e5563d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ipc/IPCClient.java @@ -0,0 +1,67 @@ +/* + * 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.ipc; + +import java.io.IOException; +import java.net.UnixDomainSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; + +import net.fabricmc.loom.util.IOStringConsumer; + +public final class IPCClient implements IOStringConsumer, AutoCloseable { + private final Path path; + private final SocketChannel socketChannel; + + public IPCClient(Path path) throws IOException { + this.path = path; + socketChannel = setupChannel(); + } + + private SocketChannel setupChannel() throws IOException { + final UnixDomainSocketAddress address = UnixDomainSocketAddress.of(path); + return SocketChannel.open(address); + } + + @Override + public void accept(String s) throws IOException { + synchronized (socketChannel) { + ByteBuffer buf = ByteBuffer.wrap((s + "\n").getBytes(StandardCharsets.UTF_8)); + + while (buf.hasRemaining()) { + socketChannel.write(buf); + } + } + } + + @Override + public void close() throws Exception { + synchronized (socketChannel) { + socketChannel.close(); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/util/ipc/IPCServer.java b/src/main/java/net/fabricmc/loom/util/ipc/IPCServer.java new file mode 100644 index 0000000..7c8158a --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ipc/IPCServer.java @@ -0,0 +1,88 @@ +/* + * 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.ipc; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.net.StandardProtocolFamily; +import java.net.UnixDomainSocketAddress; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.Scanner; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; + +public class IPCServer implements AutoCloseable { + private final ExecutorService loggerReceiverService = Executors.newSingleThreadExecutor(); + private final Path path; + private final Consumer consumer; + + private final CountDownLatch startupLock = new CountDownLatch(1); + + public IPCServer(Path path, Consumer consumer) { + this.path = path; + this.consumer = consumer; + + loggerReceiverService.submit(this::run); + + try { + startupLock.await(10, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException("Timed out waiting for IPC server thread to start", e); + } + } + + public void run() { + UnixDomainSocketAddress address = UnixDomainSocketAddress.of(path); + + try (ServerSocketChannel serverChannel = ServerSocketChannel.open(StandardProtocolFamily.UNIX)) { + serverChannel.bind(address); + + startupLock.countDown(); + + try (SocketChannel clientChannel = serverChannel.accept(); + Scanner scanner = new Scanner(clientChannel, StandardCharsets.UTF_8)) { + while (!Thread.currentThread().isInterrupted()) { + if (scanner.hasNextLine()) { + this.consumer.accept(scanner.nextLine()); + } + } + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to listen for IPC messages", e); + } + } + + @Override + public void close() throws InterruptedException { + loggerReceiverService.shutdownNow(); + loggerReceiverService.awaitTermination(10, TimeUnit.SECONDS); + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy index 239fa63..daa2a51 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy @@ -47,6 +47,7 @@ class DecompileTest extends Specification implements GradleProjectTestTrait { decompiler | task | version 'fernflower' | "genSources" | DEFAULT_GRADLE 'fernflower' | "genSources" | PRE_RELEASE_GRADLE - 'cfr' | "genSourcesWithExperimentalCfr" | DEFAULT_GRADLE + 'cfr' | "genSourcesWithCfr" | DEFAULT_GRADLE + 'cfr' | "genSourcesWithCfr" | PRE_RELEASE_GRADLE } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/IPCTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/IPCTest.groovy new file mode 100644 index 0000000..a53751a --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/IPCTest.groovy @@ -0,0 +1,65 @@ +/* + * 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.test.unit + +import net.fabricmc.loom.util.ipc.IPCClient +import net.fabricmc.loom.util.ipc.IPCServer +import spock.lang.Specification +import spock.lang.Timeout + +import java.nio.file.Files +import java.util.function.Consumer + +@Timeout(20) +class IPCTest extends Specification { + def "ipc test"() { + given: + def path = Files.createTempFile("loom", "ipc") + Files.deleteIfExists(path) + + def received = [] + Consumer consumer = { str -> + println str + received << str + } + + when: + def ipcServer = new IPCServer(path, consumer) + + new IPCClient(path).withCloseable { client -> + client.accept("Test") + client.accept("Hello") + } + + // Allow ipcServer to finish reading, before closing. + while (received.size() != 2) { } + ipcServer.close() + + then: + received.size() == 2 + received[0] == "Test" + received[1] == "Hello" + } +} diff --git a/src/test/resources/projects/decompile/build.gradle b/src/test/resources/projects/decompile/build.gradle index daa90b1..2c859c0 100644 --- a/src/test/resources/projects/decompile/build.gradle +++ b/src/test/resources/projects/decompile/build.gradle @@ -3,7 +3,7 @@ plugins { } dependencies { - minecraft "com.mojang:minecraft:1.16.5" - mappings "net.fabricmc:yarn:1.16.5+build.5:v2" - modImplementation "net.fabricmc:fabric-loader:0.11.2" + minecraft "com.mojang:minecraft:21w38a" + mappings "net.fabricmc:yarn:21w38a+build.11:v2" + modImplementation "net.fabricmc:fabric-loader:0.11.7" }