mirror of
https://github.com/architectury/architectury-loom.git
synced 2026-03-28 04:07:01 -05:00
Experimental Decompiler cache (#1043)
This commit is contained in:
@@ -58,7 +58,7 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration<Mapp
|
||||
// Decompiler will be passed to the constructor of GenerateSourcesTask
|
||||
project.getTasks().register(taskName, GenerateSourcesTask.class, options).configure(task -> {
|
||||
task.getInputJarName().set(minecraftJar.getName());
|
||||
task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", minecraftJar.getPath()));
|
||||
task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", minecraftJar.getPath()));
|
||||
|
||||
task.dependsOn(project.getTasks().named("validateAccessWidener"));
|
||||
task.setDescription("Decompile minecraft using %s.".formatted(decompilerName));
|
||||
|
||||
@@ -55,7 +55,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration<Ma
|
||||
|
||||
final TaskProvider<Task> commonDecompileTask = createDecompileTasks("Common", task -> {
|
||||
task.getInputJarName().set(commonJar.getName());
|
||||
task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", commonJar.getPath()));
|
||||
task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", commonJar.getPath()));
|
||||
|
||||
if (mappingConfiguration.hasUnpickDefinitions()) {
|
||||
File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-common-unpicked.jar");
|
||||
@@ -65,7 +65,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration<Ma
|
||||
|
||||
final TaskProvider<Task> clientOnlyDecompileTask = createDecompileTasks("ClientOnly", task -> {
|
||||
task.getInputJarName().set(clientOnlyJar.getName());
|
||||
task.getOutputJar().fileValue(GenerateSourcesTask.getMappedJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath()));
|
||||
task.getOutputJar().fileValue(GenerateSourcesTask.getJarFileWithSuffix("-sources.jar", clientOnlyJar.getPath()));
|
||||
|
||||
if (mappingConfiguration.hasUnpickDefinitions()) {
|
||||
File unpickJar = new File(extension.getMappingConfiguration().mappingsWorkingDir().toFile(), "minecraft-clientonly-unpicked.jar");
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.Writer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public record ClassLineNumbers(Map<String, ClassLineNumbers.Entry> lineMap) {
|
||||
public ClassLineNumbers {
|
||||
Objects.requireNonNull(lineMap, "lineMap");
|
||||
|
||||
if (lineMap.isEmpty()) {
|
||||
throw new IllegalArgumentException("lineMap is empty");
|
||||
}
|
||||
}
|
||||
|
||||
public static ClassLineNumbers readMappings(Path lineMappingsPath) {
|
||||
try (BufferedReader reader = Files.newBufferedReader(lineMappingsPath)) {
|
||||
return readMappings(reader);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Exception reading LineMappings file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static ClassLineNumbers readMappings(BufferedReader reader) {
|
||||
var lineMap = new HashMap<String, ClassLineNumbers.Entry>();
|
||||
|
||||
String line = null;
|
||||
int lineNumber = 0;
|
||||
|
||||
record CurrentClass(String className, int maxLine, int maxLineDest) {
|
||||
void putEntry(Map<String, ClassLineNumbers.Entry> entries, Map<Integer, Integer> mappings) {
|
||||
var entry = new ClassLineNumbers.Entry(className(), maxLine(), maxLineDest(), Collections.unmodifiableMap(mappings));
|
||||
|
||||
final ClassLineNumbers.Entry previous = entries.put(className(), entry);
|
||||
|
||||
if (previous != null) {
|
||||
throw new IllegalStateException("Duplicate class line mappings for " + className());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CurrentClass currentClass = null;
|
||||
Map<Integer, Integer> currentMappings = new HashMap<>();
|
||||
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String[] segments = line.trim().split("\t");
|
||||
|
||||
if (line.charAt(0) != '\t') {
|
||||
if (currentClass != null) {
|
||||
currentClass.putEntry(lineMap, currentMappings);
|
||||
currentMappings = new HashMap<>();
|
||||
}
|
||||
|
||||
currentClass = new CurrentClass(segments[0], Integer.parseInt(segments[1]), Integer.parseInt(segments[2]));
|
||||
} else {
|
||||
Objects.requireNonNull(currentClass, "No class line mappings found for line " + lineNumber);
|
||||
currentMappings.put(Integer.parseInt(segments[0]), Integer.parseInt(segments[1]));
|
||||
}
|
||||
|
||||
lineNumber++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(format("Exception reading mapping line @{0}: {1}", lineNumber, line), e);
|
||||
}
|
||||
|
||||
assert currentClass != null;
|
||||
currentClass.putEntry(lineMap, currentMappings);
|
||||
|
||||
return new ClassLineNumbers(Collections.unmodifiableMap(lineMap));
|
||||
}
|
||||
|
||||
public void write(Writer writer) throws IOException {
|
||||
for (Map.Entry<String, ClassLineNumbers.Entry> entry : lineMap.entrySet()) {
|
||||
entry.getValue().write(writer);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge two ClassLineNumbers together, throwing an exception if there are any duplicate class line mappings.
|
||||
*/
|
||||
@Nullable
|
||||
public static ClassLineNumbers merge(@Nullable ClassLineNumbers a, @Nullable ClassLineNumbers b) {
|
||||
if (a == null) {
|
||||
return b;
|
||||
} else if (b == null) {
|
||||
return a;
|
||||
}
|
||||
|
||||
var lineMap = new HashMap<>(a.lineMap());
|
||||
|
||||
for (Map.Entry<String, Entry> entry : b.lineMap().entrySet()) {
|
||||
lineMap.merge(entry.getKey(), entry.getValue(), (v1, v2) -> {
|
||||
throw new IllegalStateException("Duplicate class line mappings for " + entry.getKey());
|
||||
});
|
||||
}
|
||||
|
||||
return new ClassLineNumbers(Collections.unmodifiableMap(lineMap));
|
||||
}
|
||||
|
||||
public record Entry(String className, int maxLine, int maxLineDest, Map<Integer, Integer> lineMap) {
|
||||
public void write(Writer writer) throws IOException {
|
||||
writer.write(className);
|
||||
writer.write('\t');
|
||||
writer.write(Integer.toString(maxLine));
|
||||
writer.write('\t');
|
||||
writer.write(Integer.toString(maxLineDest));
|
||||
writer.write('\n');
|
||||
|
||||
for (Map.Entry<Integer, Integer> lineEntry : lineMap.entrySet()) {
|
||||
writer.write('\t');
|
||||
writer.write(Integer.toString(lineEntry.getKey()));
|
||||
writer.write('\t');
|
||||
writer.write(Integer.toString(lineEntry.getValue()));
|
||||
writer.write('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,11 +24,6 @@
|
||||
|
||||
package net.fabricmc.loom.decompilers;
|
||||
|
||||
import static java.text.MessageFormat.format;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileReader;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.FileVisitResult;
|
||||
@@ -37,57 +32,21 @@ import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.objectweb.asm.ClassReader;
|
||||
import org.objectweb.asm.ClassVisitor;
|
||||
import org.objectweb.asm.ClassWriter;
|
||||
import org.objectweb.asm.Label;
|
||||
import org.objectweb.asm.MethodVisitor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
import net.fabricmc.loom.util.IOStringConsumer;
|
||||
|
||||
/**
|
||||
* Created by covers1624 on 18/02/19.
|
||||
*/
|
||||
public class LineNumberRemapper {
|
||||
private final Map<String, RClass> lineMap = new HashMap<>();
|
||||
public record LineNumberRemapper(ClassLineNumbers lineNumbers) {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(LineNumberRemapper.class);
|
||||
|
||||
public void readMappings(File lineMappings) {
|
||||
try (BufferedReader reader = new BufferedReader(new FileReader(lineMappings))) {
|
||||
RClass clazz = null;
|
||||
String line = null;
|
||||
int i = 0;
|
||||
|
||||
try {
|
||||
while ((line = reader.readLine()) != null) {
|
||||
if (line.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String[] segs = line.trim().split("\t");
|
||||
|
||||
if (line.charAt(0) != '\t') {
|
||||
clazz = lineMap.computeIfAbsent(segs[0], RClass::new);
|
||||
clazz.maxLine = Integer.parseInt(segs[1]);
|
||||
clazz.maxLineDest = Integer.parseInt(segs[2]);
|
||||
} else {
|
||||
clazz.lineMap.put(Integer.parseInt(segs[0]), Integer.parseInt(segs[1]));
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(format("Exception reading mapping line @{0}: {1}", i, line), e);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Exception reading LineMappings file.", e);
|
||||
}
|
||||
}
|
||||
|
||||
public void process(IOStringConsumer logger, Path input, Path output) throws IOException {
|
||||
public void process(Path input, Path output) throws IOException {
|
||||
Files.walkFileTree(input, new SimpleFileVisitor<>() {
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
|
||||
@@ -108,9 +67,7 @@ public class LineNumberRemapper {
|
||||
|
||||
String idx = rel.substring(0, rel.length() - 6);
|
||||
|
||||
if (logger != null) {
|
||||
logger.accept("Remapping " + idx);
|
||||
}
|
||||
LOGGER.debug("Remapping line numbers for class: " + idx);
|
||||
|
||||
int dollarPos = idx.indexOf('$'); //This makes the assumption that only Java classes are to be remapped.
|
||||
|
||||
@@ -118,12 +75,12 @@ public class LineNumberRemapper {
|
||||
idx = idx.substring(0, dollarPos);
|
||||
}
|
||||
|
||||
if (lineMap.containsKey(idx)) {
|
||||
if (lineNumbers.lineMap().containsKey(idx)) {
|
||||
try (InputStream is = Files.newInputStream(file)) {
|
||||
ClassReader reader = new ClassReader(is);
|
||||
ClassWriter writer = new ClassWriter(0);
|
||||
|
||||
reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineMap.get(idx)), 0);
|
||||
reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineNumbers.lineMap().get(idx)), 0);
|
||||
Files.write(dst, writer.toByteArray());
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
@@ -137,11 +94,11 @@ public class LineNumberRemapper {
|
||||
}
|
||||
|
||||
private static class LineNumberVisitor extends ClassVisitor {
|
||||
private final RClass rClass;
|
||||
private final ClassLineNumbers.Entry lineNumbers;
|
||||
|
||||
LineNumberVisitor(int api, ClassVisitor classVisitor, RClass rClass) {
|
||||
LineNumberVisitor(int api, ClassVisitor classVisitor, ClassLineNumbers.Entry lineNumbers) {
|
||||
super(api, classVisitor);
|
||||
this.rClass = rClass;
|
||||
this.lineNumbers = lineNumbers;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -153,30 +110,19 @@ public class LineNumberRemapper {
|
||||
|
||||
if (tLine <= 0) {
|
||||
super.visitLineNumber(line, start);
|
||||
} else if (tLine >= rClass.maxLine) {
|
||||
super.visitLineNumber(rClass.maxLineDest, start);
|
||||
} else if (tLine >= lineNumbers.maxLine()) {
|
||||
super.visitLineNumber(lineNumbers.maxLineDest(), start);
|
||||
} else {
|
||||
Integer matchedLine = null;
|
||||
|
||||
while (tLine <= rClass.maxLine && ((matchedLine = rClass.lineMap.get(tLine)) == null)) {
|
||||
while (tLine <= lineNumbers.maxLine() && ((matchedLine = lineNumbers.lineMap().get(tLine)) == null)) {
|
||||
tLine++;
|
||||
}
|
||||
|
||||
super.visitLineNumber(matchedLine != null ? matchedLine : rClass.maxLineDest, start);
|
||||
super.visitLineNumber(matchedLine != null ? matchedLine : lineNumbers.maxLineDest(), start);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static class RClass {
|
||||
private final String name;
|
||||
private int maxLine;
|
||||
private int maxLineDest;
|
||||
private final Map<Integer, Integer> lineMap = new HashMap<>();
|
||||
|
||||
private RClass(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
209
src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java
vendored
Normal file
209
src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java
vendored
Normal file
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Objects;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.decompilers.ClassLineNumbers;
|
||||
|
||||
// Serialised data for a class entry in the cache
|
||||
// Uses the RIFF format, allows for appending the line numbers to the end of the file
|
||||
// Stores the source code and line numbers for the class
|
||||
public record CachedData(String className, String sources, @Nullable ClassLineNumbers.Entry lineNumbers) {
|
||||
public static final CachedFileStore.EntrySerializer<CachedData> SERIALIZER = new EntrySerializer();
|
||||
|
||||
private static final String HEADER_ID = "LOOM";
|
||||
private static final String NAME_ID = "NAME";
|
||||
private static final String SOURCES_ID = "SRC ";
|
||||
private static final String LINE_NUMBERS_ID = "LNUM";
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CachedData.class);
|
||||
|
||||
public CachedData {
|
||||
Objects.requireNonNull(className, "className");
|
||||
Objects.requireNonNull(sources, "sources");
|
||||
|
||||
if (lineNumbers != null) {
|
||||
if (!className.equals(lineNumbers.className())) {
|
||||
throw new IllegalArgumentException("Class name does not match line numbers class name");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void write(FileChannel fileChannel) {
|
||||
try (var c = new RiffChunk(HEADER_ID, fileChannel)) {
|
||||
writeClassname(fileChannel);
|
||||
writeSource(fileChannel);
|
||||
|
||||
if (lineNumbers != null) {
|
||||
writeLineNumbers(fileChannel);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to write cached data", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void writeClassname(FileChannel fileChannel) throws IOException {
|
||||
try (var c = new RiffChunk(NAME_ID, fileChannel)) {
|
||||
fileChannel.write(ByteBuffer.wrap(className.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeSource(FileChannel fileChannel) throws IOException {
|
||||
try (var c = new RiffChunk(SOURCES_ID, fileChannel)) {
|
||||
fileChannel.write(ByteBuffer.wrap(sources.getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
private void writeLineNumbers(FileChannel fileChannel) throws IOException {
|
||||
Objects.requireNonNull(lineNumbers);
|
||||
|
||||
try (var c = new RiffChunk(LINE_NUMBERS_ID, fileChannel);
|
||||
StringWriter stringWriter = new StringWriter()) {
|
||||
lineNumbers.write(stringWriter);
|
||||
fileChannel.write(ByteBuffer.wrap(stringWriter.toString().getBytes(StandardCharsets.UTF_8)));
|
||||
}
|
||||
}
|
||||
|
||||
public static CachedData read(InputStream inputStream) throws IOException {
|
||||
// Read and validate the RIFF header
|
||||
final String header = readHeader(inputStream);
|
||||
|
||||
if (!header.equals(HEADER_ID)) {
|
||||
throw new IOException("Invalid RIFF header: " + header + ", expected " + HEADER_ID);
|
||||
}
|
||||
|
||||
// Read the data length
|
||||
int length = readInt(inputStream);
|
||||
|
||||
String className = null;
|
||||
String sources = null;
|
||||
ClassLineNumbers.Entry lineNumbers = null;
|
||||
|
||||
while (inputStream.available() > 0) {
|
||||
String chunkHeader = readHeader(inputStream);
|
||||
int chunkLength = readInt(inputStream);
|
||||
byte[] chunkData = readBytes(inputStream, chunkLength);
|
||||
|
||||
switch (chunkHeader) {
|
||||
case NAME_ID -> {
|
||||
if (className != null) {
|
||||
throw new IOException("Duplicate name chunk");
|
||||
}
|
||||
|
||||
className = new String(chunkData, StandardCharsets.UTF_8);
|
||||
}
|
||||
case SOURCES_ID -> {
|
||||
if (sources != null) {
|
||||
throw new IOException("Duplicate sources chunk");
|
||||
}
|
||||
|
||||
sources = new String(chunkData, StandardCharsets.UTF_8);
|
||||
}
|
||||
case LINE_NUMBERS_ID -> {
|
||||
if (lineNumbers != null) {
|
||||
throw new IOException("Duplicate line numbers chunk");
|
||||
}
|
||||
|
||||
try (var br = new BufferedReader(new InputStreamReader(new ByteArrayInputStream(chunkData), StandardCharsets.UTF_8))) {
|
||||
ClassLineNumbers classLineNumbers = ClassLineNumbers.readMappings(br);
|
||||
|
||||
if (classLineNumbers.lineMap().size() != 1) {
|
||||
throw new IOException("Expected exactly one class line numbers entry got " + classLineNumbers.lineMap().size() + " entries");
|
||||
}
|
||||
|
||||
lineNumbers = classLineNumbers.lineMap().values().iterator().next();
|
||||
}
|
||||
}
|
||||
default -> {
|
||||
// Skip unknown chunk
|
||||
LOGGER.warn("Skipping unknown chunk: {} of size {}", chunkHeader, chunkLength);
|
||||
inputStream.skip(chunkLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (sources == null) {
|
||||
throw new IOException("Missing sources");
|
||||
}
|
||||
|
||||
return new CachedData(className, sources, lineNumbers);
|
||||
}
|
||||
|
||||
private static String readHeader(InputStream inputStream) throws IOException {
|
||||
byte[] header = readBytes(inputStream, 4);
|
||||
return new String(header, StandardCharsets.US_ASCII);
|
||||
}
|
||||
|
||||
private static int readInt(InputStream inputStream) throws IOException {
|
||||
byte[] bytes = readBytes(inputStream, 4);
|
||||
return ByteBuffer.wrap(bytes).getInt();
|
||||
}
|
||||
|
||||
private static byte[] readBytes(InputStream inputStream, int length) throws IOException {
|
||||
byte[] bytes = new byte[length];
|
||||
|
||||
int read = inputStream.read(bytes);
|
||||
|
||||
if (read != length) {
|
||||
throw new IOException("Failed to read bytes expected " + length + " bytes but got " + read + " bytes");
|
||||
}
|
||||
|
||||
return bytes;
|
||||
}
|
||||
|
||||
static class EntrySerializer implements CachedFileStore.EntrySerializer<CachedData> {
|
||||
@Override
|
||||
public CachedData read(Path path) throws IOException {
|
||||
try (var inputStream = new BufferedInputStream(Files.newInputStream(path))) {
|
||||
return CachedData.read(inputStream);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(CachedData entry, Path path) throws IOException {
|
||||
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.CREATE_NEW, StandardOpenOption.WRITE)) {
|
||||
entry.write(fileChannel);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java
vendored
Normal file
42
src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public interface CachedFileStore<T> {
|
||||
@Nullable T getEntry(String key) throws IOException;
|
||||
|
||||
void putEntry(String key, T entry) throws IOException;
|
||||
|
||||
interface EntrySerializer<T> {
|
||||
T read(Path path) throws IOException;
|
||||
|
||||
void write(T entry, Path path) throws IOException;
|
||||
}
|
||||
}
|
||||
141
src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java
vendored
Normal file
141
src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java
vendored
Normal file
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
public record CachedFileStoreImpl<T>(Path root, EntrySerializer<T> entrySerializer, CacheRules cacheRules) implements CachedFileStore<T> {
|
||||
public CachedFileStoreImpl {
|
||||
Objects.requireNonNull(root, "root");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable T getEntry(String key) throws IOException {
|
||||
Path path = resolve(key);
|
||||
|
||||
if (Files.notExists(path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Update last modified, so recently used files stay in the cache
|
||||
Files.setLastModifiedTime(path, FileTime.from(Instant.now()));
|
||||
return entrySerializer.read(path);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putEntry(String key, T data) throws IOException {
|
||||
Path path = resolve(key);
|
||||
Files.createDirectories(path.getParent());
|
||||
entrySerializer.write(data, path);
|
||||
}
|
||||
|
||||
private Path resolve(String key) {
|
||||
return root.resolve(key);
|
||||
}
|
||||
|
||||
public void prune() throws IOException {
|
||||
// Sorted oldest -> newest
|
||||
List<PathEntry> entries = new ArrayList<>();
|
||||
|
||||
// Iterate over all the files in the cache, and store them into the sorted list.
|
||||
try (Stream<Path> walk = Files.walk(root)) {
|
||||
Iterator<Path> iterator = walk.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
final Path entry = iterator.next();
|
||||
|
||||
if (!Files.isRegularFile(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
insertSorted(entries, new PathEntry(entry));
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the oldest files to get under the max file limit
|
||||
if (entries.size() > cacheRules.maxFiles) {
|
||||
for (int i = 0; i < cacheRules.maxFiles; i++) {
|
||||
PathEntry toRemove = entries.remove(0);
|
||||
Files.delete(toRemove.path);
|
||||
}
|
||||
}
|
||||
|
||||
final Instant maxAge = Instant.now().minus(cacheRules().maxAge());
|
||||
Iterator<PathEntry> iterator = entries.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
final PathEntry entry = iterator.next();
|
||||
|
||||
if (entry.lastModified().toInstant().isAfter(maxAge)) {
|
||||
// File is not longer than the max age
|
||||
// As this is a sorted list we don't need to keep checking
|
||||
break;
|
||||
}
|
||||
|
||||
// Remove all files over the max age
|
||||
iterator.remove();
|
||||
Files.delete(entry.path);
|
||||
}
|
||||
}
|
||||
|
||||
private void insertSorted(List<PathEntry> list, PathEntry entry) {
|
||||
int index = Collections.binarySearch(list, entry, Comparator.comparing(PathEntry::lastModified));
|
||||
|
||||
if (index < 0) {
|
||||
index = -index - 1;
|
||||
}
|
||||
|
||||
list.add(index, entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* The rules for the cache.
|
||||
*
|
||||
* @param maxFiles The maximum number of files in the cache
|
||||
* @param maxAge The maximum age of a file in the cache
|
||||
*/
|
||||
public record CacheRules(long maxFiles, Duration maxAge) {
|
||||
}
|
||||
|
||||
record PathEntry(Path path, FileTime lastModified) {
|
||||
PathEntry(Path path) throws IOException {
|
||||
this(path, Files.getLastModifiedTime(path));
|
||||
}
|
||||
}
|
||||
}
|
||||
268
src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java
vendored
Normal file
268
src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java
vendored
Normal file
@@ -0,0 +1,268 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.decompilers.ClassLineNumbers;
|
||||
import net.fabricmc.loom.util.FileSystemUtil;
|
||||
|
||||
public record CachedJarProcessor(CachedFileStore<CachedData> fileStore, String baseHash) {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(CachedJarProcessor.class);
|
||||
|
||||
public WorkRequest prepareJob(Path inputJar) throws IOException {
|
||||
boolean isIncomplete = false;
|
||||
boolean hasSomeExisting = false;
|
||||
|
||||
Path incompleteJar = Files.createTempFile("loom-cache-incomplete", ".jar");
|
||||
Path existingJar = Files.createTempFile("loom-cache-existing", ".jar");
|
||||
|
||||
// We must delete the empty files, so they can be created as a zip
|
||||
Files.delete(incompleteJar);
|
||||
Files.delete(existingJar);
|
||||
|
||||
// Sources name -> hash
|
||||
Map<String, String> outputNameMap = new HashMap<>();
|
||||
Map<String, ClassLineNumbers.Entry> lineNumbersMap = new HashMap<>();
|
||||
|
||||
int hits = 0;
|
||||
int misses = 0;
|
||||
|
||||
try (FileSystemUtil.Delegate inputFs = FileSystemUtil.getJarFileSystem(inputJar, false);
|
||||
FileSystemUtil.Delegate incompleteFs = FileSystemUtil.getJarFileSystem(incompleteJar, true);
|
||||
FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(existingJar, true)) {
|
||||
final List<ClassEntry> inputClasses = JarWalker.findClasses(inputFs);
|
||||
|
||||
for (ClassEntry entry : inputClasses) {
|
||||
String outputFileName = entry.sourcesFileName();
|
||||
String fullHash = baseHash + "/" + entry.hash(inputFs.getRoot());
|
||||
|
||||
final CachedData entryData = fileStore.getEntry(fullHash);
|
||||
|
||||
if (entryData == null) {
|
||||
// Cached entry was not found, so copy the input to the incomplete jar to be processed
|
||||
entry.copyTo(inputFs.getRoot(), incompleteFs.getRoot());
|
||||
isIncomplete = true;
|
||||
outputNameMap.put(outputFileName, fullHash);
|
||||
|
||||
LOGGER.debug("Cached entry ({}) not found, going to process {}", fullHash, outputFileName);
|
||||
misses++;
|
||||
} else {
|
||||
final Path outputPath = existingFs.getPath(outputFileName);
|
||||
Files.createDirectories(outputPath.getParent());
|
||||
Files.writeString(outputPath, entryData.sources());
|
||||
lineNumbersMap.put(entryData.className(), entryData.lineNumbers());
|
||||
hasSomeExisting = true;
|
||||
|
||||
LOGGER.debug("Cached entry ({}) found: {}", fullHash, outputFileName);
|
||||
hits++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A jar file that will be created by the work action, containing the newly processed items.
|
||||
Path outputJar = Files.createTempFile("loom-cache-output", ".jar");
|
||||
Files.delete(outputJar);
|
||||
|
||||
final ClassLineNumbers lineNumbers = lineNumbersMap.isEmpty() ? null : new ClassLineNumbers(Collections.unmodifiableMap(lineNumbersMap));
|
||||
final var stats = new CacheStats(hits, misses);
|
||||
|
||||
if (isIncomplete && !hasSomeExisting) {
|
||||
// The cache contained nothing of use, fully process the input jar
|
||||
Files.delete(incompleteJar);
|
||||
Files.delete(existingJar);
|
||||
|
||||
LOGGER.info("No cached entries found, going to process the whole jar");
|
||||
return new FullWorkJob(inputJar, outputJar, outputNameMap)
|
||||
.asRequest(stats, lineNumbers);
|
||||
} else if (isIncomplete) {
|
||||
// The cache did not contain everything so we have some work to do
|
||||
LOGGER.info("Some cached entries found, using partial work job");
|
||||
return new PartialWorkJob(incompleteJar, existingJar, outputJar, outputNameMap)
|
||||
.asRequest(stats, lineNumbers);
|
||||
} else {
|
||||
// The cached contained everything we need, so the existing jar is the output
|
||||
LOGGER.info("All cached entries found, using completed work job");
|
||||
Files.delete(incompleteJar);
|
||||
return new CompletedWorkJob(existingJar)
|
||||
.asRequest(stats, lineNumbers);
|
||||
}
|
||||
}
|
||||
|
||||
public void completeJob(Path output, WorkJob workJob, ClassLineNumbers lineNumbers) throws IOException {
|
||||
if (workJob instanceof CompletedWorkJob completedWorkJob) {
|
||||
// Fully complete, nothing new to cache
|
||||
Files.move(completedWorkJob.completed(), output);
|
||||
return;
|
||||
}
|
||||
|
||||
// Work has been done, we need to cache the newly processed items
|
||||
if (workJob instanceof WorkToDoJob workToDoJob) {
|
||||
// Sources name -> hash
|
||||
Map<String, String> outputNameMap = workToDoJob.outputNameMap();
|
||||
|
||||
try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(workToDoJob.output(), false);
|
||||
Stream<Path> walk = Files.walk(outputFs.getRoot())) {
|
||||
Iterator<Path> iterator = walk.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
final Path fsPath = iterator.next();
|
||||
|
||||
if (fsPath.startsWith("/META-INF/")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!Files.isRegularFile(fsPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String hash = outputNameMap.get(fsPath.toString().substring(outputFs.getRoot().toString().length()));
|
||||
|
||||
if (hash == null) {
|
||||
throw new IllegalStateException("Unexpected output: " + fsPath);
|
||||
}
|
||||
|
||||
// Trim the leading / and the .java extension
|
||||
final String className = fsPath.toString().substring(1, fsPath.toString().length() - ".java".length());
|
||||
final String sources = Files.readString(fsPath);
|
||||
|
||||
ClassLineNumbers.Entry lineMapEntry = null;
|
||||
|
||||
if (lineNumbers != null) {
|
||||
lineMapEntry = lineNumbers.lineMap().get(className);
|
||||
}
|
||||
|
||||
final var cachedData = new CachedData(className, sources, lineMapEntry);
|
||||
fileStore.putEntry(hash, cachedData);
|
||||
|
||||
LOGGER.debug("Saving processed entry ({}) to cache: {}", hash, fsPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
if (workJob instanceof PartialWorkJob partialWorkJob) {
|
||||
// Copy all the existing items to the output jar
|
||||
try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(partialWorkJob.output(), false);
|
||||
FileSystemUtil.Delegate existingFs = FileSystemUtil.getJarFileSystem(partialWorkJob.existing(), false);
|
||||
Stream<Path> walk = Files.walk(existingFs.getRoot())) {
|
||||
Iterator<Path> iterator = walk.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Path existingPath = iterator.next();
|
||||
|
||||
if (!Files.isRegularFile(existingPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final Path outputPath = outputFs.getRoot().resolve(existingPath.toString());
|
||||
|
||||
LOGGER.debug("Copying existing entry to output: {}", existingPath);
|
||||
Files.createDirectories(outputPath.getParent());
|
||||
Files.copy(existingPath, outputPath);
|
||||
}
|
||||
}
|
||||
|
||||
Files.delete(partialWorkJob.existing());
|
||||
Files.move(partialWorkJob.output(), output);
|
||||
} else if (workJob instanceof FullWorkJob fullWorkJob) {
|
||||
// Nothing to merge, just use the output jar
|
||||
Files.move(fullWorkJob.output, output);
|
||||
} else {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
public record WorkRequest(WorkJob job, CacheStats stats, @Nullable ClassLineNumbers lineNumbers) {
|
||||
}
|
||||
|
||||
public record CacheStats(int hits, int misses) {
|
||||
}
|
||||
|
||||
public sealed interface WorkJob permits CompletedWorkJob, WorkToDoJob {
|
||||
default WorkRequest asRequest(CacheStats stats, @Nullable ClassLineNumbers lineNumbers) {
|
||||
return new WorkRequest(this, stats, lineNumbers);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed interface WorkToDoJob extends WorkJob permits PartialWorkJob, FullWorkJob {
|
||||
/**
|
||||
* A path to jar file containing all the classes to be processed.
|
||||
*/
|
||||
Path incomplete();
|
||||
|
||||
/**
|
||||
* @return A jar file to be written to during processing
|
||||
*/
|
||||
Path output();
|
||||
|
||||
/**
|
||||
* @return A map of sources name to hash
|
||||
*/
|
||||
Map<String, String> outputNameMap();
|
||||
}
|
||||
|
||||
/**
|
||||
* No work to be done, all restored from cache.
|
||||
*
|
||||
* @param completed
|
||||
*/
|
||||
public record CompletedWorkJob(Path completed) implements WorkJob {
|
||||
}
|
||||
|
||||
/**
|
||||
* Some work needs to be done.
|
||||
*
|
||||
* @param incomplete A path to jar file containing all the classes to be processed
|
||||
* @param existing A path pointing to a jar containing existing classes that have previously been processed
|
||||
* @param output A path to a temporary jar where work output should be written to
|
||||
* @param outputNameMap A map of sources name to hash
|
||||
*/
|
||||
public record PartialWorkJob(Path incomplete, Path existing, Path output, Map<String, String> outputNameMap) implements WorkToDoJob {
|
||||
}
|
||||
|
||||
/**
|
||||
* The full jar must be processed.
|
||||
*
|
||||
* @param incomplete A path to jar file containing all the classes to be processed
|
||||
* @param output A path to a temporary jar where work output should be written to
|
||||
* @param outputNameMap A map of sources name to hash
|
||||
*/
|
||||
public record FullWorkJob(Path incomplete, Path output, Map<String, String> outputNameMap) implements WorkToDoJob {
|
||||
}
|
||||
}
|
||||
75
src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java
vendored
Normal file
75
src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java
vendored
Normal file
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
import net.fabricmc.loom.util.Checksum;
|
||||
|
||||
public record ClassEntry(String parentClass, List<String> innerClasses) {
|
||||
/**
|
||||
* Copy the class and its inner classes to the target root.
|
||||
* @param sourceRoot The root of the source jar
|
||||
* @param targetRoot The root of the target jar
|
||||
*
|
||||
* @throws IOException If an error occurs while copying the files
|
||||
*/
|
||||
public void copyTo(Path sourceRoot, Path targetRoot) throws IOException {
|
||||
Path targetPath = targetRoot.resolve(parentClass);
|
||||
Files.createDirectories(targetPath.getParent());
|
||||
Files.copy(sourceRoot.resolve(parentClass), targetPath);
|
||||
|
||||
for (String innerClass : innerClasses) {
|
||||
Files.copy(sourceRoot.resolve(innerClass), targetRoot.resolve(innerClass));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hash the class and its inner classes using sha256.
|
||||
* @param root The root of the jar
|
||||
* @return The hash of the class and its inner classes
|
||||
*
|
||||
* @throws IOException If an error occurs while hashing the files
|
||||
*/
|
||||
public String hash(Path root) throws IOException {
|
||||
StringJoiner joiner = new StringJoiner(",");
|
||||
|
||||
joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(parentClass))));
|
||||
|
||||
for (String innerClass : innerClasses) {
|
||||
joiner.add(Checksum.sha256Hex(Files.readAllBytes(root.resolve(innerClass))));
|
||||
}
|
||||
|
||||
return Checksum.sha256Hex(joiner.toString().getBytes());
|
||||
}
|
||||
|
||||
public String sourcesFileName() {
|
||||
return parentClass.replace(".class", ".java");
|
||||
}
|
||||
}
|
||||
108
src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java
vendored
Normal file
108
src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import net.fabricmc.loom.util.FileSystemUtil;
|
||||
|
||||
public final class JarWalker {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(JarWalker.class);
|
||||
|
||||
private JarWalker() {
|
||||
}
|
||||
|
||||
public static List<ClassEntry> findClasses(Path jar) throws IOException {
|
||||
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar)) {
|
||||
return findClasses(fs);
|
||||
}
|
||||
}
|
||||
|
||||
public static List<ClassEntry> findClasses(FileSystemUtil.Delegate fs) throws IOException {
|
||||
List<String> outerClasses = new ArrayList<>();
|
||||
Map<String, List<String>> innerClasses = new HashMap<>();
|
||||
|
||||
// Iterate over all the classes in the jar, and store them into the sorted list.
|
||||
try (Stream<Path> walk = Files.walk(fs.getRoot())) {
|
||||
Iterator<Path> iterator = walk.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
final Path entry = iterator.next();
|
||||
|
||||
if (!Files.isRegularFile(entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final String fileName = entry.toString().substring(fs.getRoot().toString().length());
|
||||
|
||||
if (!fileName.endsWith(".class")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean isInnerClass = fileName.contains("$");
|
||||
|
||||
if (isInnerClass) {
|
||||
String outerClassName = fileName.substring(0, fileName.indexOf('$')) + ".class";
|
||||
innerClasses.computeIfAbsent(outerClassName, k -> new ArrayList<>()).add(fileName);
|
||||
} else {
|
||||
outerClasses.add(fileName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LOGGER.info("Found {} outer classes and {} inner classes", outerClasses.size(), innerClasses.size());
|
||||
|
||||
Collections.sort(outerClasses);
|
||||
|
||||
List<ClassEntry> classEntries = new ArrayList<>();
|
||||
|
||||
for (String outerClass : outerClasses) {
|
||||
List<String> innerClasList = innerClasses.get(outerClass);
|
||||
|
||||
if (innerClasList == null) {
|
||||
innerClasList = Collections.emptyList();
|
||||
} else {
|
||||
Collections.sort(innerClasList);
|
||||
}
|
||||
|
||||
ClassEntry classEntry = new ClassEntry(outerClass, Collections.unmodifiableList(innerClasList));
|
||||
classEntries.add(classEntry);
|
||||
}
|
||||
|
||||
return Collections.unmodifiableList(classEntries);
|
||||
}
|
||||
}
|
||||
69
src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java
vendored
Normal file
69
src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java
vendored
Normal file
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
/**
|
||||
* Write a RIFF chunk to a file channel
|
||||
*
|
||||
* <p>Works by writing the chunk header and then reserving space for the chunk size.
|
||||
* The chunk size is then written after the chunk data has been written.
|
||||
*/
|
||||
public class RiffChunk implements AutoCloseable {
|
||||
private final long position;
|
||||
private final FileChannel fileChannel;
|
||||
|
||||
public RiffChunk(String id, FileChannel fileChannel) throws IOException {
|
||||
if (id.length() != 4) {
|
||||
throw new IllegalArgumentException("ID must be 4 characters long");
|
||||
}
|
||||
|
||||
// Write the chunk header and reserve space for the chunk size
|
||||
fileChannel.write(ByteBuffer.wrap(id.getBytes(StandardCharsets.US_ASCII)));
|
||||
this.position = fileChannel.position();
|
||||
fileChannel.write(ByteBuffer.allocate(4));
|
||||
|
||||
// Store the position and file channel for later use
|
||||
this.fileChannel = fileChannel;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
long endPosition = fileChannel.position();
|
||||
long chunkSize = endPosition - position - 4;
|
||||
|
||||
if (chunkSize > Integer.MAX_VALUE) {
|
||||
throw new IOException("Chunk size is too large");
|
||||
}
|
||||
|
||||
fileChannel.position(position);
|
||||
fileChannel.write(ByteBuffer.allocate(Integer.BYTES).putInt((int) (chunkSize)).flip());
|
||||
fileChannel.position(endPosition);
|
||||
}
|
||||
}
|
||||
@@ -50,4 +50,5 @@ public interface LoomFiles {
|
||||
File getRemapClasspathFile();
|
||||
File getGlobalMinecraftRepo();
|
||||
File getLocalMinecraftRepo();
|
||||
File getDecompileCache(String version);
|
||||
}
|
||||
|
||||
@@ -107,4 +107,9 @@ public abstract class LoomFilesBaseImpl implements LoomFiles {
|
||||
public File getLocalMinecraftRepo() {
|
||||
return new File(getRootProjectPersistentCache(), "minecraftMaven");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDecompileCache(String version) {
|
||||
return new File(getUserCache(), "decompile/" + version + ".zip");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
|
||||
package net.fabricmc.loom.task;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
@@ -36,26 +37,33 @@ import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.StringJoiner;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import org.gradle.api.file.ConfigurableFileCollection;
|
||||
import org.gradle.api.file.FileCollection;
|
||||
import org.gradle.api.file.RegularFileProperty;
|
||||
import org.gradle.api.provider.Property;
|
||||
import org.gradle.api.services.ServiceReference;
|
||||
import org.gradle.api.tasks.Input;
|
||||
import org.gradle.api.tasks.InputFile;
|
||||
import org.gradle.api.tasks.InputFiles;
|
||||
import org.gradle.api.tasks.Internal;
|
||||
import org.gradle.api.tasks.Optional;
|
||||
import org.gradle.api.tasks.OutputFile;
|
||||
import org.gradle.api.tasks.TaskAction;
|
||||
import org.gradle.api.tasks.options.Option;
|
||||
import org.gradle.process.ExecOperations;
|
||||
import org.gradle.process.ExecResult;
|
||||
import org.gradle.work.DisableCachingByDefault;
|
||||
@@ -64,8 +72,12 @@ 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.ApiStatus;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
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.DecompilerOptions;
|
||||
import net.fabricmc.loom.api.decompilers.LoomDecompiler;
|
||||
@@ -75,7 +87,12 @@ import net.fabricmc.loom.configuration.processors.MappingProcessorContextImpl;
|
||||
import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar;
|
||||
import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider;
|
||||
import net.fabricmc.loom.decompilers.ClassLineNumbers;
|
||||
import net.fabricmc.loom.decompilers.LineNumberRemapper;
|
||||
import net.fabricmc.loom.decompilers.cache.CachedData;
|
||||
import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl;
|
||||
import net.fabricmc.loom.decompilers.cache.CachedJarProcessor;
|
||||
import net.fabricmc.loom.util.Checksum;
|
||||
import net.fabricmc.loom.util.Constants;
|
||||
import net.fabricmc.loom.util.ExceptionUtil;
|
||||
import net.fabricmc.loom.util.FileSystemUtil;
|
||||
@@ -95,6 +112,8 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree;
|
||||
|
||||
@DisableCachingByDefault
|
||||
public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(GenerateSourcesTask.class);
|
||||
private static final String CACHE_VERSION = "v1";
|
||||
private final DecompilerOptions decompilerOptions;
|
||||
|
||||
/**
|
||||
@@ -122,10 +141,25 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
@Optional
|
||||
public abstract ConfigurableFileCollection getUnpickClasspath();
|
||||
|
||||
@InputFiles
|
||||
@Optional
|
||||
@ApiStatus.Internal
|
||||
public abstract ConfigurableFileCollection getUnpickRuntimeClasspath();
|
||||
|
||||
@OutputFile
|
||||
@Optional
|
||||
public abstract RegularFileProperty getUnpickOutputJar();
|
||||
|
||||
@Input
|
||||
@Option(option = "use-cache", description = "Use the decompile cache")
|
||||
@ApiStatus.Experimental
|
||||
public abstract Property<Boolean> getUseCache();
|
||||
|
||||
// Internal outputs
|
||||
@ApiStatus.Internal
|
||||
@Internal
|
||||
protected abstract RegularFileProperty getDecompileCacheFile();
|
||||
|
||||
// Injects
|
||||
@Inject
|
||||
public abstract WorkerExecutor getWorkerExecutor();
|
||||
@@ -147,6 +181,12 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
getOutputs().upToDateWhen((o) -> false);
|
||||
getClasspath().from(decompilerOptions.getClasspath()).finalizeValueOnRead();
|
||||
dependsOn(decompilerOptions.getClasspath().getBuiltBy());
|
||||
|
||||
LoomGradleExtension extension = LoomGradleExtension.get(getProject());
|
||||
getDecompileCacheFile().set(extension.getFiles().getDecompileCache(CACHE_VERSION));
|
||||
getUnpickRuntimeClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH));
|
||||
|
||||
getUseCache().convention(false);
|
||||
}
|
||||
|
||||
@TaskAction
|
||||
@@ -157,21 +197,195 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
throw new UnsupportedOperationException("GenSources task requires a 64bit JVM to run due to the memory requirements.");
|
||||
}
|
||||
|
||||
if (!getUseCache().get()) {
|
||||
try (var timer = new Timer("Decompiled sources")) {
|
||||
runWithoutCache();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.warn("Using decompile cache is experimental and may not work as expected.");
|
||||
|
||||
try (var timer = new Timer("Decompiled sources with cache")) {
|
||||
final Path cacheFile = getDecompileCacheFile().getAsFile().get().toPath();
|
||||
|
||||
// TODO ensure we have a lock on this file to prevent multiple tasks from running at the same time
|
||||
// TODO handle being unable to read the cache file
|
||||
Files.createDirectories(cacheFile.getParent());
|
||||
|
||||
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(cacheFile, true)) {
|
||||
runWithCache(fs.getRoot());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void runWithCache(Path cacheRoot) throws IOException {
|
||||
final MinecraftJar minecraftJar = rebuildInputJar();
|
||||
// Input jar is the jar to decompile, this may be unpicked.
|
||||
final var cacheRules = new CachedFileStoreImpl.CacheRules(50_000, Duration.ofDays(90));
|
||||
final var decompileCache = new CachedFileStoreImpl<>(cacheRoot, CachedData.SERIALIZER, cacheRules);
|
||||
final String cacheKey = getCacheKey();
|
||||
final CachedJarProcessor cachedJarProcessor = new CachedJarProcessor(decompileCache, cacheKey);
|
||||
final CachedJarProcessor.WorkRequest workRequest;
|
||||
|
||||
LOGGER.info("Decompile cache key: {}", cacheKey);
|
||||
|
||||
try (var timer = new Timer("Prepare job")) {
|
||||
workRequest = cachedJarProcessor.prepareJob(minecraftJar.getPath());
|
||||
}
|
||||
|
||||
final CachedJarProcessor.WorkJob job = workRequest.job();
|
||||
final CachedJarProcessor.CacheStats cacheStats = workRequest.stats();
|
||||
|
||||
getProject().getLogger().lifecycle("Decompiling: Cache stats: {} hits, {} misses", cacheStats.hits(), cacheStats.misses());
|
||||
|
||||
ClassLineNumbers outputLineNumbers = null;
|
||||
|
||||
if (job instanceof CachedJarProcessor.WorkToDoJob workToDoJob) {
|
||||
Path inputJar = workToDoJob.incomplete();
|
||||
@Nullable Path existing = (job instanceof CachedJarProcessor.PartialWorkJob partialWorkJob) ? partialWorkJob.existing() : null;
|
||||
|
||||
if (getUnpickDefinitions().isPresent()) {
|
||||
try (var timer = new Timer("Unpick")) {
|
||||
inputJar = unpickJar(inputJar, existing);
|
||||
}
|
||||
}
|
||||
|
||||
try (var timer = new Timer("Decompile")) {
|
||||
outputLineNumbers = runDecompileJob(inputJar, workToDoJob.output(), existing);
|
||||
}
|
||||
|
||||
if (Files.notExists(workToDoJob.output())) {
|
||||
throw new RuntimeException("Failed to decompile sources");
|
||||
}
|
||||
} else if (job instanceof CachedJarProcessor.CompletedWorkJob completedWorkJob) {
|
||||
// Nothing to do :)
|
||||
}
|
||||
|
||||
// The final output sources jar
|
||||
final Path sourcesJar = getOutputJar().get().getAsFile().toPath();
|
||||
Files.deleteIfExists(sourcesJar);
|
||||
|
||||
try (var timer = new Timer("Complete job")) {
|
||||
cachedJarProcessor.completeJob(sourcesJar, job, outputLineNumbers);
|
||||
}
|
||||
|
||||
// This is the minecraft jar used at runtime.
|
||||
final Path classesJar = minecraftJar.getPath();
|
||||
|
||||
// Remap the line numbers with the new and existing numbers
|
||||
final ClassLineNumbers existingLinenumbers = workRequest.lineNumbers();
|
||||
final ClassLineNumbers lineNumbers = ClassLineNumbers.merge(existingLinenumbers, outputLineNumbers);
|
||||
|
||||
if (lineNumbers == null) {
|
||||
LOGGER.info("No line numbers to remap, skipping remapping");
|
||||
return;
|
||||
}
|
||||
|
||||
Path tempJar = Files.createTempFile("loom", "linenumber-remap.jar");
|
||||
Files.delete(tempJar);
|
||||
|
||||
try (var timer = new Timer("Remap line numbers")) {
|
||||
remapLineNumbers(lineNumbers, classesJar, tempJar);
|
||||
}
|
||||
|
||||
Files.move(tempJar, classesJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
|
||||
try (var timer = new Timer("Prune cache")) {
|
||||
decompileCache.prune();
|
||||
}
|
||||
}
|
||||
|
||||
private void runWithoutCache() throws IOException {
|
||||
final MinecraftJar minecraftJar = rebuildInputJar();
|
||||
|
||||
Path inputJar = minecraftJar.getPath();
|
||||
// Runtime jar is the jar used to run the game
|
||||
final Path runtimeJar = inputJar;
|
||||
// The final output sources jar
|
||||
final Path sourcesJar = getOutputJar().get().getAsFile().toPath();
|
||||
|
||||
if (getUnpickDefinitions().isPresent()) {
|
||||
inputJar = unpickJar(inputJar);
|
||||
try (var timer = new Timer("Unpick")) {
|
||||
inputJar = unpickJar(inputJar, null);
|
||||
}
|
||||
}
|
||||
|
||||
ClassLineNumbers lineNumbers;
|
||||
|
||||
try (var timer = new Timer("Decompile")) {
|
||||
lineNumbers = runDecompileJob(inputJar, sourcesJar, null);
|
||||
}
|
||||
|
||||
if (Files.notExists(sourcesJar)) {
|
||||
throw new RuntimeException("Failed to decompile sources");
|
||||
}
|
||||
|
||||
if (lineNumbers == null) {
|
||||
LOGGER.info("No line numbers to remap, skipping remapping");
|
||||
return;
|
||||
}
|
||||
|
||||
// This is the minecraft jar used at runtime.
|
||||
final Path classesJar = minecraftJar.getPath();
|
||||
final Path tempJar = Files.createTempFile("loom", "linenumber-remap.jar");
|
||||
Files.delete(tempJar);
|
||||
|
||||
try (var timer = new Timer("Remap line numbers")) {
|
||||
remapLineNumbers(lineNumbers, classesJar, tempJar);
|
||||
}
|
||||
|
||||
Files.move(tempJar, classesJar, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
private String getCacheKey() {
|
||||
var sj = new StringJoiner(",");
|
||||
sj.add(getDecompilerCheckKey());
|
||||
sj.add(getUnpickCacheKey());
|
||||
|
||||
LOGGER.info("Decompile cache data: {}", sj);
|
||||
|
||||
try {
|
||||
return Checksum.sha256Hex(sj.toString().getBytes(StandardCharsets.UTF_8));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getDecompilerCheckKey() {
|
||||
var sj = new StringJoiner(",");
|
||||
sj.add(decompilerOptions.getDecompilerClassName().get());
|
||||
sj.add(fileCollectionHash(decompilerOptions.getClasspath()));
|
||||
|
||||
for (Map.Entry<String, String> entry : decompilerOptions.getOptions().get().entrySet()) {
|
||||
sj.add(entry.getKey() + "=" + entry.getValue());
|
||||
}
|
||||
|
||||
return sj.toString();
|
||||
}
|
||||
|
||||
private String getUnpickCacheKey() {
|
||||
if (!getUnpickDefinitions().isPresent()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
var sj = new StringJoiner(",");
|
||||
sj.add(fileHash(getUnpickDefinitions().getAsFile().get()));
|
||||
sj.add(fileCollectionHash(getUnpickConstantJar()));
|
||||
sj.add(fileCollectionHash(getUnpickRuntimeClasspath()));
|
||||
|
||||
return sj.toString();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ClassLineNumbers runDecompileJob(Path inputJar, Path outputJar, @Nullable Path existingJar) throws IOException {
|
||||
final Platform platform = Platform.CURRENT;
|
||||
final Path lineMapFile = File.createTempFile("loom", "linemap").toPath();
|
||||
Files.delete(lineMapFile);
|
||||
|
||||
if (!platform.supportsUnixDomainSockets()) {
|
||||
getProject().getLogger().warn("Decompile worker logging disabled as Unix Domain Sockets is not supported on your operating system.");
|
||||
|
||||
doWork(null, inputJar, runtimeJar);
|
||||
return;
|
||||
doWork(null, inputJar, outputJar, lineMapFile, existingJar);
|
||||
return readLineNumbers(lineMapFile);
|
||||
}
|
||||
|
||||
// Set up the IPC path to get the log output back from the forked JVM
|
||||
@@ -180,12 +394,14 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
|
||||
try (ThreadedProgressLoggerConsumer loggerConsumer = new ThreadedProgressLoggerConsumer(getProject(), decompilerOptions.getName(), "Decompiling minecraft sources");
|
||||
IPCServer logReceiver = new IPCServer(ipcPath, loggerConsumer)) {
|
||||
doWork(logReceiver, inputJar, runtimeJar);
|
||||
doWork(logReceiver, inputJar, outputJar, lineMapFile, existingJar);
|
||||
} catch (InterruptedException e) {
|
||||
throw new RuntimeException("Failed to shutdown log receiver", e);
|
||||
} finally {
|
||||
Files.deleteIfExists(ipcPath);
|
||||
}
|
||||
|
||||
return readLineNumbers(lineMapFile);
|
||||
}
|
||||
|
||||
// Re-run the named minecraft provider to give us a fresh jar to decompile.
|
||||
@@ -214,13 +430,13 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
);
|
||||
}
|
||||
|
||||
private Path unpickJar(Path inputJar) {
|
||||
private Path unpickJar(Path inputJar, @Nullable Path existingJar) {
|
||||
final Path outputJar = getUnpickOutputJar().get().getAsFile().toPath();
|
||||
final List<String> args = getUnpickArgs(inputJar, outputJar);
|
||||
final List<String> args = getUnpickArgs(inputJar, outputJar, existingJar);
|
||||
|
||||
ExecResult result = getExecOperations().javaexec(spec -> {
|
||||
spec.getMainClass().set("daomephsta.unpick.cli.Main");
|
||||
spec.classpath(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH));
|
||||
spec.classpath(getUnpickRuntimeClasspath());
|
||||
spec.args(args);
|
||||
spec.systemProperty("java.util.logging.config.file", writeUnpickLogConfig().getAbsolutePath());
|
||||
});
|
||||
@@ -230,7 +446,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
return outputJar;
|
||||
}
|
||||
|
||||
private List<String> getUnpickArgs(Path inputJar, Path outputJar) {
|
||||
private List<String> getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingJar) {
|
||||
var fileArgs = new ArrayList<File>();
|
||||
|
||||
fileArgs.add(inputJar.toFile());
|
||||
@@ -247,6 +463,10 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
fileArgs.add(file);
|
||||
}
|
||||
|
||||
if (existingJar != null) {
|
||||
fileArgs.add(existingJar.toFile());
|
||||
}
|
||||
|
||||
return fileArgs.stream()
|
||||
.map(File::getAbsolutePath)
|
||||
.toList();
|
||||
@@ -265,25 +485,40 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
return unpickLoggingConfigFile;
|
||||
}
|
||||
|
||||
private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path runtimeJar) {
|
||||
private void remapLineNumbers(ClassLineNumbers lineNumbers, Path inputJar, Path outputJar) throws IOException {
|
||||
Objects.requireNonNull(lineNumbers, "lineNumbers");
|
||||
final var remapper = new LineNumberRemapper(lineNumbers);
|
||||
|
||||
try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(inputJar, false);
|
||||
FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(outputJar, true)) {
|
||||
remapper.process(inFs.get().getPath("/"), outFs.get().getPath("/"));
|
||||
}
|
||||
}
|
||||
|
||||
private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path outputJar, Path linemapFile, @Nullable Path existingJar) {
|
||||
final String jvmMarkerValue = UUID.randomUUID().toString();
|
||||
final WorkQueue workQueue = createWorkQueue(jvmMarkerValue);
|
||||
|
||||
ConfigurableFileCollection classpath = getProject().files();
|
||||
classpath.from(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
|
||||
|
||||
if (existingJar != null) {
|
||||
classpath.from(existingJar);
|
||||
}
|
||||
|
||||
workQueue.submit(DecompileAction.class, params -> {
|
||||
params.getDecompilerOptions().set(decompilerOptions.toDto());
|
||||
|
||||
params.getInputJar().set(inputJar.toFile());
|
||||
params.getRuntimeJar().set(runtimeJar.toFile());
|
||||
params.getSourcesDestinationJar().set(getOutputJar());
|
||||
params.getLinemap().set(getMappedJarFileWithSuffix("-sources.lmap", runtimeJar));
|
||||
params.getLinemapJar().set(getMappedJarFileWithSuffix("-linemapped.jar", runtimeJar));
|
||||
params.getOutputJar().set(outputJar.toFile());
|
||||
params.getLinemapFile().set(linemapFile.toFile());
|
||||
params.getMappings().set(getMappings().toFile());
|
||||
|
||||
if (ipcServer != null) {
|
||||
params.getIPCPath().set(ipcServer.getPath().toFile());
|
||||
}
|
||||
|
||||
params.getClassPath().setFrom(getProject().getConfigurations().getByName(Constants.Configurations.MINECRAFT_COMPILE_LIBRARIES));
|
||||
params.getClassPath().setFrom(classpath);
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -325,10 +560,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
Property<DecompilerOptions.Dto> getDecompilerOptions();
|
||||
|
||||
RegularFileProperty getInputJar();
|
||||
RegularFileProperty getRuntimeJar();
|
||||
RegularFileProperty getSourcesDestinationJar();
|
||||
RegularFileProperty getLinemap();
|
||||
RegularFileProperty getLinemapJar();
|
||||
RegularFileProperty getOutputJar();
|
||||
RegularFileProperty getLinemapFile();
|
||||
RegularFileProperty getMappings();
|
||||
|
||||
RegularFileProperty getIPCPath();
|
||||
@@ -356,10 +589,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
|
||||
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 Path linemap = getParameters().getLinemapFile().get().getAsFile().toPath();
|
||||
final Path outputJar = getParameters().getOutputJar().get().getAsFile().toPath();
|
||||
|
||||
final DecompilerOptions.Dto decompilerOptions = getParameters().getDecompilerOptions().get();
|
||||
|
||||
@@ -375,7 +606,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
throw new RuntimeException("Failed to create decompiler", e);
|
||||
}
|
||||
|
||||
DecompilationMetadata metadata = new DecompilationMetadata(
|
||||
final var metadata = new DecompilationMetadata(
|
||||
decompilerOptions.maxThreads(),
|
||||
getParameters().getMappings().get().getAsFile().toPath(),
|
||||
getLibraries(),
|
||||
@@ -385,7 +616,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
|
||||
decompiler.decompile(
|
||||
inputJar,
|
||||
sourcesDestinationJar,
|
||||
outputJar,
|
||||
linemap,
|
||||
metadata
|
||||
);
|
||||
@@ -396,28 +627,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
} 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 (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(oldCompiledJar.toFile(), true);
|
||||
FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(linemappedJarDestination.toFile(), true)) {
|
||||
remapper.process(logger, inFs.get().getPath("/"), outFs.get().getPath("/"));
|
||||
}
|
||||
}
|
||||
|
||||
private Collection<Path> getLibraries() {
|
||||
@@ -425,16 +634,6 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
}
|
||||
}
|
||||
|
||||
public static File getMappedJarFileWithSuffix(String suffix, Path runtimeJar) {
|
||||
final String path = runtimeJar.toFile().getAbsolutePath();
|
||||
|
||||
if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) {
|
||||
throw new RuntimeException("Invalid mapped JAR path: " + path);
|
||||
}
|
||||
|
||||
return new File(path.substring(0, path.length() - 4) + suffix);
|
||||
}
|
||||
|
||||
private Path getMappings() {
|
||||
Path inputMappings = getExtension().getMappingConfiguration().tinyMappings;
|
||||
|
||||
@@ -493,8 +692,25 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
return outputMappings;
|
||||
}
|
||||
|
||||
public interface MappingsProcessor {
|
||||
boolean transform(MemoryMappingTree mappings);
|
||||
public static File getJarFileWithSuffix(String suffix, Path runtimeJar) {
|
||||
final String path = runtimeJar.toFile().getAbsolutePath();
|
||||
|
||||
if (!path.toLowerCase(Locale.ROOT).endsWith(".jar")) {
|
||||
throw new RuntimeException("Invalid mapped JAR path: " + path);
|
||||
}
|
||||
|
||||
return new File(path.substring(0, path.length() - 4) + suffix);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static ClassLineNumbers readLineNumbers(Path linemapFile) throws IOException {
|
||||
if (Files.notExists(linemapFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try (BufferedReader reader = Files.newBufferedReader(linemapFile, StandardCharsets.UTF_8)) {
|
||||
return ClassLineNumbers.readMappings(reader);
|
||||
}
|
||||
}
|
||||
|
||||
private static Constructor<LoomDecompiler> getDecompilerConstructor(String clazz) {
|
||||
@@ -507,4 +723,43 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String fileHash(File file) {
|
||||
try {
|
||||
return Checksum.sha256Hex(Files.readAllBytes(file.toPath()));
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private static String fileCollectionHash(FileCollection files) {
|
||||
var sj = new StringJoiner(",");
|
||||
|
||||
files.getFiles()
|
||||
.stream()
|
||||
.sorted(Comparator.comparing(File::getAbsolutePath))
|
||||
.map(GenerateSourcesTask::fileHash)
|
||||
.forEach(sj::add);
|
||||
|
||||
return sj.toString();
|
||||
}
|
||||
|
||||
public interface MappingsProcessor {
|
||||
boolean transform(MemoryMappingTree mappings);
|
||||
}
|
||||
|
||||
private final class Timer implements AutoCloseable {
|
||||
private final String name;
|
||||
private final long start;
|
||||
|
||||
Timer(String name) {
|
||||
this.name = name;
|
||||
this.start = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
getProject().getLogger().info("{} took {}ms", name, System.currentTimeMillis() - start);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,6 +67,11 @@ public class Checksum {
|
||||
}
|
||||
}
|
||||
|
||||
public static String sha256Hex(byte[] input) throws IOException {
|
||||
HashCode hash = ByteSource.wrap(input).hash(Hashing.sha256());
|
||||
return Checksum.toHex(hash.asBytes());
|
||||
}
|
||||
|
||||
public static String sha1Hex(Path path) throws IOException {
|
||||
HashCode hash = Files.asByteSource(path.toFile()).hash(Hashing.sha1());
|
||||
return toHex(hash.asBytes());
|
||||
|
||||
@@ -43,6 +43,10 @@ public final class FileSystemUtil {
|
||||
return get().getPath(path, more);
|
||||
}
|
||||
|
||||
public Path getRoot() {
|
||||
return get().getPath("/");
|
||||
}
|
||||
|
||||
public byte[] readAllBytes(String path) throws IOException {
|
||||
Path fsPath = getPath(path);
|
||||
|
||||
|
||||
@@ -79,13 +79,13 @@ public class ZipUtils {
|
||||
|
||||
public static void unpackAll(Path zip, Path output) throws IOException {
|
||||
try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(zip, false);
|
||||
Stream<Path> walk = Files.walk(fs.get().getPath("/"))) {
|
||||
Stream<Path> walk = Files.walk(fs.getRoot())) {
|
||||
Iterator<Path> iterator = walk.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Path fsPath = iterator.next();
|
||||
if (!Files.isRegularFile(fsPath)) continue;
|
||||
Path dstPath = output.resolve(fs.get().getPath("/").relativize(fsPath).toString());
|
||||
Path dstPath = output.resolve(fs.getRoot().relativize(fsPath).toString());
|
||||
Path dstPathParent = dstPath.getParent();
|
||||
if (dstPathParent != null) Files.createDirectories(dstPathParent);
|
||||
Files.copy(fsPath, dstPath, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.COPY_ATTRIBUTES);
|
||||
|
||||
@@ -84,7 +84,7 @@ class DebugLineNumbersTest extends Specification implements GradleProjectTestTra
|
||||
'''
|
||||
when:
|
||||
// First generate sources
|
||||
def genSources = gradle.run(task: "genSources")
|
||||
def genSources = gradle.run(task: "genSources", args: ["--info"])
|
||||
genSources.task(":genSources").outcome == SUCCESS
|
||||
|
||||
// Print out the source of the file
|
||||
|
||||
@@ -74,4 +74,36 @@ class DecompileTest extends Specification implements GradleProjectTestTrait {
|
||||
where:
|
||||
version << STANDARD_TEST_VERSIONS
|
||||
}
|
||||
|
||||
def "decompile cache"() {
|
||||
setup:
|
||||
def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE)
|
||||
gradle.buildSrc("decompile")
|
||||
gradle.buildGradle << '''
|
||||
dependencies {
|
||||
minecraft "com.mojang:minecraft:1.20.4"
|
||||
mappings "net.fabricmc:yarn:1.20.4+build.3:v2"
|
||||
}
|
||||
'''
|
||||
|
||||
when:
|
||||
def result = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"])
|
||||
|
||||
// Add fabric API to the project, this introduces some transitive access wideners
|
||||
gradle.buildGradle << '''
|
||||
dependencies {
|
||||
modImplementation "net.fabricmc.fabric-api:fabric-api:0.96.4+1.20.4"
|
||||
}
|
||||
'''
|
||||
|
||||
def result2 = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"])
|
||||
|
||||
// And run again, with no changes
|
||||
def result3 = gradle.run(tasks: ["genSourcesWithVineflower"], args: ["--use-cache", "--info"])
|
||||
|
||||
then:
|
||||
result.task(":genSourcesWithVineflower").outcome == SUCCESS
|
||||
result2.task(":genSourcesWithVineflower").outcome == SUCCESS
|
||||
result3.task(":genSourcesWithVineflower").outcome == SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ package net.fabricmc.loom.test.integration.buildSrc.decompile
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
import com.google.common.io.Files
|
||||
|
||||
import net.fabricmc.loom.api.decompilers.DecompilationMetadata
|
||||
import net.fabricmc.loom.api.decompilers.LoomDecompiler
|
||||
|
||||
@@ -33,5 +35,6 @@ class CustomDecompiler implements LoomDecompiler {
|
||||
@Override
|
||||
void decompile(Path compiledJar, Path sourcesDestination, Path linemapDestination, DecompilationMetadata metaData) {
|
||||
println("Running custom decompiler")
|
||||
Files.touch(sourcesDestination.toFile())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* 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
|
||||
* 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 spock.lang.Specification
|
||||
|
||||
import net.fabricmc.loom.decompilers.ClassLineNumbers
|
||||
|
||||
class ClassLineNumbersTest extends Specification {
|
||||
def "read linemap"() {
|
||||
when:
|
||||
def reader = new BufferedReader(new StringReader(LINE_MAP))
|
||||
def lineNumbers = ClassLineNumbers.readMappings(reader)
|
||||
def lineMap = lineNumbers.lineMap()
|
||||
|
||||
then:
|
||||
lineMap.size() == 2
|
||||
lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].lineMap().size() == 39
|
||||
lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].maxLine() == 203
|
||||
lineMap["net/minecraft/server/dedicated/ServerPropertiesHandler"].maxLineDest() == 187
|
||||
|
||||
lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].lineMap().size() == 6
|
||||
lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].maxLine() == 25
|
||||
lineMap["net/minecraft/server/dedicated/ServerPropertiesLoader"].maxLineDest() == 30
|
||||
}
|
||||
|
||||
private static final String LINE_MAP = """
|
||||
net/minecraft/server/dedicated/ServerPropertiesHandler\t203\t187
|
||||
\t48\t187
|
||||
\t91\t92
|
||||
\t96\t97
|
||||
\t110\t108
|
||||
\t112\t109
|
||||
\t113\t110
|
||||
\t115\t111
|
||||
\t116\t112
|
||||
\t118\t113
|
||||
\t119\t113
|
||||
\t120\t113
|
||||
\t122\t114
|
||||
\t130\t115
|
||||
\t147\t129
|
||||
\t149\t131
|
||||
\t151\t133
|
||||
\t154\t136
|
||||
\t158\t141
|
||||
\t159\t142
|
||||
\t163\t144
|
||||
\t164\t145
|
||||
\t165\t146
|
||||
\t166\t147
|
||||
\t168\t149
|
||||
\t169\t150
|
||||
\t170\t151
|
||||
\t172\t153
|
||||
\t175\t155
|
||||
\t176\t156
|
||||
\t177\t157
|
||||
\t178\t158
|
||||
\t181\t160
|
||||
\t186\t165
|
||||
\t187\t166
|
||||
\t192\t171
|
||||
\t194\t173
|
||||
\t195\t174
|
||||
\t197\t176
|
||||
\t203\t182
|
||||
|
||||
net/minecraft/server/dedicated/ServerPropertiesLoader\t25\t30
|
||||
\t11\t15
|
||||
\t12\t16
|
||||
\t16\t20
|
||||
\t20\t24
|
||||
\t24\t28
|
||||
\t25\t30
|
||||
"""
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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 spock.lang.Specification
|
||||
|
||||
import net.fabricmc.loom.decompilers.cache.JarWalker
|
||||
import net.fabricmc.loom.test.util.ZipTestUtils
|
||||
import net.fabricmc.loom.util.FileSystemUtil
|
||||
|
||||
class JarWalkerTest extends Specification {
|
||||
def "find classes in jar"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip([
|
||||
"net/fabricmc/Test.class": "",
|
||||
"net/fabricmc/other/Test.class": "",
|
||||
"net/fabricmc/other/Test\$Inner.class": "",
|
||||
"net/fabricmc/other/Test\$1.class": "",
|
||||
])
|
||||
when:
|
||||
def entries = JarWalker.findClasses(jar)
|
||||
then:
|
||||
entries.size() == 2
|
||||
|
||||
entries[0].parentClass() == "net/fabricmc/Test.class"
|
||||
entries[0].sourcesFileName() == "net/fabricmc/Test.java"
|
||||
entries[0].innerClasses().size() == 0
|
||||
|
||||
entries[1].parentClass() == "net/fabricmc/other/Test.class"
|
||||
entries[1].sourcesFileName() == "net/fabricmc/other/Test.java"
|
||||
entries[1].innerClasses().size() == 2
|
||||
entries[1].innerClasses()[0] == "net/fabricmc/other/Test\$1.class"
|
||||
entries[1].innerClasses()[1] == "net/fabricmc/other/Test\$Inner.class"
|
||||
}
|
||||
|
||||
def "Hash Classes"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(zipEntries)
|
||||
when:
|
||||
def entries = JarWalker.findClasses(jar)
|
||||
def hash = FileSystemUtil.getJarFileSystem(jar).withCloseable { fs ->
|
||||
return entries[0].hash(fs.root)
|
||||
}
|
||||
then:
|
||||
entries.size() == 1
|
||||
hash == expectedHash
|
||||
where:
|
||||
expectedHash | zipEntries
|
||||
"2339de144d8a4a1198adf8142b6d3421ec0baacea13c9ade42a93071b6d62e43" | [
|
||||
"net/fabricmc/Test.class": "abc123",
|
||||
]
|
||||
"1053cfadf4e371ec89ff5b58d9b3bdb80373f3179e804b2e241171223709f4d1" | [
|
||||
"net/fabricmc/other/Test.class": "Hello",
|
||||
"net/fabricmc/other/Test\$Inner.class": "World",
|
||||
"net/fabricmc/other/Test\$Inner\$2.class": "123",
|
||||
"net/fabricmc/other/Test\$1.class": "test",
|
||||
]
|
||||
"f30b705f3a921b60103a4ee9951aff59b6db87cc289ba24563743d753acff433" | [
|
||||
"net/fabricmc/other/Test.class": "Hello",
|
||||
"net/fabricmc/other/Test\$Inner.class": "World",
|
||||
"net/fabricmc/other/Test\$Inner\$2.class": "abc123",
|
||||
"net/fabricmc/other/Test\$1.class": "test",
|
||||
]
|
||||
}
|
||||
}
|
||||
62
src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy
vendored
Normal file
62
src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy
vendored
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache
|
||||
|
||||
import java.nio.channels.FileChannel
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.StandardOpenOption
|
||||
|
||||
import spock.lang.Specification
|
||||
import spock.lang.TempDir
|
||||
|
||||
import net.fabricmc.loom.decompilers.ClassLineNumbers
|
||||
import net.fabricmc.loom.decompilers.cache.CachedData
|
||||
|
||||
class CachedDataTest extends Specification {
|
||||
@TempDir
|
||||
Path testPath
|
||||
|
||||
// Simple test to check if the CachedData class can be written and read from a file
|
||||
def "Read + Write CachedData"() {
|
||||
given:
|
||||
def lineNumberEntry = new ClassLineNumbers.Entry("net/test/TestClass", 1, 2, [1: 2, 4: 7])
|
||||
def cachedData = new CachedData("net/test/TestClass", "Example sources", lineNumberEntry)
|
||||
def path = testPath.resolve("cachedData.bin")
|
||||
when:
|
||||
// Write the cachedData to a file
|
||||
FileChannel.open(path, StandardOpenOption.CREATE, StandardOpenOption.WRITE).withCloseable {
|
||||
cachedData.write(it)
|
||||
}
|
||||
|
||||
// And read it back
|
||||
def readCachedData = Files.newInputStream(path).withCloseable {
|
||||
return CachedData.read(it)
|
||||
}
|
||||
|
||||
then:
|
||||
cachedData == readCachedData
|
||||
}
|
||||
}
|
||||
132
src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy
vendored
Normal file
132
src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy
vendored
Normal file
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.attribute.FileTime
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
import spock.lang.Specification
|
||||
import spock.lang.TempDir
|
||||
|
||||
import net.fabricmc.loom.decompilers.cache.CachedFileStore
|
||||
import net.fabricmc.loom.decompilers.cache.CachedFileStoreImpl
|
||||
import net.fabricmc.loom.util.FileSystemUtil
|
||||
|
||||
class CachedFileStoreTest extends Specification {
|
||||
@TempDir
|
||||
Path testPath
|
||||
|
||||
FileSystemUtil.Delegate zipDelegate
|
||||
Path root
|
||||
|
||||
void setup() {
|
||||
zipDelegate = FileSystemUtil.getJarFileSystem(testPath.resolve("cache.zip"), true)
|
||||
root = zipDelegate.get().getPath("/")
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
zipDelegate.close()
|
||||
}
|
||||
|
||||
def "putEntry"() {
|
||||
given:
|
||||
def cacheRules = new CachedFileStoreImpl.CacheRules(100, Duration.ofDays(7))
|
||||
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
|
||||
when:
|
||||
store.putEntry("abc", "Hello world".bytes)
|
||||
then:
|
||||
Files.exists(root.resolve("abc"))
|
||||
}
|
||||
|
||||
def "getEntry"() {
|
||||
given:
|
||||
def cacheRules = new CachedFileStoreImpl.CacheRules(100, Duration.ofDays(7))
|
||||
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
|
||||
when:
|
||||
store.putEntry("abc", "Hello world".bytes)
|
||||
def entry = store.getEntry("abc")
|
||||
def unknownEntry = store.getEntry("123")
|
||||
then:
|
||||
entry == "Hello world".bytes
|
||||
unknownEntry == null
|
||||
}
|
||||
|
||||
def "pruneManyFiles"() {
|
||||
given:
|
||||
def cacheRules = new CachedFileStoreImpl.CacheRules(250, Duration.ofDays(7))
|
||||
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
|
||||
when:
|
||||
|
||||
for (i in 0..<500) {
|
||||
def key = "test_" + i
|
||||
store.putEntry(key, "Hello world".bytes)
|
||||
// Higher files are older and should be removed.
|
||||
Files.setLastModifiedTime(root.resolve(key), FileTime.from(Instant.now().minusSeconds(i)))
|
||||
}
|
||||
|
||||
store.prune()
|
||||
|
||||
then:
|
||||
Files.exists(root.resolve("test_0"))
|
||||
Files.exists(root.resolve("test_100"))
|
||||
Files.notExists(root.resolve("test_300"))
|
||||
}
|
||||
|
||||
def "pruneOldFiles"() {
|
||||
given:
|
||||
def cacheRules = new CachedFileStoreImpl.CacheRules(1000, Duration.ofSeconds(250))
|
||||
def store = new CachedFileStoreImpl(root, BYTE_ARRAY_SERIALIZER, cacheRules)
|
||||
when:
|
||||
|
||||
for (i in 0..<500) {
|
||||
def key = "test_" + i
|
||||
store.putEntry(key, "Hello world".bytes)
|
||||
// Higher files are older and should be removed.
|
||||
Files.setLastModifiedTime(root.resolve(key), FileTime.from(Instant.now().minusSeconds(i)))
|
||||
}
|
||||
|
||||
store.prune()
|
||||
|
||||
then:
|
||||
Files.exists(root.resolve("test_0"))
|
||||
Files.exists(root.resolve("test_100"))
|
||||
Files.notExists(root.resolve("test_300"))
|
||||
}
|
||||
|
||||
private static CachedFileStore.EntrySerializer<byte[]> BYTE_ARRAY_SERIALIZER = new CachedFileStore.EntrySerializer<byte[]>() {
|
||||
@Override
|
||||
byte[] read(Path path) throws IOException {
|
||||
return Files.readAllBytes(path)
|
||||
}
|
||||
|
||||
@Override
|
||||
void write(byte[] entry, Path path) throws IOException {
|
||||
Files.write(path, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
241
src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy
vendored
Normal file
241
src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
/*
|
||||
* This file is part of fabric-loom, licensed under the MIT License (MIT).
|
||||
*
|
||||
* Copyright (c) 2024 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.cache
|
||||
|
||||
import java.nio.file.Files
|
||||
|
||||
import spock.lang.Specification
|
||||
|
||||
import net.fabricmc.loom.decompilers.ClassLineNumbers
|
||||
import net.fabricmc.loom.decompilers.cache.CachedData
|
||||
import net.fabricmc.loom.decompilers.cache.CachedFileStore
|
||||
import net.fabricmc.loom.decompilers.cache.CachedJarProcessor
|
||||
import net.fabricmc.loom.test.util.ZipTestUtils
|
||||
import net.fabricmc.loom.util.ZipUtils
|
||||
|
||||
class CachedJarProcessorTest extends Specification {
|
||||
static Map<String, String> jarEntries = [
|
||||
"net/fabricmc/Example.class": "",
|
||||
"net/fabricmc/other/Test.class": "",
|
||||
"net/fabricmc/other/Test\$Inner.class": "",
|
||||
"net/fabricmc/other/Test\$1.class": "",
|
||||
]
|
||||
|
||||
static String ExampleHash = "abc123/cd372fb85148700fa88095e3492d3f9f5beb43e555e5ff26d95f5a6adc36f8e6"
|
||||
static String TestHash = "abc123/ecd40b16ec50b636a390cb8da716a22606965f14e526e3051144dd567f336bc5"
|
||||
|
||||
static CachedData ExampleCachedData = new CachedData("net/fabricmc/Example", "Example sources", lineNumber("net/fabricmc/Example"))
|
||||
static CachedData TestCachedData = new CachedData("net/fabricmc/other/Test", "Test sources", lineNumber("net/fabricmc/other/Test"))
|
||||
|
||||
def "prepare full work job"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(jarEntries)
|
||||
def cache = Mock(CachedFileStore)
|
||||
def processor = new CachedJarProcessor(cache, "abc123")
|
||||
|
||||
when:
|
||||
def workRequest = processor.prepareJob(jar)
|
||||
def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob
|
||||
|
||||
then:
|
||||
workRequest.lineNumbers() == null
|
||||
workJob.outputNameMap().size() == 2
|
||||
|
||||
// Expect two calls looking for the existing entry in the cache
|
||||
2 * cache.getEntry(_) >> null
|
||||
|
||||
0 * _ // Strict mock
|
||||
}
|
||||
|
||||
def "prepare partial work job"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(jarEntries)
|
||||
def cache = Mock(CachedFileStore)
|
||||
def processor = new CachedJarProcessor(cache, "abc123")
|
||||
|
||||
when:
|
||||
def workRequest = processor.prepareJob(jar)
|
||||
def workJob = workRequest.job() as CachedJarProcessor.PartialWorkJob
|
||||
def lineMap = workRequest.lineNumbers().lineMap()
|
||||
|
||||
then:
|
||||
lineMap.size() == 1
|
||||
lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers()
|
||||
|
||||
workJob.outputNameMap().size() == 1
|
||||
ZipUtils.unpackNullable(workJob.existing(), "net/fabricmc/Example.java") == "Example sources".bytes
|
||||
|
||||
// Provide one cached entry
|
||||
// And then one call not finding the entry in the cache
|
||||
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
|
||||
1 * cache.getEntry(_) >> null
|
||||
|
||||
0 * _ // Strict mock
|
||||
}
|
||||
|
||||
def "prepare completed work job"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(jarEntries)
|
||||
def cache = Mock(CachedFileStore)
|
||||
def processor = new CachedJarProcessor(cache, "abc123")
|
||||
|
||||
when:
|
||||
def workRequest = processor.prepareJob(jar)
|
||||
def workJob = workRequest.job() as CachedJarProcessor.CompletedWorkJob
|
||||
def lineMap = workRequest.lineNumbers().lineMap()
|
||||
|
||||
then:
|
||||
lineMap.size() == 2
|
||||
lineMap.get("net/fabricmc/Example") == ExampleCachedData.lineNumbers()
|
||||
lineMap.get("net/fabricmc/other/Test") == TestCachedData.lineNumbers()
|
||||
|
||||
workJob.completed() != null
|
||||
ZipUtils.unpackNullable(workJob.completed(), "net/fabricmc/Example.java") == "Example sources".bytes
|
||||
ZipUtils.unpackNullable(workJob.completed(), "net/fabricmc/other/Test.java") == "Test sources".bytes
|
||||
|
||||
// Provide one cached entry
|
||||
// And then two calls not finding the entry in the cache
|
||||
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
|
||||
1 * cache.getEntry(TestHash) >> TestCachedData
|
||||
|
||||
0 * _ // Strict mock
|
||||
}
|
||||
|
||||
def "complete full work job"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(jarEntries)
|
||||
def cache = Mock(CachedFileStore)
|
||||
def processor = new CachedJarProcessor(cache, "abc123")
|
||||
|
||||
when:
|
||||
def workRequest = processor.prepareJob(jar)
|
||||
def workJob = workRequest.job() as CachedJarProcessor.FullWorkJob
|
||||
|
||||
// Do the work, such as decompiling.
|
||||
ZipUtils.add(workJob.output(), "net/fabricmc/Example.java", "Example sources")
|
||||
ZipUtils.add(workJob.output(), "net/fabricmc/other/Test.java", "Test sources")
|
||||
|
||||
def outputJar = Files.createTempFile("loom-test-output", ".jar")
|
||||
Files.delete(outputJar)
|
||||
|
||||
ClassLineNumbers lineNumbers = lineNumbers([
|
||||
"net/fabricmc/Example",
|
||||
"net/fabricmc/other/Test"
|
||||
])
|
||||
processor.completeJob(outputJar, workJob, lineNumbers)
|
||||
|
||||
then:
|
||||
workJob.outputNameMap().size() == 2
|
||||
|
||||
ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes
|
||||
ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes
|
||||
|
||||
// Expect two calls looking for the existing entry in the cache
|
||||
1 * cache.getEntry(ExampleHash) >> null
|
||||
1 * cache.getEntry(TestHash) >> null
|
||||
|
||||
// Expect the new work to be put into the cache
|
||||
1 * cache.putEntry(ExampleHash, ExampleCachedData)
|
||||
1 * cache.putEntry(TestHash, TestCachedData)
|
||||
|
||||
0 * _ // Strict mock
|
||||
}
|
||||
|
||||
def "complete partial work job"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(jarEntries)
|
||||
def cache = Mock(CachedFileStore)
|
||||
def processor = new CachedJarProcessor(cache, "abc123")
|
||||
|
||||
when:
|
||||
def workRequest = processor.prepareJob(jar)
|
||||
def workJob = workRequest.job() as CachedJarProcessor.PartialWorkJob
|
||||
|
||||
// Do the work
|
||||
ZipUtils.add(workJob.output(), "net/fabricmc/other/Test.java", "Test sources")
|
||||
|
||||
def outputJar = Files.createTempFile("loom-test-output", ".jar")
|
||||
Files.delete(outputJar)
|
||||
|
||||
ClassLineNumbers lineNumbers = lineNumbers([
|
||||
"net/fabricmc/Example",
|
||||
"net/fabricmc/other/Test"
|
||||
])
|
||||
processor.completeJob(outputJar, workJob, lineNumbers)
|
||||
|
||||
then:
|
||||
workJob.outputNameMap().size() == 1
|
||||
|
||||
ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes
|
||||
ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes
|
||||
|
||||
// The cache already contains sources for example, but not for test
|
||||
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
|
||||
1 * cache.getEntry(TestHash) >> null
|
||||
|
||||
// Expect the new work to be put into the cache
|
||||
1 * cache.putEntry(TestHash, TestCachedData)
|
||||
|
||||
0 * _ // Strict mock
|
||||
}
|
||||
|
||||
def "complete completed work job"() {
|
||||
given:
|
||||
def jar = ZipTestUtils.createZip(jarEntries)
|
||||
def cache = Mock(CachedFileStore)
|
||||
def processor = new CachedJarProcessor(cache, "abc123")
|
||||
|
||||
when:
|
||||
def workRequest = processor.prepareJob(jar)
|
||||
def workJob = workRequest.job() as CachedJarProcessor.CompletedWorkJob
|
||||
|
||||
def outputJar = Files.createTempFile("loom-test-output", ".jar")
|
||||
Files.delete(outputJar)
|
||||
|
||||
ClassLineNumbers lineNumbers = lineNumbers([
|
||||
"net/fabricmc/Example",
|
||||
"net/fabricmc/other/Test"
|
||||
])
|
||||
processor.completeJob(outputJar, workJob, lineNumbers)
|
||||
|
||||
then:
|
||||
ZipUtils.unpackNullable(outputJar, "net/fabricmc/Example.java") == "Example sources".bytes
|
||||
ZipUtils.unpackNullable(outputJar, "net/fabricmc/other/Test.java") == "Test sources".bytes
|
||||
|
||||
// The cache already contains sources for example, but not for test
|
||||
1 * cache.getEntry(ExampleHash) >> ExampleCachedData
|
||||
1 * cache.getEntry(TestHash) >> TestCachedData
|
||||
|
||||
0 * _ // Strict mock
|
||||
}
|
||||
|
||||
private static ClassLineNumbers lineNumbers(List<String> names) {
|
||||
return new ClassLineNumbers(names.collectEntries { [it, lineNumber(it)] })
|
||||
}
|
||||
|
||||
private static ClassLineNumbers.Entry lineNumber(String name) {
|
||||
return new ClassLineNumbers.Entry(name, 0, 0, [:])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user