Support the Vineflower decompiler (#951)

This commit is contained in:
modmuss
2023-09-11 11:29:01 +01:00
committed by GitHub
parent 0a3779f41d
commit 71b7bea854
17 changed files with 790 additions and 64 deletions

View File

@@ -8,11 +8,37 @@ plugins {
id 'checkstyle'
id 'jacoco'
id 'codenarc'
alias(libs.plugins.kotlin)
alias(libs.plugins.kotlin) apply false // Delay this so we can perform magic 🪄 first.
alias(libs.plugins.spotless)
alias(libs.plugins.retry)
}
/**
* Haha this is fun :) The Kotlin gradle plugin triggers deprecation warnings for custom configurations (https://youtrack.jetbrains.com/issue/KT-60879)
* We need to make DefaultConfiguration.isSpecialCaseOfChangingUsage think that our configurstion is a special case and not deprecated.
* We do this by setting DefaultConfiguration.roleAtCreation to LEGACY, thus isInLegacyRole will now return true.
*
* Yeah I know we can just ignore the deprecation warning, but doing so wouldn't alert us to issues when testing against pre-release Gradle versions. Also this is more fun :)
*/
def brokenConfigurations = [
"commonDecompilerRuntimeClasspath",
"fernflowerRuntimeClasspath",
"cfrRuntimeClasspath",
"vineflowerRuntimeClasspath"
]
configurations.configureEach {
if (brokenConfigurations.contains(it.name)) {
// For some reason Gradle stops us from using Groovy magic to do this, so lets do it the boring way.
def field = org.gradle.api.internal.artifacts.configurations.DefaultConfiguration.class.getDeclaredField("roleAtCreation")
field.setAccessible(true)
field.set(it, ConfigurationRoles.LEGACY)
}
}
// Ensure we apply the Kotlin plugin after, to allow for the above configuration to take place first
apply plugin: libs.plugins.kotlin.get().pluginId
tasks.withType(JavaCompile).configureEach {
it.options.encoding = "UTF-8"
}
@@ -62,6 +88,29 @@ configurations.all {
}
}
sourceSets {
commonDecompiler {
java {
srcDir("src/decompilers/common")
}
}
fernflower {
java {
srcDir("src/decompilers/fernflower")
}
}
cfr {
java {
srcDir("src/decompilers/cfr")
}
}
vineflower {
java {
srcDir("src/decompilers/vineflower")
}
}
}
dependencies {
implementation gradleApi()
@@ -89,8 +138,23 @@ dependencies {
}
// decompilers
compileOnly runtimeLibs.fernflower
compileOnly runtimeLibs.cfr
fernflowerCompileOnly runtimeLibs.fernflower
fernflowerCompileOnly libs.fabric.mapping.io
cfrCompileOnly runtimeLibs.cfr
cfrCompileOnly libs.fabric.mapping.io
vineflowerCompileOnly runtimeLibs.vineflower
vineflowerCompileOnly libs.fabric.mapping.io
fernflowerApi sourceSets.commonDecompiler.output
cfrApi sourceSets.commonDecompiler.output
vineflowerApi sourceSets.commonDecompiler.output
implementation sourceSets.commonDecompiler.output
implementation sourceSets.fernflower.output
implementation sourceSets.cfr.output
implementation sourceSets.vineflower.output
// source code remapping
implementation libs.fabric.mercury
@@ -130,6 +194,10 @@ jar {
}
from configurations.bootstrap.collect { it.isDirectory() ? it : zipTree(it) }
from sourceSets.commonDecompiler.output.classesDirs
from sourceSets.cfr.output.classesDirs
from sourceSets.fernflower.output.classesDirs
from sourceSets.vineflower.output.classesDirs
}
base {
@@ -222,6 +290,8 @@ test {
}
}
import org.gradle.api.internal.artifacts.configurations.ConfigurationRoles
import org.gradle.launcher.cli.KotlinDslVersion
import org.gradle.util.GradleVersion
import org.w3c.dom.Document

View File

@@ -2,6 +2,7 @@
# Decompilers
fernflower = "2.0.0"
cfr = "0.2.1"
vineflower = "1.9.3"
# Runtime depedencies
mixin-compile-extensions = "0.6.0"
@@ -14,6 +15,7 @@ native-support = "1.0.1"
# Decompilers
fernflower = { module = "net.fabricmc:fabric-fernflower", version.ref = "fernflower" }
cfr = { module = "net.fabricmc:cfr", version.ref = "cfr" }
vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" }
# Runtime depedencies
mixin-compile-extensions = { module = "net.fabricmc:fabric-mixin-compile-extensions", version.ref = "mixin-compile-extensions" }

