diff --git a/build.gradle b/build.gradle index a16bc5c..744197c 100644 --- a/build.gradle +++ b/build.gradle @@ -54,6 +54,7 @@ dependencies { api libs.srgutils implementation libs.bundles.asm implementation libs.toml + implementation libs.gson } tasks.named('jar', Jar) { diff --git a/settings.gradle b/settings.gradle index 3c557a9..3902bec 100644 --- a/settings.gradle +++ b/settings.gradle @@ -34,6 +34,7 @@ dependencyResolutionManagement.versionCatalogs.register('libs') { library 'srgutils', 'net.minecraftforge', 'srgutils' version '0.6.0' library 'powermock', 'org.powermock', 'powermock-core' version '2.0.9' library 'toml', 'com.github.jezza', 'toml' version '1.2-java-8' + library 'gson', 'com.google.code.gson', 'gson' version '2.14.0' version 'asm', '9.9.1' library 'asm', 'org.ow2.asm', 'asm' versionRef 'asm' diff --git a/src/main/java/net/minecraftforge/renamer/Main.java b/src/main/java/net/minecraftforge/renamer/Main.java index fd26c0e..e4570ba 100644 --- a/src/main/java/net/minecraftforge/renamer/Main.java +++ b/src/main/java/net/minecraftforge/renamer/Main.java @@ -79,6 +79,7 @@ public static void main(String[] args) throws IOException { OptionSpec accessTransformersO = parser.accepts("access-transformers", "Enable renaming of access transformers. Located via: FMLAT manifest entry, `META-INF/accesstransformer.cfg` or MANIFEST/mods.toml").availableIf(mapO); OptionSpec legacyATFormatO = parser.accepts("legacy-access-transformers", "If Access Transformers are enabled, will output the transformer in legacy format, which is used in FML <1.6.4").availableIf(mapO); OptionSpec storeO = parser.accepts("store", "Disables compression, this is designed to produce stable output archives no matter what zlib implementation the user has installed"); + OptionSpec mixinsO = parser.accepts("mixins", "Enable transforming of Mixin related files. Only RefMaps are currently supported."); OptionSpec helpO = parser.accepts("help", "Prints help and exits").forHelp(); OptionSet options; @@ -171,6 +172,10 @@ public static void main(String[] args) throws IOException { log.accept("Rename Access Transformers" + (legacy ? " (legacy)" : "")); renamer.accessTransformers(legacy); } + if (options.has(mixinsO)) { + log.accept("Rename Mixins"); + renamer.mixins(); + } builder.add(renamer.build()); } else { log.accept("Names: null"); diff --git a/src/main/java/net/minecraftforge/renamer/api/Transformer.java b/src/main/java/net/minecraftforge/renamer/api/Transformer.java index c140b1d..14ba693 100644 --- a/src/main/java/net/minecraftforge/renamer/api/Transformer.java +++ b/src/main/java/net/minecraftforge/renamer/api/Transformer.java @@ -391,6 +391,14 @@ static Renamer builder(IMappingFile map) { */ Renamer accessTransformers(boolean legacyFormat); + /** + * Enables renaming of Mixin related files. + * Currently only refmaps are supported. It is possible to support inherited mappings for Mixin features + * such as the Shadow annotation. Making the AnnotationProcessor's extra mappings file unneeded. + * However this would require documenting each feature that would require inheritance. + */ + Renamer mixins(); + Factory build(); } } diff --git a/src/main/java/net/minecraftforge/renamer/internal/EnhancedRemapper.java b/src/main/java/net/minecraftforge/renamer/internal/EnhancedRemapper.java index b33fe3d..9c9c804 100644 --- a/src/main/java/net/minecraftforge/renamer/internal/EnhancedRemapper.java +++ b/src/main/java/net/minecraftforge/renamer/internal/EnhancedRemapper.java @@ -97,7 +97,7 @@ public String mapAnnotationAttributeName(final String descriptor, final String n return lst.get(0).getMapped(); } - private final String naive(String value) { + final String naive(String value) { return this.naiveSrgMap == null ? value : this.naiveSrgMap.getOrDefault(value, value); } diff --git a/src/main/java/net/minecraftforge/renamer/internal/MixinRenamer.java b/src/main/java/net/minecraftforge/renamer/internal/MixinRenamer.java new file mode 100644 index 0000000..dbeba27 --- /dev/null +++ b/src/main/java/net/minecraftforge/renamer/internal/MixinRenamer.java @@ -0,0 +1,276 @@ +/* + * Copyright (c) Forge Development LLC + * SPDX-License-Identifier: LGPL-2.1-only + */ +package net.minecraftforge.renamer.internal; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.Reader; +import java.nio.charset.StandardCharsets; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.function.Consumer; +import java.util.jar.Attributes; +import java.util.jar.Manifest; + +import org.jetbrains.annotations.Nullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import net.minecraftforge.renamer.api.Transformer; + +final class MixinRenamer implements Transformer { + private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; + private static final Attributes.Name MIXIN_CONFIGS = new Attributes.Name("MixinConfigs"); + private static final Gson GSON = new GsonBuilder() + .disableHtmlEscaping() + .setPrettyPrinting() + .create(); + + private final Set refmaps = new HashSet<>(); + private final Consumer logger; + private final EnhancedRemapper remapper; + + MixinRenamer(Consumer logger, EnhancedRemapper remapper) { + this.logger = logger; + this.remapper = remapper; + } + + boolean isTarget(String entry) { + return this.refmaps.contains(entry); + } + + @Override + public void preprocess(Map entries) { + Entry entry = entries.get(MANIFEST_NAME); + if (entry == null) // No Manifest, so no configs. + return; + + String[] cfgs = findConfigs(entries.get(MANIFEST_NAME)); + if (cfgs == null) + return; + + for (String cfg : cfgs) { + entry = entries.get(cfg); + if (entry == null) { + logger.accept("Missing mixin config: " + cfg); + continue; + } + + String refmap = findRefMap(entry); + if (refmap != null) + refmaps.add(refmap); + } + } + + @Override + public ResourceEntry process(ResourceEntry resource) { + if (!refmaps.contains(resource.getName())) + return resource; + + Refmap refmap = null; + try (Reader reader = new InputStreamReader(new ByteArrayInputStream(resource.getData()))) { + refmap = GSON.fromJson(reader, Refmap.class); + } catch (IOException e) { + logger.accept("Failed to parse Mixin Refmap " + resource.getName() + ": " + e); + return resource; + } + + if (refmap.mappings == null || refmap.mappings.isEmpty()) + return resource; + + Map> output = new LinkedHashMap<>(refmap.mappings.size()); + for (Map.Entry> mixin : refmap.mappings.entrySet()) { + Map _new = new LinkedHashMap<>(mixin.getValue().size()); + output.put(mixin.getKey(), _new); + + for (Map.Entry entry : mixin.getValue().entrySet()) { + MemberInfo old = MemberInfo.parse(entry.getValue()); + MemberInfo mapped = old.map(this.remapper); + _new.put(entry.getKey(), mapped.toString()); + } + } + + refmap.mappings = output; + String json = GSON.toJson(refmap); + return ResourceEntry.create(resource.getName(), resource.getTime(), json.getBytes(StandardCharsets.UTF_8)); + } + + private String @Nullable [] findConfigs(Entry entry) { + try { + Manifest mf = new Manifest(new ByteArrayInputStream(entry.getData())); + String value = (String)mf.getMainAttributes().get(MIXIN_CONFIGS); + return value == null ? null : value.split(","); + } catch (IOException e) { + logger.accept("Failed to parse manifest: " + e); + return null; + } + } + + private @Nullable String findRefMap(Entry entry) { + try (Reader reader = new InputStreamReader(new ByteArrayInputStream(entry.getData()))) { + MixinConfig cfg = GSON.fromJson(reader, MixinConfig.class); + return cfg.refmap; + } catch (IOException e) { + logger.accept("Failed to parse Mixin Config " + entry.getName() + ": " + e); + return null; + } + } + + private static class MixinConfig { + /* + * I could read these values, and then parse the mixins themselves to fully remap things. + * But that would require special casing all mixin annotations and keeping up to date on them + * Which is a much bigger task then I want to do right now. + * If someone DID want to tackle that, I have most of the logic done in Srg2Source + * https://github.com/MinecraftForge/Srg2Source/tree/master/src/main/java/net/minecraftforge/srg2source/mixin + @SerializedName("package") + public String mixinPackage; + public List mixins; + public List client; + public List server; + */ + public String refmap; + } + + private static class Refmap { + public Map> mappings; + // This might be useful information, but for the current use case, we just nuke this info + //public @Nullable Map>> data; + } + + private static final class MemberInfo { + private final String owner; + private final String name; + private final String quantifier; + private final String desc; + private final String tail; + + private final String asString; + + private MemberInfo(String owner, String name, String quantifier, String desc, String tail) { + this.owner = owner; + this.name = name; + this.quantifier = quantifier; + this.desc = desc; + this.tail = tail; + + StringBuilder buf = new StringBuilder(); + if (this.owner != null) + buf.append('L').append(this.owner).append(';'); + if (this.name != null) + buf.append(this.name); + if (this.quantifier != null) + buf.append(this.quantifier); + if (this.desc != null) { + if (this.desc.charAt(0) != '(') + buf.append(':'); + buf.append(this.desc); + } + if (this.tail != null) + buf.append(this.tail); + this.asString = buf.toString(); + } + + @Override + public String toString() { + return this.asString; + } + + private MemberInfo map(EnhancedRemapper remapper) { + String owner = this.owner == null ? null : remapper.map(this.owner); + + String name = null; + if (this.name != null) { + // No owner, means we need to do the 'naive' srg lookup + if (this.owner == null) { + name = remapper.naive(this.name); + } else { + if (this.desc != null && this.desc.charAt(0) == '(') + name = remapper.mapMethodName(this.owner, this.name, this.desc); + else + name = remapper.mapFieldName(this.owner, this.name, this.desc); + } + } + + String desc = null; + if (this.desc != null) + desc = this.desc.charAt(0) == '(' ? remapper.mapMethodDesc(this.desc) : remapper.mapDesc(this.desc); + + return new MemberInfo(owner, name, this.quantifier, desc, this.tail); + } + + private static MemberInfo parse(final String input) { + String owner = null; + String name = input.replaceAll("\\s", ""); + String quantifier = null; + String desc = null; + String tail = null; + + // Find tail, I think this is just legacy, but support it anyways + int pos = name.indexOf("->"); + if (pos > -1) { + tail = name.substring(pos); + name = name.substring(0, pos); + } + + // Find the desc, can be either a field with : or a normal method desc + pos = name.lastIndexOf(':'); + if (pos != -1) { // Field name:desc + desc = name.substring(pos + 1); + name = name.substring(0, pos); + } else { + pos = name.lastIndexOf('('); + if (pos != -1) { + desc = name.substring(pos); + name = name.substring(0, pos); + } + } + + pos = name.lastIndexOf('.'); + if (pos != -1) { // Legacy format: owner.name + owner = name.substring(0, pos).replace('.', '/'); + name = name.substring(pos + 1); + } else if (name.charAt(0) == 'L') { // Modern format: Lowner;name + pos = name.indexOf(';'); + if (pos != -1) { + owner = name.substring(1, pos).replace('.', '/'); + name = name.substring(pos + 1); + } + } + + if (owner == null) { // Possibly a full class name + name = name.replace('.', '/'); + if (name.indexOf('/') != -1) { + owner = name; + name = ""; + } + } + + // Pull out the qualifier if there is one + if (!name.isEmpty()) { + char last = name.charAt(name.length() - 1); + if (last == '*' || last == '+') { + quantifier = name.substring(name.length() - 1); + name = name.substring(0, name.length() - 1); + } else { + pos = name.indexOf('{'); + if (pos != -1) { + quantifier = name.substring(pos); + name = name.substring(0, pos); + } + } + } + + if (name.isEmpty()) + name = null; + + return new MemberInfo(owner, name, quantifier, desc, tail); + } + } +} + diff --git a/src/main/java/net/minecraftforge/renamer/internal/RenamingTransformer.java b/src/main/java/net/minecraftforge/renamer/internal/RenamingTransformer.java index 82209ab..efcc5b2 100644 --- a/src/main/java/net/minecraftforge/renamer/internal/RenamingTransformer.java +++ b/src/main/java/net/minecraftforge/renamer/internal/RenamingTransformer.java @@ -23,6 +23,7 @@ import java.util.jar.Manifest; import java.util.stream.Collectors; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassWriter; import org.objectweb.asm.commons.ClassRemapper; @@ -48,6 +49,7 @@ public class RenamingTransformer implements Transformer { private final boolean legacyForamt; private final Consumer logger; private final Set atPaths = new HashSet<>(); + private final @Nullable MixinRenamer mixins; private RenamingTransformer(Transformer.Context ctx, Builder builder) { this.collectAbstractParams = builder.collectAbstractParameters; @@ -55,6 +57,7 @@ private RenamingTransformer(Transformer.Context ctx, Builder builder) { this.legacyForamt = builder.legacyFormat; this.logger = ctx.getLog(); this.remapper = new EnhancedRemapper(ctx.getClassProvider(), builder.map, this.logger, builder.naiveSrg); + this.mixins = builder.mixins ? new MixinRenamer(this.logger, this.remapper) : null; } @Override @@ -103,6 +106,8 @@ public void preprocess(Map entries) { } } } + if (this.mixins != null) + this.mixins.preprocess(entries); } @Override @@ -129,6 +134,9 @@ public ResourceEntry process(ResourceEntry entry) { if (this.atPaths.contains(entry.getName())) return renameAccessTransformer(entry); + if (this.mixins != null && this.mixins.isTarget(entry.getName())) + return this.mixins.process(entry); + return entry; } @@ -210,6 +218,7 @@ public static class Builder implements Transformer.Renamer { private boolean collectAbstractParameters = false; private boolean renameAts = false; private boolean legacyFormat = false; + private boolean mixins = false; public Builder(IMappingFile map) { this.map = map; @@ -238,5 +247,11 @@ public Builder accessTransformers(boolean legacyFormat) { this.legacyFormat = legacyFormat; return this; } + + @Override + public Builder mixins() { + this.mixins = true; + return this; + } } }