From f2c0d7a35dd1ce0ebe61cc2da472ff352068d42b Mon Sep 17 00:00:00 2001 From: Juuz <6596629+Juuxel@users.noreply.github.com> Date: Sat, 7 Jan 2023 01:51:32 +0200 Subject: [PATCH] Split Architectury extensions to Fabric classes into new classes (#116) * Split custom RemapJarTask logic into a new class * Split a lot of logic for Arch and Quilt mod metadata into new classes * ArchitecturyCommonJson: Fix outdated error message * Add minimal unit tests for ACJ and QMJ * QuiltModJson: Fix error when there are no injected interfaces * QuiltModJsonTest: Add test for mixin configs * QuiltModJsonTest: Move to correct package * Add tests for creating ACJ and QMJ instances --- build.gradle | 1 + .../loom/extensions/ModBuildExtensions.java | 101 ++++++++++++++ .../loom/metadata/ArchitecturyCommonJson.java | 89 ++++++++++++ .../loom/metadata/ModMetadataFile.java | 12 ++ .../loom/metadata/QuiltModJson.java | 120 ++++++++++++++++ .../accesswidener/AccessWidenerFile.java | 35 ++--- .../InterfaceInjectionProcessor.java | 53 +------ .../net/fabricmc/loom/task/RemapJarTask.java | 117 +--------------- .../ArchitecturyCommonJsonTest.groovy | 116 ++++++++++++++++ .../test/unit/quilt/QuiltModJsonTest.groovy | 131 ++++++++++++++++++ 10 files changed, 590 insertions(+), 185 deletions(-) create mode 100644 src/main/java/dev/architectury/loom/extensions/ModBuildExtensions.java create mode 100644 src/main/java/dev/architectury/loom/metadata/ArchitecturyCommonJson.java create mode 100644 src/main/java/dev/architectury/loom/metadata/ModMetadataFile.java create mode 100644 src/main/java/dev/architectury/loom/metadata/QuiltModJson.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/architectury/ArchitecturyCommonJsonTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/quilt/QuiltModJsonTest.groovy diff --git a/build.gradle b/build.gradle index 68226024..4c4c585d 100644 --- a/build.gradle +++ b/build.gradle @@ -166,6 +166,7 @@ spotless { java { licenseHeaderFile(rootProject.file("HEADER")).yearSeparator("-") targetExclude("**/loom/util/DownloadUtil.java", "**/loom/util/FileSystemUtil.java") + targetExclude("**/dev/architectury/**") } groovy { diff --git a/src/main/java/dev/architectury/loom/extensions/ModBuildExtensions.java b/src/main/java/dev/architectury/loom/extensions/ModBuildExtensions.java new file mode 100644 index 00000000..39f61a1f --- /dev/null +++ b/src/main/java/dev/architectury/loom/extensions/ModBuildExtensions.java @@ -0,0 +1,101 @@ +package dev.architectury.loom.extensions; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.io.Writer; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileAlreadyExistsException; +import java.nio.file.FileSystem; +import java.nio.file.Files; +import java.nio.file.NoSuchFileException; +import java.nio.file.Path; +import java.util.Set; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import java.util.jar.Manifest; + +import org.cadixdev.at.AccessTransformSet; +import org.cadixdev.at.io.AccessTransformFormats; +import org.cadixdev.lorenz.MappingSet; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.SetProperty; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.loom.task.service.MappingsService; +import net.fabricmc.loom.util.Constants; +import net.fabricmc.loom.util.FileSystemUtil; +import net.fabricmc.loom.util.LfWriter; +import net.fabricmc.loom.util.aw2at.Aw2At; +import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; +import net.fabricmc.lorenztiny.TinyMappingsReader; + +public final class ModBuildExtensions { + public static Set readMixinConfigsFromManifest(File jarFile) { + try (JarFile jar = new JarFile(jarFile)) { + @Nullable Manifest manifest = jar.getManifest(); + + if (manifest != null) { + Attributes attributes = manifest.getMainAttributes(); + String mixinConfigs = attributes.getValue(Constants.Forge.MIXIN_CONFIGS_MANIFEST_KEY); + + if (mixinConfigs != null) { + return Set.of(mixinConfigs.split(",")); + } + } + + return Set.of(); + } catch (IOException e) { + throw new UncheckedIOException("Could not read mixin configs from jar " + jarFile.getAbsolutePath(), e); + } + } + + public static void convertAwToAt(SetProperty atAccessWidenersProperty, Path outputFile, Property mappingBuildServiceUuid) throws IOException { + if (!atAccessWidenersProperty.isPresent()) { + return; + } + + Set atAccessWideners = atAccessWidenersProperty.get(); + + if (atAccessWideners.isEmpty()) { + return; + } + + AccessTransformSet at = AccessTransformSet.create(); + + try (FileSystemUtil.Delegate fileSystem = FileSystemUtil.getJarFileSystem(outputFile, false)) { + FileSystem fs = fileSystem.get(); + Path atPath = fs.getPath(Constants.Forge.ACCESS_TRANSFORMER_PATH); + + if (Files.exists(atPath)) { + throw new FileAlreadyExistsException("Jar " + outputFile + " already contains an access transformer - cannot convert AWs!"); + } + + for (String aw : atAccessWideners) { + Path awPath = fs.getPath(aw); + + if (Files.notExists(awPath)) { + throw new NoSuchFileException("Could not find AW '" + aw + "' to convert into AT!"); + } + + try (BufferedReader reader = Files.newBufferedReader(awPath, StandardCharsets.UTF_8)) { + at.merge(Aw2At.toAccessTransformSet(reader)); + } + + Files.delete(awPath); + } + + MappingsService service = UnsafeWorkQueueHelper.get(mappingBuildServiceUuid, MappingsService.class); + + try (TinyMappingsReader reader = new TinyMappingsReader(service.getMemoryMappingTree(), service.getFromNamespace(), service.getToNamespace())) { + MappingSet mappingSet = reader.read(); + at = at.remap(mappingSet); + } + + try (Writer writer = new LfWriter(Files.newBufferedWriter(atPath))) { + AccessTransformFormats.FML.write(writer, at); + } + } + } +} diff --git a/src/main/java/dev/architectury/loom/metadata/ArchitecturyCommonJson.java b/src/main/java/dev/architectury/loom/metadata/ArchitecturyCommonJson.java new file mode 100644 index 00000000..ccb42e17 --- /dev/null +++ b/src/main/java/dev/architectury/loom/metadata/ArchitecturyCommonJson.java @@ -0,0 +1,89 @@ +package dev.architectury.loom.metadata; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.loom.LoomGradlePlugin; +import net.fabricmc.loom.configuration.ifaceinject.InterfaceInjectionProcessor; + +public final class ArchitecturyCommonJson implements ModMetadataFile { + private static final String ACCESS_WIDENER_KEY = "accessWidener"; + + private final JsonObject json; + + private ArchitecturyCommonJson(JsonObject json) { + this.json = Objects.requireNonNull(json, "json"); + } + + public static ArchitecturyCommonJson of(byte[] utf8) { + return of(new String(utf8, StandardCharsets.UTF_8)); + } + + public static ArchitecturyCommonJson of(String text) { + return of(LoomGradlePlugin.GSON.fromJson(text, JsonObject.class)); + } + + public static ArchitecturyCommonJson of(Path path) throws IOException { + return of(Files.readString(path, StandardCharsets.UTF_8)); + } + + public static ArchitecturyCommonJson of(File file) throws IOException { + return of(file.toPath()); + } + + public static ArchitecturyCommonJson of(JsonObject json) { + return new ArchitecturyCommonJson(json); + } + + @Override + public @Nullable String getAccessWidener() { + if (json.has(ACCESS_WIDENER_KEY)) { + return json.get(ACCESS_WIDENER_KEY).getAsString(); + } else { + return null; + } + } + + @Override + public List getInjectedInterfaces(@Nullable String modId) { + if (modId == null) { + throw new IllegalArgumentException("getInjectedInterfaces: mod ID has to be provided for architectury.common.json"); + } + + return getInjectedInterfaces(json, modId); + } + + static List getInjectedInterfaces(JsonObject json, String modId) { + Objects.requireNonNull(modId, "mod ID"); + + if (json.has("injected_interfaces")) { + JsonObject addedIfaces = json.getAsJsonObject("injected_interfaces"); + + final List result = new ArrayList<>(); + + for (String className : addedIfaces.keySet()) { + final JsonArray ifaceNames = addedIfaces.getAsJsonArray(className); + + for (JsonElement ifaceName : ifaceNames) { + result.add(new InterfaceInjectionProcessor.InjectedInterface(modId, className, ifaceName.getAsString())); + } + } + + return result; + } + + return Collections.emptyList(); + } +} diff --git a/src/main/java/dev/architectury/loom/metadata/ModMetadataFile.java b/src/main/java/dev/architectury/loom/metadata/ModMetadataFile.java new file mode 100644 index 00000000..d4141693 --- /dev/null +++ b/src/main/java/dev/architectury/loom/metadata/ModMetadataFile.java @@ -0,0 +1,12 @@ +package dev.architectury.loom.metadata; + +import java.util.List; + +import org.jetbrains.annotations.Nullable; + +import net.fabricmc.loom.configuration.ifaceinject.InterfaceInjectionProcessor; + +public interface ModMetadataFile { + @Nullable String getAccessWidener(); + List getInjectedInterfaces(@Nullable String modId); +} diff --git a/src/main/java/dev/architectury/loom/metadata/QuiltModJson.java b/src/main/java/dev/architectury/loom/metadata/QuiltModJson.java new file mode 100644 index 00000000..2c651b6a --- /dev/null +++ b/src/main/java/dev/architectury/loom/metadata/QuiltModJson.java @@ -0,0 +1,120 @@ +package dev.architectury.loom.metadata; + +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import net.fabricmc.loom.LoomGradlePlugin; +import net.fabricmc.loom.configuration.ifaceinject.InterfaceInjectionProcessor; + +public final class QuiltModJson implements ModMetadataFile { + private static final Logger LOGGER = LoggerFactory.getLogger(QuiltModJson.class); + private static final String ACCESS_WIDENER_KEY = "access_widener"; + private static final String MIXIN_KEY = "mixin"; + + private final JsonObject json; + + private QuiltModJson(JsonObject json) { + this.json = Objects.requireNonNull(json, "json"); + } + + public static QuiltModJson of(byte[] utf8) { + return of(new String(utf8, StandardCharsets.UTF_8)); + } + + public static QuiltModJson of(String text) { + return of(LoomGradlePlugin.GSON.fromJson(text, JsonObject.class)); + } + + public static QuiltModJson of(Path path) throws IOException { + return of(Files.readString(path, StandardCharsets.UTF_8)); + } + + public static QuiltModJson of(File file) throws IOException { + return of(file.toPath()); + } + + public static QuiltModJson of(JsonObject json) { + return new QuiltModJson(json); + } + + @Override + public @Nullable String getAccessWidener() { + if (json.has(ACCESS_WIDENER_KEY)) { + if (json.get(ACCESS_WIDENER_KEY).isJsonArray()) { + JsonArray array = json.get(ACCESS_WIDENER_KEY).getAsJsonArray(); + + // TODO (1.1): Support multiple access wideners in Quilt mods + if (array.size() != 1) { + throw new UnsupportedOperationException("Loom does not support multiple access wideners in one mod!"); + } + + return array.get(0).getAsString(); + } else { + return json.get(ACCESS_WIDENER_KEY).getAsString(); + } + } else { + return null; + } + } + + @Override + public List getInjectedInterfaces(@Nullable String modId) { + try { + modId = Objects.requireNonNullElseGet(modId, () -> json.getAsJsonObject("quilt_loader").get("id").getAsString()); + } catch (NullPointerException e) { + throw new IllegalArgumentException("Could not determine mod ID for Quilt mod and no fallback provided"); + } + + // Quilt injected interfaces have the same format as architectury.common.json + if (json.has("quilt_loom")) { + JsonElement quiltLoom = json.get("quilt_loom"); + + if (quiltLoom.isJsonObject()) { + return ArchitecturyCommonJson.getInjectedInterfaces(json.getAsJsonObject("quilt_loom"), modId); + } else { + LOGGER.warn("Unexpected type for 'quilt_loom' in quilt.mod.json: {}", quiltLoom.getClass()); + } + } + + return List.of(); + } + + public List getMixinConfigs() { + // RFC 0002: The `mixin` field: + // Type: Array/String + // Required: False + + if (json.has(MIXIN_KEY)) { + JsonElement mixin = json.get(MIXIN_KEY); + + if (mixin.isJsonPrimitive()) { + return List.of(mixin.getAsString()); + } else if (mixin.isJsonArray()) { + List mixinConfigs = new ArrayList<>(); + + for (JsonElement child : mixin.getAsJsonArray()) { + mixinConfigs.add(child.getAsString()); + } + + return mixinConfigs; + } else { + LOGGER.warn("'mixin' key in quilt.mod.json is of unexpected type {}", mixin.getClass()); + } + } + + return List.of(); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerFile.java b/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerFile.java index 31ebc2dc..8afcef1b 100644 --- a/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerFile.java +++ b/src/main/java/net/fabricmc/loom/configuration/accesswidener/AccessWidenerFile.java @@ -32,8 +32,9 @@ import java.util.Arrays; import java.util.Objects; import com.google.gson.Gson; -import com.google.gson.JsonArray; import com.google.gson.JsonObject; +import dev.architectury.loom.metadata.ArchitecturyCommonJson; +import dev.architectury.loom.metadata.QuiltModJson; import net.fabricmc.loom.util.ZipUtils; @@ -56,7 +57,7 @@ public record AccessWidenerFile( if (modJsonBytes == null) { if (ZipUtils.contains(modJarPath, "architectury.common.json")) { - String awPath = null; + String awPath; byte[] commonJsonBytes; try { @@ -66,13 +67,8 @@ public record AccessWidenerFile( } if (commonJsonBytes != null) { - JsonObject jsonObject = new Gson().fromJson(new String(commonJsonBytes, StandardCharsets.UTF_8), JsonObject.class); - - if (jsonObject.has("accessWidener")) { - awPath = jsonObject.get("accessWidener").getAsString(); - } else { - return null; - } + awPath = ArchitecturyCommonJson.of(commonJsonBytes).getAccessWidener(); + if (awPath == null) return null; } else { // ??????????? throw new IllegalArgumentException("The architectury.common.json file does not exist."); @@ -94,7 +90,7 @@ public record AccessWidenerFile( } if (ZipUtils.contains(modJarPath, "quilt.mod.json")) { - String awPath = null; + String awPath; byte[] quiltModBytes; try { @@ -104,23 +100,8 @@ public record AccessWidenerFile( } if (quiltModBytes != null) { - JsonObject jsonObject = new Gson().fromJson(new String(quiltModBytes, StandardCharsets.UTF_8), JsonObject.class); - - if (jsonObject.has("access_widener")) { - if (jsonObject.get("access_widener").isJsonArray()) { - JsonArray array = jsonObject.get("access_widener").getAsJsonArray(); - - if (array.size() != 1) { - throw new UnsupportedOperationException("Loom does not support multiple access wideners in one mod!"); - } - - awPath = array.get(0).getAsString(); - } else { - awPath = jsonObject.get("access_widener").getAsString(); - } - } else { - return null; - } + awPath = QuiltModJson.of(quiltModBytes).getAccessWidener(); + if (awPath == null) return null; } else { // ??????????? throw new IllegalArgumentException("The quilt.mod.json file does not exist."); diff --git a/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java b/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java index c8a87540..7838450a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java +++ b/src/main/java/net/fabricmc/loom/configuration/ifaceinject/InterfaceInjectionProcessor.java @@ -44,10 +44,11 @@ import java.util.stream.Stream; import com.google.common.base.Preconditions; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; -import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import dev.architectury.loom.metadata.ArchitecturyCommonJson; +import dev.architectury.loom.metadata.QuiltModJson; import dev.architectury.tinyremapper.TinyRemapper; import org.gradle.api.Project; import org.gradle.api.tasks.SourceSet; @@ -213,16 +214,11 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource .matching(patternFilterable -> patternFilterable.include("architectury.common.json")) .getSingleFile(); - final String jsonString; - try { - jsonString = Files.readString(archCommonJson.toPath(), StandardCharsets.UTF_8); + return ArchitecturyCommonJson.of(archCommonJson).getInjectedInterfaces(archCommonJson.getAbsolutePath()); } catch (IOException e2) { throw new UncheckedIOException("Failed to read architectury.common.json", e2); } - - JsonObject jsonObject = new Gson().fromJson(jsonString, JsonObject.class); - return InjectedInterface.fromJsonArch(jsonObject, archCommonJson.getAbsolutePath()); } catch (IllegalStateException e2) { File quiltModJson; @@ -231,20 +227,11 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource .matching(patternFilterable -> patternFilterable.include("quilt.mod.json")) .getSingleFile(); - final String jsonString; - try { - jsonString = Files.readString(quiltModJson.toPath(), StandardCharsets.UTF_8); + return QuiltModJson.of(quiltModJson).getInjectedInterfaces(quiltModJson.getAbsolutePath()); } catch (IOException e3) { throw new UncheckedIOException("Failed to read quilt.mod.json", e3); } - - JsonObject jsonObject = new Gson().fromJson(jsonString, JsonObject.class); - - if (jsonObject.has("quilt_loom")) { - // quilt injected interfaces has the same format as architectury.common.json - return InjectedInterface.fromJsonArch(jsonObject.getAsJsonObject("quilt_loom"), quiltModJson.getAbsolutePath()); - } } catch (IllegalStateException e3) { // File not found } @@ -310,7 +297,7 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource return comment; } - private record InjectedInterface(String modId, String className, String ifaceName) { + public record InjectedInterface(String modId, String className, String ifaceName) { /** * Reads the injected interfaces contained in a mod jar, or returns empty if there is none. */ @@ -327,8 +314,7 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource } if (commonJsonBytes != null) { - JsonObject commonJsonObject = new Gson().fromJson(new String(commonJsonBytes, StandardCharsets.UTF_8), JsonObject.class); - return fromJsonArch(commonJsonObject, modJarPath.toString()); + return ArchitecturyCommonJson.of(commonJsonBytes).getInjectedInterfaces(modJarPath.toString()); } else { try { commonJsonBytes = ZipUtils.unpackNullable(modJarPath, "quilt.mod.json"); @@ -337,12 +323,7 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource } if (commonJsonBytes != null) { - JsonObject commonJsonObject = new Gson().fromJson(new String(commonJsonBytes, StandardCharsets.UTF_8), JsonObject.class); - - if (commonJsonObject.has("quilt_loom")) { - // quilt injected interfaces has the same format as architectury.common.json - return fromJsonArch(commonJsonObject.getAsJsonObject("quilt_loom"), modJarPath.toString()); - } + return QuiltModJson.of(commonJsonBytes).getInjectedInterfaces(modJarPath.toString()); } } @@ -383,26 +364,6 @@ public class InterfaceInjectionProcessor implements JarProcessor, GenerateSource return result; } - - public static List fromJsonArch(JsonObject jsonObject, String modId) { - if (jsonObject.has("injected_interfaces")) { - JsonObject addedIfaces = jsonObject.getAsJsonObject("injected_interfaces"); - - final List result = new ArrayList<>(); - - for (String className : addedIfaces.keySet()) { - final JsonArray ifaceNames = addedIfaces.getAsJsonArray(className); - - for (JsonElement ifaceName : ifaceNames) { - result.add(new InjectedInterface(modId, className, ifaceName.getAsString())); - } - } - - return result; - } - - return Collections.emptyList(); - } } private static class InjectingClassVisitor extends ClassVisitor { diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index 198ffa0a..f978e6f9 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -24,46 +24,30 @@ package net.fabricmc.loom.task; -import java.io.BufferedReader; -import java.io.ByteArrayInputStream; import java.io.File; import java.io.IOException; -import java.io.InputStreamReader; import java.io.Serializable; -import java.io.UncheckedIOException; -import java.io.Writer; -import java.nio.charset.StandardCharsets; -import java.nio.file.FileAlreadyExistsException; -import java.nio.file.FileSystem; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.util.ArrayList; import java.util.Collection; -import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; import java.util.function.Supplier; -import java.util.jar.Attributes; -import java.util.jar.JarFile; -import java.util.jar.Manifest; import java.util.stream.Collectors; import java.util.stream.Stream; -import java.util.stream.StreamSupport; import javax.inject.Inject; import com.google.common.base.Suppliers; -import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import dev.architectury.loom.extensions.ModBuildExtensions; +import dev.architectury.loom.metadata.QuiltModJson; import dev.architectury.tinyremapper.OutputConsumerPath; import dev.architectury.tinyremapper.TinyRemapper; -import org.cadixdev.at.AccessTransformSet; -import org.cadixdev.at.io.AccessTransformFormats; -import org.cadixdev.lorenz.MappingSet; import org.gradle.api.artifacts.Configuration; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; @@ -79,7 +63,6 @@ import org.gradle.api.tasks.Internal; import org.gradle.api.tasks.SourceSet; import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskDependency; -import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -87,7 +70,6 @@ import net.fabricmc.accesswidener.AccessWidenerReader; import net.fabricmc.accesswidener.AccessWidenerRemapper; import net.fabricmc.accesswidener.AccessWidenerWriter; import net.fabricmc.loom.LoomGradleExtension; -import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.build.MixinRefmapHelper; import net.fabricmc.loom.build.nesting.IncludedJarFactory; import net.fabricmc.loom.build.nesting.IncludedJarFactory.LazyNestedFile; @@ -100,16 +82,12 @@ import net.fabricmc.loom.task.service.MappingsService; import net.fabricmc.loom.task.service.TinyRemapperService; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.ExceptionUtil; -import net.fabricmc.loom.util.FileSystemUtil; -import net.fabricmc.loom.util.LfWriter; import net.fabricmc.loom.util.ModPlatform; import net.fabricmc.loom.util.ModUtils; import net.fabricmc.loom.util.Pair; import net.fabricmc.loom.util.SidedClassVisitor; import net.fabricmc.loom.util.ZipUtils; -import net.fabricmc.loom.util.aw2at.Aw2At; import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; -import net.fabricmc.lorenztiny.TinyMappingsReader; public abstract class RemapJarTask extends AbstractRemapJarTask { @InputFiles @@ -250,22 +228,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { byte[] bytes = ZipUtils.unpackNullable(getInputFile().getAsFile().get().toPath(), "quilt.mod.json"); if (bytes != null) { - JsonObject json = LoomGradlePlugin.GSON.fromJson(new InputStreamReader(new ByteArrayInputStream(bytes)), JsonObject.class); - JsonElement mixins = json.has("mixin") ? json.get("mixin") : json.get("mixins"); - - if (mixins != null) { - if (mixins.isJsonPrimitive()) { - allMixinConfigs = Collections.singletonList(mixins.getAsString()); - } else if (mixins.isJsonArray()) { - allMixinConfigs = StreamSupport.stream(mixins.getAsJsonArray().spliterator(), false) - .map(JsonElement::getAsString) - .collect(Collectors.toList()); - } else { - throw new RuntimeException("Unknown mixin type: " + mixins.getClass().getName()); - } - } else { - allMixinConfigs = Collections.emptyList(); - } + allMixinConfigs = QuiltModJson.of(bytes).getMixinConfigs(); } } catch (IOException e) { throw new RuntimeException("Cannot read file quilt.mod.json in the jar.", e); @@ -273,7 +236,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { } if (allMixinConfigs == null && getReadMixinConfigsFromManifest().get()) { - allMixinConfigs = readMixinConfigsFromManifest(); + allMixinConfigs = ModBuildExtensions.readMixinConfigsFromManifest(getInputFile().get().getAsFile()); } if (allMixinConfigs == null) { @@ -309,27 +272,6 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { } } - private Collection readMixinConfigsFromManifest() { - File inputJar = getInputFile().get().getAsFile(); - - try (JarFile jar = new JarFile(inputJar)) { - @Nullable Manifest manifest = jar.getManifest(); - - if (manifest != null) { - Attributes attributes = manifest.getMainAttributes(); - String mixinConfigs = attributes.getValue(Constants.Forge.MIXIN_CONFIGS_MANIFEST_KEY); - - if (mixinConfigs != null) { - return Set.of(mixinConfigs.split(",")); - } - } - - return Set.of(); - } catch (IOException e) { - throw new UncheckedIOException("Could not read mixin configs from input jar", e); - } - } - public interface RemapParams extends AbstractRemapParams { ConfigurableFileCollection getNestedJars(); @@ -381,7 +323,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { addRefmaps(); addNestedJars(); - convertAwToAt(); + ModBuildExtensions.convertAwToAt(getParameters().getAtAccessWideners(), outputFile, getParameters().getMappingBuildServiceUuid()); if (getParameters().getPlatform().get() != ModPlatform.FORGE) { modifyJarManifest(); @@ -455,55 +397,6 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { ZipUtils.replace(outputFile, accessWidenerFile.path(), remapped); } - private void convertAwToAt() throws IOException { - if (!this.getParameters().getAtAccessWideners().isPresent()) { - return; - } - - Set atAccessWideners = this.getParameters().getAtAccessWideners().get(); - - if (atAccessWideners.isEmpty()) { - return; - } - - AccessTransformSet at = AccessTransformSet.create(); - File jar = outputFile.toFile(); - - try (FileSystemUtil.Delegate fileSystem = FileSystemUtil.getJarFileSystem(jar, false)) { - FileSystem fs = fileSystem.get(); - Path atPath = fs.getPath(Constants.Forge.ACCESS_TRANSFORMER_PATH); - - if (Files.exists(atPath)) { - throw new FileAlreadyExistsException("Jar " + jar + " already contains an access transformer - cannot convert AWs!"); - } - - for (String aw : atAccessWideners) { - Path awPath = fs.getPath(aw); - - if (Files.notExists(awPath)) { - throw new NoSuchFileException("Could not find AW '" + aw + "' to convert into AT!"); - } - - try (BufferedReader reader = Files.newBufferedReader(awPath, StandardCharsets.UTF_8)) { - at.merge(Aw2At.toAccessTransformSet(reader)); - } - - Files.delete(awPath); - } - - MappingsService service = UnsafeWorkQueueHelper.get(getParameters().getMappingBuildServiceUuid(), MappingsService.class); - - try (TinyMappingsReader reader = new TinyMappingsReader(service.getMemoryMappingTree(), service.getFromNamespace(), service.getToNamespace())) { - MappingSet mappingSet = reader.read(); - at = at.remap(mappingSet); - } - - try (Writer writer = new LfWriter(Files.newBufferedWriter(atPath))) { - AccessTransformFormats.FML.write(writer, at); - } - } - } - private byte[] remapAccessWidener(byte[] input) { int version = AccessWidenerReader.readVersion(input); diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/architectury/ArchitecturyCommonJsonTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/architectury/ArchitecturyCommonJsonTest.groovy new file mode 100644 index 00000000..8e8ca991 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/architectury/ArchitecturyCommonJsonTest.groovy @@ -0,0 +1,116 @@ +/* + * 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.architectury + +import com.google.gson.JsonObject +import dev.architectury.loom.metadata.ArchitecturyCommonJson +import spock.lang.Specification +import spock.lang.TempDir + +import java.nio.charset.StandardCharsets +import java.nio.file.Path + +class ArchitecturyCommonJsonTest extends Specification { + private static final String OF_TEST_INPUT = '{"accessWidener":"foo.accesswidener"}' + + @TempDir + Path tempDir + + def "create from byte[]"() { + given: + def bytes = OF_TEST_INPUT.getBytes(StandardCharsets.UTF_8) + when: + def acj = ArchitecturyCommonJson.of(bytes) + then: + acj.accessWidener == 'foo.accesswidener' + } + + def "create from String"() { + when: + def acj = ArchitecturyCommonJson.of(OF_TEST_INPUT) + then: + acj.accessWidener == 'foo.accesswidener' + } + + def "create from File"() { + given: + def file = new File(tempDir.toFile(), 'architectury.common.json') + file.text = OF_TEST_INPUT + when: + def acj = ArchitecturyCommonJson.of(file) + then: + acj.accessWidener == 'foo.accesswidener' + } + + def "create from Path"() { + given: + def path = tempDir.resolve('architectury.common.json') + path.text = OF_TEST_INPUT + when: + def acj = ArchitecturyCommonJson.of(path) + then: + acj.accessWidener == 'foo.accesswidener' + } + + def "create from JsonObject"() { + given: + def json = new JsonObject() + json.addProperty('accessWidener', 'foo.accesswidener') + when: + def acj = ArchitecturyCommonJson.of(json) + then: + acj.accessWidener == 'foo.accesswidener' + } + + def "read access widener"() { + given: + def acj = ArchitecturyCommonJson.of(jsonText) + when: + def accessWidenerName = acj.accessWidener + then: + accessWidenerName == expectedAw + where: + jsonText | expectedAw + '{}' | null + '{"accessWidener":"foo.accesswidener"}' | 'foo.accesswidener' + } + + def "read injected interfaces"() { + given: + def acj = ArchitecturyCommonJson.of(jsonText) + when: + def injectedInterfaces = acj.getInjectedInterfaces('foo') + Map> itfMap = [:] + for (def entry : injectedInterfaces) { + itfMap.computeIfAbsent(entry.className()) { [] }.add(entry.ifaceName()) + } + then: + itfMap == expected + where: + jsonText | expected + '{}' | [:] + '{"injected_interfaces":{"target/class/Here":["my/Interface","another/Itf"]}}' | ['target/class/Here': ['my/Interface', 'another/Itf']] + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/quilt/QuiltModJsonTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/quilt/QuiltModJsonTest.groovy new file mode 100644 index 00000000..e606d9e2 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/quilt/QuiltModJsonTest.groovy @@ -0,0 +1,131 @@ +/* + * 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.quilt + +import com.google.gson.JsonObject +import dev.architectury.loom.metadata.QuiltModJson +import spock.lang.Specification +import spock.lang.TempDir + +import java.nio.charset.StandardCharsets +import java.nio.file.Path + +class QuiltModJsonTest extends Specification { + private static final String OF_TEST_INPUT = '{"access_widener":"foo.accesswidener"}' + + @TempDir + Path tempDir + + def "create from byte[]"() { + given: + def bytes = OF_TEST_INPUT.getBytes(StandardCharsets.UTF_8) + when: + def qmj = QuiltModJson.of(bytes) + then: + qmj.accessWidener == 'foo.accesswidener' + } + + def "create from String"() { + when: + def qmj = QuiltModJson.of(OF_TEST_INPUT) + then: + qmj.accessWidener == 'foo.accesswidener' + } + + def "create from File"() { + given: + def file = new File(tempDir.toFile(), 'quilt.mod.json') + file.text = OF_TEST_INPUT + when: + def qmj = QuiltModJson.of(file) + then: + qmj.accessWidener == 'foo.accesswidener' + } + + def "create from Path"() { + given: + def path = tempDir.resolve('quilt.mod.json') + path.text = OF_TEST_INPUT + when: + def qmj = QuiltModJson.of(path) + then: + qmj.accessWidener == 'foo.accesswidener' + } + + def "create from JsonObject"() { + given: + def json = new JsonObject() + json.addProperty('access_widener', 'foo.accesswidener') + when: + def qmj = QuiltModJson.of(json) + then: + qmj.accessWidener == 'foo.accesswidener' + } + + def "read access widener"() { + given: + def qmj = QuiltModJson.of(jsonText) + when: + def accessWidenerName = qmj.accessWidener + then: + accessWidenerName == expectedAw + where: + jsonText | expectedAw + '{}' | null + '{"access_widener":"foo.accesswidener"}' | 'foo.accesswidener' + '{"access_widener":["bar.accesswidener"]}' | 'bar.accesswidener' + } + + def "read injected interfaces"() { + given: + def qmj = QuiltModJson.of(jsonText) + when: + def injectedInterfaces = qmj.getInjectedInterfaces('foo') + Map> itfMap = [:] + for (def entry : injectedInterfaces) { + itfMap.computeIfAbsent(entry.className()) { [] }.add(entry.ifaceName()) + } + then: + itfMap == expected + where: + jsonText | expected + '{}' | [:] + '{"quilt_loom":{"injected_interfaces":{"target/class/Here":["my/Interface","another/Itf"]}}}' | ['target/class/Here': ['my/Interface', 'another/Itf']] + } + + def "read mixin configs"() { + given: + def qmj = QuiltModJson.of(jsonText) + when: + def mixinConfigs = qmj.mixinConfigs + then: + mixinConfigs == expected + where: + jsonText | expected + '{}' | [] + '{"mixin":"foo.mixins.json"}' | ['foo.mixins.json'] + '{"mixin":["foo.mixins.json","bar.mixins.json"]}' | ['foo.mixins.json', 'bar.mixins.json'] + } +}