Refactor to the transformer system, bump version to 2.0 for an unstable test

This commit is contained in:
shedaniel
2021-01-20 17:11:26 +08:00
parent b608269474
commit f8cbbdeabd
17 changed files with 984 additions and 1016 deletions

View File

@@ -9,7 +9,7 @@ plugins {
}
group "me.shedaniel"
version = "1.3." + (System.getenv("GITHUB_RUN_NUMBER") == null ? (((short) new Random().nextInt()).abs() + 1000).toString() : System.getenv("GITHUB_RUN_NUMBER"))
version = "2.0." + (System.getenv("GITHUB_RUN_NUMBER") == null ? (((short) new Random().nextInt()).abs() + 1000).toString() : System.getenv("GITHUB_RUN_NUMBER"))
logger.lifecycle(":building architectury plugin v${version}")

View File

@@ -1,5 +1,6 @@
package me.shedaniel.architect.plugin
import me.shedaniel.architect.plugin.transformers.*
import net.fabricmc.loom.util.LoggerFilter
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -17,29 +18,46 @@ class ArchitectPlugin : Plugin<Project> {
)
)
project.extensions.create("architect", ArchitectPluginExtension::class.java, project)
project.extensions.add("architectury", project.extensions.getByName("architect"))
project.extensions.create("architectury", ArchitectPluginExtension::class.java, project)
project.tasks.register("transformForge", RemapMCPTask::class.java) {
it.fakeMod = false
it.remapMcp = false
project.tasks.register("transformProductionFabric", TransformingTask::class.java) {
it.group = "Architectury"
it(RemapMixinVariables)
it(TransformExpectPlatform)
it(TransformInjectables)
it(AddRefmapName)
}
project.tasks.register("transformForgeFakeMod", RemapMCPTask::class.java) {
it.fakeMod = true
it.remapMcp = false
project.tasks.register("transformDevelopmentFabric", TransformingTask::class.java) {
it.group = "Architectury"
it(GenerateFakeFabricModJson)
it(TransformExpectPlatform)
it(TransformInjectables)
}
project.tasks.register("transformArchitectJar", TransformTask::class.java) {
project.tasks.register("transformProductionForge", TransformingTask::class.java) {
it.group = "Architectury"
it.runtime = false
it(RemapMixinVariables)
it(TransformExpectPlatform)
it(TransformInjectables)
it(AddRefmapName)
it(TransformForgeBytecode)
it(RemoveFabricModJson)
it(TransformForgeEnvironment)
it(FixForgeMixin)
}
project.tasks.register("transformArchitectJarRuntime", TransformTask::class.java) {
project.tasks.register("transformDevelopmentForge", TransformingTask::class.java) {
it.group = "Architectury"
it.runtime = true
it(TransformExpectPlatform)
it(TransformInjectables)
it(TransformForgeBytecode)
it(RemoveFabricModJson)
it(TransformForgeEnvironment)
it(GenerateFakeForgeMod)
it(FixForgeMixin)
}
project.repositories.apply {

View File

@@ -30,15 +30,13 @@ open class ArchitectPluginExtension(val project: Project) {
add("compileOnly", "me.shedaniel:architectury-injectables:1.0.4")
}
}
if (forgeEnabled) {
project.configurations.create("mcp")
project.configurations.create("mcpGenerateMod")
project.configurations.create("transformForge")
project.configurations.create("transformForgeFakeMod")
project.configurations.create("transformProductionForge")
project.configurations.create("transformDevelopmentForge")
}
project.configurations.create("transformed")
project.configurations.create("transformedRuntime")
project.configurations.create("transformProductionFabric")
project.configurations.create("transformDevelopmentFabric")
val buildTask = project.tasks.getByName("build")
val jarTask = project.tasks.getByName("jar") {
@@ -46,101 +44,73 @@ open class ArchitectPluginExtension(val project: Project) {
it.archiveClassifier.set("dev")
} as AbstractArchiveTask
val transformArchitectJarTask = project.tasks.getByName("transformArchitectJar") {
it as TransformTask
val transformProductionFabricTask = project.tasks.getByName("transformProductionFabric") {
it as TransformingTask
it.archiveClassifier.set("transformed")
it.archiveClassifier.set("transformProductionFabric")
it.input.set(jarTask.archiveFile.get())
project.artifacts.add("archives", it)
project.artifacts.add("transformProductionFabric", it)
it.dependsOn(jarTask)
buildTask.dependsOn(it)
it.outputs.upToDateWhen { false }
} as TransformTask
val transformArchitectRuntimeJarTask = project.tasks.getByName("transformArchitectJarRuntime") {
it as TransformTask
} as TransformingTask
val transformDevelopmentFabricTask = project.tasks.getByName("transformDevelopmentFabric") {
it as TransformingTask
it.archiveClassifier.set("transformedRuntime")
it.archiveClassifier.set("transformDevelopmentFabric")
it.input.set(jarTask.archiveFile.get())
project.artifacts.add("archives", it)
project.artifacts.add("transformDevelopmentFabric", it)
it.dependsOn(jarTask)
buildTask.dependsOn(it)
it.outputs.upToDateWhen { false }
} as TransformTask
} as TransformingTask
val remapJarTask = project.tasks.getByName("remapJar") {
it as RemapJarTask
it.archiveClassifier.set("")
it.input.set(transformArchitectJarTask.archiveFile.get())
it.dependsOn(transformArchitectJarTask)
it.mustRunAfter(transformArchitectJarTask)
it.input.set(transformProductionFabricTask.archiveFile.get())
it.dependsOn(transformProductionFabricTask)
it.mustRunAfter(transformProductionFabricTask)
} as RemapJarTask
if (forgeEnabled) {
val transformForgeTask = project.tasks.getByName("transformForge") {
it as RemapMCPTask
val transformProductionForgeTask = project.tasks.getByName("transformProductionForge") {
it as TransformingTask
it.input.set(transformArchitectJarTask.archiveFile.get())
it.archiveClassifier.set("transformedForge")
it.dependsOn(transformArchitectJarTask)
it.input.set(jarTask.archiveFile.get())
it.archiveClassifier.set("transformProductionForge")
project.artifacts.add("transformProductionForge", it)
it.dependsOn(jarTask)
buildTask.dependsOn(it)
it.outputs.upToDateWhen { false }
} as RemapMCPTask
} as TransformingTask
val transformForgeFakeModTask = project.tasks.getByName("transformForgeFakeMod") {
it as RemapMCPTask
val transformDevelopmentForgeTask = project.tasks.getByName("transformDevelopmentForge") {
it as TransformingTask
it.input.set(transformArchitectRuntimeJarTask.archiveFile.get())
it.archiveClassifier.set("transformedForgeFakeMod")
it.dependsOn(transformArchitectRuntimeJarTask)
it.input.set(jarTask.archiveFile.get())
it.archiveClassifier.set("transformDevelopmentForge")
project.artifacts.add("transformDevelopmentForge", it) { artifact ->
artifact.builtBy(it)
}
it.dependsOn(jarTask)
buildTask.dependsOn(it)
it.outputs.upToDateWhen { false }
} as RemapMCPTask
} as TransformingTask
transformForgeTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
transformForgeFakeModTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
project.artifacts {
it.add(
"transformForge", mapOf(
"file" to transformForgeTask.archiveFile.get().asFile,
"type" to "jar",
"builtBy" to transformForgeTask
)
)
it.add(
"transformForgeFakeMod", mapOf(
"file" to transformForgeFakeModTask.archiveFile.get().asFile,
"type" to "jar",
"builtBy" to transformForgeFakeModTask
)
)
}
transformProductionForgeTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
transformDevelopmentForgeTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
project.extensions.getByType(LoomGradleExtension::class.java).generateSrgTiny = true
}
transformArchitectJarTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
transformArchitectRuntimeJarTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
project.artifacts {
it.add(
"transformed", mapOf(
"file" to transformArchitectJarTask.archiveFile.get().asFile,
"type" to "jar",
"builtBy" to transformArchitectJarTask
)
)
it.add(
"transformedRuntime", mapOf(
"file" to transformArchitectRuntimeJarTask.archiveFile.get().asFile,
"type" to "jar",
"builtBy" to transformArchitectRuntimeJarTask
)
)
}
transformProductionFabricTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
transformDevelopmentFabricTask.archiveFile.get().asFile.takeUnless { it.exists() }?.createEmptyJar()
}
}

View File

@@ -1,540 +0,0 @@
@file:Suppress("UnstableApiUsage")
package me.shedaniel.architect.plugin
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.shedaniel.architect.plugin.utils.GradleSupport
import me.shedaniel.architect.plugin.utils.validateJarFs
import net.fabricmc.loom.LoomGradleExtension
import net.fabricmc.loom.util.LoggerFilter
import net.fabricmc.loom.util.TinyRemapperMappingsHelper
import net.fabricmc.mapping.tree.TinyTree
import net.fabricmc.tinyremapper.*
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.tasks.Jar
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
import org.zeroturnaround.zip.ZipUtil
import java.io.*
import java.net.URL
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import java.util.jar.Manifest
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
import kotlin.collections.LinkedHashSet
open class RemapMCPTask : Jar() {
private val fromM: String = "named"
var remapMcp = true
var fakeMod = false
val input: RegularFileProperty = GradleSupport.getFileProperty(project)
private val environmentClass = "net/fabricmc/api/Environment"
@TaskAction
fun doTask() {
LoggerFilter.replaceSystemOut()
val input: Path = this.input.asFile.get().toPath()
val intermediate: Path = input.parent.resolve(input.toFile().nameWithoutExtension + "-intermediate.jar")
val output: Path = this.archiveFile.get().asFile.toPath()
intermediate.toFile().delete()
output.toFile().delete()
if (!Files.exists(input)) {
throw FileNotFoundException(input.toString())
}
run {
val zipOutputStream = ZipOutputStream(intermediate.toFile().outputStream())
zipOutputStream.use {
ZipInputStream(Files.newInputStream(input)).use {
while (true) {
val entry = it.nextEntry ?: break
zipOutputStream.putNextEntry(ZipEntry(entry.name))
var allBytes = it.readBytes()
if (entry.name.toString().endsWith(".class")) {
val reader = ClassReader(allBytes)
if ((reader.access and Opcodes.ACC_MODULE) == 0) {
val node = ClassNode(Opcodes.ASM8)
reader.accept(node, ClassReader.EXPAND_FRAMES)
val writer = ClassWriter(0)
transform(node).accept(writer)
allBytes = writer.toByteArray()
}
}
zipOutputStream.write(allBytes)
zipOutputStream.closeEntry()
}
}
}
if (ZipUtil.containsEntry(intermediate.toFile(), "fabric.mod.json")) {
ZipUtil.removeEntry(intermediate.toFile(), "fabric.mod.json")
}
}
val classpathFiles: Set<File> = LinkedHashSet(
project.configurations.getByName("compileClasspath").files
)
val classpath = classpathFiles.asSequence().map { obj: File -> obj.toPath() }
.filter { p: Path -> input != p && Files.exists(p) }.toList().toTypedArray()
val remapperBuilder: TinyRemapper.Builder = TinyRemapper.newRemapper()
if (remapMcp) {
val mappings = getMappings()
val mojmapToMcpClass: Map<String, String> = createMojmapToMcpClass(mappings)
remapperBuilder.withMappings(
remapToMcp(
TinyRemapperMappingsHelper.create(mappings, fromM, fromM, false),
mojmapToMcpClass
)
)
remapperBuilder.skipLocalVariableMapping(true)
} else {
remapperBuilder.withMappings(remapToMcp(null, null))
remapperBuilder.skipLocalVariableMapping(true)
}
mapMixin(remapperBuilder)
project.logger.lifecycle(
":${
listOfNotNull(
"remapping".takeIf { remapMcp },
"transforming"
).joinToString(" and ")
} " + input.fileName + " => " + output.fileName + if (fakeMod) " (with fake mod)" else ""
)
val architectFolder = project.rootProject.buildDir.resolve("tmp/architect")
architectFolder.deleteRecursively()
architectFolder.mkdirs()
val fakeModId = "generated_" + UUID.randomUUID().toString().filterNot { it == '-' }.take(7)
if (fakeMod) {
val modsToml = architectFolder.resolve("META-INF/mods.toml")
modsToml.parentFile.mkdirs()
modsToml.writeText(
"""
modLoader = "javafml"
loaderVersion = "[33,)"
license = "Generated"
[[mods]]
modId = "$fakeModId"
""".trimIndent()
)
val mcmeta = architectFolder.resolve("pack.mcmeta")
mcmeta.parentFile.mkdirs()
mcmeta.writeText(
"""
{"pack":{"description":"Generated","pack_format":4}}
""".trimIndent()
)
}
val remapper = remapperBuilder.build()
try {
project.validateJarFs(output)
OutputConsumerPath.Builder(output).build().use { outputConsumer ->
outputConsumer.addNonClassFiles(input, NonClassCopyMode.FIX_META_INF, null)
outputConsumer.addNonClassFiles(architectFolder.toPath(), NonClassCopyMode.UNCHANGED, null)
remapper.readClassPath(*classpath)
remapper.readInputs(intermediate)
remapper.apply(outputConsumer)
if (fakeMod) {
val className = "generated/$fakeModId"
val classWriter = ClassWriter(0)
classWriter.visit(52, Opcodes.ACC_PUBLIC, className, null, "java/lang/Object", null)
val modAnnotation = classWriter.visitAnnotation("Lnet/minecraftforge/fml/common/Mod;", false)
modAnnotation.visit("value", fakeModId)
modAnnotation.visitEnd()
classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, arrayOf()).also {
it.visitVarInsn(Opcodes.ALOAD, 0)
it.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
it.visitInsn(Opcodes.RETURN)
it.visitMaxs(1, 1)
it.visitEnd()
}
classWriter.visitEnd()
outputConsumer.accept(className, classWriter.toByteArray())
}
}
} catch (e: Exception) {
remapper.finish()
throw RuntimeException("Failed to remap $input to $output", e)
}
architectFolder.deleteRecursively()
remapper.finish()
intermediate.toFile().delete()
fixMixins(output.toFile())
if (!Files.exists(output)) {
throw RuntimeException("Failed to remap $input to $output - file missing!")
}
}
private fun mapMixin(remapperBuilder: TinyRemapper.Builder) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
val srg = project.extensions.getByType(LoomGradleExtension::class.java).mappingsProvider.mappingsWithSrg
for (mixinMapFile in loomExtension.allMixinMappings) {
if (mixinMapFile.exists()) {
remapperBuilder.withMappings { sink ->
TinyUtils.createTinyMappingProvider(mixinMapFile.toPath(), "named", "intermediary").load(object :
IMappingProvider.MappingAcceptor {
override fun acceptClass(srcName: String, dstName: String) {
sink.acceptClass(dstName, srg.classes
.firstOrNull { it.getName("intermediary") == dstName }
?.getName("srg") ?: dstName
)
}
override fun acceptMethod(method: IMappingProvider.Member, dstName: String) {
sink.acceptMethod(
IMappingProvider.Member(method.owner, dstName, method.desc),
srg.classes
.flatMap { it.methods }
.firstOrNull { it.getName("intermediary") == dstName }
?.getName("srg") ?: dstName)
}
override fun acceptField(field: IMappingProvider.Member, dstName: String) {
sink.acceptField(
IMappingProvider.Member(field.owner, dstName, field.desc),
srg.classes
.flatMap { it.fields }
.firstOrNull { it.getName("intermediary") == dstName }
?.getName("srg") ?: dstName)
}
override fun acceptMethodArg(method: IMappingProvider.Member, lvIndex: Int, dstName: String) {}
override fun acceptMethodVar(
method: IMappingProvider.Member,
lvIndex: Int,
startOpIdx: Int,
asmIndex: Int,
dstName: String
) {
}
})
}
}
}
}
private fun fixMixins(output: File) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
val gson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()
val mixinConfigs = mutableListOf<String>()
val refmap = loomExtension.getRefmapName()
ZipUtil.iterate(output) { stream, entry ->
if (!entry.isDirectory && entry.name.endsWith(".json") &&
!entry.name.contains("/") && !entry.name.contains("\\")
) {
try {
InputStreamReader(stream).use { reader ->
val json: JsonObject? = gson.fromJson<JsonObject>(reader, JsonObject::class.java)
if (json != null) {
val hasMixins = json.has("mixins") && json["mixins"].isJsonArray
val hasClient = json.has("client") && json["client"].isJsonArray
val hasServer = json.has("server") && json["server"].isJsonArray
if (json.has("package") && (hasMixins || hasClient || hasServer)) {
mixinConfigs.add(entry.name)
}
}
}
} catch (ignored: Exception) {
}
}
}
if (mixinConfigs.isNotEmpty()) {
if (ZipUtil.containsEntry(output, "META-INF/MANIFEST.MF")) {
ZipUtil.transformEntry(output, "META-INF/MANIFEST.MF") { input, zipEntry, out ->
val manifest = Manifest(input)
manifest.mainAttributes.putValue("MixinConfigs", mixinConfigs.joinToString(","))
out.putNextEntry(ZipEntry(zipEntry.name))
manifest.write(out)
out.closeEntry()
}
}
}
if (ZipUtil.containsEntry(output, refmap)) {
ZipUtil.transformEntry(output, refmap) { input, zipEntry, out ->
val refmapElement: JsonObject = JsonParser().parse(InputStreamReader(input)).asJsonObject.deepCopy()
if (refmapElement.has("mappings")) {
refmapElement["mappings"].asJsonObject.entrySet().forEach { (_, value) ->
remapRefmap(value.asJsonObject)
}
}
if (refmapElement.has("data")) {
val data = refmapElement["data"].asJsonObject
if (data.has("named:intermediary")) {
data.add("searge", data["named:intermediary"].deepCopy().also {
it.asJsonObject.entrySet().forEach { (_, value) ->
remapRefmap(value.asJsonObject)
}
})
data.remove("named:intermediary")
}
}
out.putNextEntry(ZipEntry(zipEntry.name))
out.write(gson.toJson(refmapElement).toByteArray())
out.closeEntry()
}
} else {
project.logger.warn("Failed to locate refmap: $refmap")
}
}
private fun remapRefmap(obj: JsonObject) {
val srg = project.extensions.getByType(LoomGradleExtension::class.java).mappingsProvider.mappingsWithSrg
val methodPattern = "L(.*);(.*)(\\(.*)".toRegex()
val methodPatternWithoutClass = "(.*)(\\(.*)".toRegex()
val fieldPattern = "(.*):(.*)".toRegex()
obj.keySet().forEach { key ->
val originalRef = obj[key].asString
val methodMatch = methodPattern.matchEntire(originalRef)
val fieldMatch = fieldPattern.matchEntire(originalRef)
val methodMatchWithoutClass = methodPatternWithoutClass.matchEntire(originalRef)
when {
methodMatch != null -> {
val matchedClass =
srg.classes.firstOrNull { it.getName("intermediary") == methodMatch.groups[1]!!.value }
val replacementName: String = srg.classes.asSequence()
.flatMap { it.methods.asSequence() }
.filter { it.getName("intermediary") == methodMatch.groups[2]!!.value }
.firstOrNull { it.getDescriptor("intermediary") == methodMatch.groups[3]!!.value }
?.getName("srg") ?: methodMatch.groups[2]!!.value
obj.addProperty(
key, originalRef
.replaceFirst(
methodMatch.groups[1]!!.value,
matchedClass?.getName("srg") ?: methodMatch.groups[1]!!.value
)
.replaceFirst(methodMatch.groups[2]!!.value, replacementName)
.replaceFirst(methodMatch.groups[3]!!.value, methodMatch.groups[3]!!.value.remapDescriptor {
srg.classes.firstOrNull { def -> def.getName("intermediary") == it }?.getName("srg")
?: it
})
)
}
fieldMatch != null -> {
val replacementName: String = srg.classes.asSequence()
.flatMap { it.fields.asSequence() }
.filter { it.getName("intermediary") == fieldMatch.groups[1]!!.value }
.firstOrNull { it.getDescriptor("intermediary") == fieldMatch.groups[2]!!.value }
?.getName("srg") ?: fieldMatch.groups[1]!!.value
obj.addProperty(
key, originalRef
.replaceFirst(fieldMatch.groups[1]!!.value, replacementName)
.replaceFirst(fieldMatch.groups[2]!!.value, fieldMatch.groups[2]!!.value.remapDescriptor {
srg.classes.firstOrNull { def -> def.getName("intermediary") == it }?.getName("srg")
?: it
})
)
}
methodMatchWithoutClass != null -> {
val replacementName: String = srg.classes.asSequence()
.flatMap { it.methods.asSequence() }
.filter { it.getName("intermediary") == methodMatchWithoutClass.groups[1]!!.value }
.firstOrNull { it.getDescriptor("intermediary") == methodMatchWithoutClass.groups[2]!!.value }
?.getName("srg") ?: methodMatchWithoutClass.groups[1]!!.value
obj.addProperty(
key, originalRef
.replaceFirst(methodMatchWithoutClass.groups[1]!!.value, replacementName)
.replaceFirst(
methodMatchWithoutClass.groups[2]!!.value,
methodMatchWithoutClass.groups[2]!!.value.remapDescriptor {
srg.classes.firstOrNull { def -> def.getName("intermediary") == it }?.getName("srg")
?: it
})
)
}
else -> logger.warn("Failed to remap refmap value: $originalRef")
}
}
}
private fun String.remapDescriptor(classMappings: (String) -> String): String {
return try {
val reader = StringReader(this)
val result = StringBuilder()
var insideClassName = false
val className = StringBuilder()
while (true) {
val c: Int = reader.read()
if (c == -1) {
break
}
if (c == ';'.toInt()) {
insideClassName = false
result.append(classMappings(className.toString()))
}
if (insideClassName) {
className.append(c.toChar())
} else {
result.append(c.toChar())
}
if (!insideClassName && c == 'L'.toInt()) {
insideClassName = true
className.setLength(0)
}
}
result.toString()
} catch (e: IOException) {
throw AssertionError(e)
}
}
private fun remapToMcp(parent: IMappingProvider?, mojmapToMcpClass: Map<String, String>?): IMappingProvider =
IMappingProvider { out ->
out.acceptClass("net/fabricmc/api/Environment", "net/minecraftforge/api/distmarker/OnlyIn")
out.acceptClass("net/fabricmc/api/EnvType", "net/minecraftforge/api/distmarker/Dist")
out.acceptField(
IMappingProvider.Member("net/fabricmc/api/EnvType", "SERVER", "Lnet/fabricmc/api/EnvType;"),
"DEDICATED_SERVER"
)
parent?.load(object : IMappingProvider.MappingAcceptor {
override fun acceptClass(srcName: String?, dstName: String?) {
out.acceptClass(srcName, mojmapToMcpClass!![srcName] ?: srcName)
}
override fun acceptMethod(method: IMappingProvider.Member?, dstName: String?) {
}
override fun acceptMethodArg(method: IMappingProvider.Member?, lvIndex: Int, dstName: String?) {
}
override fun acceptMethodVar(
method: IMappingProvider.Member?,
lvIndex: Int,
startOpIdx: Int,
asmIndex: Int,
dstName: String?
) {
}
override fun acceptField(field: IMappingProvider.Member?, dstName: String?) {
}
})
}
private fun getMappings(): TinyTree {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
return loomExtension.mappingsProvider.mappings
}
private fun getRootExtension(): ArchitectPluginExtension =
project.rootProject.extensions.getByType(ArchitectPluginExtension::class.java)
private fun createMojmapToMcpClass(mappings: TinyTree): Map<String, String> {
val mcpMappings = readMCPMappings(getRootExtension().minecraft)
val mutableMap = mutableMapOf<String, String>()
mappings.classes.forEach { clazz ->
val official = clazz.getName("official")
val named = clazz.getName("named")
val mcp = mcpMappings[official]
if (mcp != null) {
mutableMap[named] = mcp
}
}
return mutableMap
}
private fun readMCPMappings(version: String): Map<String, String> {
val file = project.rootProject.file(".gradle/mappings/mcp-$version.tsrg")
if (file.exists().not()) {
file.parentFile.mkdirs()
file.writeText(URL("https://raw.githubusercontent.com/MinecraftForge/MCPConfig/master/versions/release/$version/joined.tsrg").readText())
}
return mutableMapOf<String, String>().also { readMappings(it, file.inputStream()) }
}
private fun readMappings(mutableMap: MutableMap<String, String>, inputStream: InputStream) {
inputStream.bufferedReader().forEachLine {
if (!it.startsWith("\t")) {
val split = it.split(" ")
val obf = split[0]
val className = split[1]
mutableMap[obf] = className
}
}
}
val forgeEvent = "Lme/shedaniel/architectury/ForgeEvent;"
val forgeEventCancellable = "Lme/shedaniel/architectury/ForgeEventCancellable;"
val cancellable = "Lnet/minecraftforge/eventbus/api/Cancelable;"
private fun transform(node: ClassNode): ClassNode {
if (node.access and Opcodes.ACC_INTERFACE == 0) {
if (node.visibleAnnotations?.any { it.desc == forgeEvent || it.desc == forgeEventCancellable } == true) {
node.superName = "net/minecraftforge/eventbus/api/Event"
node.methods.forEach {
if (it.name == "<init>") {
for (insnNode in it.instructions) {
if (insnNode.opcode == Opcodes.INVOKESPECIAL) {
insnNode as MethodInsnNode
if (insnNode.name == "<init>" && insnNode.owner == "java/lang/Object") {
insnNode.owner = "net/minecraftforge/eventbus/api/Event"
break
}
}
}
}
}
node.signature?.let {
node.signature = it.substringBeforeLast('L') + "Lnet/minecraftforge/eventbus/api/Event;"
}
// if @ForgeEventCancellable, add the cancellable annotation from forge
node.visibleAnnotations.apply {
if (any { it.desc == forgeEventCancellable }) {
add(AnnotationNode(cancellable))
}
}
}
}
node.visibleAnnotations = (node.visibleAnnotations ?: mutableListOf()).apply {
val invisibleEnvironments =
node.invisibleAnnotations?.filter { it.desc == "L${environmentClass};" } ?: emptyList()
node.invisibleAnnotations?.removeAll(invisibleEnvironments)
addAll(invisibleEnvironments)
}
node.fields.forEach { field ->
field.visibleAnnotations = (field.visibleAnnotations ?: mutableListOf()).apply {
val invisibleEnvironments =
field.invisibleAnnotations?.filter { it.desc == "L${environmentClass};" } ?: emptyList()
field.invisibleAnnotations?.removeAll(invisibleEnvironments)
addAll(invisibleEnvironments)
}
}
node.methods.forEach { method ->
method.visibleAnnotations = (method.visibleAnnotations ?: mutableListOf()).apply {
val invisibleEnvironments =
method.invisibleAnnotations?.filter { it.desc == "L${environmentClass};" } ?: emptyList()
method.invisibleAnnotations?.removeAll(invisibleEnvironments)
addAll(invisibleEnvironments)
}
}
return node
}
}

