Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Loom's Interface Injection support for Fabric providers + Fix Integration tests #47

Merged
merged 10 commits into from
Dec 1, 2023
4 changes: 2 additions & 2 deletions .github/workflows/test-push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:

steps:
- uses: actions/checkout@v3
- run: gradle test --tests ${{ matrix.test }} --stacktrace --warning-mode fail
- run: gradle test --tests ${{ matrix.test }} --stacktrace --info --warning-mode fail
env:
TEST_WARNING_MODE: fail
id: test
Expand Down Expand Up @@ -90,7 +90,7 @@ jobs:
with:
java-version: ${{ matrix.java }}
distribution: 'temurin'
- run: ./gradlew test --tests ${{ matrix.test }} --stacktrace --warning-mode fail
- run: ./gradlew test --tests ${{ matrix.test }} --stacktrace --info --warning-mode fail
env:
TEST_WARNING_MODE: fail
id: test
Expand Down
11 changes: 11 additions & 0 deletions src/api/kotlin/xyz/wagyourtail/unimined/util/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import java.security.MessageDigest
import java.util.*
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlin.collections.HashMap
import kotlin.io.path.*
import kotlin.reflect.KClass
import kotlin.reflect.KProperty1
Expand Down Expand Up @@ -118,6 +119,16 @@ fun Path.getShortSha1(): String = getSha1().substring(0, 7)

fun File.getShortSha1() = toPath().getShortSha1()

fun <K, V> HashMap<K, V>.getSha1(): String {
val digestSha1 = MessageDigest.getInstance("SHA-1")
digestSha1.update(toString().toByteArray())
val hashBytes = digestSha1.digest()
return hashBytes.joinToString("") { String.format("%02x", it)}
}

fun <K, V> HashMap<K, V>.getShortSha1(): String = getSha1().substring(0, 7)



//fun runJarInSubprocess(
// jar: Path,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package xyz.wagyourtail.unimined.internal.mapping.ii

import org.gradle.api.logging.Logger
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.ClassNode
import xyz.wagyourtail.unimined.util.openZipFileSystem
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.StandardCopyOption
import java.nio.file.StandardOpenOption
import kotlin.io.path.inputStream

