diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java index 5b7c804ce..69843adb6 100644 --- a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/TestMixinGuiHelper.java @@ -1,5 +1,8 @@ package net.fabricmc.minecraft.test.server_only; +import org.quiltmc.loader.api.minecraft.DedicatedServerOnly; + +@DedicatedServerOnly public class TestMixinGuiHelper { public static void help() { diff --git a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java index 699f8da12..d1573bf03 100644 --- a/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java +++ b/minecraft/minecraft-test/src/main/java/net/fabricmc/minecraft/test/server_only/package-info.java @@ -1,3 +1,4 @@ -// @org.quiltmc.loader.api.minecraft.DedicatedServerOnly +//@org.quiltmc.loader.api.minecraft.DedicatedServerOnly +//@org.quiltmc.loader.api.Requires({"trinkets", "quilt_loader", "minecraft", "buildcraft"}) // Uncomment the above line to test out package based stripping. package net.fabricmc.minecraft.test.server_only; diff --git a/src/main/java/org/quiltmc/loader/api/Requires.java b/src/main/java/org/quiltmc/loader/api/Requires.java index c6ddeadc9..6e8e19f8e 100644 --- a/src/main/java/org/quiltmc/loader/api/Requires.java +++ b/src/main/java/org/quiltmc/loader/api/Requires.java @@ -25,7 +25,7 @@ /** Applied to declare that the annotated element requires specific mods to exist. *

- * When applied to mod code this will result in quilt-loader removing that element when running without the specified mods. + * When applied to mod code this will result in quilt-loader removing that element when running without all of the specified mods. *

* When the annotated element is removed, bytecode associated with the element will not be removed. For example, if a * field is removed, its initializer code will not, and will cause an error on execution. diff --git a/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java b/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java index e3f54fa05..2845928ab 100644 --- a/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java +++ b/src/main/java/org/quiltmc/loader/impl/launch/common/QuiltLauncher.java @@ -23,6 +23,7 @@ import java.net.URL; import java.nio.file.Path; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.jar.Manifest; @@ -40,6 +41,7 @@ public interface QuiltLauncher { void setAllowedPrefixes(Path path, String... prefixes); void setTransformCache(URL insideTransformCache); void setHiddenClasses(Set classes); + void setHiddenClasses(Map classes); void hideParentUrl(URL hidden); void hideParentPath(Path obf); void validateGameClassLoader(Object gameInstance); diff --git a/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java b/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java index b32e8a17a..9053671cd 100644 --- a/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java +++ b/src/main/java/org/quiltmc/loader/impl/launch/knot/Knot.java @@ -313,6 +313,11 @@ public void setHiddenClasses(Set hiddenClasses) { classLoader.getDelegate().setHiddenClasses(hiddenClasses); } + @Override + public void setHiddenClasses(Map hiddenClasses) { + classLoader.getDelegate().setHiddenClasses(hiddenClasses); + } + @Override public void hideParentUrl(URL parent) { classLoader.getDelegate().hideParentUrl(parent); diff --git a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java index f63748dbf..853c64421 100644 --- a/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java +++ b/src/main/java/org/quiltmc/loader/impl/launch/knot/KnotClassDelegate.java @@ -52,6 +52,8 @@ import java.security.CodeSource; import java.security.cert.Certificate; import java.util.Collections; +import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -97,7 +99,7 @@ public Optional getQuiltMod() { private IMixinTransformer mixinTransformer; private boolean transformInitialized = false; private boolean transformFinishedLoading = false; - private Set hiddenClasses = Collections.emptySet(); + private Map hiddenClasses = Collections.emptyMap(); private String transformCacheUrl; private final Map allowedPrefixes = new ConcurrentHashMap<>(); private final Set parentSourcedClasses = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -106,8 +108,8 @@ public Optional getQuiltMod() { * in this loader. */ private final Set parentHiddenUrls = Collections.newSetFromMap(new ConcurrentHashMap<>()); - /** Map of package to whether we can load it in this environment. */ - private final Map packageSideCache = new ConcurrentHashMap<>(); + /** Map of package to the reason why it cannot be loaded. If the package can be loaded then the value is the empty string. */ + private final Map packageLoadDenyCache = new ConcurrentHashMap<>(); KnotClassDelegate(boolean isDevelopment, EnvType envType, KnotClassLoaderInterface itf, GameProvider provider) { this.isDevelopment = isDevelopment; @@ -238,6 +240,25 @@ Class tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound } } + int pkgDelimiterPos = name.lastIndexOf('.'); + String pkgString = pkgDelimiterPos > 0 ? name.substring(0, pkgDelimiterPos) : null; + + if (pkgString != null) { + final boolean allowFromParentFinal = allowFromParent; + String denyReason = packageLoadDenyCache.computeIfAbsent(pkgString, pkgName -> { + return computePackageDenyLoadReason(pkgName, allowFromParentFinal); + }); + + if (denyReason != null && !denyReason.isEmpty()) { + throw new RuntimeException("Cannot load package " + pkgString + " " + denyReason); + } + } + + String hideReason = hiddenClasses.get(name); + if (hideReason != null) { + throw new RuntimeException("Cannot load " + name + " " + hideReason); + } + byte[] input = getPostMixinClassByteArray(url, name); if (input == null) return null; @@ -247,8 +268,6 @@ Class tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound KnotClassDelegate.Metadata metadata = getMetadata(name, url); - int pkgDelimiterPos = name.lastIndexOf('.'); - final String modId; if (metadata.codeSource == null) { @@ -268,19 +287,8 @@ Class tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound return c; } - if (pkgDelimiterPos > 0) { + if (pkgString != null) { // TODO: package definition stub - String pkgString = name.substring(0, pkgDelimiterPos); - - final boolean allowFromParentFinal = allowFromParent; - Boolean permitted = packageSideCache.computeIfAbsent(pkgString, pkgName -> { - return computeCanLoadPackage(pkgName, allowFromParentFinal); - }); - - if (permitted != null && !permitted) { - throw new RuntimeException("Cannot load package " + pkgString + " in environment type " + envType); - } - Package pkg = itf.getPackage(pkgString); if (pkg == null) { @@ -310,28 +318,15 @@ Class tryLoadClass(String name, boolean allowFromParent) throws ClassNotFound return c; } + private boolean shouldRerouteToParent(String name) { return name.startsWith("org.slf4j.") || name.startsWith("org.apache.logging.log4j."); } - boolean computeCanLoadPackage(String pkgName, boolean allowFromParent) { + private String computePackageDenyLoadReason(String pkgName, boolean allowFromParent) { String fileName = pkgName + ".package-info"; - try { - byte[] bytes = getRawClassByteArray(fileName, allowFromParent); - if (bytes == null) { - // No package-info class file - return true; - } - - ClassReader reader = new ClassReader(bytes); - - PackageStrippingData strippingData = new PackageStrippingData(QuiltLoaderImpl.ASM_VERSION, envType, modCodeSourceMap); - reader.accept(strippingData, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); - - return !strippingData.stripEntirePackage(); - } catch (IOException e) { - throw new RuntimeException("Unable to load " + fileName, e); - } + String hideReason = hiddenClasses.get(fileName); + return hideReason != null ? hideReason : ""; } Metadata getMetadata(String name, URL resourceURL) { @@ -508,10 +503,6 @@ public byte[] getRawClassByteArray(String name, boolean allowFromParent) throws } public byte[] getRawClassByteArray(URL url, String name) throws IOException { - if (hiddenClasses.contains(name)) { - return null; - } - try (InputStream inputStream = (url != null ? url.openStream() : null)) { if (inputStream == null) { return null; @@ -533,6 +524,14 @@ void setTransformCache(URL insideTransformCache) { } void setHiddenClasses(Set hiddenClasses) { + Map map = new HashMap<>(); + for (String cl : hiddenClasses) { + map.put(cl, "unknown reason"); + } + setHiddenClasses(map); + } + + void setHiddenClasses(Map hiddenClasses) { this.hiddenClasses = hiddenClasses; } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/AbstractStripData.java b/src/main/java/org/quiltmc/loader/impl/transformer/AbstractStripData.java new file mode 100644 index 000000000..ecfcaa2f1 --- /dev/null +++ b/src/main/java/org/quiltmc/loader/impl/transformer/AbstractStripData.java @@ -0,0 +1,110 @@ +package org.quiltmc.loader.impl.transformer; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +import org.objectweb.asm.ClassVisitor; +import org.quiltmc.loader.api.Requires; +import org.quiltmc.loader.impl.util.QuiltLoaderInternal; +import org.quiltmc.loader.impl.util.QuiltLoaderInternalType; + +import net.fabricmc.api.EnvType; + +/** Contains string processing for both {@link PackageStrippingData} and {@link ClassStrippingData} */ +@QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) +public abstract class AbstractStripData extends ClassVisitor { + + protected final EnvType envType; + protected final Set mods; + + protected final List denyLoadReasons = new ArrayList<>(); + + protected AbstractStripData(int api, EnvType envType, Set mods) { + this(api, null, envType, mods); + } + + protected AbstractStripData(int api, ClassVisitor classVisitor, EnvType envType, Set mods) { + super(api, classVisitor); + this.envType = envType; + this.mods = mods; + } + + /** @return What this represents - generally "package" or "class". */ + protected abstract String type(); + + public List getDenyLoadReasons() { + return Collections.unmodifiableList(denyLoadReasons); + } + + public String summarizeDenyLoadReasons() { + if (denyLoadReasons.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder(); + if (denyLoadReasons.size() == 1) { + sb.append("because "); + sb.append(denyLoadReasons.get(0)); + } else { + sb.append("because:"); + for (String reason : denyLoadReasons) { + sb.append("\n- "); + sb.append(reason); + } + } + return sb.toString(); + } + + /** Appends a client-only deny reason to {@link #denyLoadReasons}. */ + protected void denyClientOnlyLoad() { + denyLoadReasons.add("the " + type() + " is annotated with @ClientOnly but we're on the dedicated server"); + } + + /** Appends a dedicated server only deny reason to {@link #denyLoadReasons}. */ + protected void denyDediServerOnlyLoad() { + denyLoadReasons.add("the " + type() + " is annotated with @DedicatedServerOnly but we're on the client"); + } + + /** Checks to see if all mods given are in {@link #mods}, and appends a deny reason to {@link #denyLoadReasons} if + * any are missing. Assumes the annotation is {@link Requires} in the error message */ + protected void checkHasAllMods(List requiredMods) { + List missingMods = new ArrayList<>(); + for (String mod : requiredMods) { + if (!mods.contains(mod)) { + missingMods.add(mod); + } + } + + checkHasAllMods(requiredMods, missingMods); + } + + /** Checks to see if the missing mods list is empty, and appends a deny reason to {@link #denyLoadReasons} if it is + * not. Assumes the annotation is {@link Requires} in the error message */ + protected void checkHasAllMods(List requiredMods, List missingMods) { + if (!missingMods.isEmpty()) { + StringBuilder all = new StringBuilder(); + if (requiredMods.size() > 1) { + all.append("{"); + } + + for (String mod : requiredMods) { + if (all.length() > 1) { + all.append(", "); + } + all.append("\""); + all.append(mod); + all.append("\""); + } + + if (requiredMods.size() > 1) { + all.append("}"); + } + + denyLoadReasons.add( + "the " + type() + " is annotated with @Requires(" + all + ") but the mods " + missingMods + + " are not loaded!" + ); + } + } +} diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java b/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java index 6db35bbb1..3ed98e0bb 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/ChasmInvoker.java @@ -213,7 +213,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO case REMOVED: { QuiltMetadata qm = this_value_is_actually_nullable(result.getMetadata().get(QuiltMetadata.class)); if (qm != null) { - cache.hideClass(LoaderUtil.getClassNameFromTransformCache(qm.name)); + cache.hideClass(LoaderUtil.getClassNameFromTransformCache(qm.name), "it was removed by a chasm transformer"); } else { throw new UnsupportedOperationException("Cannot remove unknown class"); } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/ClassStrippingData.java similarity index 78% rename from src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java rename to src/main/java/org/quiltmc/loader/impl/transformer/ClassStrippingData.java index 8b62d840c..304da8302 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/StrippingData.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/ClassStrippingData.java @@ -20,6 +20,7 @@ import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.FieldVisitor; import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.TypePath; import org.objectweb.asm.TypeReference; @@ -35,14 +36,16 @@ import net.fabricmc.api.EnvironmentInterface; import net.fabricmc.api.EnvironmentInterfaces; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; +import java.util.Set; import java.util.stream.Collectors; /** Scans a class for Environment, EnvironmentInterface and Requires annotations to figure out what needs to be stripped. */ @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) -public class StrippingData extends ClassVisitor { +public class ClassStrippingData extends AbstractStripData { // Fabric annotations private static final String ENVIRONMENT_DESCRIPTOR = Type.getDescriptor(Environment.class); @@ -54,11 +57,8 @@ public class StrippingData extends ClassVisitor { private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class); private static final String REQUIRES_DESCRIPTOR = Type.getDescriptor(Requires.class); - private final EnvType envType; private final String envTypeString; - private final List mods; - private boolean stripEntireClass = false; private final Collection stripInterfaces = new HashSet<>(); private final Collection stripFields = new HashSet<>(); private final Collection stripMethods = new HashSet<>(); @@ -66,6 +66,7 @@ public class StrippingData extends ClassVisitor { /** Every method contained in this will also be contained in {@link #stripMethods}. */ final Collection stripMethodLambdas = new HashSet<>(); + private String type = "class"; private String[] interfaces; private class FabricEnvironmentAnnotationVisitor extends AnnotationVisitor { @@ -142,14 +143,28 @@ public void visitEnd() { } } + @FunctionalInterface + private interface OnModsMissing { + void onModsMissing(List required, List missing); + } + private class QuiltRequiresAnnotationVisitor extends AnnotationVisitor { + private final OnModsMissing onModsMissingDetailed; private final Runnable onModsMismatch; private final Runnable onModsMismatchLambdas; private boolean stripLambdas = true; + private QuiltRequiresAnnotationVisitor(int api, OnModsMissing onModsMissing) { + super(api); + this.onModsMissingDetailed = onModsMissing; + this.onModsMismatch = null; + this.onModsMismatchLambdas = null; + } + private QuiltRequiresAnnotationVisitor(int api, Runnable onModsMismatch, Runnable onModsMismatchLambdas) { super(api); + this.onModsMissingDetailed = null; this.onModsMismatch = onModsMismatch; this.onModsMismatchLambdas = onModsMismatchLambdas; } @@ -164,19 +179,51 @@ public void visit(String name, Object value) { @Override public AnnotationVisitor visitArray(String name) { if ("value".equals(name)) { - return new AnnotationVisitor(api) { + if (onModsMissingDetailed == null) { + return new AnnotationVisitor(api) { - @Override - public void visit(String name, Object value) { - if (!mods.contains(String.valueOf(value))) { - onModsMismatch.run(); + boolean anyMissing = false; - if (stripLambdas && onModsMismatchLambdas != null) { - onModsMismatchLambdas.run(); + @Override + public void visit(String name, Object value) { + if (!mods.contains(String.valueOf(value))) { + anyMissing = true; } } - } - }; + + @Override + public void visitEnd() { + if (anyMissing) { + onModsMismatch.run(); + + if (stripLambdas && onModsMismatchLambdas != null) { + onModsMismatchLambdas.run(); + } + } + } + }; + } else { + return new AnnotationVisitor(api) { + List requiredMods = new ArrayList<>(); + List missingMods = new ArrayList<>(); + + @Override + public void visit(String name, Object value) { + String mod = String.valueOf(value); + requiredMods.add(mod); + if (!mods.contains(mod)) { + missingMods.add(mod); + } + } + + @Override + public void visitEnd() { + if (!missingMods.isEmpty()) { + onModsMissingDetailed.onModsMissing(requiredMods, missingMods); + } + } + }; + } } else { return null; @@ -205,32 +252,49 @@ private AnnotationVisitor visitMemberAnnotation(String descriptor, boolean visib return null; } - public StrippingData(int api, EnvType envType, List mods) { - super(api); - this.envType = envType; + public ClassStrippingData(int api, EnvType envType, List mods) { + super(api, envType, mods.stream().map(ModLoadOption::id).collect(Collectors.toSet())); this.envTypeString = envType.name(); - this.mods = mods.stream().map(ModLoadOption::id).collect(Collectors.toList()); } @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { this.interfaces = interfaces; + + if (name.endsWith("/package-info")) { + type = "package"; + } else if ((access & Opcodes.ACC_ENUM) != 0) { + type = "enum"; + } else if ((access & Opcodes.ACC_RECORD) != 0) { + type = "record"; + } else if ((access & Opcodes.ACC_INTERFACE) != 0) { + type = "interface"; + } else if ((access & Opcodes.ACC_ANNOTATION) != 0) { + type = "annotation"; + } else { + type = "class"; + } + } + + @Override + protected String type() { + return type; } @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { if (envType == EnvType.SERVER) { - stripEntireClass = true; + denyClientOnlyLoad(); } } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { if (envType == EnvType.CLIENT) { - stripEntireClass = true; + denyDediServerOnlyLoad(); } } else if (REQUIRES_DESCRIPTOR.equals(descriptor)) { - return new QuiltRequiresAnnotationVisitor(api, () -> stripEntireClass = true, null); + return new QuiltRequiresAnnotationVisitor(api, this::checkHasAllMods); } else if (ENVIRONMENT_DESCRIPTOR.equals(descriptor)) { - return new FabricEnvironmentAnnotationVisitor(api, () -> stripEntireClass = true); + return new FabricEnvironmentAnnotationVisitor(api, () -> denyLoadReasons.add("Mismatched @Envrionment")); } else if (ENVIRONMENT_INTERFACE_DESCRIPTOR.equals(descriptor)) { return new FabricEnvironmentInterfaceAnnotationVisitor(api); } else if (ENVIRONMENT_INTERFACES_DESCRIPTOR.equals(descriptor)) { @@ -316,7 +380,7 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { } public boolean stripEntireClass() { - return this.stripEntireClass; + return denyLoadReasons.size() > 0; } public Collection getStripInterfaces() { diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java index 32e268bc9..e26559c8d 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/EnvironmentStrippingData.java @@ -28,10 +28,10 @@ import net.fabricmc.api.EnvType; -/** Deprecated. All stuff were moved to {@link StrippingData}. */ +/** Deprecated. All stuff were moved to {@link ClassStrippingData}. */ @Deprecated @QuiltLoaderInternal(QuiltLoaderInternalType.LEGACY_EXPOSED) -public class EnvironmentStrippingData extends StrippingData { +public class EnvironmentStrippingData extends ClassStrippingData { public EnvironmentStrippingData(int api, EnvType envType) { super(api, envType, new ArrayList<>()); diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java b/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java index 63b459cea..e52dec5ca 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/PackageStrippingData.java @@ -17,11 +17,11 @@ package org.quiltmc.loader.impl.transformer; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; import java.util.Map; import org.objectweb.asm.AnnotationVisitor; -import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Type; import org.quiltmc.loader.api.Requires; import org.quiltmc.loader.api.minecraft.ClientOnly; @@ -32,32 +32,25 @@ import net.fabricmc.api.EnvType; @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) -public class PackageStrippingData extends ClassVisitor { +public class PackageStrippingData extends AbstractStripData { private static final String CLIENT_ONLY_DESCRIPTOR = Type.getDescriptor(ClientOnly.class); private static final String SERVER_ONLY_DESCRIPTOR = Type.getDescriptor(DedicatedServerOnly.class); private static final String REQUIRES_DESCRIPTOR = Type.getDescriptor(Requires.class); - private final EnvType envType; - private final List mods; - - private boolean stripEntirePackage = false; - public PackageStrippingData(int api, EnvType envType, Map modCodeSourceMap) { - super(api); - this.envType = envType; - this.mods = new ArrayList<>(modCodeSourceMap.keySet()); + super(api, envType, new HashSet<>(modCodeSourceMap.keySet())); } @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { if (CLIENT_ONLY_DESCRIPTOR.equals(descriptor)) { if (envType == EnvType.SERVER) { - stripEntirePackage = true; + denyClientOnlyLoad(); } } else if (SERVER_ONLY_DESCRIPTOR.equals(descriptor)) { if (envType == EnvType.CLIENT) { - stripEntirePackage = true; + denyDediServerOnlyLoad(); } } else if (REQUIRES_DESCRIPTOR.equals(descriptor)) { return new AnnotationVisitor(api) { @@ -65,11 +58,17 @@ public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { public AnnotationVisitor visitArray(String name) { if ("value".equals(name)) { return new AnnotationVisitor(api) { + + final List requiredMods = new ArrayList<>(); + @Override public void visit(String name, Object value) { - if (!mods.contains(String.valueOf(value))) { - stripEntirePackage = true; - } + requiredMods.add(String.valueOf(value)); + } + + @Override + public void visitEnd() { + checkHasAllMods(requiredMods); } }; } @@ -82,7 +81,12 @@ public void visit(String name, Object value) { return null; } + @Override + protected String type() { + return "package"; + } + public boolean stripEntirePackage() { - return this.stripEntirePackage; + return denyLoadReasons.size() > 0; } } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java b/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java index ede8c5b1c..29acd7c99 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/QuiltTransformer.java @@ -52,11 +52,11 @@ final class QuiltTransformer { int visitorCount = 0; if (strip) { - StrippingData data = new StrippingData(QuiltLoaderImpl.ASM_VERSION, envType, cache.getMods()); + ClassStrippingData data = new ClassStrippingData(QuiltLoaderImpl.ASM_VERSION, envType, cache.getMods()); classReader.accept(data, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); if (data.stripEntireClass()) { - cache.hideClass(name); + cache.hideClass(name, data.summarizeDenyLoadReasons()); return null; } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java b/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java index 944e810ca..59d534dc2 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/RuntimeModRemapper.java @@ -170,6 +170,9 @@ private static List getRemapClasspath() throws IOException { private static boolean requiresMixinRemap(Path inputPath) throws IOException { final Manifest manifest = ManifestUtil.readManifest(inputPath); + if (manifest == null) { + return false; + } final Attributes mainAttributes = manifest.getMainAttributes(); return REMAP_TYPE_STATIC.equalsIgnoreCase(mainAttributes.getValue(REMAP_TYPE_MANIFEST_KEY)); } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java index f5d6654c9..937fb5a74 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCache.java @@ -58,7 +58,7 @@ class TransformCache { private final Path root; private final Map modRoots = new HashMap<>(); private final List orderedMods; - private final Set hiddenClasses = new HashSet<>(); + private final Map hiddenClasses = new HashMap<>(); private static final boolean COPY_ON_WRITE = true; public TransformCache(Path root, List orderedMods) { @@ -165,8 +165,8 @@ public List getMods() { return Collections.unmodifiableList(orderedMods); } - public Set getHiddenClasses() { - return Collections.unmodifiableSet(hiddenClasses); + public Map getHiddenClasses() { + return Collections.unmodifiableMap(hiddenClasses); } public void forEachClassFile(ClassConsumer action) @@ -176,8 +176,10 @@ public void forEachClassFile(ClassConsumer action) } } - public void hideClass(String className) { - hiddenClasses.add(className); + public void hideClass(String className, String denyReason) { + hiddenClasses.merge(className, denyReason, (current, nval) -> { + return current + "\n" + nval; + }); } private static void copyFile(Path path, Path modSrc, Path modDst, CopyOption... copyOptions) { @@ -239,7 +241,7 @@ public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IO String fileName = file.getFileName().toString(); if (fileName.endsWith(".class") && couldBeJavaElement(fileName, true)) { String name = LoaderUtil.getClassNameFromTransformCache(file.toString()); - if (!hiddenClasses.contains(name)) { + if (!hiddenClasses.containsKey(name)) { byte[] result = action.run(mod, name, file); if (result != null) { Files.write(file, result); diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java index 034a1916a..63280c26d 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheManager.java @@ -17,6 +17,7 @@ package org.quiltmc.loader.impl.transformer; import java.io.BufferedReader; +import java.io.BufferedWriter; import java.io.IOError; import java.io.IOException; import java.net.URI; @@ -28,6 +29,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -52,6 +54,9 @@ import org.quiltmc.loader.impl.util.SystemProperties; import org.quiltmc.loader.impl.util.log.Log; import org.quiltmc.loader.impl.util.log.LogCategory; +import org.quiltmc.parsers.json.JsonReader; +import org.quiltmc.parsers.json.JsonToken; +import org.quiltmc.parsers.json.JsonWriter; @QuiltLoaderInternal(QuiltLoaderInternalType.NEW_INTERNAL) public class TransformCacheManager { @@ -64,7 +69,7 @@ public class TransformCacheManager { private static final String CACHE_FILE = "files.zip"; private static final String FILE_TRANSFORM_COMPLETE = "__TRANSFORM_COMPLETE"; - private static final String HIDDEN_CLASSES_PATH = "hidden_classes.txt"; + private static final String DENY_LOAD_REASONS_PATH = "deny_load_reasons.json"; public static TransformCacheResult populateTransformBundle(Path transformCacheFolder, List modList, Map modOriginHash, ModSolveResult result) throws ModResolutionException { @@ -103,7 +108,17 @@ public static TransformCacheResult populateTransformBundle(Path transformCacheFo FilePreloadHelper.preLoad(transformCacheFolder.resolve(CACHE_FILE)); } try { - return new TransformCacheResult(existing, isNewlyGenerated, new HashSet<>(Files.readAllLines(existing.resolve(HIDDEN_CLASSES_PATH)))); + Map hiddenClasses = new HashMap<>(); + try (JsonReader reader = JsonReader.json(existing.resolve(DENY_LOAD_REASONS_PATH))) { + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + String clName = reader.nextName(); + String clReason = reader.nextString(); + hiddenClasses.put(clName, clReason); + } + reader.endObject(); + } + return new TransformCacheResult(existing, isNewlyGenerated, hiddenClasses); } catch (IOException e) { throw new ModResolutionException("Failed to read hidden classes in the transform cache file!", e); } @@ -287,7 +302,17 @@ private static void writeTransformCache(String options, List modL TransformCache cache = TransformCacheGenerator.generate(root, modList); QuiltMapFileSystem.dumpEntries(root.getFileSystem(), "after-populate"); Files.write(root.resolve("options.txt"), options.getBytes(StandardCharsets.UTF_8)); - Files.write(root.resolve(HIDDEN_CLASSES_PATH), cache.getHiddenClasses()); + try (JsonWriter json = JsonWriter.json(Files.newBufferedWriter(root.resolve(DENY_LOAD_REASONS_PATH)))) { + if (true) { + json.setIndent(" "); + } + json.beginObject(); + for (Map.Entry entry : cache.getHiddenClasses().entrySet()) { + json.name(entry.getKey()); + json.value(entry.getValue()); + } + json.endObject(); + } Files.createFile(root.resolve(FILE_TRANSFORM_COMPLETE)); } diff --git a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java index 02c02b180..de637ce06 100644 --- a/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java +++ b/src/main/java/org/quiltmc/loader/impl/transformer/TransformCacheResult.java @@ -16,7 +16,7 @@ package org.quiltmc.loader.impl.transformer; -import java.util.Set; +import java.util.Map; import org.quiltmc.loader.impl.filesystem.QuiltZipPath; import org.quiltmc.loader.impl.util.QuiltLoaderInternal; @@ -26,9 +26,9 @@ public class TransformCacheResult { public final QuiltZipPath transformCacheRoot; public final boolean isNewlyGenerated; - public final Set hiddenClasses; + public final Map hiddenClasses; - TransformCacheResult(QuiltZipPath transformCacheRoot, boolean isNewlyGenerated, Set hiddenClasses) { + TransformCacheResult(QuiltZipPath transformCacheRoot, boolean isNewlyGenerated, Map hiddenClasses) { this.isNewlyGenerated = isNewlyGenerated; this.transformCacheRoot = transformCacheRoot; this.hiddenClasses = hiddenClasses; diff --git a/src/main/resources/changelog/0.24.1.txt b/src/main/resources/changelog/0.24.1.txt index e03081868..ad26b9ef3 100644 --- a/src/main/resources/changelog/0.24.1.txt +++ b/src/main/resources/changelog/0.24.1.txt @@ -1,5 +1,19 @@ Features: +- [#414] Add a new `@Requires` annotation. + - This is similar to `@ClientOnly` or `@DedicatedServerOnly`, but instead removes an annotated element if the mod(s) specified are not present. + For example: + +```java +public class MyOptionalTrinketItem extends Item implements @Requires("trinkets") Trinket { + @Override + @Requires("trinkets") + public Multimap getModifiers(ItemStack stack, SlotReference slot, LivingEntity entity, UUID uuid) { + // ... + } +} +``` + - [#413] Include the reason in breakage errors (EnnuiL) - Also check the game provider jar to see if it contains a mod. - This was added for Cosmic Quilt, it doesn't affect Minecraft, but it may be useful to other game providers. @@ -7,3 +21,4 @@ Features: Bug Fixes: - Fixed the forked error window throwing errors too early if it cannot create the new process +- Fixed @DedicatedServerOnly and @ClientOnly not working when applied to packages. diff --git a/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java b/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java index a9938d63e..95659b154 100644 --- a/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java +++ b/src/test/java/org/quiltmc/test/lambda_strip/LambdaStripTester.java @@ -34,7 +34,7 @@ import net.fabricmc.api.EnvType; -import org.quiltmc.loader.impl.transformer.StrippingData; +import org.quiltmc.loader.impl.transformer.ClassStrippingData; public class LambdaStripTester { @@ -65,7 +65,7 @@ public static void main(String[] args) throws IOException { | ClassReader.SKIP_FRAMES ); - StrippingData strip = new StrippingData(Opcodes.ASM9, EnvType.SERVER, new ArrayList<>()); + ClassStrippingData strip = new ClassStrippingData(Opcodes.ASM9, EnvType.SERVER, new ArrayList<>()); reader.accept(strip, ClassReader.SKIP_CODE | ClassReader.SKIP_FRAMES); Collection stripMethods = strip.getStripMethods();