View File

@@ -1,218 +0,0 @@
package me.shedaniel.architect.plugin
import me.shedaniel.architect.plugin.utils.ClassTransformer
import me.shedaniel.architect.plugin.utils.Transform
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Handle
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.commons.Remapper
import org.objectweb.asm.tree.*
import org.zeroturnaround.zip.ZipUtil
import java.io.File
import java.io.InputStream
import java.lang.invoke.CallSite
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.util.*
import java.util.zip.ZipEntry
import kotlin.properties.Delegates
const val expectPlatform = "Lme/shedaniel/architectury/ExpectPlatform;"
const val expectPlatformNew = "Lme/shedaniel/architectury/annotations/ExpectPlatform;"
fun Project.projectUniqueIdentifier(): String {
val cache = File(project.file(".gradle"), "architectury-cache")
cache.mkdirs()
val uniqueIdFile = File(cache, "projectID")
var id by Delegates.notNull<String>()
if (uniqueIdFile.exists()) {
id = uniqueIdFile.readText()
} else {
id = UUID.randomUUID().toString().filterNot { it == '-' }
uniqueIdFile.writeText(id)
}
var name = project.name
if (project.rootProject != project) name = project.rootProject.name + "_" + name
return "architectury_inject_${name}_$id".filter { Character.isJavaIdentifierPart(it) }
}
fun transformExpectPlatform(project: Project): ClassTransformer {
val projectUniqueIdentifier by lazy { project.projectUniqueIdentifier() }
var injectedClass = !project.extensions.getByType(ArchitectPluginExtension::class.java).injectInjectables
return { clazz, classAdder ->
if (!injectedClass) {
injectedClass = true
Transform::class.java.getResourceAsStream("/annotations-inject/injection.jar").use { stream ->
ZipUtil.iterate(stream) { input: InputStream, entry: ZipEntry ->
if (entry.name.endsWith(".class")) {
val newName = "$projectUniqueIdentifier/${
entry.name.substringBeforeLast(".class").substringAfterLast('/')
}"
classAdder(newName, input.readBytes().let {
val node = ClassNode(Opcodes.ASM8)
ClassReader(it).accept(node, ClassReader.EXPAND_FRAMES)
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
val remapper = ClassRemapper(writer, object : Remapper() {
override fun map(internalName: String?): String {
if (internalName?.startsWith("me/shedaniel/architect/plugin/callsite") == true) {
return internalName.replace(
"me/shedaniel/architect/plugin/callsite",
projectUniqueIdentifier
)
}
return super.map(internalName)
}
})
node.apply {
name = newName
}.accept(remapper)
writer.toByteArray()
})
}
}
}
}
clazz.methods.mapNotNull { method ->
when {
method?.visibleAnnotations?.any { it.desc == expectPlatform } == true -> method to "me/shedaniel/architectury/PlatformMethods"
method?.invisibleAnnotations?.any { it.desc == expectPlatformNew } == true -> {
method to "$projectUniqueIdentifier/PlatformMethods"
}
else -> null
}
}.forEach { (method, platformMethodsClass) ->
if (method.access and Opcodes.ACC_STATIC == 0) {
System.err.println("@ExpectPlatform can only apply to static methods!")
} else {
method.instructions.clear()
val endOfDesc = method.desc.lastIndexOf(')')
val returnValue = method.desc.substring(endOfDesc + 1)
val args = method.desc.substring(1, endOfDesc)
var cursor = 0
var inClass = false
var index = 0
while (cursor < args.length) {
val char = args[cursor]
if (inClass) {
if (char == ';') {
method.instructions.addLoad(char, index++)
inClass = false
}
} else when (char) {
'[' -> Unit
'L' -> inClass = true
else -> method.instructions.addLoad(char, when (char) {
'J', 'D' -> index.also { index += 2 }
else -> index++
})
}
cursor++
}
val methodType = MethodType.methodType(
CallSite::class.java,
MethodHandles.Lookup::class.java,
String::class.java,
MethodType::class.java
)
val handle = Handle(
Opcodes.H_INVOKESTATIC,
platformMethodsClass,
"platform",
methodType.toMethodDescriptorString(),
false
)
method.instructions.add(
InvokeDynamicInsnNode(
method.name,
method.desc,
handle
)
)
method.instructions.addReturn(returnValue.first { it != '[' })
method.maxStack = -1
}
}
clazz
}
}
private fun InsnList.addLoad(type: Char, index: Int) {
when (type) {
';' -> add(
VarInsnNode(
Opcodes.ALOAD,
index
)
)
'I', 'S', 'B', 'C', 'Z' -> add(
VarInsnNode(
Opcodes.ILOAD,
index
)
)
'F' -> add(
VarInsnNode(
Opcodes.FLOAD,
index
)
)
'J' -> add(
VarInsnNode(
Opcodes.LLOAD,
index
)
)
'D' -> add(
VarInsnNode(
Opcodes.DLOAD,
index
)
)
else -> throw IllegalStateException("Invalid Type: $type")
}
}
private fun InsnList.addReturn(type: Char) {
when (type) {
'L' -> add(
InsnNode(
Opcodes.ARETURN
)
)
'I', 'S', 'B', 'C', 'Z' -> add(
InsnNode(
Opcodes.IRETURN
)
)
'F' -> add(
InsnNode(
Opcodes.FRETURN
)
)
'J' -> add(
InsnNode(
Opcodes.LRETURN
)
)
'D' -> add(
InsnNode(
Opcodes.DRETURN
)
)
'V' -> add(
InsnNode(
Opcodes.RETURN
)
)
}
}