object InterfaceInjectionMinecraftTransformer {
fun transform(
injections: Map<String, List<String>>,
baseMinecraft: Path,
output: Path,
logger: Logger
): Boolean {
if (injections.isNotEmpty()) {
Files.copy(baseMinecraft, output, StandardCopyOption.REPLACE_EXISTING)
output.openZipFileSystem(mapOf("mutable" to true)).use { fs ->
logger.debug("Transforming $output with ${injections.values.sumOf { it.size }} interface injections")

for (target in injections.keys) {
try {
val targetClass = "/" + target.replace(".", "/") + ".class"
val targetPath = fs.getPath(targetClass)
logger.debug("Transforming $targetPath")
if (Files.exists(targetPath)) {
val reader = ClassReader(targetPath.inputStream())
val writer = ClassWriter(0)

val node = ClassNode(Opcodes.ASM9)
reader.accept(node, 0)

if (node.interfaces == null) {
node.interfaces = arrayListOf();
}

for (injected in injections[target]!!) {
if (!node.interfaces.contains(injected)) node.interfaces.add(injected)
}

if (node.signature != null) {
val resultingSignature = StringBuilder(node.signature)

for (injected in injections[target]!!) {
val computedSignature = "L" + injected.replace(".", "/") + ";"

if (!resultingSignature.contains(computedSignature)) resultingSignature.append(computedSignature)
}

node.signature = resultingSignature.toString()
}

node.accept(writer);
Files.write(
targetPath,
writer.toByteArray(),
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
} else {
logger.warn("Could not find class $targetClass in $output")
}
} catch (e: Exception) {
logger.warn(
"An error occurred while transforming $target with interface injection in $output",
e
)
}
}
}


return true
}

return false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import xyz.wagyourtail.unimined.api.task.RemapJarTask
import xyz.wagyourtail.unimined.api.unimined
import xyz.wagyourtail.unimined.internal.mapping.at.AccessTransformerMinecraftTransformer
import xyz.wagyourtail.unimined.internal.mapping.aw.AccessWidenerMinecraftTransformer
import xyz.wagyourtail.unimined.internal.mapping.ii.InterfaceInjectionMinecraftTransformer
import xyz.wagyourtail.unimined.internal.mapping.task.ExportMappingsTaskImpl
import xyz.wagyourtail.unimined.internal.minecraft.MinecraftProvider
import xyz.wagyourtail.unimined.internal.minecraft.patch.AbstractMinecraftTransformer
Expand Down Expand Up @@ -211,7 +212,9 @@ abstract class FabricLikeMinecraftTransformer(
provider.minecraftLibraries.dependencies.add(dep)
}

override fun afterRemap(baseMinecraft: MinecraftJar): MinecraftJar =
override fun afterRemap(baseMinecraft: MinecraftJar): MinecraftJar = applyInterfaceInjection(applyAccessWideners(baseMinecraft))

private fun applyAccessWideners(baseMinecraft: MinecraftJar): MinecraftJar =
if (accessWidener != null) {
val output = MinecraftJar(
baseMinecraft,
Expand All @@ -237,6 +240,71 @@ abstract class FabricLikeMinecraftTransformer(
}
} else baseMinecraft

private fun applyInterfaceInjection(baseMinecraft: MinecraftJar): MinecraftJar {
val injections = hashMapOf<String, List<String>>()

this.collectInterfaceInjections(baseMinecraft, injections)

return if (injections.isNotEmpty()) {
val oldSuffix = if (baseMinecraft.awOrAt != null) baseMinecraft.awOrAt + "+" else ""

val output = MinecraftJar(
baseMinecraft,
parentPath = provider.localCache.resolve("fabric").createDirectories(),
awOrAt = "${oldSuffix}ii+${injections.getShortSha1()}"
)

if (!output.path.exists() || project.unimined.forceReload) {
if (InterfaceInjectionMinecraftTransformer.transform(
injections,
baseMinecraft.path,
output.path,
project.logger
)
) {
output
} else baseMinecraft
} else output
} else baseMinecraft
}

abstract fun collectInterfaceInjections(baseMinecraft: MinecraftJar, injections: HashMap<String, List<String>>)
fun collectInterfaceInjections(baseMinecraft: MinecraftJar, injections: HashMap<String, List<String>>, interfaces: JsonObject) {
injections.putAll(interfaces.entrySet()
.filterNotNull()
.filter { it.key != null && it.value != null && it.value.isJsonArray }
.map {
val element = it.value!!

Pair(it.key!!, if (element.isJsonArray) {
element.asJsonArray.mapNotNull { name -> name.asString }
} else arrayListOf())
}
.map {
var target = it.first

val clazz = provider.mappings.mappingTree.getClass(
target,
provider.mappings.mappingTree.getNamespaceId(prodNamespace.name)
)

if (clazz != null) {
var newTarget = clazz.getName(provider.mappings.mappingTree.getNamespaceId(baseMinecraft.mappingNamespace.name))

if (newTarget == null) {
newTarget = clazz.getName(provider.mappings.mappingTree.getNamespaceId(baseMinecraft.fallbackNamespace.name))
}

if (newTarget != null) {
target = newTarget
}
}

Pair(target, it.second)
}
)
}

val intermediaryClasspath: Path = provider.localCache.resolve("remapClasspath.txt".withSourceSet(provider.sourceSet))

override fun afterEvaluate() {
Expand Down Expand Up @@ -415,4 +483,8 @@ abstract class FabricLikeMinecraftTransformer(
// fabric provides its own asm, exclude asm-all from vanilla minecraftLibraries
return !library.name.startsWith("org.ow2.asm:asm-all")
}

fun getModJsonPath(): File? {
return provider.sourceSet.resources.first { it.name.equals(modJsonName) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,20 @@ package xyz.wagyourtail.unimined.internal.minecraft.patch.fabric

import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import xyz.wagyourtail.unimined.api.minecraft.EnvType
import xyz.wagyourtail.unimined.api.runs.RunConfig
import xyz.wagyourtail.unimined.api.unimined
import xyz.wagyourtail.unimined.internal.mapping.ii.InterfaceInjectionMinecraftTransformer
import xyz.wagyourtail.unimined.internal.minecraft.MinecraftProvider
import xyz.wagyourtail.unimined.internal.minecraft.patch.MinecraftJar
import xyz.wagyourtail.unimined.util.getShortSha1
import java.io.InputStreamReader
import java.nio.file.Files
import kotlin.io.path.createDirectories
import kotlin.io.path.exists

abstract class FabricMinecraftTransformer(
project: Project,
Expand Down Expand Up @@ -60,4 +68,22 @@ abstract class FabricMinecraftTransformer(
"-Dfabric.remapClasspathFile=${intermediaryClasspath}"
)
}

override fun collectInterfaceInjections(baseMinecraft: MinecraftJar, injections: HashMap<String, List<String>>) {
val modJsonPath = this.getModJsonPath()

if (modJsonPath != null && modJsonPath.exists()) {
val json = JsonParser.parseReader(InputStreamReader(Files.newInputStream(modJsonPath.toPath()))).asJsonObject

val custom = json.getAsJsonObject("custom")
thecatcore marked this conversation as resolved.
Show resolved Hide resolved

if (custom != null) {
val interfaces = custom.getAsJsonObject("loom:injected_interfaces")

if (interfaces != null) {
collectInterfaceInjections(baseMinecraft, injections, interfaces)
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ package xyz.wagyourtail.unimined.internal.minecraft.patch.fabric

import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import org.gradle.api.Project
import org.gradle.api.artifacts.Dependency
import xyz.wagyourtail.unimined.api.minecraft.EnvType
import xyz.wagyourtail.unimined.api.runs.RunConfig
import xyz.wagyourtail.unimined.api.unimined
import xyz.wagyourtail.unimined.internal.minecraft.MinecraftProvider
import xyz.wagyourtail.unimined.internal.minecraft.patch.MinecraftJar
import java.io.InputStreamReader
import java.nio.file.Files

class QuiltMinecraftTransformer(
project: Project,
Expand All @@ -29,6 +33,34 @@ class QuiltMinecraftTransformer(
}
}

override fun collectInterfaceInjections(baseMinecraft: MinecraftJar, injections: HashMap<String, List<String>>) {
val modJsonPath = this.getModJsonPath()

if (modJsonPath != null && modJsonPath.exists()) {
val json = JsonParser.parseReader(InputStreamReader(Files.newInputStream(modJsonPath.toPath()))).asJsonObject

val custom = json.getAsJsonObject("custom")

if (custom != null) {
val quiltLoom = custom.getAsJsonObject("quilt_loom")

if (quiltLoom != null) {
val interfaces = quiltLoom.getAsJsonObject("injected_interfaces")

if (interfaces != null) collectInterfaceInjections(baseMinecraft, injections, interfaces)
}
}

val quiltLoom = json.getAsJsonObject("quilt_loom")

if (quiltLoom != null) {
val interfaces = quiltLoom.getAsJsonObject("injected_interfaces")

if (interfaces != null) collectInterfaceInjections(baseMinecraft, injections, interfaces)
}
}
}

override fun loader(dep: Any, action: Dependency.() -> Unit) {
fabric.dependencies.add(
(if (dep is String && !dep.contains(":")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class MinecraftForgeMinecraftTransformer(project: Project, provider: MinecraftPr

override fun addMavens() {
project.unimined.minecraftForgeMaven()
project.unimined.neoForgedMaven()
}

override fun loader(dep: Any, action: Dependency.() -> Unit) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
package xyz.wagyourtail.unimined.test.integration

import org.gradle.testkit.runner.TaskOutcome
import org.gradle.testkit.runner.UnexpectedBuildFailure
import org.junit.jupiter.api.Test
import xyz.wagyourtail.unimined.util.runTestProject

class BabricModloaderB1_7_3Test {
@Test
fun test_babric_modloader_b1_7_3() {
val result = runTestProject("b1.7.3-Babric-Modloader")
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
try {
val result = runTestProject("b1.7.3-Babric-Modloader")

try {
result.task(":build")?.outcome?.let {
if (it != TaskOutcome.SUCCESS) throw Exception("build failed")
} ?: throw Exception("build failed")
} catch (e: Exception) {
println(result.output)
throw Exception(e)
}
} catch (e: UnexpectedBuildFailure) {
println(e)
throw Exception("build failed", e)
}
}
}
Loading