From 04ca22c225174549f8e782e05eacf95b1a016cd1 Mon Sep 17 00:00:00 2001 From: LlamaLad7 Date: Mon, 4 Mar 2024 09:40:09 +0000 Subject: [PATCH 01/20] Refactor/better kotlin metadata (#1061) * Kotlin: Don't depend on metadata internals. * Kotlin: Remap type parameter annotations. * Kotlin: Bump metadata to 0.9.0 --- .../kotlin/remapping/JvmExtensionWrapper.java | 271 ------------------ .../kotlin/KotlinRemapperClassloader.java | 4 +- .../kotlin/remapping/KotlinClassRemapper.kt | 107 ++----- .../remapping/KotlinMetadataExtensions.kt | 100 ------- 4 files changed, 20 insertions(+), 462 deletions(-) delete mode 100644 src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java delete mode 100644 src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt diff --git a/src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java b/src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java deleted file mode 100644 index 1dea4eda..00000000 --- a/src/main/java/net/fabricmc/loom/kotlin/remapping/JvmExtensionWrapper.java +++ /dev/null @@ -1,271 +0,0 @@ -/* - * 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.kotlin.remapping; - -import java.util.List; - -import kotlinx.metadata.KmAnnotation; -import kotlinx.metadata.KmProperty; -import kotlinx.metadata.internal.extensions.KmClassExtension; -import kotlinx.metadata.internal.extensions.KmConstructorExtension; -import kotlinx.metadata.internal.extensions.KmFunctionExtension; -import kotlinx.metadata.internal.extensions.KmPackageExtension; -import kotlinx.metadata.internal.extensions.KmPropertyExtension; -import kotlinx.metadata.internal.extensions.KmTypeExtension; -import kotlinx.metadata.internal.extensions.KmTypeParameterExtension; -import kotlinx.metadata.jvm.JvmFieldSignature; -import kotlinx.metadata.jvm.JvmMethodSignature; -import kotlinx.metadata.jvm.internal.JvmClassExtension; -import kotlinx.metadata.jvm.internal.JvmConstructorExtension; -import kotlinx.metadata.jvm.internal.JvmFunctionExtension; -import kotlinx.metadata.jvm.internal.JvmPackageExtension; -import kotlinx.metadata.jvm.internal.JvmPropertyExtension; -import kotlinx.metadata.jvm.internal.JvmTypeExtension; -import kotlinx.metadata.jvm.internal.JvmTypeParameterExtension; -import org.jetbrains.annotations.Nullable; - -/* - * This is a fun meme. All of these kotlin classes are marked as "internal" so Kotlin code cannot compile against them. - * However, luckily for us the Java compiler has no idea about this, so they can compile against it :D - * - * This file contains Java wrappers around Kotlin classes, to used by Kotlin. - */ -public interface JvmExtensionWrapper { - record Class(JvmClassExtension extension) implements JvmExtensionWrapper { - @Nullable - public static Class get(KmClassExtension classExtension) { - if (classExtension instanceof JvmClassExtension jvmClassExtension) { - return new Class(jvmClassExtension); - } - - return null; - } - - public List getLocalDelegatedProperties() { - return extension.getLocalDelegatedProperties(); - } - - @Nullable - public String getModuleName() { - return extension.getModuleName(); - } - - public void setModuleName(@Nullable String name) { - extension.setModuleName(name); - } - - @Nullable - public String getAnonymousObjectOriginName() { - return extension.getAnonymousObjectOriginName(); - } - - public void setAnonymousObjectOriginName(@Nullable String name) { - extension.setAnonymousObjectOriginName(name); - } - - public int getJvmFlags() { - return extension.getJvmFlags(); - } - - public void setJvmFlags(int flags) { - extension.setJvmFlags(flags); - } - } - - record Package(JvmPackageExtension extension) { - @Nullable - public static Package get(KmPackageExtension packageExtension) { - if (packageExtension instanceof JvmPackageExtension jvmPackageExtension) { - return new Package(jvmPackageExtension); - } - - return null; - } - - public List getLocalDelegatedProperties() { - return extension.getLocalDelegatedProperties(); - } - - @Nullable - public String getModuleName() { - return extension.getModuleName(); - } - - public void setModuleName(@Nullable String name) { - extension.setModuleName(name); - } - } - - record Function(JvmFunctionExtension extension) { - @Nullable - public static Function get(KmFunctionExtension functionExtension) { - if (functionExtension instanceof JvmFunctionExtension jvmFunctionExtension) { - return new Function(jvmFunctionExtension); - } - - return null; - } - - @Nullable - public JvmMethodSignature getSignature() { - return extension.getSignature(); - } - - public void setSignature(@Nullable JvmMethodSignature signature) { - extension.setSignature(signature); - } - - @Nullable - public String getLambdaClassOriginName() { - return extension.getLambdaClassOriginName(); - } - - public void setLambdaClassOriginName(@Nullable String name) { - extension.setLambdaClassOriginName(name); - } - } - - record Property(JvmPropertyExtension extension) { - @Nullable - public static Property get(KmPropertyExtension propertyExtension) { - if (propertyExtension instanceof JvmPropertyExtension jvmPropertyExtension) { - return new Property(jvmPropertyExtension); - } - - return null; - } - - public int getJvmFlags() { - return extension.getJvmFlags(); - } - - public void setJvmFlags(int flags) { - extension.setJvmFlags(flags); - } - - @Nullable - public JvmFieldSignature getFieldSignature() { - return extension.getFieldSignature(); - } - - public void setFieldSignature(@Nullable JvmFieldSignature signature) { - extension.setFieldSignature(signature); - } - - @Nullable - public JvmMethodSignature getGetterSignature() { - return extension.getGetterSignature(); - } - - public void setGetterSignature(@Nullable JvmMethodSignature signature) { - extension.setGetterSignature(signature); - } - - @Nullable - public JvmMethodSignature getSetterSignature() { - return extension.getSetterSignature(); - } - - public void setSetterSignature(@Nullable JvmMethodSignature signature) { - extension.setSetterSignature(signature); - } - - @Nullable - public JvmMethodSignature getSyntheticMethodForAnnotations() { - return extension.getSyntheticMethodForAnnotations(); - } - - public void setSyntheticMethodForAnnotations(@Nullable JvmMethodSignature signature) { - extension.setSyntheticMethodForAnnotations(signature); - } - - @Nullable - public JvmMethodSignature getSyntheticMethodForDelegate() { - return extension.getSyntheticMethodForDelegate(); - } - - public void setSyntheticMethodForDelegate(@Nullable JvmMethodSignature signature) { - extension.setSyntheticMethodForDelegate(signature); - } - } - - record Constructor(JvmConstructorExtension extension) { - @Nullable - public static Constructor get(KmConstructorExtension constructorExtension) { - if (constructorExtension instanceof JvmConstructorExtension jvmConstructorExtension) { - return new Constructor(jvmConstructorExtension); - } - - return null; - } - - @Nullable - public JvmMethodSignature getSignature() { - return extension.getSignature(); - } - - public void setSignature(@Nullable JvmMethodSignature signature) { - extension.setSignature(signature); - } - } - - record TypeParameter(JvmTypeParameterExtension extension) { - @Nullable - public static TypeParameter get(KmTypeParameterExtension typeParameterExtension) { - if (typeParameterExtension instanceof JvmTypeParameterExtension jvmTypeParameterExtension) { - return new TypeParameter(jvmTypeParameterExtension); - } - - return null; - } - - public List getAnnotations() { - return extension.getAnnotations(); - } - } - - record Type(JvmTypeExtension extension) { - @Nullable - public static Type get(KmTypeExtension typeExtension) { - if (typeExtension instanceof JvmTypeExtension jvmTypeExtension) { - return new Type(jvmTypeExtension); - } - - return null; - } - - public boolean isRaw() { - return extension.isRaw(); - } - - public void setRaw(boolean raw) { - extension.setRaw(raw); - } - - public List getAnnotations() { - return extension.getAnnotations(); - } - } -} diff --git a/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java b/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java index 7d1825fd..45ad0873 100644 --- a/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java +++ b/src/main/java/net/fabricmc/loom/util/kotlin/KotlinRemapperClassloader.java @@ -31,7 +31,6 @@ import java.util.List; import java.util.stream.Stream; import net.fabricmc.loom.LoomGradlePlugin; -import net.fabricmc.loom.kotlin.remapping.JvmExtensionWrapper; import net.fabricmc.loom.kotlin.remapping.KotlinMetadataTinyRemapperExtensionImpl; /** @@ -62,8 +61,7 @@ public class KotlinRemapperClassloader extends URLClassLoader { public static KotlinRemapperClassloader create(KotlinClasspath classpathProvider) { // Include the libraries that are not on the kotlin classpath. final Stream loomUrls = getClassUrls( - KotlinMetadataTinyRemapperExtensionImpl.class, // Loom (Kotlin) - JvmExtensionWrapper.class // Loom (Java) + KotlinMetadataTinyRemapperExtensionImpl.class // Loom (Kotlin) ); final URL[] urls = Stream.concat( diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt index ffb59ccc..d77a0d48 100644 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt +++ b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinClassRemapper.kt @@ -40,18 +40,17 @@ import kotlinx.metadata.KmTypeAlias import kotlinx.metadata.KmTypeParameter import kotlinx.metadata.KmTypeProjection import kotlinx.metadata.KmValueParameter -import kotlinx.metadata.internal.extensions.KmClassExtension -import kotlinx.metadata.internal.extensions.KmConstructorExtension -import kotlinx.metadata.internal.extensions.KmFunctionExtension -import kotlinx.metadata.internal.extensions.KmPackageExtension -import kotlinx.metadata.internal.extensions.KmPropertyExtension -import kotlinx.metadata.internal.extensions.KmTypeAliasExtension -import kotlinx.metadata.internal.extensions.KmTypeExtension -import kotlinx.metadata.internal.extensions.KmTypeParameterExtension -import kotlinx.metadata.internal.extensions.KmValueParameterExtension import kotlinx.metadata.isLocalClassName import kotlinx.metadata.jvm.JvmFieldSignature import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.annotations +import kotlinx.metadata.jvm.fieldSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.localDelegatedProperties +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature +import kotlinx.metadata.jvm.syntheticMethodForAnnotations +import kotlinx.metadata.jvm.syntheticMethodForDelegate import kotlinx.metadata.jvm.toJvmInternalName import org.objectweb.asm.commons.Remapper @@ -68,7 +67,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { clazz.nestedClasses.replaceAll(this::remap) clazz.sealedSubclasses.replaceAll(this::remap) clazz.contextReceiverTypes.replaceAll(this::remap) - clazz.getExtensions().replaceAll(this::remap) + clazz.localDelegatedProperties.replaceAll(this::remap) return clazz } @@ -81,7 +80,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { pkg.functions.replaceAll(this::remap) pkg.properties.replaceAll(this::remap) pkg.typeAliases.replaceAll(this::remap) - pkg.getExtensions().replaceAll(this::remap) + pkg.localDelegatedProperties.replaceAll(this::remap) return pkg } @@ -107,7 +106,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { type.abbreviatedType = type.abbreviatedType?.let { remap(it) } type.outerType = type.outerType?.let { remap(it) } type.flexibleTypeUpperBound = type.flexibleTypeUpperBound?.let { remap(it) } - type.getExtensions().replaceAll(this::remap) + type.annotations.replaceAll(this::remap) return type } @@ -117,7 +116,7 @@ class KotlinClassRemapper(private val remapper: Remapper) { function.contextReceiverTypes.replaceAll(this::remap) function.valueParameters.replaceAll(this::remap) function.returnType = remap(function.returnType) - function.getExtensions().replaceAll(this::remap) + function.signature = function.signature?.let { remap(it) } return function } @@ -127,7 +126,11 @@ class KotlinClassRemapper(private val remapper: Remapper) { property.contextReceiverTypes.replaceAll(this::remap) property.setterParameter = property.setterParameter?.let { remap(it) } property.returnType = remap(property.returnType) - property.getExtensions().replaceAll(this::remap) + property.fieldSignature = property.fieldSignature?.let { remap(it) } + property.getterSignature = property.getterSignature?.let { remap(it) } + property.setterSignature = property.setterSignature?.let { remap(it) } + property.syntheticMethodForAnnotations = property.syntheticMethodForAnnotations?.let { remap(it) } + property.syntheticMethodForDelegate = property.syntheticMethodForDelegate?.let { remap(it) } return property } @@ -136,19 +139,18 @@ class KotlinClassRemapper(private val remapper: Remapper) { typeAlias.underlyingType = remap(typeAlias.underlyingType) typeAlias.expandedType = remap(typeAlias.expandedType) typeAlias.annotations.replaceAll(this::remap) - typeAlias.getExtensions().replaceAll(this::remap) return typeAlias } private fun remap(constructor: KmConstructor): KmConstructor { constructor.valueParameters.replaceAll(this::remap) - constructor.getExtensions().replaceAll(this::remap) + constructor.signature = constructor.signature?.let { remap(it) } return constructor } private fun remap(typeParameter: KmTypeParameter): KmTypeParameter { typeParameter.upperBounds.replaceAll(this::remap) - typeParameter.getExtensions().replaceAll(this::remap) + typeParameter.annotations.replaceAll(this::remap) return typeParameter } @@ -163,7 +165,6 @@ class KotlinClassRemapper(private val remapper: Remapper) { private fun remap(valueParameter: KmValueParameter): KmValueParameter { valueParameter.type = remap(valueParameter.type) valueParameter.varargElementType = valueParameter.varargElementType?.let { remap(it) } - valueParameter.getExtensions().replaceAll(this::remap) return valueParameter } @@ -171,76 +172,6 @@ class KotlinClassRemapper(private val remapper: Remapper) { return KmAnnotation(remap(annotation.className), annotation.arguments) } - private fun remap(classExtension: KmClassExtension): KmClassExtension { - JvmExtensionWrapper.Class.get(classExtension)?.let { - it.localDelegatedProperties.replaceAll(this::remap) - return it.extension - } - - return classExtension - } - - private fun remap(packageExtension: KmPackageExtension): KmPackageExtension { - JvmExtensionWrapper.Package.get(packageExtension)?.let { - it.localDelegatedProperties.replaceAll(this::remap) - return it.extension - } - - return packageExtension - } - - private fun remap(typeExtension: KmTypeExtension): KmTypeExtension { - JvmExtensionWrapper.Type.get(typeExtension)?.let { - it.annotations.replaceAll(this::remap) - return it.extension - } - - return typeExtension - } - - private fun remap(functionExtension: KmFunctionExtension): KmFunctionExtension { - JvmExtensionWrapper.Function.get(functionExtension)?.let { - it.signature = it.signature?.let { sig -> remap(sig) } - return it.extension - } - - return functionExtension - } - - private fun remap(propertyExtension: KmPropertyExtension): KmPropertyExtension { - JvmExtensionWrapper.Property.get(propertyExtension)?.let { - it.fieldSignature = it.fieldSignature?.let { sig -> remap(sig) } - it.getterSignature = it.getterSignature?.let { sig -> remap(sig) } - it.setterSignature = it.setterSignature?.let { sig -> remap(sig) } - it.syntheticMethodForAnnotations = it.syntheticMethodForAnnotations?.let { sig -> remap(sig) } - it.syntheticMethodForDelegate = it.syntheticMethodForDelegate?.let { sig -> remap(sig) } - return it.extension - } - - return propertyExtension - } - - private fun remap(typeAliasExtension: KmTypeAliasExtension): KmTypeAliasExtension { - return typeAliasExtension - } - - private fun remap(typeParameterExtension: KmTypeParameterExtension): KmTypeParameterExtension { - return typeParameterExtension - } - - private fun remap(valueParameterExtension: KmValueParameterExtension): KmValueParameterExtension { - return valueParameterExtension - } - - private fun remap(constructorExtension: KmConstructorExtension): KmConstructorExtension { - JvmExtensionWrapper.Constructor.get(constructorExtension)?.let { - it.signature = it.signature?.let { sig -> remap(sig) } - return it.extension - } - - return constructorExtension - } - private fun remap(signature: JvmMethodSignature): JvmMethodSignature { return JvmMethodSignature(signature.name, remapper.mapMethodDesc(signature.descriptor)) } diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt deleted file mode 100644 index e3061018..00000000 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataExtensions.kt +++ /dev/null @@ -1,100 +0,0 @@ -/* - * 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. - */ - -@file:Suppress("UNCHECKED_CAST") - -package net.fabricmc.loom.kotlin.remapping - -import kotlinx.metadata.KmClass -import kotlinx.metadata.KmConstructor -import kotlinx.metadata.KmFunction -import kotlinx.metadata.KmPackage -import kotlinx.metadata.KmProperty -import kotlinx.metadata.KmType -import kotlinx.metadata.KmTypeAlias -import kotlinx.metadata.KmTypeParameter -import kotlinx.metadata.KmValueParameter -import kotlinx.metadata.internal.extensions.KmClassExtension -import kotlinx.metadata.internal.extensions.KmConstructorExtension -import kotlinx.metadata.internal.extensions.KmFunctionExtension -import kotlinx.metadata.internal.extensions.KmPackageExtension -import kotlinx.metadata.internal.extensions.KmPropertyExtension -import kotlinx.metadata.internal.extensions.KmTypeAliasExtension -import kotlinx.metadata.internal.extensions.KmTypeExtension -import kotlinx.metadata.internal.extensions.KmTypeParameterExtension -import kotlinx.metadata.internal.extensions.KmValueParameterExtension -import java.lang.reflect.Field -import kotlin.reflect.KClass - -val KM_CLASS_EXTENSIONS = getField(KmClass::class) -val KM_PACKAGE_EXTENSIONS = getField(KmPackage::class) -val KM_TYPE_EXTENSIONS = getField(KmType::class) -val KM_FUNCTION_EXTENSIONS = getField(KmFunction::class) -val KM_PROPERTY_EXTENSIONS = getField(KmProperty::class) -val KM_TYPE_ALIAS_EXTENSIONS = getField(KmTypeAlias::class) -val KM_TYPE_PARAMETER_EXTENSIONS = getField(KmTypeParameter::class) -val KM_VALUE_PARAMETER_EXTENSIONS = getField(KmValueParameter::class) -val KM_CONSTRUCTOR_EXTENSIONS = getField(KmConstructor::class) - -fun KmClass.getExtensions(): MutableList { - return KM_CLASS_EXTENSIONS.get(this) as MutableList -} - -fun KmPackage.getExtensions(): MutableList { - return KM_PACKAGE_EXTENSIONS.get(this) as MutableList -} - -fun KmType.getExtensions(): MutableList { - return KM_TYPE_EXTENSIONS.get(this) as MutableList -} - -fun KmFunction.getExtensions(): MutableList { - return KM_FUNCTION_EXTENSIONS.get(this) as MutableList -} - -fun KmProperty.getExtensions(): MutableList { - return KM_PROPERTY_EXTENSIONS.get(this) as MutableList -} - -fun KmTypeAlias.getExtensions(): MutableList { - return KM_TYPE_ALIAS_EXTENSIONS.get(this) as MutableList -} - -fun KmTypeParameter.getExtensions(): MutableList { - return KM_TYPE_PARAMETER_EXTENSIONS.get(this) as MutableList -} - -fun KmValueParameter.getExtensions(): MutableList { - return KM_VALUE_PARAMETER_EXTENSIONS.get(this) as MutableList -} - -fun KmConstructor.getExtensions(): MutableList { - return KM_CONSTRUCTOR_EXTENSIONS.get(this) as MutableList -} - -private fun getField(clazz: KClass<*>): Field { - val field = clazz.java.getDeclaredField("extensions") - field.isAccessible = true - return field -} From b44e4ec3d52ce0c4a8f1dab0333a8931102672ea Mon Sep 17 00:00:00 2001 From: modmuss Date: Sun, 10 Mar 2024 13:37:45 +0000 Subject: [PATCH 02/20] Add API to get named minecraft jars. (#1063) --- .../net/fabricmc/loom/api/LoomGradleExtensionAPI.java | 6 ++++++ .../loom/extension/LoomGradleExtensionApiImpl.java | 9 +++++++++ 2 files changed, 15 insertions(+) diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index 6570178e..255c5da5 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -32,6 +32,7 @@ import org.gradle.api.NamedDomainObjectContainer; import org.gradle.api.NamedDomainObjectList; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.provider.ListProperty; import org.gradle.api.provider.Property; @@ -231,4 +232,9 @@ public interface LoomGradleExtensionAPI { * @return The minecraft version, as a {@link Provider}. */ Provider getMinecraftVersion(); + + /** + * @return A lazily evaluated {@link FileCollection} containing the named minecraft jars. + */ + FileCollection getNamedMinecraftJars(); } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index 30c63cc9..e81a0560 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -37,6 +37,7 @@ import org.gradle.api.Project; import org.gradle.api.UncheckedIOException; import org.gradle.api.artifacts.Dependency; import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; import org.gradle.api.file.RegularFileProperty; import org.gradle.api.model.ObjectFactory; import org.gradle.api.provider.ListProperty; @@ -54,6 +55,7 @@ import net.fabricmc.loom.api.ModSettings; import net.fabricmc.loom.api.RemapConfigurationSettings; import net.fabricmc.loom.api.decompilers.DecompilerOptions; import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.api.mappings.layered.spec.LayeredMappingSpecBuilder; import net.fabricmc.loom.api.processor.MinecraftJarProcessor; import net.fabricmc.loom.api.remapping.RemapperExtension; @@ -432,6 +434,13 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA return getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftProvider().minecraftVersion()); } + @Override + public FileCollection getNamedMinecraftJars() { + final ConfigurableFileCollection jars = getProject().getObjects().fileCollection(); + jars.from(getProject().provider(() -> LoomGradleExtension.get(getProject()).getMinecraftJars(MappingsNamespace.NAMED))); + return jars; + } + // This is here to ensure that LoomGradleExtensionApiImpl compiles without any unimplemented methods private final class EnsureCompile extends LoomGradleExtensionApiImpl { private EnsureCompile() { From 2e74a84d37924aaeaec58aa140541e5370fafb2d Mon Sep 17 00:00:00 2001 From: modmuss Date: Sun, 10 Mar 2024 13:38:37 +0000 Subject: [PATCH 03/20] Check Minecraft java version (#1059) * Fix deprecation warning in Gradle 8.7 (#1056) * Check Minecraft java version * Fix * Fix * Fix --- .../providers/minecraft/MinecraftProvider.java | 12 ++++++++++++ .../providers/minecraft/MinecraftVersionMeta.java | 6 +++++- .../LayeredMappingsTestConstants.groovy | 4 ++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index f206a7a9..0c966c2e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -31,6 +31,7 @@ import java.util.List; import java.util.Objects; import com.google.common.base.Preconditions; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.logging.Logger; import org.jetbrains.annotations.Nullable; @@ -86,6 +87,17 @@ public abstract class MinecraftProvider { getExtension()::download ); + final MinecraftVersionMeta.JavaVersion javaVersion = getVersionInfo().javaVersion(); + + if (javaVersion != null) { + final int requiredMajorJavaVersion = getVersionInfo().javaVersion().majorVersion(); + final JavaVersion requiredJavaVersion = JavaVersion.toVersion(requiredMajorJavaVersion); + + if (!JavaVersion.current().isCompatibleWith(requiredJavaVersion)) { + throw new IllegalStateException("Minecraft " + minecraftVersion + " requires Java " + requiredJavaVersion + " but Gradle is using " + JavaVersion.current()); + } + } + downloadJars(); if (provideServer()) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java index 7a311fbb..5325cc38 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftVersionMeta.java @@ -47,7 +47,8 @@ public record MinecraftVersionMeta( int minimumLauncherVersion, String releaseTime, String time, - String type + String type, + @Nullable JavaVersion javaVersion ) { private static Map OS_NAMES = Map.of( Platform.OperatingSystem.WINDOWS, "windows", @@ -168,4 +169,7 @@ public record MinecraftVersionMeta( return new File(baseDirectory, path()); } } + + public record JavaVersion(String component, int majorVersion) { + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy index fffbc67a..a5bc09ab 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy @@ -34,13 +34,13 @@ interface LayeredMappingsTestConstants { client_mappings: new MinecraftVersionMeta.Download(null, "227d16f520848747a59bef6f490ae19dc290a804", 6431705, "https://launcher.mojang.com/v1/objects/227d16f520848747a59bef6f490ae19dc290a804/client.txt"), server_mappings: new MinecraftVersionMeta.Download(null, "84d80036e14bc5c7894a4fad9dd9f367d3000334", 4948536, "https://launcher.mojang.com/v1/objects/84d80036e14bc5c7894a4fad9dd9f367d3000334/server.txt") ] - public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, null, null, null) + public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, null, null, null, null) public static final Map DOWNLOADS_1_16_5 = [ client_mappings: new MinecraftVersionMeta.Download(null, "e3dfb0001e1079a1af72ee21517330edf52e6192", 5746047, "https://launcher.mojang.com/v1/objects/e3dfb0001e1079a1af72ee21517330edf52e6192/client.txt"), server_mappings: new MinecraftVersionMeta.Download(null, "81d5c793695d8cde63afddb40dde88e3a88132ac", 4400926, "https://launcher.mojang.com/v1/objects/81d5c793695d8cde63afddb40dde88e3a88132ac/server.txt") ] - public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, null, null, null) + public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, null, null, null, null) public static final String PARCHMENT_NOTATION = "org.parchmentmc.data:parchment-1.16.5:20210608-SNAPSHOT@zip" public static final String PARCHMENT_URL = "https://maven.parchmentmc.net/org/parchmentmc/data/parchment-1.16.5/20210608-SNAPSHOT/parchment-1.16.5-20210608-SNAPSHOT.zip" From 21b7dd9b992183ec08a47b1eb1865d6094354906 Mon Sep 17 00:00:00 2001 From: Flemmli97 <34157027+Flemmli97@users.noreply.github.com> Date: Sun, 10 Mar 2024 14:39:22 +0100 Subject: [PATCH 04/20] Use last 16 chars for checksum (#1064) * Fix deprecation warning in Gradle 8.7 (#1056) * use last 16 chars for checksum * hash the path Co-authored-by: Flemmli97 --- src/main/java/net/fabricmc/loom/util/Checksum.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/loom/util/Checksum.java b/src/main/java/net/fabricmc/loom/util/Checksum.java index d90493fd..50fcbec0 100644 --- a/src/main/java/net/fabricmc/loom/util/Checksum.java +++ b/src/main/java/net/fabricmc/loom/util/Checksum.java @@ -96,6 +96,7 @@ public class Checksum { public static String projectHash(Project project) { String str = project.getProjectDir().getAbsolutePath() + ":" + project.getPath(); - return toHex(str.getBytes(StandardCharsets.UTF_8)).substring(0, 16); + String hex = sha1Hex(str.getBytes(StandardCharsets.UTF_8)); + return hex.substring(hex.length() - 16); } } From f0ca06f912bd976cb9d1a51589a227ed01f7fcb3 Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 11 Mar 2024 11:36:52 +0000 Subject: [PATCH 05/20] Cleanup MinecraftJarConfiguration (#1070) * Cleanup MinecraftJarConfiguration * Fixes * Fixes --- .../loom/api/LoomGradleExtensionAPI.java | 2 +- .../configuration/CompileConfiguration.java | 14 +- .../ide/idea/DownloadSourcesHook.java | 3 +- .../minecraft/MinecraftJarConfiguration.java | 163 ++++++++++-------- .../minecraft/MinecraftLibraryProvider.java | 4 +- .../minecraft/SingleJarMinecraftProvider.java | 69 +++++--- .../extension/LoomGradleExtensionApiImpl.java | 7 +- .../net/fabricmc/loom/task/LoomTasks.java | 17 +- 8 files changed, 160 insertions(+), 119 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index 255c5da5..3dc9325f 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -204,7 +204,7 @@ public interface LoomGradleExtensionAPI { */ Property getIntermediaryUrl(); - Property getMinecraftJarConfiguration(); + Property> getMinecraftJarConfiguration(); default void serverOnlyMinecraftJar() { getMinecraftJarConfiguration().set(MinecraftJarConfiguration.SERVER_ONLY); diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index db6e8847..94ba3ea4 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -63,7 +63,6 @@ import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.ModJavadocProcessor; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; -import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider; @@ -152,10 +151,10 @@ public abstract class CompileConfiguration implements Runnable { private synchronized void setupMinecraft(ConfigContext configContext) throws Exception { final Project project = configContext.project(); final LoomGradleExtension extension = configContext.extension(); - final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get(); + final var jarConfiguration = extension.getMinecraftJarConfiguration().get(); // Provide the vanilla mc jars -- TODO share across getProject()s. - final MinecraftProvider minecraftProvider = jarConfiguration.getMinecraftProviderFunction().apply(configContext); + final MinecraftProvider minecraftProvider = jarConfiguration.createMinecraftProvider(configContext); extension.setMinecraftProvider(minecraftProvider); minecraftProvider.provide(); @@ -168,15 +167,15 @@ public abstract class CompileConfiguration implements Runnable { mappingConfiguration.applyToProject(getProject(), mappingsDep); // Provide the remapped mc jars - final IntermediaryMinecraftProvider intermediaryMinecraftProvider = jarConfiguration.getIntermediaryMinecraftProviderBiFunction().apply(project, minecraftProvider); - NamedMinecraftProvider namedMinecraftProvider = jarConfiguration.getNamedMinecraftProviderBiFunction().apply(project, minecraftProvider); + final IntermediaryMinecraftProvider intermediaryMinecraftProvider = jarConfiguration.createIntermediaryMinecraftProvider(project); + NamedMinecraftProvider namedMinecraftProvider = jarConfiguration.createNamedMinecraftProvider(project); registerGameProcessors(configContext); MinecraftJarProcessorManager minecraftJarProcessorManager = MinecraftJarProcessorManager.create(getProject()); if (minecraftJarProcessorManager != null) { // Wrap the named MC provider for one that will provide the processed jars - namedMinecraftProvider = jarConfiguration.getProcessedNamedMinecraftProviderBiFunction().apply(namedMinecraftProvider, minecraftJarProcessorManager); + namedMinecraftProvider = jarConfiguration.createProcessedNamedMinecraftProvider(namedMinecraftProvider, minecraftJarProcessorManager); } final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(true, extension.refreshDeps(), configContext); @@ -237,8 +236,7 @@ public abstract class CompileConfiguration implements Runnable { final LoomGradleExtension extension = configContext.extension(); extension.getMinecraftJarConfiguration().get() - .getDecompileConfigurationBiFunction() - .apply(configContext.project(), extension.getNamedMinecraftProvider()) + .createDecompileConfiguration(getProject()) .afterEvaluation(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java b/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java index 98244e3a..d799b45d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/idea/DownloadSourcesHook.java @@ -116,8 +116,7 @@ record DownloadSourcesHook(Project project, Task task) { private String getGenSourcesTaskName(MinecraftJar.Type jarType) { LoomGradleExtension extension = LoomGradleExtension.get(project); return extension.getMinecraftJarConfiguration().get() - .getDecompileConfigurationBiFunction() - .apply(project, extension.getNamedMinecraftProvider()) + .createDecompileConfiguration(project) .getTaskName(jarType); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java index 34e8f42e..0efb21c2 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java @@ -25,11 +25,10 @@ package net.fabricmc.loom.configuration.providers.minecraft; import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; import org.gradle.api.Project; +import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.decompile.DecompileConfiguration; import net.fabricmc.loom.configuration.decompile.SingleJarDecompileConfiguration; @@ -40,85 +39,111 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.MappedMinecraf import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.mapped.ProcessedNamedMinecraftProvider; -public enum MinecraftJarConfiguration { - MERGED( - MergedMinecraftProvider::new, - IntermediaryMinecraftProvider.MergedImpl::new, - NamedMinecraftProvider.MergedImpl::new, - ProcessedNamedMinecraftProvider.MergedImpl::new, - SingleJarDecompileConfiguration::new, - List.of("client", "server") - ), - SERVER_ONLY( - SingleJarMinecraftProvider::server, - IntermediaryMinecraftProvider.SingleJarImpl::server, - NamedMinecraftProvider.SingleJarImpl::server, - ProcessedNamedMinecraftProvider.SingleJarImpl::server, - SingleJarDecompileConfiguration::new, - List.of("server") - ), - CLIENT_ONLY( - SingleJarMinecraftProvider::client, - IntermediaryMinecraftProvider.SingleJarImpl::client, - NamedMinecraftProvider.SingleJarImpl::client, - ProcessedNamedMinecraftProvider.SingleJarImpl::client, - SingleJarDecompileConfiguration::new, - List.of("client") - ), - SPLIT( - SplitMinecraftProvider::new, - IntermediaryMinecraftProvider.SplitImpl::new, - NamedMinecraftProvider.SplitImpl::new, - ProcessedNamedMinecraftProvider.SplitImpl::new, - SplitDecompileConfiguration::new, - List.of("client", "server") - ); +public record MinecraftJarConfiguration< + M extends MinecraftProvider, + N extends NamedMinecraftProvider, + Q extends MappedMinecraftProvider>( + MinecraftProviderFactory minecraftProviderFactory, + IntermediaryMinecraftProviderFactory intermediaryMinecraftProviderFactory, + NamedMinecraftProviderFactory namedMinecraftProviderFactory, + ProcessedNamedMinecraftProviderFactory processedNamedMinecraftProviderFactory, + DecompileConfigurationFactory decompileConfigurationFactory, + List supportedEnvironments) { + public static final MinecraftJarConfiguration< + MergedMinecraftProvider, + NamedMinecraftProvider.MergedImpl, + MappedMinecraftProvider> MERGED = new MinecraftJarConfiguration<>( + MergedMinecraftProvider::new, + IntermediaryMinecraftProvider.MergedImpl::new, + NamedMinecraftProvider.MergedImpl::new, + ProcessedNamedMinecraftProvider.MergedImpl::new, + SingleJarDecompileConfiguration::new, + List.of("client", "server") + ); + public static final MinecraftJarConfiguration< + SingleJarMinecraftProvider, + NamedMinecraftProvider.SingleJarImpl, + MappedMinecraftProvider> SERVER_ONLY = new MinecraftJarConfiguration<>( + SingleJarMinecraftProvider::server, + IntermediaryMinecraftProvider.SingleJarImpl::server, + NamedMinecraftProvider.SingleJarImpl::server, + ProcessedNamedMinecraftProvider.SingleJarImpl::server, + SingleJarDecompileConfiguration::new, + List.of("server") + ); + public static final MinecraftJarConfiguration< + SingleJarMinecraftProvider, + NamedMinecraftProvider.SingleJarImpl, + MappedMinecraftProvider> CLIENT_ONLY = new MinecraftJarConfiguration<>( + SingleJarMinecraftProvider::client, + IntermediaryMinecraftProvider.SingleJarImpl::client, + NamedMinecraftProvider.SingleJarImpl::client, + ProcessedNamedMinecraftProvider.SingleJarImpl::client, + SingleJarDecompileConfiguration::new, + List.of("client") + ); + public static final MinecraftJarConfiguration< + SplitMinecraftProvider, + NamedMinecraftProvider.SplitImpl, + MappedMinecraftProvider.Split> SPLIT = new MinecraftJarConfiguration<>( + SplitMinecraftProvider::new, + IntermediaryMinecraftProvider.SplitImpl::new, + NamedMinecraftProvider.SplitImpl::new, + ProcessedNamedMinecraftProvider.SplitImpl::new, + SplitDecompileConfiguration::new, + List.of("client", "server") + ); - private final Function minecraftProviderFunction; - private final BiFunction> intermediaryMinecraftProviderBiFunction; - private final BiFunction> namedMinecraftProviderBiFunction; - private final BiFunction, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider> processedNamedMinecraftProviderBiFunction; - private final BiFunction> decompileConfigurationBiFunction; - private final List supportedEnvironments; - - @SuppressWarnings("unchecked") // Just a bit of a generic mess :) - , Q extends MappedMinecraftProvider> MinecraftJarConfiguration( - Function minecraftProviderFunction, - BiFunction> intermediaryMinecraftProviderBiFunction, - BiFunction namedMinecraftProviderBiFunction, - BiFunction> processedNamedMinecraftProviderBiFunction, - BiFunction> decompileConfigurationBiFunction, - List supportedEnvironments - ) { - this.minecraftProviderFunction = (Function) minecraftProviderFunction; - this.intermediaryMinecraftProviderBiFunction = (BiFunction>) (Object) intermediaryMinecraftProviderBiFunction; - this.namedMinecraftProviderBiFunction = (BiFunction>) namedMinecraftProviderBiFunction; - this.processedNamedMinecraftProviderBiFunction = (BiFunction, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider>) (Object) processedNamedMinecraftProviderBiFunction; - this.decompileConfigurationBiFunction = (BiFunction>) decompileConfigurationBiFunction; - this.supportedEnvironments = supportedEnvironments; + public MinecraftProvider createMinecraftProvider(ConfigContext context) { + return minecraftProviderFactory.create(context); } - public Function getMinecraftProviderFunction() { - return minecraftProviderFunction; + public IntermediaryMinecraftProvider createIntermediaryMinecraftProvider(Project project) { + return intermediaryMinecraftProviderFactory.create(project, getMinecraftProvider(project)); } - public BiFunction> getIntermediaryMinecraftProviderBiFunction() { - return intermediaryMinecraftProviderBiFunction; + public NamedMinecraftProvider createNamedMinecraftProvider(Project project) { + return namedMinecraftProviderFactory.create(project, getMinecraftProvider(project)); } - public BiFunction> getNamedMinecraftProviderBiFunction() { - return namedMinecraftProviderBiFunction; + public ProcessedNamedMinecraftProvider createProcessedNamedMinecraftProvider(NamedMinecraftProvider namedMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager) { + return processedNamedMinecraftProviderFactory.create((N) namedMinecraftProvider, jarProcessorManager); } - public BiFunction, MinecraftJarProcessorManager, ProcessedNamedMinecraftProvider> getProcessedNamedMinecraftProviderBiFunction() { - return processedNamedMinecraftProviderBiFunction; + public DecompileConfiguration createDecompileConfiguration(Project project) { + return decompileConfigurationFactory.create(project, getMappedMinecraftProvider(project)); } - public BiFunction> getDecompileConfigurationBiFunction() { - return decompileConfigurationBiFunction; + private M getMinecraftProvider(Project project) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + //noinspection unchecked + return (M) extension.getMinecraftProvider(); } - public List getSupportedEnvironments() { - return supportedEnvironments; + private Q getMappedMinecraftProvider(Project project) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + //noinspection unchecked + return (Q) extension.getNamedMinecraftProvider(); + } + + // Factory interfaces: + private interface MinecraftProviderFactory { + M create(ConfigContext configContext); + } + + private interface IntermediaryMinecraftProviderFactory { + IntermediaryMinecraftProvider create(Project project, M minecraftProvider); + } + + private interface NamedMinecraftProviderFactory { + NamedMinecraftProvider create(Project project, M minecraftProvider); + } + + private interface ProcessedNamedMinecraftProviderFactory> { + ProcessedNamedMinecraftProvider create(N namedMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager); + } + + private interface DecompileConfigurationFactory { + DecompileConfiguration create(Project project, M minecraftProvider); } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java index 62fa2bdd..896a2bd5 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftLibraryProvider.java @@ -82,8 +82,8 @@ public class MinecraftLibraryProvider { final LoomGradleExtension extension = LoomGradleExtension.get(project); final MinecraftJarConfiguration jarConfiguration = extension.getMinecraftJarConfiguration().get(); - final boolean provideClient = jarConfiguration.getSupportedEnvironments().contains("client"); - final boolean provideServer = jarConfiguration.getSupportedEnvironments().contains("server"); + final boolean provideClient = jarConfiguration.supportedEnvironments().contains("client"); + final boolean provideServer = jarConfiguration.supportedEnvironments().contains("server"); assert provideClient || provideServer; if (provideClient) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index 50e4cdd9..0b57b23d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -34,29 +34,26 @@ import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; -public final class SingleJarMinecraftProvider extends MinecraftProvider { - private final Environment environment; - +public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvider permits SingleJarMinecraftProvider.Server, SingleJarMinecraftProvider.Client { private Path minecraftEnvOnlyJar; - private SingleJarMinecraftProvider(ConfigContext configContext, Environment environment) { + private SingleJarMinecraftProvider(ConfigContext configContext) { super(configContext); - this.environment = environment; } - public static SingleJarMinecraftProvider server(ConfigContext configContext) { - return new SingleJarMinecraftProvider(configContext, new Server()); + public static SingleJarMinecraftProvider.Server server(ConfigContext configContext) { + return new SingleJarMinecraftProvider.Server(configContext); } - public static SingleJarMinecraftProvider client(ConfigContext configContext) { - return new SingleJarMinecraftProvider(configContext, new Client()); + public static SingleJarMinecraftProvider.Client client(ConfigContext configContext) { + return new SingleJarMinecraftProvider.Client(configContext); } @Override protected void initFiles() { super.initFiles(); - minecraftEnvOnlyJar = path("minecraft-%s-only.jar".formatted(environment.type())); + minecraftEnvOnlyJar = path("minecraft-%s-only.jar".formatted(type())); } @Override @@ -79,7 +76,7 @@ public final class SingleJarMinecraftProvider extends MinecraftProvider { return; } - final Path inputJar = environment.getInputJar(this); + final Path inputJar = getInputJar(this); TinyRemapper remapper = null; @@ -96,7 +93,7 @@ public final class SingleJarMinecraftProvider extends MinecraftProvider { } } catch (Exception e) { Files.deleteIfExists(minecraftEnvOnlyJar); - throw new RuntimeException("Failed to process %s only jar".formatted(environment.type()), e); + throw new RuntimeException("Failed to process %s only jar".formatted(type()), e); } finally { if (remapper != null) { remapper.finish(); @@ -104,27 +101,19 @@ public final class SingleJarMinecraftProvider extends MinecraftProvider { } } - @Override - protected boolean provideClient() { - return environment instanceof Client; - } - - @Override - protected boolean provideServer() { - return environment instanceof Server; - } - public Path getMinecraftEnvOnlyJar() { return minecraftEnvOnlyJar; } - private interface Environment { - SingleJarEnvType type(); + abstract SingleJarEnvType type(); - Path getInputJar(SingleJarMinecraftProvider provider) throws Exception; - } + abstract Path getInputJar(SingleJarMinecraftProvider provider) throws Exception; + + public static final class Server extends SingleJarMinecraftProvider { + private Server(ConfigContext configContext) { + super(configContext); + } - private static final class Server implements Environment { @Override public SingleJarEnvType type() { return SingleJarEnvType.SERVER; @@ -141,9 +130,23 @@ public final class SingleJarMinecraftProvider extends MinecraftProvider { provider.extractBundledServerJar(); return provider.getMinecraftExtractedServerJar().toPath(); } + + @Override + protected boolean provideServer() { + return true; + } + + @Override + protected boolean provideClient() { + return false; + } } - private static final class Client implements Environment { + public static final class Client extends SingleJarMinecraftProvider { + private Client(ConfigContext configContext) { + super(configContext); + } + @Override public SingleJarEnvType type() { return SingleJarEnvType.CLIENT; @@ -153,5 +156,15 @@ public final class SingleJarMinecraftProvider extends MinecraftProvider { public Path getInputJar(SingleJarMinecraftProvider provider) throws Exception { return provider.getMinecraftClientJar().toPath(); } + + @Override + protected boolean provideServer() { + return false; + } + + @Override + protected boolean provideClient() { + return true; + } } } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java index e81a0560..49bc25b4 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionApiImpl.java @@ -91,7 +91,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA protected final Property intermediateMappingsProvider; private final Property runtimeOnlyLog4j; private final Property splitModDependencies; - private final Property minecraftJarConfiguration; + private final Property> minecraftJarConfiguration; private final Property splitEnvironmentalSourceSet; private final InterfaceInjectionExtensionAPI interfaceInjectionExtension; @@ -144,7 +144,8 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA this.minecraftJarProcessors = (ListProperty>) (Object) project.getObjects().listProperty(MinecraftJarProcessor.class); this.minecraftJarProcessors.finalizeValueOnRead(); - this.minecraftJarConfiguration = project.getObjects().property(MinecraftJarConfiguration.class).convention(MinecraftJarConfiguration.MERGED); + //noinspection unchecked + this.minecraftJarConfiguration = project.getObjects().property((Class>) (Class) MinecraftJarConfiguration.class).convention(MinecraftJarConfiguration.MERGED); this.minecraftJarConfiguration.finalizeValueOnRead(); this.accessWidener.finalizeValueOnRead(); @@ -341,7 +342,7 @@ public abstract class LoomGradleExtensionApiImpl implements LoomGradleExtensionA } @Override - public Property getMinecraftJarConfiguration() { + public Property> getMinecraftJarConfiguration() { return minecraftJarConfiguration; } diff --git a/src/main/java/net/fabricmc/loom/task/LoomTasks.java b/src/main/java/net/fabricmc/loom/task/LoomTasks.java index e32cfa5d..794f26ac 100644 --- a/src/main/java/net/fabricmc/loom/task/LoomTasks.java +++ b/src/main/java/net/fabricmc/loom/task/LoomTasks.java @@ -164,13 +164,18 @@ public abstract class LoomTasks implements Runnable { // Remove the client or server run config when not required. Done by name to not remove any possible custom run configs GradleUtils.afterSuccessfulEvaluation(getProject(), () -> { - String taskName = switch (extension.getMinecraftJarConfiguration().get()) { - case SERVER_ONLY -> "client"; - case CLIENT_ONLY -> "server"; - default -> null; - }; + String taskName; - if (taskName == null) { + boolean serverOnly = extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.SERVER_ONLY; + boolean clientOnly = extension.getMinecraftJarConfiguration().get() == MinecraftJarConfiguration.CLIENT_ONLY; + + if (serverOnly) { + // Server only, remove the client run config + taskName = "client"; + } else if (clientOnly) { + // Client only, remove the server run config + taskName = "server"; + } else { return; } From 994a97e3de49017d4fd1410d7a1a450012637f81 Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 11 Mar 2024 12:57:52 +0000 Subject: [PATCH 06/20] Refactor MinecraftMetadataProvider to create it earlier (#1072) --- .../configuration/CompileConfiguration.java | 8 ++- .../minecraft/MergedMinecraftProvider.java | 11 +++- .../minecraft/MinecraftJarConfiguration.java | 6 +- .../minecraft/MinecraftMetadataProvider.java | 28 ++++++++- .../minecraft/MinecraftProvider.java | 60 ++++++++----------- .../minecraft/SingleJarMinecraftProvider.java | 20 +++---- .../minecraft/SplitMinecraftProvider.java | 4 +- 7 files changed, 80 insertions(+), 57 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 94ba3ea4..7cf9c62d 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -63,6 +63,7 @@ import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.ModJavadocProcessor; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMetadataProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.mapped.AbstractMappedMinecraftProvider; @@ -151,10 +152,13 @@ public abstract class CompileConfiguration implements Runnable { private synchronized void setupMinecraft(ConfigContext configContext) throws Exception { final Project project = configContext.project(); final LoomGradleExtension extension = configContext.extension(); + + final MinecraftMetadataProvider metadataProvider = MinecraftMetadataProvider.create(configContext); + final var jarConfiguration = extension.getMinecraftJarConfiguration().get(); - // Provide the vanilla mc jars -- TODO share across getProject()s. - final MinecraftProvider minecraftProvider = jarConfiguration.createMinecraftProvider(configContext); + // Provide the vanilla mc jars + final MinecraftProvider minecraftProvider = jarConfiguration.createMinecraftProvider(metadataProvider, configContext); extension.setMinecraftProvider(minecraftProvider); minecraftProvider.provide(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java index 40206306..b0ff9d65 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java @@ -31,13 +31,18 @@ import java.nio.file.Path; import java.util.List; import java.util.Objects; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import net.fabricmc.loom.configuration.ConfigContext; public final class MergedMinecraftProvider extends MinecraftProvider { + private static final Logger LOGGER = LoggerFactory.getLogger(MergedMinecraftProvider.class); + private Path minecraftMergedJar; - public MergedMinecraftProvider(ConfigContext configContext) { - super(configContext); + public MergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); } @Override @@ -74,7 +79,7 @@ public final class MergedMinecraftProvider extends MinecraftProvider { } private void mergeJars() throws IOException { - getLogger().info(":merging jars"); + LOGGER.info(":merging jars"); File jarToMerge = getMinecraftServerJar(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java index 0efb21c2..927cbe72 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java @@ -94,8 +94,8 @@ public record MinecraftJarConfiguration< List.of("client", "server") ); - public MinecraftProvider createMinecraftProvider(ConfigContext context) { - return minecraftProviderFactory.create(context); + public MinecraftProvider createMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext context) { + return minecraftProviderFactory.create(metadataProvider, context); } public IntermediaryMinecraftProvider createIntermediaryMinecraftProvider(Project project) { @@ -128,7 +128,7 @@ public record MinecraftJarConfiguration< // Factory interfaces: private interface MinecraftProviderFactory { - M create(ConfigContext configContext); + M create(MinecraftMetadataProvider metadataProvider, ConfigContext configContext); } private interface IntermediaryMinecraftProviderFactory { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java index 840207f6..fd2eea8e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftMetadataProvider.java @@ -35,6 +35,9 @@ import org.jetbrains.annotations.Nullable; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradlePlugin; +import net.fabricmc.loom.configuration.ConfigContext; +import net.fabricmc.loom.configuration.DependencyInfo; +import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.MirrorUtil; import net.fabricmc.loom.util.download.DownloadBuilder; @@ -45,11 +48,34 @@ public final class MinecraftMetadataProvider { private ManifestVersion.Versions versionEntry; private MinecraftVersionMeta versionMeta; - public MinecraftMetadataProvider(Options options, Function download) { + private MinecraftMetadataProvider(Options options, Function download) { this.options = options; this.download = download; } + public static MinecraftMetadataProvider create(ConfigContext configContext) { + final String minecraftVersion = resolveMinecraftVersion(configContext.project()); + final Path workingDir = MinecraftProvider.minecraftWorkingDirectory(configContext.project(), minecraftVersion).toPath(); + + return new MinecraftMetadataProvider( + MinecraftMetadataProvider.Options.create( + minecraftVersion, + configContext.project(), + workingDir.resolve("minecraft-info.json") + ), + configContext.extension()::download + ); + } + + private static String resolveMinecraftVersion(Project project) { + final DependencyInfo dependency = DependencyInfo.create(project, Constants.Configurations.MINECRAFT); + return dependency.getDependency().getVersion(); + } + + public String getMinecraftVersion() { + return options.minecraftVersion(); + } + public MinecraftVersionMeta getVersionMeta() { try { if (versionEntry == null) { diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index 0c966c2e..0e6a0682 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -33,23 +33,22 @@ import java.util.Objects; import com.google.common.base.Preconditions; import org.gradle.api.JavaVersion; import org.gradle.api.Project; -import org.gradle.api.logging.Logger; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.ConfigContext; -import net.fabricmc.loom.configuration.DependencyInfo; import net.fabricmc.loom.configuration.providers.BundleMetadata; -import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.download.DownloadExecutor; import net.fabricmc.loom.util.download.GradleDownloadProgressListener; import net.fabricmc.loom.util.gradle.ProgressGroup; public abstract class MinecraftProvider { - private String minecraftVersion; - private MinecraftMetadataProvider metadataProvider; + private static final Logger LOGGER = LoggerFactory.getLogger(MinecraftProvider.class); + + private final MinecraftMetadataProvider metadataProvider; - private File workingDir; private File minecraftClientJar; // Note this will be the boostrap jar starting with 21w39a private File minecraftServerJar; @@ -58,10 +57,11 @@ public abstract class MinecraftProvider { @Nullable private BundleMetadata serverBundleMetadata; - private final Project project; + private final ConfigContext configContext; - public MinecraftProvider(ConfigContext configContext) { - this.project = configContext.project(); + public MinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + this.metadataProvider = metadataProvider; + this.configContext = configContext; } protected boolean provideClient() { @@ -73,20 +73,8 @@ public abstract class MinecraftProvider { } public void provide() throws Exception { - final DependencyInfo dependency = DependencyInfo.create(getProject(), Constants.Configurations.MINECRAFT); - minecraftVersion = dependency.getDependency().getVersion(); - initFiles(); - metadataProvider = new MinecraftMetadataProvider( - MinecraftMetadataProvider.Options.create( - minecraftVersion, - getProject(), - file("minecraft-info.json").toPath() - ), - getExtension()::download - ); - final MinecraftVersionMeta.JavaVersion javaVersion = getVersionInfo().javaVersion(); if (javaVersion != null) { @@ -94,7 +82,7 @@ public abstract class MinecraftProvider { final JavaVersion requiredJavaVersion = JavaVersion.toVersion(requiredMajorJavaVersion); if (!JavaVersion.current().isCompatibleWith(requiredJavaVersion)) { - throw new IllegalStateException("Minecraft " + minecraftVersion + " requires Java " + requiredJavaVersion + " but Gradle is using " + JavaVersion.current()); + throw new IllegalStateException("Minecraft " + minecraftVersion() + " requires Java " + requiredJavaVersion + " but Gradle is using " + JavaVersion.current()); } } @@ -104,14 +92,11 @@ public abstract class MinecraftProvider { serverBundleMetadata = BundleMetadata.fromJar(minecraftServerJar.toPath()); } - final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, project); + final MinecraftLibraryProvider libraryProvider = new MinecraftLibraryProvider(this, configContext.project()); libraryProvider.provide(); } protected void initFiles() { - workingDir = new File(getExtension().getFiles().getUserCache(), minecraftVersion); - workingDir.mkdirs(); - if (provideClient()) { minecraftClientJar = file("minecraft-client.jar"); } @@ -147,17 +132,17 @@ public abstract class MinecraftProvider { Preconditions.checkArgument(provideServer(), "Not configured to provide server jar"); Objects.requireNonNull(getServerBundleMetadata(), "Cannot bundled mc jar from none bundled server jar"); - getLogger().info(":Extracting server jar from bootstrap"); + LOGGER.info(":Extracting server jar from bootstrap"); if (getServerBundleMetadata().versions().size() != 1) { throw new UnsupportedOperationException("Expected only 1 version in META-INF/versions.list, but got %d".formatted(getServerBundleMetadata().versions().size())); } - getServerBundleMetadata().versions().get(0).unpackEntry(minecraftServerJar.toPath(), getMinecraftExtractedServerJar().toPath(), project); + getServerBundleMetadata().versions().get(0).unpackEntry(minecraftServerJar.toPath(), getMinecraftExtractedServerJar().toPath(), configContext.project()); } public File workingDir() { - return workingDir; + return minecraftWorkingDirectory(configContext.project(), minecraftVersion()); } public File dir(String path) { @@ -193,7 +178,7 @@ public abstract class MinecraftProvider { } public String minecraftVersion() { - return minecraftVersion; + return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getMinecraftVersion(); } public MinecraftVersionMeta getVersionInfo() { @@ -205,21 +190,24 @@ public abstract class MinecraftProvider { return serverBundleMetadata; } - protected Logger getLogger() { - return getProject().getLogger(); - } - public abstract List getMinecraftJars(); protected Project getProject() { - return project; + return configContext.project(); } protected LoomGradleExtension getExtension() { - return LoomGradleExtension.get(getProject()); + return configContext.extension(); } public boolean refreshDeps() { return getExtension().refreshDeps(); } + + public static File minecraftWorkingDirectory(Project project, String version) { + LoomGradleExtension extension = LoomGradleExtension.get(project); + File workingDir = new File(extension.getFiles().getUserCache(), version); + workingDir.mkdirs(); + return workingDir; + } } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index 0b57b23d..389873c6 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -37,16 +37,16 @@ import net.fabricmc.tinyremapper.TinyRemapper; public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvider permits SingleJarMinecraftProvider.Server, SingleJarMinecraftProvider.Client { private Path minecraftEnvOnlyJar; - private SingleJarMinecraftProvider(ConfigContext configContext) { - super(configContext); + private SingleJarMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); } - public static SingleJarMinecraftProvider.Server server(ConfigContext configContext) { - return new SingleJarMinecraftProvider.Server(configContext); + public static SingleJarMinecraftProvider.Server server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + return new SingleJarMinecraftProvider.Server(metadataProvider, configContext); } - public static SingleJarMinecraftProvider.Client client(ConfigContext configContext) { - return new SingleJarMinecraftProvider.Client(configContext); + public static SingleJarMinecraftProvider.Client client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + return new SingleJarMinecraftProvider.Client(metadataProvider, configContext); } @Override @@ -110,8 +110,8 @@ public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvide abstract Path getInputJar(SingleJarMinecraftProvider provider) throws Exception; public static final class Server extends SingleJarMinecraftProvider { - private Server(ConfigContext configContext) { - super(configContext); + private Server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); } @Override @@ -143,8 +143,8 @@ public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvide } public static final class Client extends SingleJarMinecraftProvider { - private Client(ConfigContext configContext) { - super(configContext); + private Client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); } @Override diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java index f970aa16..69f0be26 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java @@ -35,8 +35,8 @@ public final class SplitMinecraftProvider extends MinecraftProvider { private Path minecraftClientOnlyJar; private Path minecraftCommonJar; - public SplitMinecraftProvider(ConfigContext configContext) { - super(configContext); + public SplitMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); } @Override From c60b456f7eeec57410aff67ce0f3daad8b4507f4 Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 11 Mar 2024 21:16:46 +0000 Subject: [PATCH 07/20] Print info about locked files during configuration or genSources (#1066) * Print info about locked files during configuration or genSources * Use release version * Output adjustments * Fix build * Add user back --- build.gradle | 3 +- gradle/libs.versions.toml | 2 + .../configuration/CompileConfiguration.java | 71 +----------- .../loom/task/GenerateSourcesTask.java | 3 +- .../net/fabricmc/loom/util/ExceptionUtil.java | 47 +++++++- .../net/fabricmc/loom/util/ProcessUtil.java | 109 ++++++++++++++++++ .../loom/test/unit/ProcessUtilTest.groovy | 41 +++++++ 7 files changed, 206 insertions(+), 70 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/util/ProcessUtil.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/ProcessUtilTest.groovy diff --git a/build.gradle b/build.gradle index c15a009d..8c426029 100644 --- a/build.gradle +++ b/build.gradle @@ -131,11 +131,12 @@ dependencies { implementation libs.fabric.tiny.remapper implementation libs.fabric.access.widener implementation libs.fabric.mapping.io - implementation (libs.fabric.lorenz.tiny) { transitive = false } + implementation libs.fabric.loom.nativelib + // decompilers fernflowerCompileOnly runtimeLibs.fernflower fernflowerCompileOnly libs.fabric.mapping.io diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index acb5aa82..a4b1d3bc 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ mapping-io = "0.5.1" lorenz-tiny = "4.0.2" mercury = "0.4.1" kotlinx-metadata = "0.9.0" +loom-native = "0.1.0" # Plugins spotless = "6.25.0" @@ -38,6 +39,7 @@ fabric-access-widener = { module = "net.fabricmc:access-widener", version.ref = fabric-mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping-io" } fabric-lorenz-tiny = { module = "net.fabricmc:lorenz-tiny", version.ref = "lorenz-tiny" } fabric-mercury = { module = "net.fabricmc:mercury", version.ref = "mercury" } +fabric-loom-nativelib = { module = "net.fabricmc:fabric-loom-native", version.ref = "loom-native" } # Misc kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 7cf9c62d..1e59e9fc 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -32,8 +32,6 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; -import java.util.ArrayList; -import java.util.List; import java.util.Optional; import java.util.function.Consumer; @@ -41,7 +39,6 @@ import javax.inject.Inject; import org.gradle.api.GradleException; import org.gradle.api.Project; -import org.gradle.api.logging.LogLevel; import org.gradle.api.logging.Logger; import org.gradle.api.logging.Logging; import org.gradle.api.plugins.JavaPlugin; @@ -72,6 +69,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraft import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.util.Checksum; import net.fabricmc.loom.util.ExceptionUtil; +import net.fabricmc.loom.util.ProcessUtil; import net.fabricmc.loom.util.gradle.GradleUtils; import net.fabricmc.loom.util.gradle.SourceSetHelper; import net.fabricmc.loom.util.service.ScopedSharedServiceManager; @@ -110,6 +108,7 @@ public abstract class CompileConfiguration implements Runnable { try { setupMinecraft(configContext); } catch (Exception e) { + ExceptionUtil.printFileLocks(e, getProject()); throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to setup Minecraft", e); } @@ -315,7 +314,8 @@ public abstract class CompileConfiguration implements Runnable { Files.deleteIfExists(lockFile.file); abrupt = true; } else { - logger.lifecycle(printWithParents(handle.get())); + ProcessUtil processUtil = ProcessUtil.create(getProject()); + logger.lifecycle(processUtil.printWithParents(handle.get())); logger.lifecycle("Waiting for lock to be released..."); long sleptMs = 0; @@ -353,69 +353,6 @@ public abstract class CompileConfiguration implements Runnable { return abrupt ? LockResult.ACQUIRED_PREVIOUS_OWNER_MISSING : LockResult.ACQUIRED_CLEAN; } - private String printWithParents(ProcessHandle processHandle) { - var output = new StringBuilder(); - - List chain = getParentChain(null, processHandle); - - for (int i = 0; i < chain.size(); i++) { - ProcessHandle handle = chain.get(i); - - output.append("\t".repeat(i)); - - if (i != 0) { - output.append("└─ "); - } - - output.append(getInfoString(handle)); - - if (i < chain.size() - 1) { - output.append('\n'); - } - } - - return output.toString(); - } - - private String getInfoString(ProcessHandle handle) { - return "(%s) pid %s '%s%s'%s".formatted( - handle.info().user().orElse("unknown user"), - handle.pid(), - handle.info().command().orElse("unknown command"), - handle.info().arguments().map(arr -> { - if (getProject().getGradle().getStartParameter().getLogLevel() != LogLevel.INFO - && getProject().getGradle().getStartParameter().getLogLevel() != LogLevel.DEBUG) { - return " (run with --info or --debug to show arguments, may reveal sensitive info)"; - } - - String join = String.join(" ", arr); - - if (join.isBlank()) { - return ""; - } - - return " " + join; - }).orElse(" (unknown arguments)"), - handle.info().startInstant().map(instant -> " started at " + instant).orElse("") - ); - } - - private List getParentChain(List collectTo, ProcessHandle processHandle) { - if (collectTo == null) { - collectTo = new ArrayList<>(); - } - - Optional parent = processHandle.parent(); - - if (parent.isPresent()) { - getParentChain(collectTo, parent.get()); - } - - collectTo.add(processHandle); - - return collectTo; - } - private void releaseLock() { final Path lock = getLockFile().file; diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 51850533..0e235b0b 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -198,7 +198,8 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { final var provideContext = new AbstractMappedMinecraftProvider.ProvideContext(false, true, configContext); minecraftJars = getExtension().getNamedMinecraftProvider().provide(provideContext); } catch (Exception e) { - throw new RuntimeException("Failed to rebuild input jars", e); + ExceptionUtil.printFileLocks(e, getProject()); + throw ExceptionUtil.createDescriptiveWrapper(RuntimeException::new, "Failed to rebuild input jars", e); } for (MinecraftJar minecraftJar : minecraftJars) { diff --git a/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java b/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java index 3683301e..8f942260 100644 --- a/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java +++ b/src/main/java/net/fabricmc/loom/util/ExceptionUtil.java @@ -1,7 +1,7 @@ /* * This file is part of fabric-loom, licensed under the MIT License (MIT). * - * Copyright (c) 2022 FabricMC + * Copyright (c) 2022-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 @@ -24,8 +24,17 @@ package net.fabricmc.loom.util; +import java.nio.file.FileSystemException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; import java.util.function.BiFunction; +import org.gradle.api.Project; + +import net.fabricmc.loom.nativeplatform.LoomNativePlatform; + public final class ExceptionUtil { /** * Creates a descriptive user-facing wrapper exception for an underlying cause. @@ -44,4 +53,40 @@ public final class ExceptionUtil { String descriptiveMessage = "%s, %s: %s".formatted(message, cause.getClass().getName(), cause.getMessage()); return constructor.apply(descriptiveMessage, cause); } + + public static void printFileLocks(Throwable e, Project project) { + Throwable cause = e; + + while (cause != null) { + if (cause instanceof FileSystemException fse) { + printFileLocks(fse.getFile(), project); + break; + } + + cause = cause.getCause(); + } + } + + private static void printFileLocks(String filename, Project project) { + final Path path = Paths.get(filename); + + if (!Files.exists(path)) { + return; + } + + final List processes = LoomNativePlatform.getProcessesWithLockOn(path); + + if (processes.isEmpty()) { + return; + } + + final ProcessUtil processUtil = ProcessUtil.create(project); + + final String noun = processes.size() == 1 ? "process has" : "processes have"; + project.getLogger().error("The following {} a lock on the file '{}':", noun, path); + + for (ProcessHandle process : processes) { + project.getLogger().error(processUtil.printWithParents(process)); + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/ProcessUtil.java b/src/main/java/net/fabricmc/loom/util/ProcessUtil.java new file mode 100644 index 00000000..93f8e1e3 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/ProcessUtil.java @@ -0,0 +1,109 @@ +/* + * 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.util; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.StringJoiner; + +import org.gradle.api.Project; +import org.gradle.api.logging.LogLevel; + +import net.fabricmc.loom.nativeplatform.LoomNativePlatform; + +public record ProcessUtil(LogLevel logLevel) { + private static final String EXPLORER_COMMAND = "C:\\Windows\\explorer.exe"; + + public static ProcessUtil create(Project project) { + return new ProcessUtil(project.getGradle().getStartParameter().getLogLevel()); + } + + public String printWithParents(ProcessHandle handle) { + String result = printWithParents(handle, 0).trim(); + + if (logLevel != LogLevel.INFO && logLevel != LogLevel.DEBUG) { + return "Run with --info or --debug to show arguments, may reveal sensitive info\n" + result; + } + + return result; + } + + private String printWithParents(ProcessHandle handle, int depth) { + var lines = new ArrayList(); + getWindowTitles(handle).ifPresent(titles -> lines.add("title: " + titles)); + lines.add("pid: " + handle.pid()); + handle.info().command().ifPresent(command -> lines.add("command: " + command)); + getProcessArguments(handle).ifPresent(arguments -> lines.add("arguments: " + arguments)); + handle.info().startInstant().ifPresent(instant -> lines.add("started at: " + instant)); + handle.info().user().ifPresent(user -> lines.add("user: " + user)); + handle.parent().ifPresent(parent -> lines.add("parent:\n" + printWithParents(parent, depth + 1))); + + StringBuilder sj = new StringBuilder(); + + for (String line : lines) { + sj.append("\t".repeat(depth)).append("- ").append(line).append('\n'); + } + + return sj.toString(); + } + + private Optional getProcessArguments(ProcessHandle handle) { + if (logLevel != LogLevel.INFO && logLevel != LogLevel.DEBUG) { + return Optional.empty(); + } + + return handle.info().arguments().map(arr -> { + String join = String.join(" ", arr); + + if (join.isBlank()) { + return ""; + } + + return " " + join; + }); + } + + private Optional getWindowTitles(ProcessHandle processHandle) { + if (processHandle.info().command().orElse("").equals(EXPLORER_COMMAND)) { + // Explorer is a single process, so the window titles are not useful + return Optional.empty(); + } + + List titles = LoomNativePlatform.getWindowTitlesForPid(processHandle.pid()); + + if (titles.isEmpty()) { + return Optional.empty(); + } + + final StringJoiner joiner = new StringJoiner(", "); + + for (String title : titles) { + joiner.add("'" + title + "'"); + } + + return Optional.of(joiner.toString()); + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ProcessUtilTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ProcessUtilTest.groovy new file mode 100644 index 00000000..76a96ea3 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ProcessUtilTest.groovy @@ -0,0 +1,41 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 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.test.unit + +import org.gradle.api.logging.LogLevel +import spock.lang.Specification + +import net.fabricmc.loom.util.ProcessUtil + +class ProcessUtilTest extends Specification { + def "print process info"() { + when: + def output = new ProcessUtil(LogLevel.DEBUG).printWithParents(ProcessHandle.current()) + + then: + // Just a simple check to see if the output is not empty + !output.isEmpty() + } +} From 5caac7ba8eea0320b8eecb367e3d1609341c9ae5 Mon Sep 17 00:00:00 2001 From: modmuss Date: Tue, 12 Mar 2024 13:26:14 +0000 Subject: [PATCH 08/20] Fix possible race condition in PrepareJarRemapTask (#1065) * Fix possible race condition in PrepareJarRemapTask --- src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java b/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java index 9569968a..a774ac25 100644 --- a/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java +++ b/src/main/java/net/fabricmc/loom/task/PrepareJarRemapTask.java @@ -100,6 +100,6 @@ public abstract class PrepareJarRemapTask extends AbstractLoomTask { } static void prepare(TinyRemapperService tinyRemapperService, Path inputFile) { - tinyRemapperService.getTinyRemapperForInputs().readInputs(tinyRemapperService.getOrCreateTag(inputFile), inputFile); + tinyRemapperService.getTinyRemapperForInputs().readInputsAsync(tinyRemapperService.getOrCreateTag(inputFile), inputFile); } } From dbebbdb9442f2557c492799b4185788b5254ee87 Mon Sep 17 00:00:00 2001 From: modmuss Date: Tue, 12 Mar 2024 19:11:26 +0000 Subject: [PATCH 09/20] Add RemapJarTask.getOptimizeFabricModJson() (#1068) * Optimise fabric.mod.json files * Fixes * Make opt-in * Revert * Fix --- .../signatures/SignatureFixesLayerImpl.java | 2 +- .../parchment/ParchmentMappingLayer.java | 2 +- .../net/fabricmc/loom/task/RemapJarTask.java | 25 ++++ .../java/net/fabricmc/loom/util/ZipUtils.java | 10 +- .../loom/util/fmj/FabricModJsonFactory.java | 2 +- .../loom/util/fmj/FabricModJsonUtils.java | 27 +++- .../loom/test/unit/ZipUtilsTest.groovy | 25 ++++ .../unit/fmj/FabricModJsonUtilsTest.groovy | 128 ++++++++++++++++++ 8 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java index 4328311b..b4ea05c8 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/extras/signatures/SignatureFixesLayerImpl.java @@ -47,7 +47,7 @@ public record SignatureFixesLayerImpl(Path mappingsFile) implements MappingLayer public Map getSignatureFixes() { try { //noinspection unchecked - return ZipUtils.unpackJackson(mappingsFile(), SIGNATURE_FIXES_PATH, Map.class); + return ZipUtils.unpackJson(mappingsFile(), SIGNATURE_FIXES_PATH, Map.class); } catch (IOException e) { throw new RuntimeException("Failed to extract signature fixes", e); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java index 0e2f68c6..16af9d5c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/parchment/ParchmentMappingLayer.java @@ -47,6 +47,6 @@ public record ParchmentMappingLayer(Path parchmentFile, boolean removePrefix) im } private ParchmentTreeV1 getParchmentData() throws IOException { - return ZipUtils.unpackJackson(parchmentFile, PARCHMENT_DATA_FILE_NAME, ParchmentTreeV1.class); + return ZipUtils.unpackJson(parchmentFile, PARCHMENT_DATA_FILE_NAME, ParchmentTreeV1.class); } } diff --git a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java index 0948b353..54dd46bb 100644 --- a/src/main/java/net/fabricmc/loom/task/RemapJarTask.java +++ b/src/main/java/net/fabricmc/loom/task/RemapJarTask.java @@ -75,6 +75,7 @@ import net.fabricmc.loom.util.SidedClassVisitor; import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.fmj.FabricModJson; import net.fabricmc.loom.util.fmj.FabricModJsonFactory; +import net.fabricmc.loom.util.fmj.FabricModJsonUtils; import net.fabricmc.loom.util.service.BuildSharedServiceManager; import net.fabricmc.loom.util.service.UnsafeWorkQueueHelper; import net.fabricmc.tinyremapper.OutputConsumerPath; @@ -87,6 +88,14 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { @Input public abstract Property getAddNestedDependencies(); + /** + * Whether to optimize the fabric.mod.json file, by default this is false. + * + *

The schemaVersion entry will be placed first in the json file + */ + @Input + public abstract Property getOptimizeFabricModJson(); + @Input @ApiStatus.Internal public abstract Property getUseMixinAP(); @@ -100,6 +109,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { final ConfigurationContainer configurations = getProject().getConfigurations(); getClasspath().from(configurations.getByName(JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME)); getAddNestedDependencies().convention(true).finalizeValueOnRead(); + getOptimizeFabricModJson().convention(false).finalizeValueOnRead(); Configuration includeConfiguration = configurations.getByName(Constants.Configurations.INCLUDE); getNestedJars().from(new IncludedJarFactory(getProject()).getNestedJars(includeConfiguration)); @@ -156,6 +166,8 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { final var refmapRemapType = mixinAp ? ArtifactMetadata.MixinRemapType.MIXIN : ArtifactMetadata.MixinRemapType.STATIC; params.getManifestAttributes().put(Constants.Manifest.MIXIN_REMAP_TYPE, refmapRemapType.manifestValue()); } + + params.getOptimizeFmj().set(getOptimizeFabricModJson().get()); }); } @@ -197,6 +209,7 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { Property getUseMixinExtension(); Property getMultiProjectOptimisation(); + Property getOptimizeFmj(); record RefmapData(List mixinConfigs, String refmapName) implements Serializable { } ListProperty getMixinData(); @@ -243,6 +256,10 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { modifyJarManifest(); rewriteJar(); + if (getParameters().getOptimizeFmj().get()) { + optimizeFMJ(); + } + if (tinyRemapperService != null && !getParameters().getMultiProjectOptimisation().get()) { tinyRemapperService.close(); } @@ -349,6 +366,14 @@ public abstract class RemapJarTask extends AbstractRemapJarTask { } } } + + private void optimizeFMJ() throws IOException { + if (!ZipUtils.contains(outputFile, FabricModJsonFactory.FABRIC_MOD_JSON)) { + return; + } + + ZipUtils.transformJson(JsonObject.class, outputFile, FabricModJsonFactory.FABRIC_MOD_JSON, FabricModJsonUtils::optimizeFmj); + } } @Override diff --git a/src/main/java/net/fabricmc/loom/util/ZipUtils.java b/src/main/java/net/fabricmc/loom/util/ZipUtils.java index 9b387ad9..8fdf1fd6 100644 --- a/src/main/java/net/fabricmc/loom/util/ZipUtils.java +++ b/src/main/java/net/fabricmc/loom/util/ZipUtils.java @@ -121,7 +121,7 @@ public class ZipUtils { } } - public static T unpackJackson(Path zip, String path, Class clazz) throws IOException { + public static T unpackJson(Path zip, String path, Class clazz) throws IOException { final byte[] bytes = unpack(zip, path); return LoomGradlePlugin.GSON.fromJson(new String(bytes, StandardCharsets.UTF_8), clazz); } @@ -209,6 +209,14 @@ public class ZipUtils { s -> LoomGradlePlugin.GSON.toJson(s, typeOfT).getBytes(StandardCharsets.UTF_8)); } + public static void transformJson(Class typeOfT, Path zip, String path, UnsafeUnaryOperator transformer) throws IOException { + int transformed = transformJson(typeOfT, zip, Map.of(path, transformer)); + + if (transformed != 1) { + throw new IOException("Failed to transform " + path + " in " + zip); + } + } + public static int transform(Path zip, Collection>> transforms) throws IOException { return transform(zip, transforms.stream()); } diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java index bbd22916..4bdeb403 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java @@ -46,7 +46,7 @@ import net.fabricmc.loom.util.ZipUtils; import net.fabricmc.loom.util.gradle.SourceSetHelper; public final class FabricModJsonFactory { - private static final String FABRIC_MOD_JSON = "fabric.mod.json"; + public static final String FABRIC_MOD_JSON = "fabric.mod.json"; private FabricModJsonFactory() { } diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java index 78b94032..5559551b 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonUtils.java @@ -25,13 +25,14 @@ package net.fabricmc.loom.util.fmj; import java.util.Locale; +import java.util.Map; import java.util.function.Predicate; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; -final class FabricModJsonUtils { +public final class FabricModJsonUtils { private FabricModJsonUtils() { } @@ -49,6 +50,30 @@ final class FabricModJsonUtils { return element.getAsInt(); } + // Ensure that the schemaVersion json entry, is first in the json file + // This exercises an optimisation here: https://github.com/FabricMC/fabric-loader/blob/d69cb72d26497e3f387cf46f9b24340b402a4644/src/main/java/net/fabricmc/loader/impl/metadata/ModMetadataParser.java#L62 + public static JsonObject optimizeFmj(JsonObject json) { + if (!json.has("schemaVersion")) { + // No schemaVersion, something will explode later?! + return json; + } + + // Create a new json object with the schemaVersion first + var out = new JsonObject(); + out.add("schemaVersion", json.get("schemaVersion")); + + for (Map.Entry entry : json.entrySet()) { + if (entry.getKey().equals("schemaVersion")) { + continue; + } + + // Add all other entries + out.add(entry.getKey(), entry.getValue()); + } + + return out; + } + private static JsonElement getElement(JsonObject jsonObject, String key) { final JsonElement element = jsonObject.get(key); diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy index e967638e..7798a5c0 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ZipUtilsTest.groovy @@ -28,6 +28,7 @@ import java.nio.charset.StandardCharsets import java.nio.file.Files import java.time.ZoneId +import com.google.gson.JsonObject import spock.lang.Specification import net.fabricmc.loom.util.Checksum @@ -188,4 +189,28 @@ class ZipUtilsTest extends Specification { "Etc/GMT-6" | _ "Etc/GMT+9" | _ } + + def "transform json"() { + given: + def dir = File.createTempDir() + def zip = File.createTempFile("loom-zip-test", ".zip").toPath() + new File(dir, "test.json").text = """ + { + "test": "This is a test of transforming" + } + """ + ZipUtils.pack(dir.toPath(), zip) + + when: + ZipUtils.transformJson(JsonObject.class, zip, "test.json") { json -> + def test = json.get("test").getAsString() + json.addProperty("test", test.toUpperCase()) + json + } + + def transformed = ZipUtils.unpackJson(zip, "test.json", JsonObject.class) + + then: + transformed.get("test").asString == "THIS IS A TEST OF TRANSFORMING" + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy new file mode 100644 index 00000000..c0c42284 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/fmj/FabricModJsonUtilsTest.groovy @@ -0,0 +1,128 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 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.fmj + +import com.google.gson.GsonBuilder +import com.google.gson.JsonObject +import org.intellij.lang.annotations.Language +import spock.lang.Specification + +import net.fabricmc.loom.util.fmj.FabricModJsonUtils + +class FabricModJsonUtilsTest extends Specification { + // Test that the schemaVersion is moved to the first position + def "optimize FMJ"() { + given: + // Matches LoomGradlePlugin + def gson = new GsonBuilder().setPrettyPrinting().create() + def json = gson.fromJson(INPUT_FMJ, JsonObject.class) + when: + def outputJson = FabricModJsonUtils.optimizeFmj(json) + def output = gson.toJson(outputJson) + then: + output == OUTPUT_FMJ + true + } + + // schemaVersion is not first + @Language("json") + static String INPUT_FMJ = """ +{ + "id": "modid", + "version": "1.0.0", + "name": "Example mod", + "description": "This is an example description! Tell everyone what your mod is about!", + "license": "CC0-1.0", + "icon": "assets/modid/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.example.ExampleMod" + ], + "client": [ + "com.example.ExampleModClient" + ] + }, + "schemaVersion": 1, + "mixins": [ + "modid.mixins.json", + { + "config": "modid.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": "\\u003e\\u003d0.15.0", + "minecraft": "~1.20.4", + "java": "\\u003e\\u003d17", + "fabric-api": "*" + }, + "suggests": { + "another-mod": "*" + } +} + +""".trim() + + // schemaVersion is first, everything else is unchanged + @Language("json") + static String OUTPUT_FMJ = """ +{ + "schemaVersion": 1, + "id": "modid", + "version": "1.0.0", + "name": "Example mod", + "description": "This is an example description! Tell everyone what your mod is about!", + "license": "CC0-1.0", + "icon": "assets/modid/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + "com.example.ExampleMod" + ], + "client": [ + "com.example.ExampleModClient" + ] + }, + "mixins": [ + "modid.mixins.json", + { + "config": "modid.client.mixins.json", + "environment": "client" + } + ], + "depends": { + "fabricloader": "\\u003e\\u003d0.15.0", + "minecraft": "~1.20.4", + "java": "\\u003e\\u003d17", + "fabric-api": "*" + }, + "suggests": { + "another-mod": "*" + } +} + +""".trim() +} From 7bb1224642389c3ed544e370242e89e910c83959 Mon Sep 17 00:00:00 2001 From: modmuss Date: Sun, 17 Mar 2024 22:28:47 +0000 Subject: [PATCH 10/20] Experimental Decompiler cache (#1043) --- .../SingleJarDecompileConfiguration.java | 2 +- .../SplitDecompileConfiguration.java | 4 +- .../loom/decompilers/ClassLineNumbers.java | 158 ++++++++ .../loom/decompilers/LineNumberRemapper.java | 84 +--- .../loom/decompilers/cache/CachedData.java | 209 ++++++++++ .../decompilers/cache/CachedFileStore.java | 42 ++ .../cache/CachedFileStoreImpl.java | 141 +++++++ .../decompilers/cache/CachedJarProcessor.java | 268 +++++++++++++ .../loom/decompilers/cache/ClassEntry.java | 75 ++++ .../loom/decompilers/cache/JarWalker.java | 108 +++++ .../loom/decompilers/cache/RiffChunk.java | 69 ++++ .../fabricmc/loom/extension/LoomFiles.java | 1 + .../loom/extension/LoomFilesBaseImpl.java | 5 + .../loom/task/GenerateSourcesTask.java | 377 +++++++++++++++--- .../java/net/fabricmc/loom/util/Checksum.java | 5 + .../fabricmc/loom/util/FileSystemUtil.java | 4 + .../java/net/fabricmc/loom/util/ZipUtils.java | 4 +- .../integration/DebugLineNumbersTest.groovy | 2 +- .../test/integration/DecompileTest.groovy | 32 ++ .../decompile/CustomDecompiler.groovy | 3 + .../test/unit/ClassLineNumbersTest.groovy | 99 +++++ .../loom/test/unit/JarWalkerTest.groovy | 87 ++++ .../test/unit/cache/CachedDataTest.groovy | 62 +++ .../unit/cache/CachedFileStoreTest.groovy | 132 ++++++ .../unit/cache/CachedJarProcessorTest.groovy | 241 +++++++++++ 25 files changed, 2078 insertions(+), 136 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java create mode 100644 src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java index 6965a0bf..e7ffd658 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/SingleJarDecompileConfiguration.java @@ -58,7 +58,7 @@ public class SingleJarDecompileConfiguration extends DecompileConfiguration { 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)); diff --git a/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java index 0c8df985..99c67973 100644 --- a/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/decompile/SplitDecompileConfiguration.java @@ -55,7 +55,7 @@ public final class SplitDecompileConfiguration extends DecompileConfiguration 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 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"); diff --git a/src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java b/src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java new file mode 100644 index 00000000..2c872406 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/ClassLineNumbers.java @@ -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 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 line = null; + int lineNumber = 0; + + record CurrentClass(String className, int maxLine, int maxLineDest) { + void putEntry(Map entries, Map 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 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 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 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 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 lineEntry : lineMap.entrySet()) { + writer.write('\t'); + writer.write(Integer.toString(lineEntry.getKey())); + writer.write('\t'); + writer.write(Integer.toString(lineEntry.getValue())); + writer.write('\n'); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java index 307a340e..86720e05 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java +++ b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java @@ -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 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 lineMap = new HashMap<>(); - - private RClass(String name) { - this.name = name; - } - } } diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java new file mode 100644 index 00000000..3674fe78 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedData.java @@ -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 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 { + @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); + } + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java new file mode 100644 index 00000000..ad73ec4c --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStore.java @@ -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 { + @Nullable T getEntry(String key) throws IOException; + + void putEntry(String key, T entry) throws IOException; + + interface EntrySerializer { + T read(Path path) throws IOException; + + void write(T entry, Path path) throws IOException; + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java new file mode 100644 index 00000000..545c6b7e --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedFileStoreImpl.java @@ -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(Path root, EntrySerializer entrySerializer, CacheRules cacheRules) implements CachedFileStore { + 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 entries = new ArrayList<>(); + + // Iterate over all the files in the cache, and store them into the sorted list. + try (Stream walk = Files.walk(root)) { + Iterator 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 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 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)); + } + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java new file mode 100644 index 00000000..82952210 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/CachedJarProcessor.java @@ -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 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 outputNameMap = new HashMap<>(); + Map 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 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 outputNameMap = workToDoJob.outputNameMap(); + + try (FileSystemUtil.Delegate outputFs = FileSystemUtil.getJarFileSystem(workToDoJob.output(), false); + Stream walk = Files.walk(outputFs.getRoot())) { + Iterator 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 walk = Files.walk(existingFs.getRoot())) { + Iterator 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 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 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 outputNameMap) implements WorkToDoJob { + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java new file mode 100644 index 00000000..f01db22f --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/ClassEntry.java @@ -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 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"); + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java new file mode 100644 index 00000000..ab2f9924 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/JarWalker.java @@ -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 findClasses(Path jar) throws IOException { + try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar)) { + return findClasses(fs); + } + } + + public static List findClasses(FileSystemUtil.Delegate fs) throws IOException { + List outerClasses = new ArrayList<>(); + Map> innerClasses = new HashMap<>(); + + // Iterate over all the classes in the jar, and store them into the sorted list. + try (Stream walk = Files.walk(fs.getRoot())) { + Iterator 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 classEntries = new ArrayList<>(); + + for (String outerClass : outerClasses) { + List 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); + } +} diff --git a/src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java b/src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java new file mode 100644 index 00000000..73c8afae --- /dev/null +++ b/src/main/java/net/fabricmc/loom/decompilers/cache/RiffChunk.java @@ -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 + * + *

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); + } +} diff --git a/src/main/java/net/fabricmc/loom/extension/LoomFiles.java b/src/main/java/net/fabricmc/loom/extension/LoomFiles.java index 9d115ab0..f0d1a8b9 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomFiles.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomFiles.java @@ -50,4 +50,5 @@ public interface LoomFiles { File getRemapClasspathFile(); File getGlobalMinecraftRepo(); File getLocalMinecraftRepo(); + File getDecompileCache(String version); } diff --git a/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java index d7f0c469..8b23e4f9 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomFilesBaseImpl.java @@ -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"); + } } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 0e235b0b..74769dbc 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -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 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 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 args = getUnpickArgs(inputJar, outputJar); + final List 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 getUnpickArgs(Path inputJar, Path outputJar) { + private List getUnpickArgs(Path inputJar, Path outputJar, @Nullable Path existingJar) { var fileArgs = new ArrayList(); 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 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 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 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); + } + } } diff --git a/src/main/java/net/fabricmc/loom/util/Checksum.java b/src/main/java/net/fabricmc/loom/util/Checksum.java index 50fcbec0..0b13b980 100644 --- a/src/main/java/net/fabricmc/loom/util/Checksum.java +++ b/src/main/java/net/fabricmc/loom/util/Checksum.java @@ -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()); diff --git a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java index 0a097c83..07fe08cf 100644 --- a/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java +++ b/src/main/java/net/fabricmc/loom/util/FileSystemUtil.java @@ -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); diff --git a/src/main/java/net/fabricmc/loom/util/ZipUtils.java b/src/main/java/net/fabricmc/loom/util/ZipUtils.java index 8fdf1fd6..714dce83 100644 --- a/src/main/java/net/fabricmc/loom/util/ZipUtils.java +++ b/src/main/java/net/fabricmc/loom/util/ZipUtils.java @@ -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 walk = Files.walk(fs.get().getPath("/"))) { + Stream walk = Files.walk(fs.getRoot())) { Iterator 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); diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy index dd220320..e7eda02b 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DebugLineNumbersTest.groovy @@ -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 diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy index 7431aae0..dd8dca6d 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy @@ -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 + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy index 90829f74..31609a91 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/decompile/CustomDecompiler.groovy @@ -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()) } } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy new file mode 100644 index 00000000..37fd125b --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/ClassLineNumbersTest.groovy @@ -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 +""" +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy new file mode 100644 index 00000000..7f0d0c57 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/JarWalkerTest.groovy @@ -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", + ] + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy new file mode 100644 index 00000000..d43417ea --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedDataTest.groovy @@ -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 + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy new file mode 100644 index 00000000..04292ab4 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedFileStoreTest.groovy @@ -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_ARRAY_SERIALIZER = new CachedFileStore.EntrySerializer() { + @Override + byte[] read(Path path) throws IOException { + return Files.readAllBytes(path) + } + + @Override + void write(byte[] entry, Path path) throws IOException { + Files.write(path, entry) + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy new file mode 100644 index 00000000..6b157603 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/cache/CachedJarProcessorTest.groovy @@ -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 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 names) { + return new ClassLineNumbers(names.collectEntries { [it, lineNumber(it)] }) + } + + private static ClassLineNumbers.Entry lineNumber(String name) { + return new ClassLineNumbers.Entry(name, 0, 0, [:]) + } +} From c2a9c2f18d66957145253d0be0ddb5c2e7e23514 Mon Sep 17 00:00:00 2001 From: modmuss Date: Mon, 18 Mar 2024 15:42:57 +0000 Subject: [PATCH 11/20] Async line number remapping (#1074) --- .../loom/decompilers/LineNumberRemapper.java | 33 +++--- .../loom/task/GenerateSourcesTask.java | 6 +- .../fabricmc/loom/util/AsyncZipProcessor.java | 109 ++++++++++++++++++ .../test/integration/DecompileTest.groovy | 2 +- .../test/unit/AsyncZipProcessorTest.groovy | 78 +++++++++++++ .../test/util/GradleProjectTestTrait.groovy | 2 +- 6 files changed, 205 insertions(+), 25 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy diff --git a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java index 86720e05..9fefae44 100644 --- a/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java +++ b/src/main/java/net/fabricmc/loom/decompilers/LineNumberRemapper.java @@ -26,12 +26,11 @@ package net.fabricmc.loom.decompilers; import java.io.IOException; import java.io.InputStream; -import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.SimpleFileVisitor; import java.nio.file.StandardCopyOption; -import java.nio.file.attribute.BasicFileAttributes; +import java.util.HashSet; +import java.util.Set; import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; @@ -41,31 +40,30 @@ import org.objectweb.asm.MethodVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.fabricmc.loom.util.AsyncZipProcessor; import net.fabricmc.loom.util.Constants; public record LineNumberRemapper(ClassLineNumbers lineNumbers) { private static final Logger LOGGER = LoggerFactory.getLogger(LineNumberRemapper.class); public void process(Path input, Path output) throws IOException { - Files.walkFileTree(input, new SimpleFileVisitor<>() { + AsyncZipProcessor.processEntries(input, output, new AsyncZipProcessor() { + private final Set createdParents = new HashSet<>(); + @Override - public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { - String rel = input.relativize(file).toString(); - Path dst = output.resolve(rel); + public void processEntryAsync(Path file, Path dst) throws IOException { Path parent = dst.getParent(); - if (parent != null) { - Files.createDirectories(parent); + synchronized (createdParents) { + if (parent != null && createdParents.add(parent)) { + Files.createDirectories(parent); + } } - String fName = file.getFileName().toString(); + String fileName = file.getFileName().toString(); - if (fName.endsWith(".class")) { - if (Files.exists(dst)) { - Files.delete(dst); - } - - String idx = rel.substring(0, rel.length() - 6); + if (fileName.endsWith(".class")) { + String idx = fileName.substring(0, fileName.length() - 6); LOGGER.debug("Remapping line numbers for class: " + idx); @@ -82,13 +80,12 @@ public record LineNumberRemapper(ClassLineNumbers lineNumbers) { reader.accept(new LineNumberVisitor(Constants.ASM_VERSION, writer, lineNumbers.lineMap().get(idx)), 0); Files.write(dst, writer.toByteArray()); - return FileVisitResult.CONTINUE; + return; } } } Files.copy(file, dst, StandardCopyOption.REPLACE_EXISTING); - return FileVisitResult.CONTINUE; } }); } diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 74769dbc..13ce91ff 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -488,11 +488,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { 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("/")); - } + remapper.process(inputJar, outputJar); } private void doWork(@Nullable IPCServer ipcServer, Path inputJar, Path outputJar, Path linemapFile, @Nullable Path existingJar) { diff --git a/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java b/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java new file mode 100644 index 00000000..51cd7cf0 --- /dev/null +++ b/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java @@ -0,0 +1,109 @@ +/* + * 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.util; + +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.nio.file.FileVisitResult; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.SimpleFileVisitor; +import java.nio.file.attribute.BasicFileAttributes; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +import org.gradle.api.JavaVersion; + +public interface AsyncZipProcessor { + static void processEntries(Path inputZip, Path outputZip, AsyncZipProcessor processor) throws IOException { + try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(inputZip, false); + FileSystemUtil.Delegate outFs = FileSystemUtil.getJarFileSystem(outputZip, true)) { + final Path inRoot = inFs.get().getPath("/"); + final Path outRoot = outFs.get().getPath("/"); + + List> futures = new ArrayList<>(); + final ExecutorService executor = getExecutor(); + + Files.walkFileTree(inRoot, new SimpleFileVisitor<>() { + @Override + public FileVisitResult visitFile(Path inputFile, BasicFileAttributes attrs) throws IOException { + final CompletableFuture future = CompletableFuture.supplyAsync(() -> { + try { + final String rel = inRoot.relativize(inputFile).toString(); + final Path outputFile = outRoot.resolve(rel); + processor.processEntryAsync(inputFile, outputFile); + } catch (IOException e) { + throw new CompletionException(e); + } + + return null; + }); + + futures.add(future); + return FileVisitResult.CONTINUE; + } + }); + + // Wait for all futures to complete + for (CompletableFuture future : futures) { + try { + future.join(); + } catch (CompletionException e) { + if (e.getCause() instanceof IOException ioe) { + throw ioe; + } + + throw new RuntimeException("Failed to process zip", e.getCause()); + } + } + + executor.shutdown(); + } + } + + /** + * On Java 21 return the virtual thread pool, otherwise return a fixed thread pool. + */ + private static ExecutorService getExecutor() { + if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { + // I'm not sure if this is actually faster, but its fun to use loom in loom :D + try { + Method m = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); + return (ExecutorService) m.invoke(null); + } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { + throw new RuntimeException("Failed to create virtual thread executor", e); + } + } + + return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); + } + + void processEntryAsync(Path inputEntry, Path outputEntry) throws IOException; +} diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy index dd8dca6d..120d9e73 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DecompileTest.groovy @@ -77,7 +77,7 @@ class DecompileTest extends Specification implements GradleProjectTestTrait { def "decompile cache"() { setup: - def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE) + def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE, gradleHomeDir: File.createTempDir()) gradle.buildSrc("decompile") gradle.buildGradle << ''' dependencies { diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy new file mode 100644 index 00000000..eca7d8ed --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy @@ -0,0 +1,78 @@ +/* + * 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 java.nio.file.Files +import java.nio.file.Path + +import spock.lang.Specification + +import net.fabricmc.loom.test.util.ZipTestUtils +import net.fabricmc.loom.util.AsyncZipProcessor +import net.fabricmc.loom.util.ZipUtils + +class AsyncZipProcessorTest extends Specification { + def "process async"() { + given: + def inputZip = ZipTestUtils.createZip(createEntries()) + def outputZip = ZipTestUtils.createZip(Collections.emptyMap()) + Files.delete(outputZip) + + when: + // Process the input zip asynchronously, converting all entries to uppercase + AsyncZipProcessor.processEntries(inputZip, outputZip) { Path inputEntry, Path outputEntry -> + def str = Files.readString(inputEntry) + Files.writeString(outputEntry, str.toUpperCase()) + } + + then: + ZipUtils.unpack(outputZip, "file1.txt") == "FILE1".bytes + ZipUtils.unpack(outputZip, "file500.txt") == "FILE500".bytes + ZipUtils.unpack(outputZip, "file800.txt") == "FILE800".bytes + } + + def "re throws"() { + given: + def inputZip = ZipTestUtils.createZip(createEntries()) + def outputZip = ZipTestUtils.createZip(Collections.emptyMap()) + Files.delete(outputZip) + + when: + AsyncZipProcessor.processEntries(inputZip, outputZip) { Path inputEntry, Path outputEntry -> + throw new IOException("Test exception") + } + + then: + thrown(IOException) + } + + Map createEntries(int count = 1000) { + Map entries = [:] + for (int i = 0; i < count; i++) { + entries.put("file" + i + ".txt", "file$i") + } + return entries + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy index 11e2a707..4062cfeb 100644 --- a/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/util/GradleProjectTestTrait.groovy @@ -45,7 +45,7 @@ trait GradleProjectTestTrait { String gradleVersion = options.version as String ?: LoomTestConstants.DEFAULT_GRADLE String warningMode = options.warningMode as String ?: "fail" File projectDir = options.projectDir as File ?: options.sharedFiles ? sharedProjectDir : File.createTempDir() - File gradleHomeDir = gradleHomeDir + File gradleHomeDir = options.gradleHomeDir as File ?: gradleHomeDir setupProject(options, projectDir) From 30ef45e8780e6570279becab4f16811ff5cf6dfe Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Mon, 18 Mar 2024 16:24:07 +0000 Subject: [PATCH 12/20] Fix async zip processor executor. Virtual threads are much slower, lets not worry about them for now. --- .../fabricmc/loom/util/AsyncZipProcessor.java | 25 ++----------------- .../test/unit/AsyncZipProcessorTest.groovy | 2 +- 2 files changed, 3 insertions(+), 24 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java b/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java index 51cd7cf0..4d3af845 100644 --- a/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java +++ b/src/main/java/net/fabricmc/loom/util/AsyncZipProcessor.java @@ -25,8 +25,6 @@ package net.fabricmc.loom.util; import java.io.IOException; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; import java.nio.file.FileVisitResult; import java.nio.file.Files; import java.nio.file.Path; @@ -39,8 +37,6 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import org.gradle.api.JavaVersion; - public interface AsyncZipProcessor { static void processEntries(Path inputZip, Path outputZip, AsyncZipProcessor processor) throws IOException { try (FileSystemUtil.Delegate inFs = FileSystemUtil.getJarFileSystem(inputZip, false); @@ -49,7 +45,7 @@ public interface AsyncZipProcessor { final Path outRoot = outFs.get().getPath("/"); List> futures = new ArrayList<>(); - final ExecutorService executor = getExecutor(); + final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); Files.walkFileTree(inRoot, new SimpleFileVisitor<>() { @Override @@ -64,7 +60,7 @@ public interface AsyncZipProcessor { } return null; - }); + }, executor); futures.add(future); return FileVisitResult.CONTINUE; @@ -88,22 +84,5 @@ public interface AsyncZipProcessor { } } - /** - * On Java 21 return the virtual thread pool, otherwise return a fixed thread pool. - */ - private static ExecutorService getExecutor() { - if (JavaVersion.current().isCompatibleWith(JavaVersion.VERSION_21)) { - // I'm not sure if this is actually faster, but its fun to use loom in loom :D - try { - Method m = Executors.class.getMethod("newVirtualThreadPerTaskExecutor"); - return (ExecutorService) m.invoke(null); - } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) { - throw new RuntimeException("Failed to create virtual thread executor", e); - } - } - - return Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors()); - } - void processEntryAsync(Path inputEntry, Path outputEntry) throws IOException; } diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy index eca7d8ed..684147e3 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/AsyncZipProcessorTest.groovy @@ -68,7 +68,7 @@ class AsyncZipProcessorTest extends Specification { thrown(IOException) } - Map createEntries(int count = 1000) { + Map createEntries(int count = 10000) { Map entries = [:] for (int i = 0; i < count; i++) { entries.put("file" + i + ".txt", "file$i") From 3670ccb9594c92d5858f2f3810fa93dd00798a66 Mon Sep 17 00:00:00 2001 From: Space Walker <48224626+SpaceWalkerRS@users.noreply.github.com> Date: Tue, 19 Mar 2024 18:25:57 +0100 Subject: [PATCH 13/20] Support merging pre 1.3 Minecraft versions. (#1026) Co-authored-by: modmuss50 --- .../fabricmc/loom/LoomGradleExtension.java | 2 +- .../loom/api/LoomGradleExtensionAPI.java | 1 + .../IntermediateMappingsProvider.java | 8 ++ .../mappings/layered/MappingsNamespace.java | 20 ++++- .../configuration/CompileConfiguration.java | 8 +- .../mappings/IntermediateMappingsService.java | 16 +++- .../mappings/MappingConfiguration.java | 2 +- .../NoOpIntermediateMappingsProvider.java | 5 +- .../mappings/tiny/MappingsMerger.java | 34 ++++++- .../LegacyMergedMinecraftProvider.java | 83 ++++++++++++++++++ .../minecraft/MergedMinecraftProvider.java | 32 +++++-- .../minecraft/MinecraftClassMerger.java | 16 ++++ .../minecraft/MinecraftJarConfiguration.java | 11 +++ .../minecraft/MinecraftJarMerger.java | 2 + .../minecraft/MinecraftProvider.java | 11 +++ .../minecraft/SingleJarMinecraftProvider.java | 34 +++++-- .../minecraft/SplitMinecraftProvider.java | 6 ++ .../AbstractMappedMinecraftProvider.java | 6 +- .../mapped/IntermediaryMinecraftProvider.java | 50 +++++++++-- .../mapped/NamedMinecraftProvider.java | 59 ++++++++++++- .../ProcessedNamedMinecraftProvider.java | 12 +++ .../extension/LoomGradleExtensionImpl.java | 9 ++ .../net/fabricmc/loom/util/Constants.java | 1 + .../loom/util/TinyRemapperHelper.java | 2 +- .../test/integration/LegacyProjectTest.groovy | 29 ++++++ .../TestPlugin.groovy | 63 +++++++++++++ .../IntermediaryMappingLayerTest.groovy | 1 + .../LayeredMappingsTestConstants.groovy | 4 +- .../mappings/1.2.5-intermediary.tiny.zip | Bin 0 -> 85026 bytes src/test/resources/mappings/ATTRIBUTIONS.md | 2 + 30 files changed, 490 insertions(+), 39 deletions(-) create mode 100644 src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java create mode 100644 src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy create mode 100644 src/test/resources/mappings/1.2.5-intermediary.tiny.zip create mode 100644 src/test/resources/mappings/ATTRIBUTIONS.md diff --git a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java index 15bb0cd4..e78939ac 100644 --- a/src/main/java/net/fabricmc/loom/LoomGradleExtension.java +++ b/src/main/java/net/fabricmc/loom/LoomGradleExtension.java @@ -89,7 +89,7 @@ public interface LoomGradleExtension extends LoomGradleExtensionAPI { return switch (mappingsNamespace) { case NAMED -> getNamedMinecraftProvider().getMinecraftJarPaths(); case INTERMEDIARY -> getIntermediaryMinecraftProvider().getMinecraftJarPaths(); - case OFFICIAL -> getMinecraftProvider().getMinecraftJars(); + case OFFICIAL, CLIENT_OFFICIAL, SERVER_OFFICIAL -> getMinecraftProvider().getMinecraftJars(); }; } diff --git a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java index 3dc9325f..1eef6031 100644 --- a/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java +++ b/src/main/java/net/fabricmc/loom/api/LoomGradleExtensionAPI.java @@ -204,6 +204,7 @@ public interface LoomGradleExtensionAPI { */ Property getIntermediaryUrl(); + @ApiStatus.Experimental Property> getMinecraftJarConfiguration(); default void serverOnlyMinecraftJar() { diff --git a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java index 594a74b8..d4987b08 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/intermediate/IntermediateMappingsProvider.java @@ -44,6 +44,14 @@ public abstract class IntermediateMappingsProvider implements Named { public abstract Property> getDownloader(); + /** + * Set to true if the minecraft version is pre 1.3. + * When true the expected src namespace is intermediary, and the expected dst namespaces are clientOfficial and/or serverOfficial + * When false the expected src namespace is named and the expected dst namespace is intermediary + */ + @ApiStatus.Experimental + public abstract Property getIsLegacyMinecraft(); + /** * Generate or download a tinyv2 mapping file with intermediary and named namespaces. * @throws IOException diff --git a/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java b/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java index 4e3a628a..c264274a 100644 --- a/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java +++ b/src/main/java/net/fabricmc/loom/api/mappings/layered/MappingsNamespace.java @@ -37,6 +37,18 @@ public enum MappingsNamespace { */ OFFICIAL, + /** + * Official names for the Minecraft client jar, usually obfuscated. + * This namespace is used for versions <1.3, where the client and server jars are obfuscated differently. + */ + CLIENT_OFFICIAL, + + /** + * Official names for the Minecraft server jar, usually obfuscated. + * This namespace is used for versions <1.3, where the client and server jars are obfuscated differently. + */ + SERVER_OFFICIAL, + /** * Intermediary mappings have been generated to provide a stable set of names across minecraft versions. * @@ -60,6 +72,8 @@ public enum MappingsNamespace { public static @Nullable MappingsNamespace of(String namespace) { return switch (namespace) { case "official" -> OFFICIAL; + case "clientOfficial" -> CLIENT_OFFICIAL; + case "serverOfficial" -> SERVER_OFFICIAL; case "intermediary" -> INTERMEDIARY; case "named" -> NAMED; default -> null; @@ -68,6 +82,10 @@ public enum MappingsNamespace { @Override public String toString() { - return name().toLowerCase(Locale.ROOT); + return switch (this) { + case CLIENT_OFFICIAL -> "clientOfficial"; + case SERVER_OFFICIAL -> "serverOfficial"; + default -> name().toLowerCase(Locale.ROOT); + }; } } diff --git a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java index 1e59e9fc..fff381f6 100644 --- a/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/CompileConfiguration.java @@ -60,6 +60,7 @@ import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.ModJavadocProcessor; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJarConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftMetadataProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; @@ -68,6 +69,7 @@ import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMi import net.fabricmc.loom.configuration.providers.minecraft.mapped.NamedMinecraftProvider; import net.fabricmc.loom.extension.MixinExtension; import net.fabricmc.loom.util.Checksum; +import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.ExceptionUtil; import net.fabricmc.loom.util.ProcessUtil; import net.fabricmc.loom.util.gradle.GradleUtils; @@ -154,7 +156,11 @@ public abstract class CompileConfiguration implements Runnable { final MinecraftMetadataProvider metadataProvider = MinecraftMetadataProvider.create(configContext); - final var jarConfiguration = extension.getMinecraftJarConfiguration().get(); + var jarConfiguration = extension.getMinecraftJarConfiguration().get(); + + if (jarConfiguration == MinecraftJarConfiguration.MERGED && !metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) { + jarConfiguration = MinecraftJarConfiguration.LEGACY_MERGED; + } // Provide the vanilla mc jars final MinecraftProvider minecraftProvider = jarConfiguration.createMinecraftProvider(metadataProvider, configContext); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java index 75af308c..a0568de9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/IntermediateMappingsService.java @@ -50,10 +50,12 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree; public final class IntermediateMappingsService implements SharedService { private final Path intermediaryTiny; + private final String expectedSrcNs; private final Supplier memoryMappingTree = Suppliers.memoize(this::createMemoryMappingTree); - private IntermediateMappingsService(Path intermediaryTiny) { + private IntermediateMappingsService(Path intermediaryTiny, String expectedSrcNs) { this.intermediaryTiny = intermediaryTiny; + this.expectedSrcNs = expectedSrcNs; } public static synchronized IntermediateMappingsService getInstance(SharedServiceManager sharedServiceManager, Project project, MinecraftProvider minecraftProvider) { @@ -84,7 +86,13 @@ public final class IntermediateMappingsService implements SharedService { throw new UncheckedIOException("Failed to provide intermediate mappings", e); } - return new IntermediateMappingsService(intermediaryTiny); + // When merging legacy versions there will be multiple named namespaces, so use intermediary as the common src ns + // Newer versions will use intermediary as the src ns + final String expectedSrcNs = minecraftProvider.isLegacyVersion() + ? MappingsNamespace.INTERMEDIARY.toString() // <1.3 + : MappingsNamespace.OFFICIAL.toString(); // >=1.3 + + return new IntermediateMappingsService(intermediaryTiny, expectedSrcNs); } private MemoryMappingTree createMemoryMappingTree() { @@ -100,6 +108,10 @@ public final class IntermediateMappingsService implements SharedService { throw new UncheckedIOException("Failed to read intermediary mappings", e); } + if (!expectedSrcNs.equals(tree.getSrcNamespace())) { + throw new RuntimeException("Invalid intermediate mappings: expected source namespace '" + expectedSrcNs + "' but found '" + tree.getSrcNamespace() + "\'"); + } + return tree; } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java index bce9d5d0..e57cb9d4 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/MappingConfiguration.java @@ -176,7 +176,7 @@ public class MappingConfiguration { // These are unmerged v2 mappings IntermediateMappingsService intermediateMappingsService = IntermediateMappingsService.getInstance(serviceManager, project, minecraftProvider); - MappingsMerger.mergeAndSaveMappings(baseTinyMappings, tinyMappings, intermediateMappingsService); + MappingsMerger.mergeAndSaveMappings(baseTinyMappings, tinyMappings, minecraftProvider, intermediateMappingsService); } else { final List minecraftJars = minecraftProvider.getMinecraftJars(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java index 28ddfc63..d198baf9 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/NoOpIntermediateMappingsProvider.java @@ -37,11 +37,12 @@ import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider; * A bit of a hack, creates an empty intermediary mapping file to be used for mc versions without any intermediate mappings. */ public abstract class NoOpIntermediateMappingsProvider extends IntermediateMappingsProvider { - private static final String HEADER = "tiny\t2\t0\tofficial\tintermediary"; + private static final String HEADER_OFFICIAL_MERGED = "tiny\t2\t0\tofficial\tintermediary"; + private static final String HEADER_OFFICIAL_LEGACY_MERGED = "tiny\t2\t0\tintermediary\tclientOfficial\tserverOfficial\t"; @Override public void provide(Path tinyMappings) throws IOException { - Files.writeString(tinyMappings, HEADER, StandardCharsets.UTF_8); + Files.writeString(tinyMappings, getIsLegacyMinecraft().get() ? HEADER_OFFICIAL_LEGACY_MERGED : HEADER_OFFICIAL_MERGED, StandardCharsets.UTF_8); } @Override diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java index db845663..e2f6ba67 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/mappings/tiny/MappingsMerger.java @@ -39,6 +39,7 @@ import org.slf4j.LoggerFactory; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.providers.mappings.IntermediateMappingsService; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.mappingio.adapter.MappingNsCompleter; import net.fabricmc.mappingio.adapter.MappingSourceNsSwitch; import net.fabricmc.mappingio.format.tiny.Tiny2FileReader; @@ -49,10 +50,20 @@ import net.fabricmc.mappingio.tree.MemoryMappingTree; public final class MappingsMerger { private static final Logger LOGGER = LoggerFactory.getLogger(MappingsMerger.class); - public static void mergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { + public static void mergeAndSaveMappings(Path from, Path out, MinecraftProvider minecraftProvider, IntermediateMappingsService intermediateMappingsService) throws IOException { Stopwatch stopwatch = Stopwatch.createStarted(); LOGGER.info(":merging mappings"); + if (minecraftProvider.isLegacyVersion()) { + legacyMergeAndSaveMappings(from, out, intermediateMappingsService); + } else { + mergeAndSaveMappings(from, out, intermediateMappingsService); + } + + LOGGER.info(":merged mappings in " + stopwatch.stop()); + } + + private static void mergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { MemoryMappingTree intermediaryTree = new MemoryMappingTree(); intermediateMappingsService.getMemoryMappingTree().accept(new MappingSourceNsSwitch(intermediaryTree, MappingsNamespace.INTERMEDIARY.toString())); @@ -70,8 +81,27 @@ public final class MappingsMerger { try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) { officialTree.accept(writer); } + } - LOGGER.info(":merged mappings in " + stopwatch.stop()); + private static void legacyMergeAndSaveMappings(Path from, Path out, IntermediateMappingsService intermediateMappingsService) throws IOException { + MemoryMappingTree intermediaryTree = new MemoryMappingTree(); + intermediateMappingsService.getMemoryMappingTree().accept(intermediaryTree); + + try (BufferedReader reader = Files.newBufferedReader(from, StandardCharsets.UTF_8)) { + Tiny2FileReader.read(reader, intermediaryTree); + } + + MemoryMappingTree officialTree = new MemoryMappingTree(); + MappingNsCompleter nsCompleter = new MappingNsCompleter(officialTree, Map.of(MappingsNamespace.CLIENT_OFFICIAL.toString(), MappingsNamespace.INTERMEDIARY.toString(), MappingsNamespace.SERVER_OFFICIAL.toString(), MappingsNamespace.INTERMEDIARY.toString())); + intermediaryTree.accept(nsCompleter); + + // versions this old strip inner class attributes + // from the obfuscated jars anyway + //inheritMappedNamesOfEnclosingClasses(officialTree); + + try (var writer = new Tiny2FileWriter(Files.newBufferedWriter(out, StandardCharsets.UTF_8), false)) { + officialTree.accept(writer); + } } /** diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java new file mode 100644 index 00000000..28284f1d --- /dev/null +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/LegacyMergedMinecraftProvider.java @@ -0,0 +1,83 @@ +/* + * 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.configuration.providers.minecraft; + +import java.nio.file.Path; +import java.util.List; + +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.configuration.ConfigContext; + +/** + * Minecraft versions prior to 1.3 obfuscate the server and client jars differently. + * The obfuscated jars must be provided separately, and can be merged after remapping. + */ +public final class LegacyMergedMinecraftProvider extends MinecraftProvider { + private final SingleJarMinecraftProvider.Server serverMinecraftProvider; + private final SingleJarMinecraftProvider.Client clientMinecraftProvider; + + public LegacyMergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + super(metadataProvider, configContext); + serverMinecraftProvider = SingleJarMinecraftProvider.server(metadataProvider, configContext); + clientMinecraftProvider = SingleJarMinecraftProvider.client(metadataProvider, configContext); + + if (!isLegacyVersion()) { + throw new RuntimeException("something has gone wrong - legacy-merged jar configuration selected but Minecraft " + metadataProvider.getMinecraftVersion() + " allows merging the obfuscated jars - the merged jar configuration should have been selected!"); + } + } + + public SingleJarMinecraftProvider.Server getServerMinecraftProvider() { + return serverMinecraftProvider; + } + + public SingleJarMinecraftProvider.Client getClientMinecraftProvider() { + return clientMinecraftProvider; + } + + @Override + public void provide() throws Exception { + if (!serverMinecraftProvider.provideServer() || !clientMinecraftProvider.provideClient()) { + throw new UnsupportedOperationException("This version does not provide both the client and server jars - please select the client-only or server-only jar configuration!"); + } + + serverMinecraftProvider.provide(); + clientMinecraftProvider.provide(); + } + + @Override + public List getMinecraftJars() { + return List.of( + serverMinecraftProvider.getMinecraftEnvOnlyJar(), + clientMinecraftProvider.getMinecraftEnvOnlyJar() + ); + } + + @Override + @Deprecated + public MappingsNamespace getOfficialNamespace() { + // Legacy merged providers do not have a single namespace as they delegate to the single jar providers + throw new UnsupportedOperationException("Cannot query the official namespace for legacy-merged minecraft providers"); + } +} diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java index b0ff9d65..3f2d4437 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MergedMinecraftProvider.java @@ -34,6 +34,7 @@ import java.util.Objects; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; public final class MergedMinecraftProvider extends MinecraftProvider { @@ -43,6 +44,10 @@ public final class MergedMinecraftProvider extends MinecraftProvider { public MergedMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { super(metadataProvider, configContext); + + if (isLegacyVersion()) { + throw new RuntimeException("something has gone wrong - merged jar configuration selected but Minecraft " + metadataProvider.getMinecraftVersion() + " does not allow merging the obfuscated jars - the legacy-merged jar configuration should have been selected!"); + } } @Override @@ -56,12 +61,17 @@ public final class MergedMinecraftProvider extends MinecraftProvider { return List.of(minecraftMergedJar); } + @Override + public MappingsNamespace getOfficialNamespace() { + return MappingsNamespace.OFFICIAL; + } + @Override public void provide() throws Exception { super.provide(); - if (!getVersionInfo().isVersionOrNewer("2012-07-25T22:00:00+00:00" /* 1.3 release date */)) { - throw new UnsupportedOperationException("Minecraft versions 1.2.5 and older cannot be merged. Please use `loom { server/clientOnlyMinecraftJar() }`"); + if (!provideServer() || !provideClient()) { + throw new UnsupportedOperationException("This version does not provide both the client and server jars - please select the client-only or server-only jar configuration!"); } if (!Files.exists(minecraftMergedJar) || getExtension().refreshDeps()) { @@ -79,18 +89,24 @@ public final class MergedMinecraftProvider extends MinecraftProvider { } private void mergeJars() throws IOException { - LOGGER.info(":merging jars"); - - File jarToMerge = getMinecraftServerJar(); + File minecraftClientJar = getMinecraftClientJar(); + File minecraftServerJar = getMinecraftServerJar(); if (getServerBundleMetadata() != null) { extractBundledServerJar(); - jarToMerge = getMinecraftExtractedServerJar(); + minecraftServerJar = getMinecraftExtractedServerJar(); } - Objects.requireNonNull(jarToMerge, "Cannot merge null input jar?"); + mergeJars(minecraftClientJar, minecraftServerJar, minecraftMergedJar.toFile()); + } - try (var jarMerger = new MinecraftJarMerger(getMinecraftClientJar(), jarToMerge, minecraftMergedJar.toFile())) { + public static void mergeJars(File clientJar, File serverJar, File mergedJar) throws IOException { + LOGGER.info(":merging jars"); + + Objects.requireNonNull(clientJar, "Cannot merge null client jar?"); + Objects.requireNonNull(serverJar, "Cannot merge null server jar?"); + + try (var jarMerger = new MinecraftJarMerger(clientJar, serverJar, mergedJar)) { jarMerger.enableSyntheticParamsOffset(); jarMerger.merge(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java index 7d38da2e..b60cb417 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftClassMerger.java @@ -257,6 +257,8 @@ public class MinecraftClassMerger { int j = 0; while (i < first.size() || j < second.size()) { + int saved = i + j; + while (i < first.size() && j < second.size() && first.get(i).equals(second.get(j))) { out.add(first.get(i)); @@ -273,6 +275,20 @@ public class MinecraftClassMerger { out.add(second.get(j)); j++; } + + // if the order is scrambled, it's not possible to merge + // the lists while preserving the order from both sides + if (i + j == saved) { + for (; i < first.size(); i++) { + out.add(first.get(i)); + } + + for (; j < second.size(); j++) { + if (!first.contains(second.get(j))) { + out.add(second.get(j)); + } + } + } } return out; diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java index 927cbe72..ca851331 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarConfiguration.java @@ -60,6 +60,17 @@ public record MinecraftJarConfiguration< SingleJarDecompileConfiguration::new, List.of("client", "server") ); + public static final MinecraftJarConfiguration< + LegacyMergedMinecraftProvider, + NamedMinecraftProvider.LegacyMergedImpl, + MappedMinecraftProvider> LEGACY_MERGED = new MinecraftJarConfiguration<>( + LegacyMergedMinecraftProvider::new, + IntermediaryMinecraftProvider.LegacyMergedImpl::new, + NamedMinecraftProvider.LegacyMergedImpl::new, + ProcessedNamedMinecraftProvider.LegacyMergedImpl::new, + SingleJarDecompileConfiguration::new, + List.of("client", "server") + ); public static final MinecraftJarConfiguration< SingleJarMinecraftProvider, NamedMinecraftProvider.SingleJarImpl, diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java index 5b0f9d9c..450010f0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftJarMerger.java @@ -82,6 +82,8 @@ public class MinecraftJarMerger implements AutoCloseable { } } + Files.createDirectories(output.toPath().getParent()); + this.inputClient = (inputClientFs = FileSystemUtil.getJarFileSystem(inputClient, false)).get().getPath("/"); this.inputServer = (inputServerFs = FileSystemUtil.getJarFileSystem(inputServer, false)).get().getPath("/"); this.outputFs = FileSystemUtil.getJarFileSystem(output, true); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java index 0e6a0682..b789a3b0 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/MinecraftProvider.java @@ -38,8 +38,10 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; +import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.download.DownloadExecutor; import net.fabricmc.loom.util.download.GradleDownloadProgressListener; import net.fabricmc.loom.util.gradle.ProgressGroup; @@ -185,6 +187,13 @@ public abstract class MinecraftProvider { return Objects.requireNonNull(metadataProvider, "Metadata provider not setup").getVersionMeta(); } + /** + * @return true if the minecraft version is older than 1.3. + */ + public boolean isLegacyVersion() { + return !getVersionInfo().isVersionOrNewer(Constants.RELEASE_TIME_1_3); + } + @Nullable public BundleMetadata getServerBundleMetadata() { return serverBundleMetadata; @@ -192,6 +201,8 @@ public abstract class MinecraftProvider { public abstract List getMinecraftJars(); + public abstract MappingsNamespace getOfficialNamespace(); + protected Project getProject() { return configContext.project(); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java index 389873c6..76b06564 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SingleJarMinecraftProvider.java @@ -28,25 +28,38 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; +import net.fabricmc.loom.util.Constants; import net.fabricmc.tinyremapper.NonClassCopyMode; import net.fabricmc.tinyremapper.OutputConsumerPath; import net.fabricmc.tinyremapper.TinyRemapper; public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvider permits SingleJarMinecraftProvider.Server, SingleJarMinecraftProvider.Client { + private final MappingsNamespace officialNamespace; private Path minecraftEnvOnlyJar; - private SingleJarMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { + private SingleJarMinecraftProvider(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) { super(metadataProvider, configContext); + this.officialNamespace = officialNamespace; } public static SingleJarMinecraftProvider.Server server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { - return new SingleJarMinecraftProvider.Server(metadataProvider, configContext); + return new SingleJarMinecraftProvider.Server(metadataProvider, configContext, getOfficialNamespace(metadataProvider, true)); } public static SingleJarMinecraftProvider.Client client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { - return new SingleJarMinecraftProvider.Client(metadataProvider, configContext); + return new SingleJarMinecraftProvider.Client(metadataProvider, configContext, getOfficialNamespace(metadataProvider, false)); + } + + private static MappingsNamespace getOfficialNamespace(MinecraftMetadataProvider metadataProvider, boolean server) { + // Versions before 1.3 don't have a common namespace, so use side specific namespaces. + if (!metadataProvider.getVersionMeta().isVersionOrNewer(Constants.RELEASE_TIME_1_3)) { + return server ? MappingsNamespace.SERVER_OFFICIAL : MappingsNamespace.CLIENT_OFFICIAL; + } + + return MappingsNamespace.OFFICIAL; } @Override @@ -66,7 +79,7 @@ public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvide super.provide(); // Server only JARs are supported on any version, client only JARs are pretty much useless after 1.3. - if (provideClient() && getVersionInfo().isVersionOrNewer("2012-07-25T22:00:00+00:00" /* 1.3 release date */)) { + if (provideClient() && !isLegacyVersion()) { getProject().getLogger().warn("Using `clientOnlyMinecraftJar()` is not recommended for Minecraft versions 1.3 or newer."); } @@ -105,13 +118,18 @@ public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvide return minecraftEnvOnlyJar; } + @Override + public MappingsNamespace getOfficialNamespace() { + return officialNamespace; + } + abstract SingleJarEnvType type(); abstract Path getInputJar(SingleJarMinecraftProvider provider) throws Exception; public static final class Server extends SingleJarMinecraftProvider { - private Server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { - super(metadataProvider, configContext); + private Server(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) { + super(metadataProvider, configContext, officialNamespace); } @Override @@ -143,8 +161,8 @@ public abstract sealed class SingleJarMinecraftProvider extends MinecraftProvide } public static final class Client extends SingleJarMinecraftProvider { - private Client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext) { - super(metadataProvider, configContext); + private Client(MinecraftMetadataProvider metadataProvider, ConfigContext configContext, MappingsNamespace officialNamespace) { + super(metadataProvider, configContext, officialNamespace); } @Override diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java index 69f0be26..d2190142 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/SplitMinecraftProvider.java @@ -28,6 +28,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.List; +import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.providers.BundleMetadata; @@ -52,6 +53,11 @@ public final class SplitMinecraftProvider extends MinecraftProvider { return List.of(minecraftClientOnlyJar, minecraftCommonJar); } + @Override + public MappingsNamespace getOfficialNamespace() { + return MappingsNamespace.OFFICIAL; + } + @Override public void provide() throws Exception { super.provide(); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java index c81e3084..fa66f429 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/AbstractMappedMinecraftProvider.java @@ -46,6 +46,7 @@ import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftVersionMeta; import net.fabricmc.loom.configuration.providers.minecraft.SignatureFixerApplyVisitor; import net.fabricmc.loom.extension.LoomFiles; import net.fabricmc.loom.util.SidedClassVisitor; @@ -190,7 +191,10 @@ public abstract class AbstractMappedMinecraftProvider remappedSignatures = SignatureFixerApplyVisitor.getRemappedSignatures(getTargetNamespace() == MappingsNamespace.INTERMEDIARY, mappingConfiguration, getProject(), configContext.serviceManager(), toM); - TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceManager(), fromM, toM, true, (builder) -> { + final MinecraftVersionMeta.JavaVersion javaVersion = minecraftProvider.getVersionInfo().javaVersion(); + final boolean fixRecords = javaVersion != null && javaVersion.majorVersion() >= 16; + + TinyRemapper remapper = TinyRemapperHelper.getTinyRemapper(getProject(), configContext.serviceManager(), fromM, toM, fixRecords, (builder) -> { builder.extraPostApplyVisitor(new SignatureFixerApplyVisitor(remappedSignatures)); configureRemapper(remappedJars, builder); }); diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java index aefed08b..70a1424e 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/IntermediaryMinecraftProvider.java @@ -29,14 +29,16 @@ import java.util.List; import org.gradle.api.Project; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider; import net.fabricmc.tinyremapper.TinyRemapper; -public abstract sealed class IntermediaryMinecraftProvider extends AbstractMappedMinecraftProvider permits IntermediaryMinecraftProvider.MergedImpl, IntermediaryMinecraftProvider.SingleJarImpl, IntermediaryMinecraftProvider.SplitImpl { +public abstract sealed class IntermediaryMinecraftProvider extends AbstractMappedMinecraftProvider permits IntermediaryMinecraftProvider.MergedImpl, IntermediaryMinecraftProvider.LegacyMergedImpl, IntermediaryMinecraftProvider.SingleJarImpl, IntermediaryMinecraftProvider.SplitImpl { public IntermediaryMinecraftProvider(Project project, M minecraftProvider) { super(project, minecraftProvider); } @@ -59,11 +61,49 @@ public abstract sealed class IntermediaryMinecraftProvider getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), minecraftProvider.getOfficialNamespace()) ); } } + public static final class LegacyMergedImpl extends IntermediaryMinecraftProvider implements Merged { + private final SingleJarImpl server; + private final SingleJarImpl client; + + public LegacyMergedImpl(Project project, LegacyMergedMinecraftProvider minecraftProvider) { + super(project, minecraftProvider); + server = new SingleJarImpl(project, minecraftProvider.getServerMinecraftProvider(), SingleJarEnvType.SERVER); + client = new SingleJarImpl(project, minecraftProvider.getClientMinecraftProvider(), SingleJarEnvType.CLIENT); + } + + @Override + public List provide(ProvideContext context) throws Exception { + // Map the client and server jars separately + server.provide(context); + client.provide(context); + + // then merge them + MergedMinecraftProvider.mergeJars( + client.getEnvOnlyJar().toFile(), + server.getEnvOnlyJar().toFile(), + getMergedJar().toFile() + ); + + return List.of(getMergedJar()); + } + + @Override + public List getRemappedJars() { + // The delegate providers will handle the remapping + throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars"); + } + + @Override + public List getDependencyTypes() { + return List.of(MinecraftJar.Type.MERGED); + } + } + public static final class SplitImpl extends IntermediaryMinecraftProvider implements Split { public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) { super(project, minecraftProvider); @@ -72,8 +112,8 @@ public abstract sealed class IntermediaryMinecraftProvider getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), MappingsNamespace.OFFICIAL), - new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), MappingsNamespace.OFFICIAL, minecraftProvider.getMinecraftCommonJar()) + new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), minecraftProvider.getOfficialNamespace()), + new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), minecraftProvider.getOfficialNamespace(), minecraftProvider.getMinecraftCommonJar()) ); } @@ -102,7 +142,7 @@ public abstract sealed class IntermediaryMinecraftProvider getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), minecraftProvider.getOfficialNamespace()) ); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java index 1bfdbe72..f2753609 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/NamedMinecraftProvider.java @@ -29,9 +29,11 @@ import java.util.List; import org.gradle.api.Project; import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; +import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; +import net.fabricmc.loom.configuration.providers.minecraft.MinecraftSourceSets; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarEnvType; import net.fabricmc.loom.configuration.providers.minecraft.SingleJarMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.SplitMinecraftProvider; @@ -60,7 +62,7 @@ public abstract class NamedMinecraftProvider extend @Override public List getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMergedJar(), getMergedJar(), minecraftProvider.getOfficialNamespace()) ); } @@ -70,6 +72,55 @@ public abstract class NamedMinecraftProvider extend } } + public static final class LegacyMergedImpl extends NamedMinecraftProvider implements Merged { + private final SingleJarImpl server; + private final SingleJarImpl client; + + public LegacyMergedImpl(Project project, LegacyMergedMinecraftProvider minecraftProvider) { + super(project, minecraftProvider); + server = new SingleJarImpl(project, minecraftProvider.getServerMinecraftProvider(), SingleJarEnvType.SERVER); + client = new SingleJarImpl(project, minecraftProvider.getClientMinecraftProvider(), SingleJarEnvType.CLIENT); + } + + @Override + public List provide(ProvideContext context) throws Exception { + final ProvideContext childContext = context.withApplyDependencies(false); + + // Map the client and server jars separately + server.provide(childContext); + client.provide(childContext); + + // then merge them + MergedMinecraftProvider.mergeJars( + client.getEnvOnlyJar().toFile(), + server.getEnvOnlyJar().toFile(), + getMergedJar().toFile() + ); + + getMavenHelper(MinecraftJar.Type.MERGED).savePom(); + + if (context.applyDependencies()) { + MinecraftSourceSets.get(getProject()).applyDependencies( + (configuration, type) -> getProject().getDependencies().add(configuration, getDependencyNotation(type)), + getDependencyTypes() + ); + } + + return List.of(getMergedJar()); + } + + @Override + public List getRemappedJars() { + // The delegate providers will handle the remapping + throw new UnsupportedOperationException("LegacyMergedImpl does not support getRemappedJars"); + } + + @Override + public List getDependencyTypes() { + return List.of(MinecraftJar.Type.MERGED); + } + } + public static final class SplitImpl extends NamedMinecraftProvider implements Split { public SplitImpl(Project project, SplitMinecraftProvider minecraftProvider) { super(project, minecraftProvider); @@ -78,8 +129,8 @@ public abstract class NamedMinecraftProvider extend @Override public List getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), MappingsNamespace.OFFICIAL), - new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), MappingsNamespace.OFFICIAL, minecraftProvider.getMinecraftCommonJar()) + new RemappedJars(minecraftProvider.getMinecraftCommonJar(), getCommonJar(), minecraftProvider.getOfficialNamespace()), + new RemappedJars(minecraftProvider.getMinecraftClientOnlyJar(), getClientOnlyJar(), minecraftProvider.getOfficialNamespace(), minecraftProvider.getMinecraftCommonJar()) ); } @@ -113,7 +164,7 @@ public abstract class NamedMinecraftProvider extend @Override public List getRemappedJars() { return List.of( - new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), MappingsNamespace.OFFICIAL) + new RemappedJars(minecraftProvider.getMinecraftEnvOnlyJar(), getEnvOnlyJar(), minecraftProvider.getOfficialNamespace()) ); } diff --git a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java index 5d28071e..d8f14e3c 100644 --- a/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java +++ b/src/main/java/net/fabricmc/loom/configuration/providers/minecraft/mapped/ProcessedNamedMinecraftProvider.java @@ -37,6 +37,7 @@ import net.fabricmc.loom.configuration.ConfigContext; import net.fabricmc.loom.configuration.mods.dependency.LocalMavenHelper; import net.fabricmc.loom.configuration.processors.MinecraftJarProcessorManager; import net.fabricmc.loom.configuration.processors.ProcessorContextImpl; +import net.fabricmc.loom.configuration.providers.minecraft.LegacyMergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MergedMinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftJar; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; @@ -177,6 +178,17 @@ public abstract class ProcessedNamedMinecraftProvider implements Merged { + public LegacyMergedImpl(NamedMinecraftProvider.LegacyMergedImpl parentMinecraftProvider, MinecraftJarProcessorManager jarProcessorManager) { + super(parentMinecraftProvider, jarProcessorManager); + } + + @Override + public MinecraftJar getMergedJar() { + return getProcessedJar(getParentMinecraftProvider().getMergedJar()); + } + } + public static final class SplitImpl extends ProcessedNamedMinecraftProvider implements Split { public SplitImpl(NamedMinecraftProvider.SplitImpl parentMinecraftProvide, MinecraftJarProcessorManager jarProcessorManager) { super(parentMinecraftProvide, jarProcessorManager); diff --git a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java index 382fbc14..851832df 100644 --- a/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java +++ b/src/main/java/net/fabricmc/loom/extension/LoomGradleExtensionImpl.java @@ -47,6 +47,7 @@ import net.fabricmc.loom.configuration.accesswidener.AccessWidenerFile; import net.fabricmc.loom.configuration.providers.mappings.IntermediaryMappingsProvider; import net.fabricmc.loom.configuration.providers.mappings.LayeredMappingsFactory; import net.fabricmc.loom.configuration.providers.mappings.MappingConfiguration; +import net.fabricmc.loom.configuration.providers.mappings.NoOpIntermediateMappingsProvider; import net.fabricmc.loom.configuration.providers.minecraft.MinecraftProvider; import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryProcessorManager; import net.fabricmc.loom.configuration.providers.minecraft.mapped.IntermediaryMinecraftProvider; @@ -165,6 +166,11 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen this.intermediaryMinecraftProvider = intermediaryMinecraftProvider; } + @Override + public void noIntermediateMappings() { + setIntermediateMappingsProvider(NoOpIntermediateMappingsProvider.class, p -> { }); + } + @Override public FileCollection getMinecraftJarsCollection(MappingsNamespace mappingsNamespace) { return getProject().files( @@ -271,6 +277,9 @@ public class LoomGradleExtensionImpl extends LoomGradleExtensionApiImpl implemen provider.getDownloader().set(this::download); provider.getDownloader().disallowChanges(); + + provider.getIsLegacyMinecraft().set(getProject().provider(() -> getMinecraftProvider().isLegacyVersion())); + provider.getIsLegacyMinecraft().disallowChanges(); } @Override diff --git a/src/main/java/net/fabricmc/loom/util/Constants.java b/src/main/java/net/fabricmc/loom/util/Constants.java index 10083571..c148ccb4 100644 --- a/src/main/java/net/fabricmc/loom/util/Constants.java +++ b/src/main/java/net/fabricmc/loom/util/Constants.java @@ -34,6 +34,7 @@ public class Constants { public static final String FABRIC_REPOSITORY = "https://maven.fabricmc.net/"; public static final int ASM_VERSION = Opcodes.ASM9; + public static final String RELEASE_TIME_1_3 = "2012-07-25T22:00:00+00:00"; private Constants() { } diff --git a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java index 202e4646..d67eb4d9 100644 --- a/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java +++ b/src/main/java/net/fabricmc/loom/util/TinyRemapperHelper.java @@ -69,7 +69,7 @@ public final class TinyRemapperHelper { MemoryMappingTree mappingTree = extension.getMappingConfiguration().getMappingsService(serviceManager).getMappingTree(); if (fixRecords && !mappingTree.getSrcNamespace().equals(fromM)) { - throw new IllegalStateException("Mappings src namespace must match remap src namespace"); + throw new IllegalStateException("Mappings src namespace must match remap src namespace, expected " + fromM + " but got " + mappingTree.getSrcNamespace()); } int intermediaryNsId = mappingTree.getNamespaceId(MappingsNamespace.INTERMEDIARY.toString()); diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy index 907de8f1..1cb8e760 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/LegacyProjectTest.groovy @@ -24,6 +24,8 @@ package net.fabricmc.loom.test.integration +import java.nio.file.Path + import spock.lang.Specification import spock.lang.Unroll @@ -118,4 +120,31 @@ class LegacyProjectTest extends Specification implements GradleProjectTestTrait 'b1.8.1' | _ 'a1.2.5' | _ } + + @Unroll + def "Legacy merged"() { + setup: + def mappings = Path.of("src/test/resources/mappings/1.2.5-intermediary.tiny.zip").toAbsolutePath() + def gradle = gradleProject(project: "minimalBase", version: PRE_RELEASE_GRADLE) + + gradle.buildGradle << """ + dependencies { + minecraft "com.mojang:minecraft:1.2.5" + mappings loom.layered() { + // No names + } + + modImplementation "net.fabricmc:fabric-loader:0.15.7" + } + """ + gradle.buildSrc("legacyMergedIntermediary") + + when: + def result = gradle.run(task: "build", args: [ + "-Ploom.test.legacyMergedIntermediary.mappingPath=${mappings}" + ]) + + then: + result.task(":build").outcome == SUCCESS + } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy new file mode 100644 index 00000000..137f629f --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/buildSrc/legacyMergedIntermediary/TestPlugin.groovy @@ -0,0 +1,63 @@ +/* + * This file is part of fabric-loom, licensed under the MIT License (MIT). + * + * Copyright (c) 2022 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.integration.buildSrc.legacyMergedIntermediary + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.provider.Property + +import net.fabricmc.loom.api.LoomGradleExtensionAPI +import net.fabricmc.loom.api.mappings.intermediate.IntermediateMappingsProvider +import net.fabricmc.loom.util.ZipUtils + +class TestPlugin implements Plugin { + @Override + void apply(Project project) { + LoomGradleExtensionAPI extension = project.getExtensions().getByName("loom") + extension.setIntermediateMappingsProvider(LegacyIntermediaryProvider.class) { + mappingPath.set(project.property("loom.test.legacyMergedIntermediary.mappingPath")) + } + } + + abstract static class LegacyIntermediaryProvider extends IntermediateMappingsProvider { + final String name = "legacyMerged" + + abstract Property getMappingPath(); + + @Override + void provide(Path tinyMappings) throws IOException { + if (getMinecraftVersion().get() != "1.2.5") { + throw new IllegalStateException("This plugin only supports Minecraft 1.2.5") + } + + byte[] data = ZipUtils.unpack(Paths.get(getMappingPath().get()), "1.2.5-intermediary.tiny") + Files.write(tinyMappings, data) + } + } +} diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy index d99f352f..5afbf436 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/IntermediaryMappingLayerTest.groovy @@ -30,6 +30,7 @@ class IntermediaryMappingLayerTest extends LayeredMappingsSpecification { def "Read intermediary mappings" () { setup: intermediaryUrl = INTERMEDIARY_1_17_URL + mockMinecraftProvider.getVersionInfo() >> VERSION_META_1_17 when: def mappings = getSingleMapping(new IntermediaryMappingsSpec()) def tiny = getTiny(mappings) diff --git a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy index a5bc09ab..c69e665e 100644 --- a/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/unit/layeredmappings/LayeredMappingsTestConstants.groovy @@ -34,13 +34,13 @@ interface LayeredMappingsTestConstants { client_mappings: new MinecraftVersionMeta.Download(null, "227d16f520848747a59bef6f490ae19dc290a804", 6431705, "https://launcher.mojang.com/v1/objects/227d16f520848747a59bef6f490ae19dc290a804/client.txt"), server_mappings: new MinecraftVersionMeta.Download(null, "84d80036e14bc5c7894a4fad9dd9f367d3000334", 4948536, "https://launcher.mojang.com/v1/objects/84d80036e14bc5c7894a4fad9dd9f367d3000334/server.txt") ] - public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, null, null, null, null) + public static final MinecraftVersionMeta VERSION_META_1_17 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_17, null, null, null, null, 0, "2021-06-08T11:00:40+00:00", null, null, null) public static final Map DOWNLOADS_1_16_5 = [ client_mappings: new MinecraftVersionMeta.Download(null, "e3dfb0001e1079a1af72ee21517330edf52e6192", 5746047, "https://launcher.mojang.com/v1/objects/e3dfb0001e1079a1af72ee21517330edf52e6192/client.txt"), server_mappings: new MinecraftVersionMeta.Download(null, "81d5c793695d8cde63afddb40dde88e3a88132ac", 4400926, "https://launcher.mojang.com/v1/objects/81d5c793695d8cde63afddb40dde88e3a88132ac/server.txt") ] - public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, null, null, null, null) + public static final MinecraftVersionMeta VERSION_META_1_16_5 = new MinecraftVersionMeta(null, null, null, 0, DOWNLOADS_1_16_5, null, null, null, null, 0, "2021-01-14T16:05:32+00:00", null, null, null) public static final String PARCHMENT_NOTATION = "org.parchmentmc.data:parchment-1.16.5:20210608-SNAPSHOT@zip" public static final String PARCHMENT_URL = "https://maven.parchmentmc.net/org/parchmentmc/data/parchment-1.16.5/20210608-SNAPSHOT/parchment-1.16.5-20210608-SNAPSHOT.zip" diff --git a/src/test/resources/mappings/1.2.5-intermediary.tiny.zip b/src/test/resources/mappings/1.2.5-intermediary.tiny.zip new file mode 100644 index 0000000000000000000000000000000000000000..0086053b72103372c1b704fb6815ab4edcc6eaf4 GIT binary patch literal 85026 zcmV)8K*qmNO9KQH00;mG0J@-ZSO5S3000000KYf|02crt05L8yE;TJ_ZggdGZDnL> zVRCscbZKsRRa6ZC2ce$$Wt^t?Wuczu8bItW| zcKt|=5S&iphruryudT6xLxQtT8z+7o3`x=Y81fLQq)(XKi@^|Oe6-qg zNHQwwV8!pskn`Qg-FIg<-@QD$JkIl0&ikOm%lzk`U*nR+>j!1T@8?S{g&?C5f5?|! z@v+iM6(#--ESI!MddR<0d7Wj2C~*L*DR*RFD?4{ZlvF%iZ<6?|yo? zKf8X%K`|y}ofLm=zbst{1{3~Eo1IlMc%Q^y+k;JVSd%3F)<)S7eeg#7z1<^y)J{wB zk9H;ayNwld9y`4LP~Kp z3oNId(pFrzD=K;~lM*+hxtZv6$R|iiCnXO2?P!xgTH5IH>B0Up*nW50hhVvRSj7GC za7AxTk#|N1$O!R%Fa#pNB`?c1!yz_WjC8q;+IpXVC1FjYmEyzbR~#gy%h6 z$DtmlP}N0DK|f=WKg|jNVpT^aK98pA5_Fd-Z|8&bT6`IuhzNOI#G}DxFxabKG2}}8 zzrXpnxWY9lA?&jCGF?+pEfAa8G~u;9Tl5MWP+|%;8za*|4_PLm*&Z{GMfk3{p;TbrCS(mg%oRef^M#>l5{TRu3Cu_me*{bGHRm5!w-dPg%LQQ-;0ZX!{&I>nNxM z84=o}o>&s;y|K_OPu}&!I2fZe*QRm5*5jnNjxdb9Znj&sBBC; zc@wr*OqkjA+D#Vc4Vx*O98cZ^lL zDsVW~*-&VU;8j-Z58bsu?S&)tq5nwA!8i+9m#Oqi9Y;Bah-0C33Lsrl*WXsBCzrs`_FZoHtldZ~&XnFn`Q{p57nt zj;bAkQ|K25%3pz(aWZ_MY zAART_FL&?r-ht$^2{y@zI9Y$$Y^jTz;&kMP>j>qgCNbBHI&4L+wOp8cRaU|!h6ei9 z4%D>n)du5kayIr@Wz&zqe`0)R_h?p08f~HL5BB_cf*g)xbvpj>q_u%X#YYX+5}qo-Rei?d3(k4CLx@$eD6lz5QiGO;nTbB*nfC9yZr7?XYaFI zQ1}e-If4YLoZiRc^yAf_F51U@Sr!>C7z~i%k;H4@ad+q9^XI{hIO|s2Ty2*M!DA0N zHJn1gS@YuJM%;dSv`jyh5HCKTj8Sel!ofLsl-aL`n`5!|^$4#7d2Pgubq(wp2Gr^q zUoojPq>9XAQnpmnBm`DO5~K%Awk(nqe7X2YN-Xw;&)%@iLSkz)NatOyu_@oR=$m7p zSHcK79UQf3<*LPoLzO?=?m5F?WhQvYWyxo6)CQW=p`9RH-tYx6&N{7hS*5l8qa?+! zIREl=7zn2j$IR?MX8YqL0U92r`7p}YB`_drE$p5e<;ziQ7)P#}z9-`6l^?b)D5pyr z1g9ZU8Lq!IM28OI{V38w%UFdPSB0so!*~_uA+Wx^gM~R-g>Fa=m_{c?;pT1#+`%(2+nu1<7jCę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ð%N}|yg2KV)(TY{aKAh3<%2HWue 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 literal 0 HcmV?d00001 diff --git a/src/test/resources/mappings/ATTRIBUTIONS.md b/src/test/resources/mappings/ATTRIBUTIONS.md new file mode 100644 index 00000000..52005c30 --- /dev/null +++ b/src/test/resources/mappings/ATTRIBUTIONS.md @@ -0,0 +1,2 @@ +The file `1.2.5-intermediary.tiny` was taken from OrnitheMC's "[Calamus](https://github.com/OrnitheMC/calamus/blob/gen2/mappings/1.2.5.tiny)" intermediaries under the CC0 license. +The file was rewritten in Tiny V2 format, but the mappings are otherwise unmodified. From 455b9d6aedaf19c343ceaa09de3e36adf0dba8c5 Mon Sep 17 00:00:00 2001 From: modmuss Date: Tue, 19 Mar 2024 20:08:35 +0000 Subject: [PATCH 14/20] Only set startFirstThread on versions using LWJGL 3 (#1076) * Only set startFirstThread on versions using LWJGL 3 * Lazily evaluate RunConfig --- .../loom/configuration/ide/RunConfig.java | 8 +++++++ .../configuration/ide/RunConfigSettings.java | 1 - .../fabricmc/loom/task/AbstractRunTask.java | 22 +++++++++---------- .../projects/runconfigs/build.gradle | 4 ++++ 4 files changed, 23 insertions(+), 12 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java index 242558e1..af1ffd3a 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfig.java @@ -43,6 +43,7 @@ import java.util.stream.Collectors; import com.google.common.collect.ImmutableMap; import com.google.gson.JsonElement; import com.google.gson.JsonObject; +import org.gradle.api.JavaVersion; import org.gradle.api.Project; import org.gradle.api.artifacts.ModuleVersionIdentifier; import org.gradle.api.artifacts.ResolvedArtifact; @@ -58,6 +59,7 @@ import net.fabricmc.loom.configuration.InstallerData; import net.fabricmc.loom.configuration.ide.idea.IdeaSyncTask; import net.fabricmc.loom.configuration.ide.idea.IdeaUtils; import net.fabricmc.loom.configuration.providers.BundleMetadata; +import net.fabricmc.loom.configuration.providers.minecraft.library.LibraryContext; import net.fabricmc.loom.util.Constants; import net.fabricmc.loom.util.gradle.SourceSetReference; @@ -135,6 +137,12 @@ public class RunConfig { public static RunConfig runConfig(Project project, RunConfigSettings settings) { LoomGradleExtension extension = LoomGradleExtension.get(project); + LibraryContext context = new LibraryContext(extension.getMinecraftProvider().getVersionInfo(), JavaVersion.current()); + + if (settings.getEnvironment().equals("client") && context.usesLWJGL3()) { + settings.startFirstThread(); + } + String name = settings.getName(); String configName = settings.getConfigName(); diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java index 73ea94cb..6cf05fbd 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/RunConfigSettings.java @@ -321,7 +321,6 @@ public class RunConfigSettings implements Named { * Configure run config with the default client options. */ public void client() { - startFirstThread(); environment("client"); defaultMainClass(Constants.Knot.KNOT_CLIENT); diff --git a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java index 1fc25bbf..dca2b9f7 100644 --- a/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java +++ b/src/main/java/net/fabricmc/loom/task/AbstractRunTask.java @@ -40,6 +40,7 @@ import org.gradle.api.Project; import org.gradle.api.file.ConfigurableFileCollection; import org.gradle.api.file.FileCollection; import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; import org.gradle.api.services.ServiceReference; import org.gradle.api.specs.Spec; import org.gradle.api.tasks.JavaExec; @@ -52,7 +53,7 @@ import net.fabricmc.loom.util.gradle.SyncTaskBuildService; public abstract class AbstractRunTask extends JavaExec { private static final CharsetEncoder ASCII_ENCODER = StandardCharsets.US_ASCII.newEncoder(); - private final RunConfig config; + private final Provider config; // We control the classpath, as we use a ArgFile to pass it over the command line: https://docs.oracle.com/javase/7/docs/technotes/tools/windows/javac.html#commandlineargfile private final ConfigurableFileCollection classpath = getProject().getObjects().fileCollection(); @@ -63,13 +64,12 @@ public abstract class AbstractRunTask extends JavaExec { public AbstractRunTask(Function configProvider) { super(); setGroup(Constants.TaskGroup.FABRIC); - this.config = configProvider.apply(getProject()); - setClasspath(config.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter())); - - args(config.programArgs); - getMainClass().set(config.mainClass); + this.config = getProject().provider(() -> configProvider.apply(getProject())); + classpath.from(config.map(runConfig -> runConfig.sourceSet.getRuntimeClasspath().filter(File::exists).filter(new LibraryFilter()))); + getArgumentProviders().add(() -> config.get().programArgs); + getMainClass().set(config.map(runConfig -> runConfig.mainClass)); getJvmArguments().addAll(getProject().provider(this::getGameJvmArgs)); } @@ -100,8 +100,8 @@ public abstract class AbstractRunTask extends JavaExec { super.setClasspath(classpath); } - setWorkingDir(new File(getProject().getProjectDir(), config.runDir)); - environment(config.environmentVariables); + setWorkingDir(new File(getProject().getProjectDir(), config.get().runDir)); + environment(config.get().environmentVariables); super.exec(); } @@ -133,7 +133,7 @@ public abstract class AbstractRunTask extends JavaExec { } } - args.addAll(config.vmArgs); + args.addAll(config.get().vmArgs); return args; } @@ -204,11 +204,11 @@ public abstract class AbstractRunTask extends JavaExec { @Override public boolean isSatisfiedBy(File element) { if (excludedLibraryPaths == null) { - excludedLibraryPaths = config.getExcludedLibraryPaths(getProject()); + excludedLibraryPaths = config.get().getExcludedLibraryPaths(getProject()); } if (excludedLibraryPaths.contains(element.getAbsolutePath())) { - getProject().getLogger().debug("Excluding library {} from {} run config", element.getName(), config.configName); + getProject().getLogger().debug("Excluding library {} from {} run config", element.getName(), config.get().configName); return false; } diff --git a/src/test/resources/projects/runconfigs/build.gradle b/src/test/resources/projects/runconfigs/build.gradle index c9ce0613..76a3db44 100644 --- a/src/test/resources/projects/runconfigs/build.gradle +++ b/src/test/resources/projects/runconfigs/build.gradle @@ -49,4 +49,8 @@ dependencies { base { archivesName = "fabric-example-mod" +} + +runClient { + // Realise this task to ensure that the runConfig is lazily evaluated } \ No newline at end of file From c1d51b1149703d5f4471c5b3cd7aed7bc33205e4 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 19 Mar 2024 21:23:29 +0100 Subject: [PATCH 15/20] Introduce SemVer version parsing to included mods/libraries (#1075) * Introduce SemVer version parsing & ".Final" suffix stripping to included mods/libraries * Address review about `CONSTANT_CASE` variables, add unit test * thanks spotless :/ --- .../build/nesting/IncludedJarFactory.java | 39 ++++++++- .../test/integration/SemVerParsingTest.groovy | 80 +++++++++++++++++++ 2 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy diff --git a/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java b/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java index 3cf13e03..43bffc29 100644 --- a/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java +++ b/src/main/java/net/fabricmc/loom/build/nesting/IncludedJarFactory.java @@ -31,6 +31,8 @@ import java.nio.charset.StandardCharsets; import java.util.Collection; import java.util.Locale; import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import com.google.common.collect.Sets; import com.google.common.hash.Hashing; @@ -49,6 +51,8 @@ import org.gradle.api.plugins.JavaPlugin; import org.gradle.api.provider.Provider; import org.gradle.api.tasks.bundling.AbstractArchiveTask; import org.jetbrains.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.LoomGradlePlugin; @@ -58,6 +62,9 @@ import net.fabricmc.loom.util.fmj.FabricModJsonFactory; public final class IncludedJarFactory { private final Project project; + private static final Logger LOGGER = LoggerFactory.getLogger(IncludedJarFactory.class); + private static final String SEMVER_REGEX = "^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$"; + private static final Pattern SEMVER_PATTERN = Pattern.compile(SEMVER_REGEX); public IncludedJarFactory(Project project) { this.project = project; @@ -190,7 +197,8 @@ public final class IncludedJarFactory { jsonObject.addProperty("schemaVersion", 1); jsonObject.addProperty("id", modId); - jsonObject.addProperty("version", metadata.version()); + String version = getVersion(metadata); + jsonObject.addProperty("version", version); jsonObject.addProperty("name", metadata.name()); JsonObject custom = new JsonObject(); @@ -209,5 +217,34 @@ public final class IncludedJarFactory { return "_" + classifier; } } + + @Override + public String toString() { + return group + ":" + name + ":" + version + classifier(); + } + } + + private static String getVersion(Metadata metadata) { + String version = metadata.version(); + + if (validSemVer(version)) { + return version; + } + + if (version.endsWith(".Final") || version.endsWith(".final")) { + String trimmedVersion = version.substring(0, version.length() - 6); + + if (validSemVer(trimmedVersion)) { + return trimmedVersion; + } + } + + LOGGER.warn("({}) is not valid semver for dependency {}", version, metadata); + return version; + } + + private static boolean validSemVer(String version) { + Matcher matcher = SEMVER_PATTERN.matcher(version); + return matcher.find(); } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy new file mode 100644 index 00000000..1ddda621 --- /dev/null +++ b/src/test/groovy/net/fabricmc/loom/test/integration/SemVerParsingTest.groovy @@ -0,0 +1,80 @@ +/* + * 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.integration + +import spock.lang.Specification +import spock.lang.Unroll + +import net.fabricmc.loom.build.nesting.IncludedJarFactory +import net.fabricmc.loom.test.util.GradleProjectTestTrait + +class SemVerParsingTest extends Specification implements GradleProjectTestTrait { + @Unroll + def "test valid Semantic Versioning strings"() { + given: + IncludedJarFactory includedJarFactory = new IncludedJarFactory(null) + + expect: + includedJarFactory.validSemVer(version) == true + + where: + version | _ + "1.0.0" | _ + "2.5.3" | _ + "3.0.0-beta.2" | _ + "4.2.1-alpha+001" | _ + "5.0.0-rc.1+build.1" | _ + } + + @Unroll + def "test non-Semantic Versioning strings"() { + given: + IncludedJarFactory includedJarFactory = new IncludedJarFactory(null) + + expect: + includedJarFactory.validSemVer(version) == false + + where: + version | _ + "1.0" | _ + "3.0.0.Beta1-120922-126" | _ + "3.0.2.Final" | _ + "4.2.1.4.RELEASE" | _ + } + + @Unroll + def "test '.Final' suffixed SemVer"() { + given: + IncludedJarFactory includedJarFactory = new IncludedJarFactory(null) + + expect: + includedJarFactory.getVersion(metadata) == expectedVersion + + where: + metadata | expectedVersion + new IncludedJarFactory.Metadata("group", "name", "1.0.0.Final", null) | "1.0.0" + new IncludedJarFactory.Metadata("group", "name", "2.5.3.final", null) | "2.5.3" + } +} From 4084fa3eb9b6dd4003e108ff47efe56b06aa7ff1 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 19 Mar 2024 22:03:01 +0000 Subject: [PATCH 16/20] Add classname to stracktrace when Kotlin remapping fails. Closes #1045 --- .../kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt index 074fe2c0..545ccd25 100644 --- a/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt +++ b/src/main/kotlin/net/fabricmc/loom/kotlin/remapping/KotlinMetadataRemappingClassVisitor.kt @@ -57,7 +57,11 @@ class KotlinMetadataRemappingClassVisitor(private val remapper: Remapper, next: var result: AnnotationVisitor? = super.visitAnnotation(descriptor, visible) if (descriptor == ANNOTATION_DESCRIPTOR && result != null) { - result = KotlinClassMetadataRemappingAnnotationVisitor(remapper, result, className) + try { + result = KotlinClassMetadataRemappingAnnotationVisitor(remapper, result, className) + } catch (e: Exception) { + throw RuntimeException("Failed to remap Kotlin metadata annotation in class $className", e) + } } return result From ae9db3fbbd995cd36a103ffacbcbf1d2fc248c7b Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 19 Mar 2024 22:15:33 +0000 Subject: [PATCH 17/20] Fix datagen run config having incorrect name. Closes #1000 --- .../net/fabricmc/loom/configuration/FabricApiExtension.java | 2 +- .../fabricmc/loom/test/integration/DataGenerationTest.groovy | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java index 4fde07fd..67d97342 100644 --- a/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java +++ b/src/main/java/net/fabricmc/loom/configuration/FabricApiExtension.java @@ -181,8 +181,8 @@ public abstract class FabricApiExtension { if (settings.getCreateRunConfiguration().get()) { extension.getRunConfigs().create("datagen", run -> { - run.setConfigName("Data Generation"); run.inherit(extension.getRunConfigs().getByName("server")); + run.setConfigName("Data Generation"); run.property("fabric-api.datagen"); run.property("fabric-api.datagen.output-dir", outputDirectory.getAbsolutePath()); diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy index 778b6611..80ee747a 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy @@ -88,12 +88,15 @@ class DataGenerationTest extends Specification implements GradleProjectTestTrait modDatagenImplementation fabricApi.module("fabric-data-generation-api-v1", "0.90.0+1.20.2") } + + println("%%" + loom.runs.datagen.configName + "%%") ''' when: def result = gradle.run(task: "runDatagen") then: result.task(":runDatagen").outcome == SUCCESS + result.output.contains("%%Data Generation%%") where: version << STANDARD_TEST_VERSIONS From d0feecfbcb4bc6e6540ffd4ce1a13cdc691358a7 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Tue, 19 Mar 2024 23:03:49 +0000 Subject: [PATCH 18/20] Don't fail setup if a project FMJ is invalid. --- .../fabricmc/loom/util/fmj/FabricModJsonFactory.java | 10 ++++++++++ .../loom/test/integration/DataGenerationTest.groovy | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java index 4bdeb403..b672aa6d 100644 --- a/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java +++ b/src/main/java/net/fabricmc/loom/util/fmj/FabricModJsonFactory.java @@ -36,9 +36,12 @@ import java.nio.file.Path; import java.util.Optional; import com.google.gson.JsonObject; +import com.google.gson.JsonSyntaxException; import org.gradle.api.tasks.SourceSet; import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.VisibleForTesting; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import net.fabricmc.loom.LoomGradlePlugin; import net.fabricmc.loom.util.FileSystemUtil; @@ -48,6 +51,8 @@ import net.fabricmc.loom.util.gradle.SourceSetHelper; public final class FabricModJsonFactory { public static final String FABRIC_MOD_JSON = "fabric.mod.json"; + private static final Logger LOGGER = LoggerFactory.getLogger(FabricModJsonFactory.class); + private FabricModJsonFactory() { } @@ -115,6 +120,11 @@ public final class FabricModJsonFactory { try (Reader reader = Files.newBufferedReader(file.toPath(), StandardCharsets.UTF_8)) { return create(LoomGradlePlugin.GSON.fromJson(reader, JsonObject.class), new FabricModJsonSource.SourceSetSource(sourceSets)); + } catch (JsonSyntaxException e) { + LOGGER.warn("Failed to parse fabric.mod.json: {}", file.getAbsolutePath()); + return null; + } catch (IOException e) { + throw new UncheckedIOException("Failed to read " + file.getAbsolutePath(), e); } } diff --git a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy index 80ee747a..66919c0f 100644 --- a/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy +++ b/src/test/groovy/net/fabricmc/loom/test/integration/DataGenerationTest.groovy @@ -88,7 +88,7 @@ class DataGenerationTest extends Specification implements GradleProjectTestTrait modDatagenImplementation fabricApi.module("fabric-data-generation-api-v1", "0.90.0+1.20.2") } - + println("%%" + loom.runs.datagen.configName + "%%") ''' when: From c3dd16d9bf22d4281559f1a13fcea6d36a59cbcc Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 20 Mar 2024 10:26:00 +0000 Subject: [PATCH 19/20] Fix idea sync task group --- .../net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java b/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java index 5da1061f..f502c216 100644 --- a/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java +++ b/src/main/java/net/fabricmc/loom/configuration/ide/idea/IdeaSyncTask.java @@ -55,12 +55,14 @@ import net.fabricmc.loom.LoomGradleExtension; import net.fabricmc.loom.configuration.ide.RunConfig; import net.fabricmc.loom.configuration.ide.RunConfigSettings; import net.fabricmc.loom.task.AbstractLoomTask; +import net.fabricmc.loom.util.Constants; public abstract class IdeaSyncTask extends AbstractLoomTask { @Inject public IdeaSyncTask() { // Always re-run this task. getOutputs().upToDateWhen(element -> false); + setGroup(Constants.TaskGroup.IDE); } @TaskAction From 713e1ff268df3b3ff1f5963f9bc3ac7b221e7666 Mon Sep 17 00:00:00 2001 From: modmuss50 Date: Wed, 20 Mar 2024 10:34:19 +0000 Subject: [PATCH 20/20] Enable decompile cache by default --- .../java/net/fabricmc/loom/task/GenerateSourcesTask.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java index 13ce91ff..acf71a2e 100644 --- a/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java +++ b/src/main/java/net/fabricmc/loom/task/GenerateSourcesTask.java @@ -186,7 +186,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { getDecompileCacheFile().set(extension.getFiles().getDecompileCache(CACHE_VERSION)); getUnpickRuntimeClasspath().from(getProject().getConfigurations().getByName(Constants.Configurations.UNPICK_CLASSPATH)); - getUseCache().convention(false); + getUseCache().convention(true); } @TaskAction @@ -205,7 +205,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { return; } - LOGGER.warn("Using decompile cache is experimental and may not work as expected."); + LOGGER.info("Using decompile cache."); try (var timer = new Timer("Decompiled sources with cache")) { final Path cacheFile = getDecompileCacheFile().getAsFile().get().toPath(); @@ -237,7 +237,7 @@ public abstract class GenerateSourcesTask extends AbstractLoomTask { final CachedJarProcessor.WorkJob job = workRequest.job(); final CachedJarProcessor.CacheStats cacheStats = workRequest.stats(); - getProject().getLogger().lifecycle("Decompiling: Cache stats: {} hits, {} misses", cacheStats.hits(), cacheStats.misses()); + getProject().getLogger().lifecycle("Decompile cache stats: {} hits, {} misses", cacheStats.hits(), cacheStats.misses()); ClassLineNumbers outputLineNumbers = null;