View File

@@ -1,175 +0,0 @@
@file:Suppress("UnstableApiUsage")
package me.shedaniel.architect.plugin
import me.shedaniel.architect.plugin.utils.GradleSupport
import me.shedaniel.architect.plugin.utils.Transform
import me.shedaniel.architect.plugin.utils.validateJarFs
import net.fabricmc.loom.LoomGradleExtension
import net.fabricmc.loom.util.LoggerFilter
import net.fabricmc.tinyremapper.*
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.tasks.Jar
import org.zeroturnaround.zip.ByteSource
import org.zeroturnaround.zip.ZipUtil
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import kotlin.collections.LinkedHashSet
open class TransformTask : Jar() {
val input: RegularFileProperty = GradleSupport.getFileProperty(project)
var runtime = false
@TaskAction
fun doTask() {
val input: Path = this.input.asFile.get().toPath()
val intermediate: Path = input.parent.resolve(input.toFile().nameWithoutExtension + "-intermediate.jar")
val intermediate2: Path = input.parent.resolve(input.toFile().nameWithoutExtension + "-intermediate2.jar")
val output: Path = this.archiveFile.get().asFile.toPath()
if (!runtime) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
var remapperBuilder = TinyRemapper.newRemapper()
for (mixinMapFile in loomExtension.allMixinMappings) {
if (mixinMapFile.exists()) {
remapperBuilder = remapperBuilder.withMappings(
TinyUtils.createTinyMappingProvider(
mixinMapFile.toPath(),
"named",
"intermediary"
)
)
}
}
val remapper = remapperBuilder.build()
val classpathFiles: Set<File> = LinkedHashSet(
project.configurations.getByName("compileClasspath").files
)
val classpath = classpathFiles.asSequence().map { obj: File -> obj.toPath() }.filter { p: Path ->
input != p && Files.exists(p)
}.toList().toTypedArray()
LoggerFilter.replaceSystemOut()
try {
project.validateJarFs(intermediate)
OutputConsumerPath.Builder(intermediate).build().use { outputConsumer ->
outputConsumer.addNonClassFiles(input)
remapper.readClassPath(*classpath)
remapper.readInputs(input)
remapper.apply(outputConsumer)
}
} catch (e: Exception) {
remapper.finish()
throw RuntimeException("Failed to remap $input to $intermediate", e)
}
remapper.finish()
} else {
Files.copy(input, intermediate)
val fakeModId = "generated_" + UUID.randomUUID().toString().filterNot { it == '-' }.take(7)
ZipUtil.addOrReplaceEntries(
intermediate.toFile(), arrayOf(
ByteSource(
"fabric.mod.json", """{
"schemaVersion": 1,
"id": "$fakeModId",
"name": "Generated Mod (Please Ignore)",
"version": "1.0.0",
"custom": {
"fabric-loom:generated": true
}
}""".toByteArray()
)
)
)
}
Files.deleteIfExists(intermediate2)
project.logger.lifecycle(":transforming " + input.fileName + " => " + intermediate.fileName)
Transform.transform(intermediate, intermediate2, transformExpectPlatform(project))
Files.deleteIfExists(intermediate)
Files.deleteIfExists(output)
if (project.extensions.getByType(ArchitectPluginExtension::class.java).injectInjectables) {
transformArchitecturyInjectables(intermediate2, output)
} else {
Files.copy(intermediate2, output)
}
Files.deleteIfExists(intermediate2)
if (!runtime) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
var refmapHelperClass: Class<*>? = null
runCatching {
refmapHelperClass = Class.forName("net.fabricmc.loom.util.MixinRefmapHelper")
}.onFailure {
runCatching {
refmapHelperClass = Class.forName("net.fabricmc.loom.build.MixinRefmapHelper")
}.onFailure {
throw ClassNotFoundException("Failed to find MixinRefmapHelper!")
}
}
val method = refmapHelperClass!!.getDeclaredMethod(
"addRefmapName",
String::class.java,
String::class.java,
Path::class.java
)
if (
method.invoke(
null,
loomExtension.getRefmapName(),
loomExtension.mixinJsonVersion,
output
) as Boolean
) {
project.logger.debug("Transformed mixin reference maps in output JAR!")
}
}
}
private fun transformArchitecturyInjectables(intermediate2: Path, output: Path) {
val remapper = TinyRemapper.newRemapper()
.withMappings { sink ->
sink.acceptClass(
"me/shedaniel/architectury/targets/ArchitecturyTarget",
project.projectUniqueIdentifier() + "/PlatformMethods"
)
sink.acceptMethod(
IMappingProvider.Member(
"me/shedaniel/architectury/targets/ArchitecturyTarget",
"getCurrentTarget",
"()Ljava/lang/String;"
), "getModLoader"
)
}
.build()
val classpathFiles: Set<File> = LinkedHashSet(
project.configurations.getByName("compileClasspath").files
)
val classpath = classpathFiles.asSequence().map { obj: File -> obj.toPath() }
.filter { p: Path -> this.input.asFile.get().toPath() != p && Files.exists(p) }.toList().toTypedArray()
try {
project.validateJarFs(output)
OutputConsumerPath.Builder(output).build().use { outputConsumer ->
outputConsumer.addNonClassFiles(intermediate2, NonClassCopyMode.UNCHANGED, null)
remapper.readClassPath(*classpath)
remapper.readInputs(intermediate2)
remapper.apply(outputConsumer)
}
} catch (e: Exception) {
remapper.finish()
throw RuntimeException("Failed to remap $intermediate2 to $output", e)
}
}
}

