diff --git a/minecraft-codev-access-widener/gradle.properties b/minecraft-codev-access-widener/gradle.properties index 240ac2f9..be60622e 100644 --- a/minecraft-codev-access-widener/gradle.properties +++ b/minecraft-codev-access-widener/gradle.properties @@ -1 +1 @@ -version=0.6.3 +version=0.6.4 diff --git a/minecraft-codev-core/gradle.properties b/minecraft-codev-core/gradle.properties index af4621a4..db1db5d6 100644 --- a/minecraft-codev-core/gradle.properties +++ b/minecraft-codev-core/gradle.properties @@ -1 +1 @@ -version=0.6.9 +version=0.6.10 diff --git a/minecraft-codev-core/src/main/kotlin/net/msrandom/minecraftcodev/core/utils/SetMultimapSerializer.kt b/minecraft-codev-core/src/main/kotlin/net/msrandom/minecraftcodev/core/utils/SetMultimapSerializer.kt new file mode 100644 index 00000000..4e508d0d --- /dev/null +++ b/minecraft-codev-core/src/main/kotlin/net/msrandom/minecraftcodev/core/utils/SetMultimapSerializer.kt @@ -0,0 +1,41 @@ +package net.msrandom.minecraftcodev.core.utils + +import com.google.common.collect.Multimaps +import com.google.common.collect.SetMultimap +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.mapSerialDescriptor +import kotlinx.serialization.descriptors.setSerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Suppress("UnstableApiUsage") +@OptIn(ExperimentalSerializationApi::class) +class SetMultimapSerializer( + private val keySerializer: KSerializer, + private val valueSerializer: KSerializer, + private val valueFactory: () -> MutableSet = { HashSet() } +) : KSerializer> { + override val descriptor = + SerialDescriptor( + "Multimap", + mapSerialDescriptor( + keySerializer.descriptor, setSerialDescriptor(valueSerializer.descriptor) + ) + ) + + override fun serialize(encoder: Encoder, value: SetMultimap) { + MapSerializer(keySerializer, SetSerializer(valueSerializer)).serialize(encoder, Multimaps.asMap(value)) + } + + override fun deserialize(decoder: Decoder): SetMultimap { + val map = Multimaps.newSetMultimap(HashMap(), valueFactory) + for (entry in MapSerializer(keySerializer, SetSerializer(valueSerializer)).deserialize(decoder)) { + map.putAll(entry.key, entry.value) + } + return map + } +} \ No newline at end of file diff --git a/minecraft-codev-decompiler/gradle.properties b/minecraft-codev-decompiler/gradle.properties index af0eb1ce..3f83643e 100644 --- a/minecraft-codev-decompiler/gradle.properties +++ b/minecraft-codev-decompiler/gradle.properties @@ -1 +1 @@ -version=0.6.0 +version=0.6.1 diff --git a/minecraft-codev-fabric/gradle.properties b/minecraft-codev-fabric/gradle.properties index f13969c2..50791624 100644 --- a/minecraft-codev-fabric/gradle.properties +++ b/minecraft-codev-fabric/gradle.properties @@ -1 +1 @@ -version=0.7.0 +version=0.7.1 diff --git a/minecraft-codev-forge/gradle.properties b/minecraft-codev-forge/gradle.properties index 19d3efe4..58b1003e 100644 --- a/minecraft-codev-forge/gradle.properties +++ b/minecraft-codev-forge/gradle.properties @@ -1 +1 @@ -version=0.8.3 +version=0.8.4 diff --git a/minecraft-codev-forge/src/main/kotlin/net/msrandom/minecraftcodev/forge/mixin/ForgeMixinListingRule.kt b/minecraft-codev-forge/src/main/kotlin/net/msrandom/minecraftcodev/forge/mixin/ForgeMixinListingRule.kt index 3687d573..2b16554f 100644 --- a/minecraft-codev-forge/src/main/kotlin/net/msrandom/minecraftcodev/forge/mixin/ForgeMixinListingRule.kt +++ b/minecraft-codev-forge/src/main/kotlin/net/msrandom/minecraftcodev/forge/mixin/ForgeMixinListingRule.kt @@ -44,7 +44,7 @@ class ForgeMixinListingRule : MixinListingRule { val mixinConfigsString = manifest.mainAttributes.getValue(MANIFEST_MIXINS_CONFIG) ?: return loadFromToml(directory) - val mixinConfigs = mixinConfigsString.split(",").map(String::trim) + val mixinConfigs = mixinConfigsString.split(",").map(String::trim).filter(String::isNotBlank) return ForgeMixinConfigHandler(mixinConfigs, true) } diff --git a/minecraft-codev-includes/gradle.properties b/minecraft-codev-includes/gradle.properties index 7743eff2..86152c29 100644 --- a/minecraft-codev-includes/gradle.properties +++ b/minecraft-codev-includes/gradle.properties @@ -1 +1 @@ -version=0.6.5 +version=0.6.6 diff --git a/minecraft-codev-includes/src/main/kotlin/net/msrandom/minecraftcodev/includes/ExtractIncludes.kt b/minecraft-codev-includes/src/main/kotlin/net/msrandom/minecraftcodev/includes/ExtractIncludes.kt index bf6f9866..9a334144 100644 --- a/minecraft-codev-includes/src/main/kotlin/net/msrandom/minecraftcodev/includes/ExtractIncludes.kt +++ b/minecraft-codev-includes/src/main/kotlin/net/msrandom/minecraftcodev/includes/ExtractIncludes.kt @@ -1,5 +1,8 @@ package net.msrandom.minecraftcodev.includes +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.runBlocking import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async import kotlinx.coroutines.awaitAll @@ -61,7 +64,7 @@ abstract class ExtractIncludes : TransformAction { classpath .map { async { hashFile(it.toPath()) } } .awaitAll() - .toSet() + .toHashSet() } for (includedJar in handler.list(root)) { diff --git a/minecraft-codev-mixins/build.gradle.kts b/minecraft-codev-mixins/build.gradle.kts index fc592f25..f6f6d513 100644 --- a/minecraft-codev-mixins/build.gradle.kts +++ b/minecraft-codev-mixins/build.gradle.kts @@ -13,7 +13,9 @@ gradlePlugin { } dependencies { - api(group = "net.fabricmc", name = "sponge-mixin", version = "0.15.0+mixin.0.8.7") + api(group = "net.fabricmc", name = "sponge-mixin", version = "0.17.0+mixin.0.8.7") + api(group = "io.github.llamalad7", name = "mixinextras-common", version = "0.5.3") + api(group = "net.fabricmc", name = "mapping-io", version = "0.7.1") implementation(projects.minecraftCodevCore) } diff --git a/minecraft-codev-mixins/gradle.properties b/minecraft-codev-mixins/gradle.properties index af0eb1ce..e6e06da0 100644 --- a/minecraft-codev-mixins/gradle.properties +++ b/minecraft-codev-mixins/gradle.properties @@ -1 +1 @@ -version=0.6.0 +version=0.6.2 diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/MinecraftCodevMixinsPlugin.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/MinecraftCodevMixinsPlugin.kt index f131b760..69bca970 100644 --- a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/MinecraftCodevMixinsPlugin.kt +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/MinecraftCodevMixinsPlugin.kt @@ -3,13 +3,9 @@ package net.msrandom.minecraftcodev.mixins import net.msrandom.minecraftcodev.core.utils.applyPlugin import net.msrandom.minecraftcodev.core.utils.createSourceSetConfigurations import net.msrandom.minecraftcodev.core.utils.disambiguateName -import net.msrandom.minecraftcodev.mixins.mixin.GradleMixinService import org.gradle.api.Plugin import org.gradle.api.plugins.PluginAware import org.gradle.api.tasks.SourceSet -import org.spongepowered.asm.launch.MixinBootstrap -import org.spongepowered.asm.mixin.MixinEnvironment -import org.spongepowered.asm.service.MixinService val SourceSet.mixinsConfigurationName get() = disambiguateName(MinecraftCodevMixinsPlugin.MIXINS_CONFIGURATION) @@ -17,9 +13,8 @@ class MinecraftCodevMixinsPlugin : Plugin { override fun apply(target: T) = applyPlugin(target) { createSourceSetConfigurations(MIXINS_CONFIGURATION) - - MixinBootstrap.init() - (MixinService.getService() as GradleMixinService).phaseConsumer.accept(MixinEnvironment.Phase.DEFAULT) + // Mixin initialization is now done in IsolatedMixinExecutor + // within an isolated classloader for each mixin operation } companion object { diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/StripMixins.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/StripMixins.kt index 9b292114..1a95853d 100644 --- a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/StripMixins.kt +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/StripMixins.kt @@ -1,25 +1,50 @@ package net.msrandom.minecraftcodev.mixins +import com.google.common.collect.HashMultimap +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import net.msrandom.minecraftcodev.core.utils.SetMultimapSerializer import net.msrandom.minecraftcodev.core.utils.toPath import net.msrandom.minecraftcodev.core.utils.zipFileSystem -import org.gradle.api.artifacts.transform.* +import org.gradle.api.artifacts.transform.CacheableTransform +import org.gradle.api.artifacts.transform.InputArtifact +import org.gradle.api.artifacts.transform.TransformAction +import org.gradle.api.artifacts.transform.TransformOutputs +import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.FileSystemLocation import org.gradle.api.provider.Provider +import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import java.nio.file.StandardCopyOption import kotlin.io.path.copyTo -import kotlin.io.path.deleteExisting import kotlin.io.path.extension import kotlin.io.path.nameWithoutExtension +import kotlin.io.path.readLines +import kotlin.io.path.writeLines @CacheableTransform -abstract class StripMixins : TransformAction { +abstract class StripMixins : TransformAction { + abstract class Parameters : TransformParameters { + abstract val appliedMixins: ConfigurableFileCollection + @InputFiles + @PathSensitive(PathSensitivity.NONE) + get + + init { + appliedMixins.convention() + } + } + abstract val inputFile: Provider @InputArtifact @PathSensitive(PathSensitivity.NONE) get + @OptIn(ExperimentalSerializationApi::class) override fun transform(outputs: TransformOutputs) { val input = inputFile.get().toPath() @@ -38,13 +63,37 @@ abstract class StripMixins : TransformAction { return } - val output = outputs.file("${input.nameWithoutExtension}-no-mixins.${input.extension}").toPath() + val appliedMixins = HashMultimap.create() + val serializer = SetMultimapSerializer(String.serializer(), String.serializer()) + for (multimap in parameters.appliedMixins.map { Json.decodeFromStream(serializer, it.inputStream()) }) { + appliedMixins.putAll(multimap) + } + + val needStrip = zipFileSystem(input).use { fs -> + val root = fs.getPath("/") + handler.list(root).any { appliedMixins.containsKey(it) } + } + + if (!needStrip) { + outputs.file(inputFile) + + return + } + + val output = outputs.file("${input.nameWithoutExtension}-mixins-stripped.${input.extension}").toPath() input.copyTo(output, StandardCopyOption.COPY_ATTRIBUTES) - zipFileSystem(output).use { + zipFileSystem(output).use { it -> val root = it.getPath("/") - handler.list(root).forEach { path -> root.resolve(path).deleteExisting() } + handler.list(root) + .mapNotNull { path -> + appliedMixins[path].takeIf { it.isNotEmpty() }?.let { it to path } + } + .forEach { (mixins, path) -> + val path = root.resolve(path) + path.writeLines(path.readLines().filterNot { line -> mixins.any { line.contains(it) } }) + } handler.remove(root) } } diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinLogger.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinLogger.kt new file mode 100644 index 00000000..d25fd5fe --- /dev/null +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinLogger.kt @@ -0,0 +1,43 @@ +package net.msrandom.minecraftcodev.mixins.mixin + +import org.slf4j.LoggerFactory +import org.spongepowered.asm.logging.Level +import org.spongepowered.asm.logging.LoggerAdapterAbstract + +class GradleMixinLogger(val name: String) : LoggerAdapterAbstract(name) { + companion object { + private const val PREFIX = "[Mixin] " + } + + private val logger = LoggerFactory.getLogger(name) + + override fun getType() = "Gradle Logger" + + override fun catching(level: Level, t: Throwable) = + logger.info("${PREFIX}Catching {}: {}", t.javaClass.getName(), t.message, t) + + override fun log(level: Level, message: String, vararg params: Any) { + val level = when (message) { + "Mixin environment was unable to detect the current side, sided mixins will not be applied" -> Level.INFO + else -> level + } + val message = PREFIX + message + when (level) { + Level.TRACE -> logger.trace(message, *params) + Level.DEBUG -> logger.debug(message, *params) + Level.INFO -> logger.info(message, *params) + Level.WARN -> logger.warn(message, *params) + Level.ERROR -> logger.error(message, *params) + Level.FATAL -> logger.error("[FATAL] $message", *params) + } + } + + override fun log(level: Level, message: String, t: Throwable) { + log(level, message, t as Any) + } + + override fun throwing(t: T): T { + logger.warn("${PREFIX}Throwing {}: {}", t.javaClass.getName(), t.message, t) + return t + } +} \ No newline at end of file diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinRecorderExtension.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinRecorderExtension.kt new file mode 100644 index 00000000..bccd24f4 --- /dev/null +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinRecorderExtension.kt @@ -0,0 +1,44 @@ +package net.msrandom.minecraftcodev.mixins.mixin + +import com.google.common.collect.SetMultimap +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.mixin.MixinEnvironment +import org.spongepowered.asm.mixin.extensibility.IMixinInfo +import org.spongepowered.asm.mixin.transformer.ext.IExtension +import org.spongepowered.asm.mixin.transformer.ext.ITargetClassContext +import java.util.* + +class GradleMixinRecorderExtension : IExtension { + companion object { + private val targetClassContextClass = + Class.forName("org.spongepowered.asm.mixin.transformer.TargetClassContext") + private val mixinsField = targetClassContextClass.getDeclaredField("mixins") + + init { + mixinsField.isAccessible = true + } + } + + var appliedMixins: SetMultimap? = null + + override fun checkActive(environment: MixinEnvironment?) = true + + override fun preApply(context: ITargetClassContext) {} + + override fun postApply(context: ITargetClassContext) { + val mixins = mixinsField.get(context) as SortedSet + if (mixins.isNotEmpty()) { + for (info in mixins) { + appliedMixins!!.put(info.config.name, info.name) + } + } + } + + override fun export( + env: MixinEnvironment?, + name: String?, + force: Boolean, + classNode: ClassNode? + ) { + } +} \ No newline at end of file diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinService.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinService.kt index 3475a085..15bca37a 100644 --- a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinService.kt +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/GradleMixinService.kt @@ -2,13 +2,14 @@ package net.msrandom.minecraftcodev.mixins.mixin import org.objectweb.asm.ClassReader import org.objectweb.asm.tree.ClassNode -import org.spongepowered.asm.launch.platform.container.IContainerHandle +import org.spongepowered.asm.launch.platform.container.ContainerHandleVirtual +import org.spongepowered.asm.logging.ILogger import org.spongepowered.asm.mixin.MixinEnvironment import org.spongepowered.asm.mixin.MixinEnvironment.Phase import org.spongepowered.asm.mixin.MixinEnvironment.Side -import org.spongepowered.asm.mixin.Mixins import org.spongepowered.asm.mixin.transformer.IMixinTransformer import org.spongepowered.asm.mixin.transformer.IMixinTransformerFactory +import org.spongepowered.asm.mixin.transformer.ext.Extensions import org.spongepowered.asm.service.IClassBytecodeProvider import org.spongepowered.asm.service.IClassProvider import org.spongepowered.asm.service.IClassTracker @@ -16,7 +17,6 @@ import org.spongepowered.asm.service.MixinServiceAbstract import org.spongepowered.asm.util.IConsumer import java.io.File import java.io.FileNotFoundException -import java.net.URL import java.net.URLClassLoader import java.nio.file.Path import javax.annotation.concurrent.NotThreadSafe @@ -35,27 +35,28 @@ class GradleMixinService : MixinServiceAbstract() { getInternal(IMixinTransformerFactory::class.java).createTransformer() } + val recorderExtension = GradleMixinRecorderExtension() + + override fun init() { + (transformer.extensions as Extensions).add(recorderExtension) + super.init() + } + /** - * Thread safe accessor + * Set up classpath and side for mixin processing. + * Each isolated classloader instance runs independently, no synchronization needed. */ fun use( classpath: Iterable, side: Side, action: GradleMixinService.() -> R, - ) = synchronized(this) { + ): R { this.classpath = URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), javaClass.classLoader) - (registeredConfigsField[null] as MutableCollection<*>).clear() - sideField[MixinEnvironment.getCurrentEnvironment()] = Side.UNKNOWN MixinEnvironment.getCurrentEnvironment().side = side - @Suppress("DEPRECATION") - MixinEnvironment.getCurrentEnvironment().mixinConfigs.clear() - - Mixins.getConfigs().clear() - - this.action() + return this.action() } override fun getName() = "Gradle" @@ -65,14 +66,24 @@ class GradleMixinService : MixinServiceAbstract() { override fun getClassProvider() = object : IClassProvider { @Deprecated("Deprecated in Java", ReplaceWith("emptyArray()", "java.net.URL")) - override fun getClassPath() = emptyArray() + override fun getClassPath() = + if (this@GradleMixinService::classpath.isInitialized) classpath.urLs else emptyArray() - override fun findClass(name: String) = Class.forName(name) + override fun findClass(name: String) = + if (this@GradleMixinService::classpath.isInitialized) classpath.loadClass(name) else Class.forName(name) override fun findClass( name: String, initialize: Boolean, - ) = Class.forName(name, initialize, javaClass.classLoader) + ): Class<*>? { + return try { + Class.forName(name, initialize, javaClass.classLoader) + } catch (e: ClassNotFoundException) { + if (this@GradleMixinService::classpath.isInitialized) + Class.forName(name, initialize, classpath) + else throw e + } + } override fun findAgentClass( name: String, @@ -115,17 +126,13 @@ class GradleMixinService : MixinServiceAbstract() { override fun getPlatformAgents() = listOf("org.spongepowered.asm.launch.platform.MixinPlatformAgentDefault") override fun getPrimaryContainer() = - object : IContainerHandle { - override fun getId() = "codev" - + object : ContainerHandleVirtual("codev") { override fun getDescription() = "Minecraft Codev Dummy Mixin Container" - - override fun getAttribute(name: String?) = null - - override fun getNestedContainers() = emptyList() } - override fun getResourceAsStream(name: String) = classpath.getResourceAsStream(name) ?: Path(name).takeIf(Path::exists)?.inputStream() + override fun getResourceAsStream(name: String) = + if (this@GradleMixinService::classpath.isInitialized) classpath.getResourceAsStream(name) + else Path(name).takeIf(Path::exists)?.inputStream() @Deprecated("Deprecated in Java") override fun wire( @@ -137,8 +144,11 @@ class GradleMixinService : MixinServiceAbstract() { this.phaseConsumer = phaseConsumer } + override fun createLogger(name: String): ILogger { + return GradleMixinLogger(name) + } + companion object { - private val registeredConfigsField = Mixins::class.java.getDeclaredField("registeredConfigs").apply { isAccessible = true } private val sideField = MixinEnvironment::class.java.getDeclaredField("side").apply { isAccessible = true } } } diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/IsolatedMixinExecutor.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/IsolatedMixinExecutor.kt new file mode 100644 index 00000000..6176f2a9 --- /dev/null +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/IsolatedMixinExecutor.kt @@ -0,0 +1,335 @@ +package net.msrandom.minecraftcodev.mixins.mixin + +import com.google.common.base.Joiner +import com.google.common.collect.HashMultimap +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.decodeFromStream +import kotlinx.serialization.json.encodeToStream +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import net.fabricmc.mappingio.format.tiny.Tiny2FileReader +import net.fabricmc.mappingio.tree.MemoryMappingTree +import net.msrandom.minecraftcodev.core.utils.SetMultimapSerializer +import net.msrandom.minecraftcodev.core.utils.zipFileSystem +import net.msrandom.minecraftcodev.mixins.MixinListingRule +import org.spongepowered.asm.launch.MixinBootstrap +import org.spongepowered.asm.launch.platform.container.ContainerHandleURI +import org.spongepowered.asm.mixin.FabricUtil +import org.spongepowered.asm.mixin.MixinEnvironment +import org.spongepowered.asm.mixin.Mixins +import org.spongepowered.asm.mixin.extensibility.IMixinConfig +import org.spongepowered.asm.mixin.transformer.Config +import org.spongepowered.asm.service.MixinService +import java.io.File +import java.nio.file.FileSystem +import java.nio.file.FileVisitResult +import java.nio.file.Path +import java.nio.file.StandardCopyOption +import java.util.* +import kotlin.io.path.* + +/** + * Executor class that runs Mixin application in an isolated classloader context. + * All Mixin framework references resolve to classes in the isolated classloader. + * This class must be loaded by IsolatingMixinClassLoader. + */ +class IsolatedMixinExecutor { + + companion object { + private val JOINER = Joiner.on('.') + + /** + * Loader version to Mixin compatibility version mapping. + * Must be in DESCENDING order (latest first). + */ + private val VERSION_MAPPINGS = listOf( + LoaderMixinVersionEntry("0.18.4", FabricUtil.COMPATIBILITY_0_17_0), + LoaderMixinVersionEntry("0.17.3", FabricUtil.COMPATIBILITY_0_16_5), + LoaderMixinVersionEntry("0.16.0", FabricUtil.COMPATIBILITY_0_14_0), + LoaderMixinVersionEntry("0.12.0", FabricUtil.COMPATIBILITY_0_10_0), + ) + + /** + * Compute Mixin compatibility level from minimum Fabric Loader version. + */ + fun computeMixinCompat(minLoaderVersion: String?): Int { + if (minLoaderVersion == null) { + return FabricUtil.COMPATIBILITY_0_9_2 + } + + val cleanVersion = minLoaderVersion.substringBefore('+').substringBefore('-') + val versionParts = cleanVersion.split('.') + + if (versionParts.size < 2) { + return FabricUtil.COMPATIBILITY_0_9_2 + } + + val major = versionParts.getOrNull(0)?.toIntOrNull() ?: 0 + val minor = versionParts.getOrNull(1)?.toIntOrNull() ?: 0 + val patch = versionParts.getOrNull(2)?.toIntOrNull() ?: 0 + + for (entry in VERSION_MAPPINGS) { + if (isVersionGreaterOrEqual(major, minor, patch, entry.loaderVersion)) { + return entry.mixinVersion + } + } + + return FabricUtil.COMPATIBILITY_0_9_2 + } + + private fun isVersionGreaterOrEqual( + major: Int, minor: Int, patch: Int, + reference: String + ): Boolean { + val refParts = reference.split('.') + val refMajor = refParts.getOrNull(0)?.toIntOrNull() ?: 0 + val refMinor = refParts.getOrNull(1)?.toIntOrNull() ?: 0 + val refPatch = refParts.getOrNull(2)?.toIntOrNull() ?: 0 + + return when { + major > refMajor -> true + major < refMajor -> false + minor > refMinor -> true + minor < refMinor -> false + else -> patch >= refPatch + } + } + } + + private data class LoaderMixinVersionEntry( + val loaderVersion: String, + val mixinVersion: Int + ) + + /** + * Extract the minimum Fabric Loader version from fabric.mod.json. + */ + private fun extractMinLoaderVersion(fileSystem: FileSystem): String? { + val modJson = fileSystem.getPath("/fabric.mod.json") + if (!modJson.exists()) { + return null + } + + return try { + modJson.inputStream().use { + val json = Json.decodeFromStream(it) + parseMinLoaderVersion(json) + } + } catch (e: Exception) { + null + } + } + + /** + * Parse minimum Fabric Loader version from fabric.mod.json content. + */ + private fun parseMinLoaderVersion(json: JsonObject): String? { + val depends = json["depends"]?.jsonObject ?: return null + + // Try both "fabricloader" and "fabric-loader" keys + val loaderDep = depends["fabricloader"] ?: depends["fabric-loader"] ?: return null + + val versionSpec = when { + loaderDep is JsonObject -> { + loaderDep["version"]?.jsonPrimitive?.content + } + loaderDep.jsonPrimitive.isString -> { + loaderDep.jsonPrimitive.content + } + else -> null + } ?: return null + + return extractMinVersion(versionSpec) + } + + /** + * Extract minimum version from a version range string. + */ + private fun extractMinVersion(versionSpec: String): String? { + val trimmed = versionSpec.trim() + + // Handle ">=x.y.z" or ">x.y.z" + if (trimmed.startsWith(">=")) { + return trimmed.substring(2).trim().split(" ").firstOrNull() + } + if (trimmed.startsWith(">")) { + return trimmed.substring(1).trim().split(" ").firstOrNull() + } + + // Handle range format "[x.y.z, ...)" + if (trimmed.startsWith("[")) { + val end = trimmed.indexOfAny(charArrayOf(',', ']')) + if (end > 1) { + return trimmed.substring(1, end).trim() + } + } + + // Handle plain version string + if (trimmed.matches(Regex("^[\\d.]+.*"))) { + return trimmed.split(" ").firstOrNull() + } + + return null + } + + /** + * Execute mixin application in an isolated context. + * All parameters are primitive/String types for cross-classloader safety. + */ + @OptIn(ExperimentalPathApi::class, ExperimentalSerializationApi::class) + fun execute( + inputFilePath: String, + outputFilePath: String, + mixinFilePaths: List, + classpathPaths: List, + side: String, + mappingsFilePath: String, + sourceNamespace: String, + targetNamespace: String, + appliedMixinsFilePath: String + ) { + // 1. Initialize Mixin system (fresh in this classloader!) + val mixinServiceClass = MixinService::class.java + val getInstanceMethod = mixinServiceClass.getDeclaredMethod("getInstance") + getInstanceMethod.setAccessible(true) + val mixinService = getInstanceMethod.invoke(null) as MixinService + val propertyServiceField = mixinServiceClass.getDeclaredField("propertyService") + propertyServiceField.setAccessible(true) + propertyServiceField.set(mixinService, GradleGlobalPropertyService()) + + System.setProperty("mixin.service", GradleMixinService::class.java.name) + + MixinBootstrap.init() + + val service = MixinService.getService() as GradleMixinService + service.phaseConsumer.accept(MixinEnvironment.Phase.DEFAULT) + + // 2. Convert string paths to File/Path objects + val inputFile = File(inputFilePath).toPath() + val outputFile = File(outputFilePath).toPath() + val mixinFiles = mixinFilePaths.map { File(it) } + val classpathFiles = classpathPaths.map { File(it) } + val mappingsFile = File(mappingsFilePath) + val appliedMixinsFile = File(appliedMixinsFilePath) + + val mixinSide = MixinEnvironment.Side.valueOf(side) + + // 3. Set up the classpath (all files including mixins and input) + val allClasspathFiles = classpathFiles + mixinFiles + listOf(inputFile.toFile()) + + service.use(allClasspathFiles, mixinSide) { + // 4. Set up mappings + val mappings = MemoryMappingTree() + Tiny2FileReader.read(mappingsFile.reader(), mappings) + + MixinEnvironment.getDefaultEnvironment().remappers.add( + MappingIoRemapperAdapter(mappings, sourceNamespace, targetNamespace) + ) + + MixinEnvironment.getDefaultEnvironment().setOption( + MixinEnvironment.Option.REFMAP_REMAP, + System.getProperty("mixin.env.remapRefMap", "true").toBoolean() + ) + + // 5. Set up recorder + val appliedMixins = HashMultimap.create() + recorderExtension.appliedMixins = appliedMixins + + // 6. Load mixin listing rules from this classloader + val mixinListingRules = ServiceLoader.load( + MixinListingRule::class.java, + this.javaClass.classLoader + ).toList() + + // 7. Add mixin configurations and set compatibility levels + val configToModMap = mutableMapOf() // config name -> min loader version + + for (mixinFile in mixinFiles + listOf(inputFile.toFile())) { + zipFileSystem(mixinFile.toPath()).use fs@{ fs -> + val root = fs.getPath("/") + + val handler = mixinListingRules.firstNotNullOfOrNull { rule -> + rule.load(root) + } + + if (handler == null) { + return@fs + } + + val configs = handler.list(root) + if (configs.isEmpty()) { + return@fs + } + + // Get minimum loader version from fabric.mod.json + val minLoaderVersion = extractMinLoaderVersion(fs) + + // Store mapping for later decoration + configs.forEach { config -> + configToModMap[config] = minLoaderVersion + } + + Mixins.addConfigurations( + configs.toTypedArray(), + ContainerHandleURI(mixinFile.toPath().toUri()) + ) + } + } + + // 7.1 Apply compatibility decorations to configs + for (rawConfig in Mixins.getConfigs()) { + val configName = rawConfig.name + val minLoaderVersion = configToModMap[configName] + val compatLevel = computeMixinCompat(minLoaderVersion) + + val config = rawConfig.config + if (config is IMixinConfig) { + config.decorate(FabricUtil.KEY_COMPATIBILITY, compatLevel) + } + } + + // 8. Transform classes + outputFile.deleteIfExists() + + zipFileSystem(inputFile).use { inputFs -> + val root = inputFs.getPath("/") + + zipFileSystem(outputFile, true).use { outputFs -> + root.visitFileTree(fileVisitor { + onVisitFile { path, _ -> + val outputPath = outputFs.getPath( + path.getName(0).name, + *path.drop(1).map { it.name }.toList().toTypedArray() + ) + + outputPath.createParentDirectories() + + if (path.extension == "class") { + val pathName = JOINER.join(root.relativize(path)) + val name = pathName.substring(0, pathName.length - ".class".length) + outputPath.writeBytes(transformer.transformClassBytes(name, name, path.readBytes())) + } else { + path.copyTo( + outputPath, + StandardCopyOption.COPY_ATTRIBUTES, + StandardCopyOption.REPLACE_EXISTING + ) + } + FileVisitResult.CONTINUE + } + }) + } + } + + // 9. Write applied mixins + Json.encodeToStream( + SetMultimapSerializer(String.serializer(), String.serializer()), + appliedMixins, + appliedMixinsFile.outputStream() + ) + } + } +} diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/IsolatingMixinClassLoader.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/IsolatingMixinClassLoader.kt new file mode 100644 index 00000000..3718534d --- /dev/null +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/IsolatingMixinClassLoader.kt @@ -0,0 +1,73 @@ +package net.msrandom.minecraftcodev.mixins.mixin + +import java.net.URL +import java.net.URLClassLoader +import java.util.Collections +import java.util.Enumeration + +/** + * A child-first classloader that isolates Mixin framework classes. + * Each instance gets its own copy of static fields for isolated packages, + * enabling multiple independent Mixin operations. + */ +class IsolatingMixinClassLoader( + urls: Array, + parent: ClassLoader +) : URLClassLoader(urls, parent) { + + companion object { + private val ISOLATED_PREFIXES = listOf( + "org.spongepowered.asm.", + "com.llamalad7.mixinextras.", + "org.objectweb.asm.", + "net.fabricmc.mappingio.", + "net.msrandom.minecraftcodev.mixins.mixin.", + ) + } + + override fun loadClass(name: String, resolve: Boolean): Class<*> { + synchronized(getClassLoadingLock(name)) { + // Check if already loaded + var c = findLoadedClass(name) + if (c != null) { + if (resolve) resolveClass(c) + return c + } + + // For isolated packages, try child first (load from our URLs) + if (ISOLATED_PREFIXES.any { name.startsWith(it) }) { + try { + c = findClass(name) + if (resolve) resolveClass(c) + return c + } catch (_: ClassNotFoundException) { + // Fall through to parent + } + } + + // Default parent-first delegation + return super.loadClass(name, resolve) + } + } + + override fun getResource(name: String): URL? { + // For isolated packages, check child first + if (ISOLATED_PREFIXES.any { name.replace('/', '.').startsWith(it) }) { + findResource(name)?.let { return it } + } + return super.getResource(name) + } + + override fun getResources(name: String): Enumeration { + // For service loader files and isolated resources, include child resources first + if (name.startsWith("META-INF/services/org.spongepowered.asm.") || + name.startsWith("META-INF/services/net.msrandom.minecraftcodev.mixins.mixin.") + ) { + val childResources = findResources(name).toList() + if (childResources.isNotEmpty()) { + return Collections.enumeration(childResources) + } + } + return super.getResources(name) + } +} diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/MappingIoRemapperAdapter.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/MappingIoRemapperAdapter.kt new file mode 100644 index 00000000..a0c692a4 --- /dev/null +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/mixin/MappingIoRemapperAdapter.kt @@ -0,0 +1,47 @@ +package net.msrandom.minecraftcodev.mixins.mixin + +import net.fabricmc.mappingio.tree.MappingTreeView +import org.spongepowered.asm.mixin.extensibility.IRemapper + +class MappingIoRemapperAdapter( + val mappings: MappingTreeView, + sourceNamespace: String, + targetNamespace: String +) : IRemapper { + private val sourceId = mappings.getNamespaceId(sourceNamespace) + private val targetId = mappings.getNamespaceId(targetNamespace) + + private val methods = HashMap() + private val fields = HashMap() + + init { + for (classMapping in mappings.classes) { + for (methodMapping in classMapping.methods) { + val sourceName = methodMapping.getName(sourceId) ?: continue + val targetName = methodMapping.getName(targetId) ?: continue + methods.put(sourceName, targetName) + } + for (fieldMapping in classMapping.fields) { + val sourceName = fieldMapping.getName(sourceId) ?: continue + val targetName = fieldMapping.getName(targetId) ?: continue + fields.put(sourceName, targetName) + } + } + } + + override fun mapMethodName(owner: String?, name: String?, desc: String?) = methods[name] ?: name + + override fun mapFieldName(owner: String?, name: String?, desc: String?) = fields[name] ?: name + + override fun map(typeName: String) = + mappings.mapClassName(typeName, sourceId, targetId) + + override fun unmap(typeName: String) = + mappings.mapClassName(typeName, targetId, sourceId) + + override fun mapDesc(desc: String) = + mappings.mapDesc(desc, sourceId, targetId) + + override fun unmapDesc(desc: String) = + mappings.mapDesc(desc, targetId, sourceId) +} \ No newline at end of file diff --git a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/task/Mixin.kt b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/task/Mixin.kt index 85b00bfe..21b96073 100644 --- a/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/task/Mixin.kt +++ b/minecraft-codev-mixins/src/main/kotlin/net/msrandom/minecraftcodev/mixins/task/Mixin.kt @@ -1,25 +1,35 @@ package net.msrandom.minecraftcodev.mixins.task +import net.fabricmc.mappingio.tree.MemoryMappingTree +import net.msrandom.minecraftcodev.core.utils.SetMultimapSerializer import net.msrandom.minecraftcodev.core.utils.getAsPath -import net.msrandom.minecraftcodev.core.utils.walk -import net.msrandom.minecraftcodev.core.utils.zipFileSystem import net.msrandom.minecraftcodev.mixins.mixin.GradleMixinService -import net.msrandom.minecraftcodev.mixins.mixinListingRules +import net.msrandom.minecraftcodev.mixins.mixin.IsolatingMixinClassLoader +import net.msrandom.minecraftcodev.mixins.mixin.MappingIoRemapperAdapter import org.gradle.api.DefaultTask import org.gradle.api.file.ConfigurableFileCollection import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property -import org.gradle.api.tasks.* +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Classpath +import org.gradle.api.tasks.CompileClasspath +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.InputFiles +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.objectweb.asm.ClassReader +import org.objectweb.asm.tree.ClassNode +import org.spongepowered.asm.launch.MixinBootstrap +import org.spongepowered.asm.mixin.MixinEnvironment import org.spongepowered.asm.mixin.MixinEnvironment.Side -import org.spongepowered.asm.mixin.Mixins -import org.spongepowered.asm.service.MixinService -import java.io.File -import java.nio.file.Path -import java.nio.file.StandardCopyOption -import kotlin.io.path.* +import java.net.URL @CacheableTask abstract class Mixin : DefaultTask() { + abstract val inputFile: RegularFileProperty @InputFile @Classpath @@ -38,9 +48,23 @@ abstract class Mixin : DefaultTask() { abstract val side: Property @Input get + abstract val mappings: RegularFileProperty + @InputFile + @PathSensitive(PathSensitivity.NONE) + get + + abstract val sourceNamespace: Property + @Input get + + abstract val targetNamespace: Property + @Input get + abstract val outputFile: RegularFileProperty @OutputFile get + abstract val appliedMixins: RegularFileProperty + @OutputFile get + init { outputFile.convention( project.layout.file( @@ -50,59 +74,106 @@ abstract class Mixin : DefaultTask() { ), ) + appliedMixins.convention( + project.layout.file( + inputFile.map { + temporaryDir.resolve("${it.asFile.nameWithoutExtension}-applied-mixins.json") + }, + ), + ) + side.convention(Side.UNKNOWN) } @TaskAction fun mixin() { - val input = inputFile.getAsPath() - val output = outputFile.getAsPath() - - (MixinService.getService() as GradleMixinService).use(classpath + mixinFiles + project.files(input), side.get()) { - CLASSPATH@ for (mixinFile in mixinFiles + project.files(input)) { - zipFileSystem(mixinFile.toPath()).use fs@{ - val root = it.getPath("/") - - val handler = - mixinListingRules.firstNotNullOfOrNull { rule -> - rule.load(root) - } - - if (handler == null) { - return@fs - } - - Mixins.addConfigurations(*handler.list(root).toTypedArray()) - } - } - - zipFileSystem(input).use { inputFs -> - val root = inputFs.getPath("/") - - zipFileSystem(output, true).use { outputFs -> - root.walk { - for (path in filter(Path::isRegularFile)) { - val pathString = path.toString() - val outputPath = outputFs.getPath(pathString) - - outputPath.parent?.createDirectories() - - if (pathString.endsWith(".class")) { - val pathName = root.relativize(path).toString() - - val name = - pathName - .substring(0, pathName.length - ".class".length) - .replace(File.separatorChar, '.') - - outputPath.writeBytes(transformer.transformClassBytes(name, name, path.readBytes())) - } else { - path.copyTo(outputPath, StandardCopyOption.COPY_ATTRIBUTES) - } - } - } - } - } + // Target classpath URLs come first (so target's mixin deps are preferred) + val targetUrls = (classpath.files + mixinFiles.files + listOf(inputFile.asFile.get())) + .map { it.toURI().toURL() } + + // Plugin JARs as fallback + val pluginUrls = collectIsolatedClasspathUrls() + + // Target first, then plugin fallback + val allUrls = (targetUrls + pluginUrls).distinct() + + val isolatedClassLoader = IsolatingMixinClassLoader( + allUrls.toTypedArray(), + javaClass.classLoader + ) + + isolatedClassLoader.use { isolatedClassLoader -> + // Load IsolatedMixinExecutor in the isolated classloader + val executorClass = isolatedClassLoader.loadClass( + "net.msrandom.minecraftcodev.mixins.mixin.IsolatedMixinExecutor" + ) + val executor = executorClass.getDeclaredConstructor().newInstance() + + // Get the execute method + val executeMethod = executorClass.getDeclaredMethod( + "execute", + String::class.java, // inputFilePath + String::class.java, // outputFilePath + List::class.java, // mixinFilePaths + List::class.java, // classpathPaths + String::class.java, // side + String::class.java, // mappingsFilePath + String::class.java, // sourceNamespace + String::class.java, // targetNamespace + String::class.java // appliedMixinsFilePath + ) + + // Prepare parameters (all String/List for cross-classloader safety) + val inputFilePath = inputFile.getAsPath().toAbsolutePath().toString() + val outputFilePath = outputFile.getAsPath().toAbsolutePath().toString() + val mixinFilePaths = mixinFiles.files.map { it.absolutePath } + val classpathPaths = classpath.files.map { it.absolutePath } + val sideStr = side.get().name + val mappingsFilePath = mappings.asFile.get().absolutePath + val sourceNs = sourceNamespace.get() + val targetNs = targetNamespace.get() + val appliedMixinsFilePath = appliedMixins.asFile.get().absolutePath + + // Invoke execute method + executeMethod.invoke( + executor, + inputFilePath, + outputFilePath, + mixinFilePaths, + classpathPaths, + sideStr, + mappingsFilePath, + sourceNs, + targetNs, + appliedMixinsFilePath + ) } } + + /** + * Collect URLs of JARs containing classes that need to be isolated. + * These include Mixin framework, ASM, mapping-io, and our mixin service classes. + */ + private fun collectIsolatedClasspathUrls(): List { + val keyClasses = listOf( + // Our plugin classes + GradleMixinService::class.java, + MappingIoRemapperAdapter::class.java, + // minecraft-codev-core classes + SetMultimapSerializer::class.java, + // Mixin library + MixinEnvironment::class.java, + MixinBootstrap::class.java, + // Mixin Extras + Class.forName("com.llamalad7.mixinextras.MixinExtrasBootstrap"), + // ASM + ClassReader::class.java, + ClassNode::class.java, + // mapping-io + MemoryMappingTree::class.java, + ) + return keyClasses.mapNotNull { + it.protectionDomain?.codeSource?.location + }.distinct() + } } diff --git a/minecraft-codev-runs/gradle.properties b/minecraft-codev-runs/gradle.properties index 6e148ff4..af4621a4 100644 --- a/minecraft-codev-runs/gradle.properties +++ b/minecraft-codev-runs/gradle.properties @@ -1 +1 @@ -version=0.6.8 +version=0.6.9