View File

@@ -6,7 +6,7 @@ mockito = "5.4.0"
java-debug = "0.48.0"
mixin = "0.11.4+mixin.0.8.5"
gradle-nightly = "8.4-20230821223421+0000"
gradle-nightly = "8.5-20230908221250+0000"
fabric-loader = "0.14.22"
fabric-installer = "0.11.1"

View File

@@ -45,7 +45,6 @@ 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;
@@ -66,7 +65,7 @@ public class CFRObfuscationMapping extends NullMapping {
private static MappingTree readMappings(Path input) {
try (BufferedReader reader = Files.newBufferedReader(input)) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString());
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, "named");
MappingReader.read(reader, nsSwitch);
return mappingTree;

View File

@@ -25,6 +25,7 @@
package net.fabricmc.loom.decompilers.cfr;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
@@ -37,23 +38,18 @@ 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;
import net.fabricmc.loom.decompilers.LoomInternalDecompiler;
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 LoomInternalDecompiler.Logger logger;
private final Set<String> addedDirectories = new HashSet<>();
private final Map<String, Map<Integer, Integer>> lineMap = new TreeMap<>();
public CFRSinkFactory(JarOutputStream outputStream, IOStringConsumer logger) {
public CFRSinkFactory(JarOutputStream outputStream, LoomInternalDecompiler.Logger logger) {
this.outputStream = outputStream;
this.logger = logger;
}
@@ -72,7 +68,7 @@ public class CFRSinkFactory implements OutputSinkFactory {
return switch (sinkType) {
case JAVA -> (Sink<T>) decompiledSink();
case LINENUMBER -> (Sink<T>) lineNumberMappingSink();
case EXCEPTION -> (e) -> ERROR_LOGGER.error((String) e);
case EXCEPTION -> (e) -> logger.error((String) e);
default -> null;
};
}
@@ -83,7 +79,7 @@ public class CFRSinkFactory implements OutputSinkFactory {
if (!filename.isEmpty()) filename += "/";
filename += sinkable.getClassName() + ".java";
byte[] data = sinkable.getJava().getBytes(Charsets.UTF_8);
byte[] data = sinkable.getJava().getBytes(StandardCharsets.UTF_8);
writeToJar(filename, data);
};

View File

@@ -45,10 +45,9 @@ 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.LoomInternalDecompiler;
public final class LoomCFRDecompiler implements LoomDecompiler {
public final class LoomCFRDecompiler implements LoomInternalDecompiler {
private static final Map<String, String> DECOMPILE_OPTIONS = Map.of(
"renameillegalidents", "true",
"trackbytecodeloc", "true",
@@ -56,16 +55,18 @@ public final class LoomCFRDecompiler implements LoomDecompiler {
);
@Override
public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
public void decompile(LoomInternalDecompiler.Context context) {
Path compiledJar = context.compiledJar();
final String path = compiledJar.toAbsolutePath().toString();
final Map<String, String> allOptions = new HashMap<>(DECOMPILE_OPTIONS);
allOptions.putAll(metaData.options());
allOptions.putAll(context.options());
final Options options = OptionsImpl.getFactory().create(allOptions);
ClassFileSourceImpl classFileSource = new ClassFileSourceImpl(options);
for (Path library : metaData.libraries()) {
for (Path library : context.libraries()) {
classFileSource.addJarContent(library.toAbsolutePath().toString(), AnalysisType.JAR);
}
@@ -73,8 +74,8 @@ public final class LoomCFRDecompiler implements LoomDecompiler {
DCCommonState state = new DCCommonState(options, classFileSource);
if (metaData.javaDocs() != null) {
state = new DCCommonState(state, new CFRObfuscationMapping(metaData.javaDocs()));
if (context.javaDocs() != null) {
state = new DCCommonState(state, new CFRObfuscationMapping(context.javaDocs()));
}
final Manifest manifest = new Manifest();
@@ -82,8 +83,8 @@ public final class LoomCFRDecompiler implements LoomDecompiler {
Map<String, Map<Integer, Integer>> lineMap;
try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(sourcesDestination), manifest)) {
CFRSinkFactory cfrSinkFactory = new CFRSinkFactory(outputStream, metaData.logger());
try (JarOutputStream outputStream = new JarOutputStream(Files.newOutputStream(context.sourcesDestination()), manifest)) {
CFRSinkFactory cfrSinkFactory = new CFRSinkFactory(outputStream, context.logger());
SinkDumperFactory dumperFactory = new SinkDumperFactory(cfrSinkFactory, options);
Driver.doJar(state, path, AnalysisType.JAR, dumperFactory);
@@ -93,7 +94,7 @@ public final class LoomCFRDecompiler implements LoomDecompiler {
throw new UncheckedIOException("Failed to decompile", e);
}
writeLineMap(linemapDestination, lineMap);
writeLineMap(context.linemapDestination(), lineMap);
}
private void writeLineMap(Path output, Map<String, Map<Integer, Integer>> lineMap) {

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2016-2022 FabricMC
* Copyright (c) 2023 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
@@ -22,23 +22,40 @@
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
package net.fabricmc.loom.decompilers;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import org.jetbrains.java.decompiler.util.InterpreterUtil;
// This is an internal interface to loom, DO NOT USE this in your own plugins.
public interface LoomInternalDecompiler {
void decompile(Context context);
import net.fabricmc.loom.util.ZipUtils;
interface Context {
Path compiledJar();
public class FernFlowerUtils {
public static byte[] getBytecode(String externalPath, String internalPath) throws IOException {
File file = new File(externalPath);
Path sourcesDestination();
if (internalPath == null) {
return InterpreterUtil.getBytes(file);
} else {
return ZipUtils.unpack(file.toPath(), internalPath);
}
Path linemapDestination();
int numberOfThreads();
Path javaDocs();
Collection<Path> libraries();
Logger logger();
Map<String, String> options();
byte[] unpackZip(Path zip, String path) throws IOException;
}
interface Logger {
void accept(String data) throws IOException;
void error(String msg);
}
}

View File

@@ -0,0 +1,86 @@
/*
* 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 java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import org.jetbrains.java.decompiler.main.Fernflower;
import org.jetbrains.java.decompiler.main.extern.IFernflowerPreferences;
import org.jetbrains.java.decompiler.main.extern.IResultSaver;
import org.jetbrains.java.decompiler.util.InterpreterUtil;
import net.fabricmc.fernflower.api.IFabricJavadocProvider;
import net.fabricmc.loom.decompilers.LoomInternalDecompiler;
public final class FabricFernFlowerDecompiler implements LoomInternalDecompiler {
@Override
public void decompile(LoomInternalDecompiler.Context context) {
Path sourcesDestination = context.sourcesDestination();
Path linemapDestination = context.linemapDestination();
final Map<String, Object> options = new HashMap<>(
Map.of(
IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1",
IFernflowerPreferences.BYTECODE_SOURCE_MAPPING, "1",
IFernflowerPreferences.REMOVE_SYNTHETIC, "1",
IFernflowerPreferences.LOG_LEVEL, "trace",
IFernflowerPreferences.THREADS, String.valueOf(context.numberOfThreads()),
IFernflowerPreferences.INDENT_STRING, "\t",
IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(context.javaDocs().toFile())
)
);
options.putAll(context.options());
IResultSaver saver = new ThreadSafeResultSaver(sourcesDestination::toFile, linemapDestination::toFile);
Fernflower ff = new Fernflower((externalPath, internalPath) -> FabricFernFlowerDecompiler.this.getBytecode(externalPath, internalPath, context), saver, options, new FernflowerLogger(context.logger()));
for (Path library : context.libraries()) {
ff.addLibrary(library.toFile());
}
ff.addSource(context.compiledJar().toFile());
try {
ff.decompileContext();
} finally {
ff.clearContext();
}
}
private byte[] getBytecode(String externalPath, String internalPath, LoomInternalDecompiler.Context context) throws IOException {
File file = new File(externalPath);
if (internalPath == null) {
return InterpreterUtil.getBytes(file);
} else {
return context.unpackZip(file.toPath(), internalPath);
}
}
}

View File

@@ -28,12 +28,12 @@ import java.io.IOException;
import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger;
import net.fabricmc.loom.util.IOStringConsumer;
import net.fabricmc.loom.decompilers.LoomInternalDecompiler;
public class FernflowerLogger extends IFernflowerLogger {
private final IOStringConsumer logger;
private final LoomInternalDecompiler.Logger logger;
public FernflowerLogger(IOStringConsumer logger) {
public FernflowerLogger(LoomInternalDecompiler.Logger logger) {
this.logger = logger;
}

View File

@@ -35,16 +35,17 @@ import org.jetbrains.java.decompiler.struct.StructClass;
import org.jetbrains.java.decompiler.struct.StructField;
import org.jetbrains.java.decompiler.struct.StructMethod;
import org.jetbrains.java.decompiler.struct.StructRecordComponent;
import org.objectweb.asm.Opcodes;
import net.fabricmc.fernflower.api.IFabricJavadocProvider;
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 TinyJavadocProvider implements IFabricJavadocProvider {
private static final int ACC_STATIC = 0x0008;
private static final int ACC_RECORD = 0x10000;
private final MappingTree mappingTree;
public TinyJavadocProvider(File tinyFile) {
@@ -93,7 +94,7 @@ public class TinyJavadocProvider implements IFabricJavadocProvider {
addedParam = true;
}
parts.add(String.format("@param %s %s", fieldMapping.getName(MappingsNamespace.NAMED.toString()), comment));
parts.add(String.format("@param %s %s", fieldMapping.getName("named"), comment));
}
}
@@ -151,7 +152,7 @@ public class TinyJavadocProvider implements IFabricJavadocProvider {
addedParam = true;
}
parts.add(String.format("@param %s %s", argMapping.getName(MappingsNamespace.NAMED.toString()), comment));
parts.add(String.format("@param %s %s", argMapping.getName("named"), comment));
}
}
@@ -168,7 +169,7 @@ public class TinyJavadocProvider implements IFabricJavadocProvider {
private static MappingTree readMappings(File input) {
try (BufferedReader reader = Files.newBufferedReader(input.toPath())) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, MappingsNamespace.NAMED.toString());
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, "named");
MappingReader.read(reader, nsSwitch);
return mappingTree;
@@ -178,10 +179,10 @@ public class TinyJavadocProvider implements IFabricJavadocProvider {
}
public static boolean isRecord(StructClass structClass) {
return (structClass.getAccessFlags() & Opcodes.ACC_RECORD) != 0;
return (structClass.getAccessFlags() & ACC_RECORD) != 0;
}
public static boolean isStatic(StructField structField) {
return (structField.getAccessFlags() & Opcodes.ACC_STATIC) != 0;
return (structField.getAccessFlags() & ACC_STATIC) != 0;
}
}

View File

@@ -0,0 +1,172 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2023 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.vineflower;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.function.Supplier;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import org.jetbrains.java.decompiler.main.DecompilerContext;
import org.jetbrains.java.decompiler.main.extern.IResultSaver;
public class ThreadSafeResultSaver implements IResultSaver {
private final Supplier<File> output;
private final Supplier<File> lineMapFile;
public Map<String, ZipOutputStream> outputStreams = new HashMap<>();
public Map<String, ExecutorService> saveExecutors = new HashMap<>();
public PrintWriter lineMapWriter;
public ThreadSafeResultSaver(Supplier<File> output, Supplier<File> lineMapFile) {
this.output = output;
this.lineMapFile = lineMapFile;
}
@Override
public void createArchive(String path, String archiveName, Manifest manifest) {
String key = path + "/" + archiveName;
File file = output.get();
try {
FileOutputStream fos = new FileOutputStream(file);
ZipOutputStream zos = manifest == null ? new ZipOutputStream(fos) : new JarOutputStream(fos, manifest);
outputStreams.put(key, zos);
saveExecutors.put(key, Executors.newSingleThreadExecutor());
} catch (IOException e) {
throw new RuntimeException("Unable to create archive: " + file, e);
}
if (lineMapFile.get() != null) {
try {
lineMapWriter = new PrintWriter(new FileWriter(lineMapFile.get()));
} catch (IOException e) {
throw new RuntimeException("Unable to create line mapping file: " + lineMapFile.get(), e);
}
}
}
@Override
public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) {
this.saveClassEntry(path, archiveName, qualifiedName, entryName, content, null);
}
@Override
public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content, int[] mapping) {
String key = path + "/" + archiveName;
ExecutorService executor = saveExecutors.get(key);
executor.submit(() -> {
ZipOutputStream zos = outputStreams.get(key);
try {
zos.putNextEntry(new ZipEntry(entryName));
if (content != null) {
zos.write(content.getBytes(StandardCharsets.UTF_8));
}
} catch (IOException e) {
DecompilerContext.getLogger().writeMessage("Cannot write entry " + entryName, e);
}
if (mapping != null && lineMapWriter != null) {
int maxLine = 0;
int maxLineDest = 0;
StringBuilder builder = new StringBuilder();
for (int i = 0; i < mapping.length; i += 2) {
maxLine = Math.max(maxLine, mapping[i]);
maxLineDest = Math.max(maxLineDest, mapping[i + 1]);
builder.append("\t").append(mapping[i]).append("\t").append(mapping[i + 1]).append("\n");
}
lineMapWriter.println(qualifiedName + "\t" + maxLine + "\t" + maxLineDest);
lineMapWriter.println(builder.toString());
}
});
}
@Override
public void closeArchive(String path, String archiveName) {
String key = path + "/" + archiveName;
ExecutorService executor = saveExecutors.get(key);
Future<?> closeFuture = executor.submit(() -> {
ZipOutputStream zos = outputStreams.get(key);
try {
zos.close();
} catch (IOException e) {
throw new RuntimeException("Unable to close zip. " + key, e);
}
});
executor.shutdown();
try {
closeFuture.get();
} catch (InterruptedException | ExecutionException e) {
throw new RuntimeException(e);
}
outputStreams.remove(key);
saveExecutors.remove(key);
if (lineMapWriter != null) {
lineMapWriter.flush();
lineMapWriter.close();
}
}
@Override
public void saveFolder(String path) {
}
@Override
public void copyFile(String source, String path, String entryName) {
}
@Override
public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) {
}
@Override
public void saveDirEntry(String path, String archiveName, String entryName) {
}
@Override
public void copyEntry(String source, String path, String archiveName, String entry) {
}
}

View File

@@ -0,0 +1,188 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2023 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.vineflower;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.java.decompiler.struct.StructClass;
import org.jetbrains.java.decompiler.struct.StructField;
import org.jetbrains.java.decompiler.struct.StructMethod;
import org.jetbrains.java.decompiler.struct.StructRecordComponent;
import net.fabricmc.fernflower.api.IFabricJavadocProvider;
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 TinyJavadocProvider implements IFabricJavadocProvider {
private static final int ACC_STATIC = 0x0008;
private static final int ACC_RECORD = 0x10000;
private final MappingTree mappingTree;
public TinyJavadocProvider(File tinyFile) {
mappingTree = readMappings(tinyFile);
}
@Override
public String getClassDoc(StructClass structClass) {
MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName);
if (classMapping == null) {
return null;
}
if (!isRecord(structClass)) {
return classMapping.getComment();
}
/**
* Handle the record component docs here.
*
* Record components are mapped via the field name, thus take the docs from the fields and display them on then class.
*/
List<String> parts = new ArrayList<>();
if (classMapping.getComment() != null) {
parts.add(classMapping.getComment());
}
boolean addedParam = false;
for (StructRecordComponent component : structClass.getRecordComponents()) {
// The component will always match the field name and descriptor
MappingTree.FieldMapping fieldMapping = classMapping.getField(component.getName(), component.getDescriptor());
if (fieldMapping == null) {
continue;
}
String comment = fieldMapping.getComment();
if (comment != null) {
if (!addedParam && classMapping.getComment() != null) {
//Add a blank line before components when the class has a comment
parts.add("");
addedParam = true;
}
parts.add(String.format("@param %s %s", fieldMapping.getName("named"), comment));
}
}
if (parts.isEmpty()) {
return null;
}
return String.join("\n", parts);
}
@Override
public String getFieldDoc(StructClass structClass, StructField structField) {
// None static fields in records are handled in the class javadoc.
if (isRecord(structClass) && !isStatic(structField)) {
return null;
}
MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName);
if (classMapping == null) {
return null;
}
MappingTree.FieldMapping fieldMapping = classMapping.getField(structField.getName(), structField.getDescriptor());
return fieldMapping != null ? fieldMapping.getComment() : null;
}
@Override
public String getMethodDoc(StructClass structClass, StructMethod structMethod) {
MappingTree.ClassMapping classMapping = mappingTree.getClass(structClass.qualifiedName);
if (classMapping == null) {
return null;
}
MappingTree.MethodMapping methodMapping = classMapping.getMethod(structMethod.getName(), structMethod.getDescriptor());
if (methodMapping != null) {
List<String> parts = new ArrayList<>();
if (methodMapping.getComment() != null) {
parts.add(methodMapping.getComment());
}
boolean addedParam = false;
for (MappingTree.MethodArgMapping argMapping : methodMapping.getArgs()) {
String comment = argMapping.getComment();
if (comment != null) {
if (!addedParam && methodMapping.getComment() != null) {
//Add a blank line before params when the method has a comment
parts.add("");
addedParam = true;
}
parts.add(String.format("@param %s %s", argMapping.getName("named"), comment));
}
}
if (parts.isEmpty()) {
return null;
}
return String.join("\n", parts);
}
return null;
}
private static MappingTree readMappings(File input) {
try (BufferedReader reader = Files.newBufferedReader(input.toPath())) {
MemoryMappingTree mappingTree = new MemoryMappingTree();
MappingSourceNsSwitch nsSwitch = new MappingSourceNsSwitch(mappingTree, "named");
MappingReader.read(reader, nsSwitch);
return mappingTree;
} catch (IOException e) {
throw new RuntimeException("Failed to read mappings", e);
}
}
public static boolean isRecord(StructClass structClass) {
return (structClass.getAccessFlags() & ACC_RECORD) != 0;
}
public static boolean isStatic(StructField structField) {
return (structField.getAccessFlags() & ACC_STATIC) != 0;
}
}

View File

@@ -1,7 +1,7 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2019-2021 FabricMC
* Copyright (c) 2019-2023 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
@@ -22,7 +22,7 @@
* SOFTWARE.
*/
package net.fabricmc.loom.decompilers.fernflower;
package net.fabricmc.loom.decompilers.vineflower;
import java.nio.file.Path;
import java.util.HashMap;
@@ -33,34 +33,36 @@ 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;
import net.fabricmc.loom.decompilers.LoomInternalDecompiler;
public final class FabricFernFlowerDecompiler implements LoomDecompiler {
public final class VineflowerDecompiler implements LoomInternalDecompiler {
@Override
public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
public void decompile(Context context) {
Path sourcesDestination = context.sourcesDestination();
Path linemapDestination = context.linemapDestination();
final Map<String, Object> options = new HashMap<>(
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.THREADS, String.valueOf(context.numberOfThreads()),
IFernflowerPreferences.INDENT_STRING, "\t",
IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(metaData.javaDocs().toFile())
IFabricJavadocProvider.PROPERTY_NAME, new TinyJavadocProvider(context.javaDocs().toFile())
)
);
options.putAll(metaData.options());
options.putAll(context.options());
IResultSaver saver = new ThreadSafeResultSaver(sourcesDestination::toFile, linemapDestination::toFile);
Fernflower ff = new Fernflower(FernFlowerUtils::getBytecode, saver, options, new FernflowerLogger(metaData.logger()));
Fernflower ff = new Fernflower(saver, options, new VineflowerLogger(context.logger()));
for (Path library : metaData.libraries()) {
for (Path library : context.libraries()) {
ff.addLibrary(library.toFile());
}
ff.addSource(compiledJar.toFile());
ff.addSource(context.compiledJar().toFile());
try {
ff.decompileContext();

View File

@@ -0,0 +1,87 @@
/*
* This file is part of fabric-loom, licensed under the MIT License (MIT).
*
* Copyright (c) 2021-2023 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.vineflower;
import java.io.IOException;
import org.jetbrains.java.decompiler.main.extern.IFernflowerLogger;
import net.fabricmc.loom.decompilers.LoomInternalDecompiler;
public class VineflowerLogger extends IFernflowerLogger {
private final LoomInternalDecompiler.Logger logger;
public VineflowerLogger(LoomInternalDecompiler.Logger logger) {
this.logger = logger;
}
@Override
public void writeMessage(String message, Severity severity) {
if (severity.ordinal() < Severity.ERROR.ordinal()) return;
System.err.println(message);
}
@Override
public void writeMessage(String message, Severity severity, Throwable t) {
if (severity.ordinal() < Severity.ERROR.ordinal()) return;
writeMessage(message, severity);
t.printStackTrace(System.err);
}
private void write(String data) {
try {
logger.accept(data);
} catch (IOException e) {
throw new RuntimeException("Failed to log", e);
}
}
@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
}
}

View File

@@ -24,17 +24,27 @@
package net.fabricmc.loom.decompilers;
import java.io.IOException;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Map;
import javax.inject.Inject;
import org.gradle.api.NamedDomainObjectProvider;
import org.gradle.api.Project;
import org.gradle.api.artifacts.Configuration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import net.fabricmc.loom.LoomGradleExtension;
import net.fabricmc.loom.api.decompilers.DecompilationMetadata;
import net.fabricmc.loom.api.decompilers.LoomDecompiler;
import net.fabricmc.loom.decompilers.cfr.LoomCFRDecompiler;
import net.fabricmc.loom.decompilers.fernflower.FabricFernFlowerDecompiler;
import net.fabricmc.loom.decompilers.vineflower.VineflowerDecompiler;
import net.fabricmc.loom.util.LoomVersions;
import net.fabricmc.loom.util.ZipUtils;
public abstract class DecompilerConfiguration implements Runnable {
@Inject
@@ -44,9 +54,11 @@ public abstract class DecompilerConfiguration implements Runnable {
public void run() {
var fernflowerConfiguration = createConfiguration("fernflower", LoomVersions.FERNFLOWER);
var cfrConfiguration = createConfiguration("cfr", LoomVersions.CFR);
var vineflowerConfiguration = createConfiguration("vineflower", LoomVersions.VINEFLOWER);
registerDecompiler(getProject(), "fernFlower", FabricFernFlowerDecompiler.class, fernflowerConfiguration);
registerDecompiler(getProject(), "cfr", LoomCFRDecompiler.class, cfrConfiguration);
registerDecompiler(getProject(), "fernFlower", BuiltinFernflower.class, fernflowerConfiguration);
registerDecompiler(getProject(), "cfr", BuiltinCfr.class, cfrConfiguration);
registerDecompiler(getProject(), "vineflower", BuiltinVineflower.class, vineflowerConfiguration);
}
private NamedDomainObjectProvider<Configuration> createConfiguration(String name, LoomVersions version) {
@@ -62,4 +74,96 @@ public abstract class DecompilerConfiguration implements Runnable {
options.getClasspath().from(configuration);
});
}
// We need to wrap the internal API with the public API.
// This is needed as the sourceset containing fabric's decompilers do not have access to loom classes.
private abstract static sealed class BuiltinDecompiler implements LoomDecompiler permits BuiltinFernflower, BuiltinCfr, BuiltinVineflower {
private final LoomInternalDecompiler internalDecompiler;
BuiltinDecompiler(LoomInternalDecompiler internalDecompiler) {
this.internalDecompiler = internalDecompiler;
}
@Override
public void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
final Logger slf4jLogger = LoggerFactory.getLogger(internalDecompiler.getClass());
final var logger = new LoomInternalDecompiler.Logger() {
@Override
public void accept(String data) throws IOException {
metaData.logger().accept(data);
}
@Override
public void error(String msg) {
slf4jLogger.error(msg);
}
};
internalDecompiler.decompile(new LoomInternalDecompiler.Context() {
@Override
public Path compiledJar() {
return compiledJar;
}
@Override
public Path sourcesDestination() {
return sourcesDestination;
}
@Override
public Path linemapDestination() {
return linemapDestination;
}
@Override
public int numberOfThreads() {
return metaData.numberOfThreads();
}
@Override
public Path javaDocs() {
return metaData.javaDocs();
}
@Override
public Collection<Path> libraries() {
return metaData.libraries();
}
@Override
public LoomInternalDecompiler.Logger logger() {
return logger;
}
@Override
public Map<String, String> options() {
return metaData.options();
}
@Override
public byte[] unpackZip(Path zip, String path) throws IOException {
return ZipUtils.unpack(zip, path);
}
});
}
}
public static final class BuiltinFernflower extends BuiltinDecompiler {
public BuiltinFernflower() {
super(new FabricFernFlowerDecompiler());
}
}
public static final class BuiltinCfr extends BuiltinDecompiler {
public BuiltinCfr() {
super(new LoomCFRDecompiler());
}
}
public static final class BuiltinVineflower extends BuiltinDecompiler {
public BuiltinVineflower() {
super(new VineflowerDecompiler());
}
}
}

View File

@@ -48,6 +48,7 @@ class DecompileTest extends Specification implements GradleProjectTestTrait {
decompiler | task | version
'fernflower' | "genSourcesWithFernFlower" | PRE_RELEASE_GRADLE
'cfr' | "genSourcesWithCfr" | PRE_RELEASE_GRADLE
'vineflower' | "genSourcesWithVineflower" | PRE_RELEASE_GRADLE
}
@Unroll