View File

@@ -0,0 +1,84 @@
package me.shedaniel.architect.plugin
import me.shedaniel.architect.plugin.utils.GradleSupport
import org.gradle.api.Project
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.TaskAction
import org.gradle.jvm.tasks.Jar
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
import kotlin.properties.Delegates
import kotlin.time.Duration
import kotlin.time.ExperimentalTime
import kotlin.time.nanoseconds
open class TransformingTask : Jar() {
val input: RegularFileProperty = GradleSupport.getFileProperty(project)
val transformers = mutableListOf<Transformer>()
@ExperimentalTime
@TaskAction
fun doTask() {
val input: Path = this.input.asFile.get().toPath()
val taskOutputs = transformers.mapIndexed { index, _ ->
project.file("build")
.resolve("architectury-plugin/" + input.toFile().nameWithoutExtension + "-intermediate-${index}.jar")
.toPath()
}
val output: Path = this.archiveFile.get().asFile.toPath()
Files.deleteIfExists(output)
transformers.forEachIndexed { index, transformer ->
val i = if (index == 0) input else taskOutputs[index - 1]
val o = if (index == taskOutputs.lastIndex) output else taskOutputs[index]
Files.deleteIfExists(o)
Files.createDirectories(o.parent)
runCatching {
measureTime {
transformer(project, i, o)
if (index != 0) {
Files.deleteIfExists(i)
}
}.let { duration ->
project.logger.lifecycle(":finished transforming step ${index + 1}/${transformers.size} [${transformer::class.simpleName}] in $duration")
}
}.onFailure {
throw RuntimeException(
"Failed transformer step ${index + 1}/${transformers.size} [${transformer::class.simpleName}]",
it
)
}
}
}
operator fun invoke(transformer: Transformer) {
transformers.add(transformer)
}
}
@ExperimentalTime
private inline fun measureTime(block: () -> Unit): Duration {
val current = System.nanoTime()
block()
val finished = System.nanoTime()
return (finished - current).nanoseconds
}
fun Project.projectUniqueIdentifier(): String {
val cache = File(project.file(".gradle"), "architectury-cache")
cache.mkdirs()
val uniqueIdFile = File(cache, "projectID")
var id by Delegates.notNull<String>()
if (uniqueIdFile.exists()) {
id = uniqueIdFile.readText()
} else {
id = UUID.randomUUID().toString().filterNot { it == '-' }
uniqueIdFile.writeText(id)
}
var name = project.name
if (project.rootProject != project) name = project.rootProject.name + "_" + name
return "architectury_inject_${name}_$id".filter { Character.isJavaIdentifierPart(it) }
}
typealias Transformer = (project: Project, input: Path, output: Path) -> Unit

View File

@@ -0,0 +1,41 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import net.fabricmc.loom.LoomGradleExtension
import org.gradle.api.Project
import java.nio.file.Files
import java.nio.file.Path
object AddRefmapName : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
Files.copy(input, output)
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
var refmapHelperClass: Class<*>? = null
runCatching {
refmapHelperClass = Class.forName("net.fabricmc.loom.util.MixinRefmapHelper")
}.onFailure {
runCatching {
refmapHelperClass = Class.forName("net.fabricmc.loom.build.MixinRefmapHelper")
}.onFailure {
throw ClassNotFoundException("Failed to find MixinRefmapHelper!")
}
}
val method = refmapHelperClass!!.getDeclaredMethod(
"addRefmapName",
String::class.java,
String::class.java,
Path::class.java
)
if (
method.invoke(
null,
loomExtension.getRefmapName(),
loomExtension.mixinJsonVersion,
output
) as Boolean
) {
project.logger.debug("Transformed mixin reference maps in output JAR!")
}
}
}

View File

@@ -0,0 +1,191 @@
package me.shedaniel.architect.plugin.transformers
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import me.shedaniel.architect.plugin.Transformer
import net.fabricmc.loom.LoomGradleExtension
import org.gradle.api.Project
import org.zeroturnaround.zip.ZipUtil
import java.io.File
import java.io.IOException
import java.io.InputStreamReader
import java.io.StringReader
import java.nio.file.Files
import java.nio.file.Path
import java.util.jar.Manifest
import java.util.zip.ZipEntry
object FixForgeMixin : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
Files.copy(input, output)
fixMixins(project, output.toFile())
}
private fun fixMixins(project: Project, output: File) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
val gson = GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create()
val mixinConfigs = mutableListOf<String>()
val refmap = loomExtension.getRefmapName()
ZipUtil.iterate(output) { stream, entry ->
if (!entry.isDirectory && entry.name.endsWith(".json") &&
!entry.name.contains("/") && !entry.name.contains("\\")
) {
try {
InputStreamReader(stream).use { reader ->
val json: JsonObject? = gson.fromJson<JsonObject>(reader, JsonObject::class.java)
if (json != null) {
val hasMixins = json.has("mixins") && json["mixins"].isJsonArray
val hasClient = json.has("client") && json["client"].isJsonArray
val hasServer = json.has("server") && json["server"].isJsonArray
if (json.has("package") && (hasMixins || hasClient || hasServer)) {
mixinConfigs.add(entry.name)
}
}
}
} catch (ignored: Exception) {
}
}
}
if (mixinConfigs.isNotEmpty()) {
if (ZipUtil.containsEntry(output, "META-INF/MANIFEST.MF")) {
ZipUtil.transformEntry(output, "META-INF/MANIFEST.MF") { input, zipEntry, out ->
val manifest = Manifest(input)
manifest.mainAttributes.putValue("MixinConfigs", mixinConfigs.joinToString(","))
out.putNextEntry(ZipEntry(zipEntry.name))
manifest.write(out)
out.closeEntry()
}
}
}
if (ZipUtil.containsEntry(output, refmap)) {
ZipUtil.transformEntry(output, refmap) { input, zipEntry, out ->
val refmapElement: JsonObject = JsonParser().parse(InputStreamReader(input)).asJsonObject.deepCopy()
if (refmapElement.has("mappings")) {
refmapElement["mappings"].asJsonObject.entrySet().forEach { (_, value) ->
remapRefmap(project, value.asJsonObject)
}
}
if (refmapElement.has("data")) {
val data = refmapElement["data"].asJsonObject
if (data.has("named:intermediary")) {
data.add("searge", data["named:intermediary"].deepCopy().also {
it.asJsonObject.entrySet().forEach { (_, value) ->
remapRefmap(project, value.asJsonObject)
}
})
data.remove("named:intermediary")
}
}
out.putNextEntry(ZipEntry(zipEntry.name))
out.write(gson.toJson(refmapElement).toByteArray())
out.closeEntry()
}
} else {
project.logger.info("Failed to locate refmap: $refmap")
}
}
private fun remapRefmap(project: Project, obj: JsonObject) {
val srg = project.extensions.getByType(LoomGradleExtension::class.java).mappingsProvider.mappingsWithSrg
val methodPattern = "L(.*);(.*)(\\(.*)".toRegex()
val methodPatternWithoutClass = "(.*)(\\(.*)".toRegex()
val fieldPattern = "(.*):(.*)".toRegex()
obj.keySet().forEach { key ->
val originalRef = obj[key].asString
val methodMatch = methodPattern.matchEntire(originalRef)
val fieldMatch = fieldPattern.matchEntire(originalRef)
val methodMatchWithoutClass = methodPatternWithoutClass.matchEntire(originalRef)
when {
methodMatch != null -> {
val matchedClass =
srg.classes.firstOrNull { it.getName("intermediary") == methodMatch.groups[1]!!.value }
val replacementName: String = srg.classes.asSequence()
.flatMap { it.methods.asSequence() }
.filter { it.getName("intermediary") == methodMatch.groups[2]!!.value }
.firstOrNull { it.getDescriptor("intermediary") == methodMatch.groups[3]!!.value }
?.getName("srg") ?: methodMatch.groups[2]!!.value
obj.addProperty(
key, originalRef
.replaceFirst(
methodMatch.groups[1]!!.value,
matchedClass?.getName("srg") ?: methodMatch.groups[1]!!.value
)
.replaceFirst(methodMatch.groups[2]!!.value, replacementName)
.replaceFirst(methodMatch.groups[3]!!.value, methodMatch.groups[3]!!.value.remapDescriptor {
srg.classes.firstOrNull { def -> def.getName("intermediary") == it }?.getName("srg")
?: it
})
)
}
fieldMatch != null -> {
val replacementName: String = srg.classes.asSequence()
.flatMap { it.fields.asSequence() }
.filter { it.getName("intermediary") == fieldMatch.groups[1]!!.value }
.firstOrNull { it.getDescriptor("intermediary") == fieldMatch.groups[2]!!.value }
?.getName("srg") ?: fieldMatch.groups[1]!!.value
obj.addProperty(
key, originalRef
.replaceFirst(fieldMatch.groups[1]!!.value, replacementName)
.replaceFirst(fieldMatch.groups[2]!!.value, fieldMatch.groups[2]!!.value.remapDescriptor {
srg.classes.firstOrNull { def -> def.getName("intermediary") == it }?.getName("srg")
?: it
})
)
}
methodMatchWithoutClass != null -> {
val replacementName: String = srg.classes.asSequence()
.flatMap { it.methods.asSequence() }
.filter { it.getName("intermediary") == methodMatchWithoutClass.groups[1]!!.value }
.firstOrNull { it.getDescriptor("intermediary") == methodMatchWithoutClass.groups[2]!!.value }
?.getName("srg") ?: methodMatchWithoutClass.groups[1]!!.value
obj.addProperty(
key, originalRef
.replaceFirst(methodMatchWithoutClass.groups[1]!!.value, replacementName)
.replaceFirst(
methodMatchWithoutClass.groups[2]!!.value,
methodMatchWithoutClass.groups[2]!!.value.remapDescriptor {
srg.classes.firstOrNull { def -> def.getName("intermediary") == it }?.getName("srg")
?: it
})
)
}
else -> project.logger.warn("Failed to remap refmap value: $originalRef")
}
}
}
private fun String.remapDescriptor(classMappings: (String) -> String): String {
return try {
val reader = StringReader(this)
val result = StringBuilder()
var insideClassName = false
val className = StringBuilder()
while (true) {
val c: Int = reader.read()
if (c == -1) {
break
}
if (c == ';'.toInt()) {
insideClassName = false
result.append(classMappings(className.toString()))
}
if (insideClassName) {
className.append(c.toChar())
} else {
result.append(c.toChar())
}
if (!insideClassName && c == 'L'.toInt()) {
insideClassName = true
className.setLength(0)
}
}
result.toString()
} catch (e: IOException) {
throw AssertionError(e)
}
}
}

View File

@@ -0,0 +1,31 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import org.gradle.api.Project
import org.zeroturnaround.zip.ByteSource
import org.zeroturnaround.zip.ZipUtil
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
object GenerateFakeFabricModJson : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
Files.copy(input, output)
val fakeModId = "generated_" + UUID.randomUUID().toString().filterNot { it == '-' }.take(7)
ZipUtil.addOrReplaceEntries(
output.toFile(), arrayOf(
ByteSource(
"fabric.mod.json", """{
"schemaVersion": 1,
"id": "$fakeModId",
"name": "Generated Mod (Please Ignore)",
"version": "1.0.0",
"custom": {
"fabric-loom:generated": true
}
}""".toByteArray()
)
)
)
}
}

View File

@@ -0,0 +1,52 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import org.gradle.api.Project
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import org.zeroturnaround.zip.ByteSource
import org.zeroturnaround.zip.ZipUtil
import java.nio.file.Files
import java.nio.file.Path
import java.util.*
object GenerateFakeForgeMod : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
val fakeModId = "generated_" + UUID.randomUUID().toString().filterNot { it == '-' }.take(7)
Files.copy(input, output)
ZipUtil.addEntries(
output.toFile(), arrayOf(
ByteSource(
"META-INF/mods.toml",
"""modLoader = "javafml"
loaderVersion = "[33,)"
license = "Generated"
[[mods]]
modId = "$fakeModId"""".toByteArray()
),
ByteSource(
"pack.mcmeta",
"""{"pack":{"description":"Generated","pack_format":4}}""".toByteArray()
),
ByteSource(
"generated/$fakeModId.class",
ClassWriter(0).let { classWriter ->
classWriter.visit(52, Opcodes.ACC_PUBLIC, "generated/$fakeModId", null, "java/lang/Object", null)
val modAnnotation = classWriter.visitAnnotation("Lnet/minecraftforge/fml/common/Mod;", false)
modAnnotation.visit("value", fakeModId)
modAnnotation.visitEnd()
classWriter.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, arrayOf()).also {
it.visitVarInsn(Opcodes.ALOAD, 0)
it.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false)
it.visitInsn(Opcodes.RETURN)
it.visitMaxs(1, 1)
it.visitEnd()
}
classWriter.visitEnd()
classWriter.toByteArray()
}
)
)
)
}
}

View File

@@ -0,0 +1,56 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import me.shedaniel.architect.plugin.utils.validateJarFs
import net.fabricmc.loom.LoomGradleExtension
import net.fabricmc.loom.util.LoggerFilter
import net.fabricmc.tinyremapper.OutputConsumerPath
import net.fabricmc.tinyremapper.TinyRemapper
import net.fabricmc.tinyremapper.TinyUtils
import org.gradle.api.Project
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
object RemapMixinVariables : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
var remapperBuilder = TinyRemapper.newRemapper()
for (mixinMapFile in loomExtension.allMixinMappings) {
if (mixinMapFile.exists()) {
remapperBuilder = remapperBuilder.withMappings(
TinyUtils.createTinyMappingProvider(
mixinMapFile.toPath(),
"named",
"intermediary"
)
)
}
}
val remapper = remapperBuilder.build()
val classpathFiles: Set<File> = LinkedHashSet(
project.configurations.getByName("compileClasspath").files
)
val classpath = classpathFiles.asSequence().map { obj: File -> obj.toPath() }.filter { p: Path ->
input != p && Files.exists(p)
}.toList().toTypedArray()
LoggerFilter.replaceSystemOut()
try {
project.validateJarFs(output)
OutputConsumerPath.Builder(output).build().use { outputConsumer ->
outputConsumer.addNonClassFiles(input)
remapper.readClassPath(*classpath)
remapper.readInputs(input)
remapper.apply(outputConsumer)
}
} catch (e: Exception) {
remapper.finish()
throw RuntimeException("Failed to remap $input to $output", e)
}
remapper.finish()
}
}

View File

@@ -0,0 +1,16 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import org.gradle.api.Project
import org.zeroturnaround.zip.ZipUtil
import java.nio.file.Files
import java.nio.file.Path
object RemoveFabricModJson : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
Files.copy(input, output)
if (ZipUtil.containsEntry(output.toFile(), "fabric.mod.json")) {
ZipUtil.removeEntry(output.toFile(), "fabric.mod.json")
}
}
}

View File

@@ -0,0 +1,206 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.ArchitectPluginExtension
import me.shedaniel.architect.plugin.Transformer
import me.shedaniel.architect.plugin.projectUniqueIdentifier
import me.shedaniel.architect.plugin.utils.ClassTransformer
import me.shedaniel.architect.plugin.utils.Transform
import org.gradle.api.Project
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Handle
import org.objectweb.asm.Opcodes
import org.objectweb.asm.commons.ClassRemapper
import org.objectweb.asm.commons.Remapper
import org.objectweb.asm.tree.*
import org.zeroturnaround.zip.ZipUtil
import java.io.InputStream
import java.lang.invoke.CallSite
import java.lang.invoke.MethodHandles
import java.lang.invoke.MethodType
import java.nio.file.Path
import java.util.zip.ZipEntry
object TransformExpectPlatform : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
Transform.transform(input, output, transformExpectPlatform(project))
}
fun transformExpectPlatform(project: Project): ClassTransformer {
val projectUniqueIdentifier by lazy { project.projectUniqueIdentifier() }
var injectedClass = !project.extensions.getByType(ArchitectPluginExtension::class.java).injectInjectables
return { clazz, classAdder ->
if (!injectedClass) {
injectedClass = true
Transform::class.java.getResourceAsStream("/annotations-inject/injection.jar").use { stream ->
ZipUtil.iterate(stream) { input: InputStream, entry: ZipEntry ->
if (entry.name.endsWith(".class")) {
val newName = "$projectUniqueIdentifier/${
entry.name.substringBeforeLast(".class").substringAfterLast('/')
}"
classAdder(newName, input.readBytes().let {
val node = ClassNode(Opcodes.ASM8)
ClassReader(it).accept(node, ClassReader.EXPAND_FRAMES)
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
val remapper = ClassRemapper(writer, object : Remapper() {
override fun map(internalName: String?): String {
if (internalName?.startsWith("me/shedaniel/architect/plugin/callsite") == true) {
return internalName.replace(
"me/shedaniel/architect/plugin/callsite",
projectUniqueIdentifier
)
}
return super.map(internalName)
}
})
node.apply {
name = newName
}.accept(remapper)
writer.toByteArray()
})
}
}
}
}
clazz.methods.mapNotNull { method ->
when {
method?.visibleAnnotations?.any { it.desc == TransformInjectables.expectPlatform } == true -> method to "me/shedaniel/architectury/PlatformMethods"
method?.invisibleAnnotations?.any { it.desc == TransformInjectables.expectPlatformNew } == true -> {
method to "$projectUniqueIdentifier/PlatformMethods"
}
else -> null
}
}.forEach { (method, platformMethodsClass) ->
if (method.access and Opcodes.ACC_STATIC == 0) {
System.err.println("@ExpectPlatform can only apply to static methods!")
} else {
method.instructions.clear()
val endOfDesc = method.desc.lastIndexOf(')')
val returnValue = method.desc.substring(endOfDesc + 1)
val args = method.desc.substring(1, endOfDesc)
var cursor = 0
var inClass = false
var index = 0
while (cursor < args.length) {
val char = args[cursor]
if (inClass) {
if (char == ';') {
method.instructions.addLoad(char, index++)
inClass = false
}
} else when (char) {
'[' -> Unit
'L' -> inClass = true
else -> method.instructions.addLoad(char, when (char) {
'J', 'D' -> index.also { index += 2 }
else -> index++
})
}
cursor++
}
val methodType = MethodType.methodType(
CallSite::class.java,
MethodHandles.Lookup::class.java,
String::class.java,
MethodType::class.java
)
val handle = Handle(
Opcodes.H_INVOKESTATIC,
platformMethodsClass,
"platform",
methodType.toMethodDescriptorString(),
false
)
method.instructions.add(
InvokeDynamicInsnNode(
method.name,
method.desc,
handle
)
)
method.instructions.addReturn(returnValue.first { it != '[' })
method.maxStack = -1
}
}
clazz
}
}
private fun InsnList.addLoad(type: Char, index: Int) {
when (type) {
';' -> add(
VarInsnNode(
Opcodes.ALOAD,
index
)
)
'I', 'S', 'B', 'C', 'Z' -> add(
VarInsnNode(
Opcodes.ILOAD,
index
)
)
'F' -> add(
VarInsnNode(
Opcodes.FLOAD,
index
)
)
'J' -> add(
VarInsnNode(
Opcodes.LLOAD,
index
)
)
'D' -> add(
VarInsnNode(
Opcodes.DLOAD,
index
)
)
else -> throw IllegalStateException("Invalid Type: $type")
}
}
private fun InsnList.addReturn(type: Char) {
when (type) {
'L' -> add(
InsnNode(
Opcodes.ARETURN
)
)
'I', 'S', 'B', 'C', 'Z' -> add(
InsnNode(
Opcodes.IRETURN
)
)
'F' -> add(
InsnNode(
Opcodes.FRETURN
)
)
'J' -> add(
InsnNode(
Opcodes.LRETURN
)
)
'D' -> add(
InsnNode(
Opcodes.DRETURN
)
)
'V' -> add(
InsnNode(
Opcodes.RETURN
)
)
}
}
}

View File

@@ -0,0 +1,72 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import me.shedaniel.architect.plugin.utils.Transform
import org.gradle.api.Project
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.MethodInsnNode
import java.nio.file.Path
object TransformForgeBytecode : Transformer {
val forgeEvent = "Lme/shedaniel/architectury/ForgeEvent;"
val forgeEventCancellable = "Lme/shedaniel/architectury/ForgeEventCancellable;"
val cancellable = "Lnet/minecraftforge/eventbus/api/Cancelable;"
private val environmentClass = "net/fabricmc/api/Environment"
override fun invoke(project: Project, input: Path, output: Path) {
Transform.transform(input, output) { node, classAdder ->
if (node.access and Opcodes.ACC_INTERFACE == 0) {
if (node.visibleAnnotations?.any { it.desc == forgeEvent || it.desc == forgeEventCancellable } == true) {
node.superName = "net/minecraftforge/eventbus/api/Event"
node.methods.forEach {
if (it.name == "<init>") {
for (insnNode in it.instructions) {
if (insnNode.opcode == Opcodes.INVOKESPECIAL) {
insnNode as MethodInsnNode
if (insnNode.name == "<init>" && insnNode.owner == "java/lang/Object") {
insnNode.owner = "net/minecraftforge/eventbus/api/Event"
break
}
}
}
}
}
node.signature?.let {
node.signature = it.substringBeforeLast('L') + "Lnet/minecraftforge/eventbus/api/Event;"
}
// if @ForgeEventCancellable, add the cancellable annotation from forge
node.visibleAnnotations.apply {
if (any { it.desc == forgeEventCancellable }) {
add(AnnotationNode(cancellable))
}
}
}
}
node.visibleAnnotations = (node.visibleAnnotations ?: mutableListOf()).apply {
val invisibleEnvironments =
node.invisibleAnnotations?.filter { it.desc == "L${environmentClass};" } ?: emptyList()
node.invisibleAnnotations?.removeAll(invisibleEnvironments)
addAll(invisibleEnvironments)
}
node.fields.forEach { field ->
field.visibleAnnotations = (field.visibleAnnotations ?: mutableListOf()).apply {
val invisibleEnvironments =
field.invisibleAnnotations?.filter { it.desc == "L${environmentClass};" } ?: emptyList()
field.invisibleAnnotations?.removeAll(invisibleEnvironments)
addAll(invisibleEnvironments)
}
}
node.methods.forEach { method ->
method.visibleAnnotations = (method.visibleAnnotations ?: mutableListOf()).apply {
val invisibleEnvironments =
method.invisibleAnnotations?.filter { it.desc == "L${environmentClass};" } ?: emptyList()
method.invisibleAnnotations?.removeAll(invisibleEnvironments)
addAll(invisibleEnvironments)
}
}
node
}
}
}

View File

@@ -0,0 +1,100 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.Transformer
import me.shedaniel.architect.plugin.utils.validateJarFs
import net.fabricmc.loom.LoomGradleExtension
import net.fabricmc.tinyremapper.*
import org.gradle.api.Project
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
object TransformForgeEnvironment : Transformer {
override fun invoke(project: Project, input: Path, output: Path) {
val remapperBuilder: TinyRemapper.Builder = TinyRemapper.newRemapper()
.withMappings(remapEnvironment())
.skipLocalVariableMapping(true)
mapMixin(project, remapperBuilder)
val classpathFiles: Set<File> = LinkedHashSet(
project.configurations.getByName("compileClasspath").files
)
val classpath = classpathFiles.asSequence().map { obj: File -> obj.toPath() }
.filter { p: Path -> input != p && Files.exists(p) }.toList().toTypedArray()
val remapper = remapperBuilder.build()
try {
project.validateJarFs(output)
OutputConsumerPath.Builder(output).build().use { outputConsumer ->
outputConsumer.addNonClassFiles(input, NonClassCopyMode.FIX_META_INF, null)
remapper.readClassPath(*classpath)
remapper.readInputs(input)
remapper.apply(outputConsumer)
}
} catch (e: Exception) {
remapper.finish()
throw RuntimeException("Failed to remap $input to $output", e)
}
}
private fun remapEnvironment(): IMappingProvider = IMappingProvider { out ->
out.acceptClass("net/fabricmc/api/Environment", "net/minecraftforge/api/distmarker/OnlyIn")
out.acceptClass("net/fabricmc/api/EnvType", "net/minecraftforge/api/distmarker/Dist")
out.acceptField(
IMappingProvider.Member("net/fabricmc/api/EnvType", "SERVER", "Lnet/fabricmc/api/EnvType;"),
"DEDICATED_SERVER"
)
}
private fun mapMixin(project: Project, remapperBuilder: TinyRemapper.Builder) {
val loomExtension = project.extensions.getByType(LoomGradleExtension::class.java)
val srg = project.extensions.getByType(LoomGradleExtension::class.java).mappingsProvider.mappingsWithSrg
for (mixinMapFile in loomExtension.allMixinMappings) {
if (mixinMapFile.exists()) {
remapperBuilder.withMappings { sink ->
TinyUtils.createTinyMappingProvider(mixinMapFile.toPath(), "named", "intermediary").load(object :
IMappingProvider.MappingAcceptor {
override fun acceptClass(srcName: String, dstName: String) {
sink.acceptClass(dstName, srg.classes
.firstOrNull { it.getName("intermediary") == dstName }
?.getName("srg") ?: dstName
)
}
override fun acceptMethod(method: IMappingProvider.Member, dstName: String) {
sink.acceptMethod(
IMappingProvider.Member(method.owner, dstName, method.desc),
srg.classes
.flatMap { it.methods }
.firstOrNull { it.getName("intermediary") == dstName }
?.getName("srg") ?: dstName)
}
override fun acceptField(field: IMappingProvider.Member, dstName: String) {
sink.acceptField(
IMappingProvider.Member(field.owner, dstName, field.desc),
srg.classes
.flatMap { it.fields }
.firstOrNull { it.getName("intermediary") == dstName }
?.getName("srg") ?: dstName)
}
override fun acceptMethodArg(method: IMappingProvider.Member, lvIndex: Int, dstName: String) {}
override fun acceptMethodVar(
method: IMappingProvider.Member,
lvIndex: Int,
startOpIdx: Int,
asmIndex: Int,
dstName: String
) {
}
})
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
package me.shedaniel.architect.plugin.transformers
import me.shedaniel.architect.plugin.ArchitectPluginExtension
import me.shedaniel.architect.plugin.Transformer
import me.shedaniel.architect.plugin.projectUniqueIdentifier
import me.shedaniel.architect.plugin.utils.validateJarFs
import net.fabricmc.tinyremapper.IMappingProvider
import net.fabricmc.tinyremapper.NonClassCopyMode
import net.fabricmc.tinyremapper.OutputConsumerPath
import net.fabricmc.tinyremapper.TinyRemapper
import org.gradle.api.Project
import java.io.File
import java.nio.file.Files
import java.nio.file.Path
object TransformInjectables : Transformer {
const val expectPlatform = "Lme/shedaniel/architectury/ExpectPlatform;"
const val expectPlatformNew = "Lme/shedaniel/architectury/annotations/ExpectPlatform;"
override fun invoke(project: Project, input: Path, output: Path) {
if (project.extensions.getByType(ArchitectPluginExtension::class.java).injectInjectables) {
transformArchitecturyInjectables(project, input, output)
} else {
Files.copy(input, output)
}
}
private fun transformArchitecturyInjectables(project: Project, input: Path, output: Path) {
val remapper = TinyRemapper.newRemapper()
.withMappings { sink ->
sink.acceptClass(
"me/shedaniel/architectury/targets/ArchitecturyTarget",
project.projectUniqueIdentifier() + "/PlatformMethods"
)
sink.acceptMethod(
IMappingProvider.Member(
"me/shedaniel/architectury/targets/ArchitecturyTarget",
"getCurrentTarget",
"()Ljava/lang/String;"
), "getModLoader"
)
}
.build()
val classpathFiles: Set<File> = LinkedHashSet(
project.configurations.getByName("compileClasspath").files
)
val classpath = classpathFiles.asSequence().map { obj: File -> obj.toPath() }
.filter { p: Path -> Files.exists(p) }.toList().toTypedArray()
try {
project.validateJarFs(output)
OutputConsumerPath.Builder(output).build().use { outputConsumer ->
outputConsumer.addNonClassFiles(input, NonClassCopyMode.UNCHANGED, null)
remapper.readClassPath(*classpath)
remapper.readInputs(input)
remapper.apply(outputConsumer)
}
} catch (e: Exception) {
remapper.finish()
throw RuntimeException("Failed to remap $input to $output", e)
}
}
}