diff --git a/build.gradle.kts b/build.gradle.kts index 2a4f4fc..d1aa86c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,3 +1,4 @@ +import xyz.wagyourtail.gradle.shadow.ShadowJar import java.net.URI plugins { @@ -15,6 +16,21 @@ base { val annotations by sourceSets.creating {} +val shared by sourceSets.creating { + compileClasspath += sourceSets.main.get().compileClasspath + runtimeClasspath += sourceSets.main.get().runtimeClasspath +} + +val agent by sourceSets.creating { + compileClasspath += shared.output + sourceSets.main.get().compileClasspath + runtimeClasspath += shared.output + sourceSets.main.get().runtimeClasspath +} + +sourceSets.main { + compileClasspath += shared.output + runtimeClasspath += shared.output +} + java { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -33,9 +49,16 @@ repositories { val asmVersion: String by project.properties +val shade by configurations.creating { + configurations.implementation.get().extendsFrom(this) +} + dependencies { implementation(gradleApi()) - implementation("org.ow2.asm:asm-tree:${asmVersion}") + + shade("org.ow2.asm:asm:${asmVersion}") + shade("org.ow2.asm:asm-commons:${asmVersion}") + shade("org.ow2.asm:asm-tree:${asmVersion}") testImplementation(kotlin("test")) } @@ -45,6 +68,7 @@ tasks.test { } tasks.jar { + from(shared.output) manifest { attributes( @@ -68,8 +92,29 @@ val annotationJar = tasks.register("annotationJar") { } } +val agentShadeJar = tasks.register("agentShadowJar") { + archiveClassifier.set("agent") + from(agent.output, shared.output) + + shadowContents.add(shade) + exclude("module-info.class") + + relocate("org.objectweb.asm", "xyz.wagyourtail.unimined.expect.asm") + + manifest { + attributes( + "Manifest-Version" to "1.0", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + "Premain-Class" to "xyz.wagyourtail.unimined.expect.ExpectPlatformAgent", + "Can-Redefine-Classes" to "true", + ) + } +} + tasks.assemble { dependsOn(annotationJar) + dependsOn(agentShadeJar) } kotlin { @@ -111,6 +156,10 @@ publishing { artifact(annotationJar) { classifier = "annotations" } + + artifact(agentShadeJar) { + classifier = "agent" + } } } } diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..5d45e42 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,45 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import java.util.* + +plugins { + kotlin("jvm") version "1.9.22" +} + +repositories { + mavenCentral() +} + +val asmVersion: String = project.properties["asmVersion"]?.toString() ?: run { + projectDir.parentFile.resolve("gradle.properties").inputStream().use { + val props = Properties() + props.load(it) + props.getProperty("asmVersion") as String + } +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType { + val targetVersion = 8 + if (JavaVersion.current().isJava9Compatible) { + options.release.set(targetVersion) + } +} + +tasks.withType { + kotlinOptions.jvmTarget = "1.8" +} + +dependencies { + implementation(gradleApi()) + + // commons compress + implementation("org.apache.commons:commons-compress:1.26.1") + + implementation("org.ow2.asm:asm:${asmVersion}") + implementation("org.ow2.asm:asm-commons:${asmVersion}") + implementation("org.ow2.asm:asm-tree:${asmVersion}") +} diff --git a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/PackageRelocateReader.kt b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/PackageRelocateReader.kt new file mode 100644 index 0000000..2b832a0 --- /dev/null +++ b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/PackageRelocateReader.kt @@ -0,0 +1,64 @@ +package xyz.wagyourtail.gradle.shadow + +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.commons.ClassRemapper +import xyz.wagyourtail.gradle.utils.MustSet +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.FilterReader +import java.io.Reader +import java.nio.charset.StandardCharsets + +class PackageRelocateReader(input: Reader): FilterReader(input) { + + var remapper: PackageRelocator by MustSet() + + val contents = ByteArrayOutputStream().use { out -> + out.writer(StandardCharsets.ISO_8859_1).use { writer -> + input.copyTo(writer) + } + input.close() + out.toByteArray() + } + + val changedContents: Reader by lazy { + val reader = ClassReader(contents) + val writer = ClassWriter(0) + reader.accept(ClassRemapper(writer, remapper), 0) + ByteArrayInputStream(writer.toByteArray()).bufferedReader(StandardCharsets.ISO_8859_1) + } + + override fun read(): Int { + return changedContents.read() + } + + override fun read(cbuf: CharArray, off: Int, len: Int): Int { + return changedContents.read(cbuf, off, len) + } + + override fun skip(n: Long): Long { + return changedContents.skip(n) + } + + override fun ready(): Boolean { + return changedContents.ready() + } + + override fun markSupported(): Boolean { + return changedContents.markSupported() + } + + override fun mark(readAheadLimit: Int) { + changedContents.mark(readAheadLimit) + } + + override fun reset() { + changedContents.reset() + } + + override fun close() { + changedContents.close() + } + +} diff --git a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/PackageRelocator.kt b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/PackageRelocator.kt new file mode 100644 index 0000000..bcd907a --- /dev/null +++ b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/PackageRelocator.kt @@ -0,0 +1,16 @@ +package xyz.wagyourtail.gradle.shadow + +import org.objectweb.asm.commons.Remapper + +class PackageRelocator(val map: Map) : Remapper() { + + override fun map(internalName: String): String { + for ((from, to) in map) { + if (internalName.startsWith(from)) { + return to + internalName.substring(from.length) + } + } + return internalName + } + +} diff --git a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/ShadowJar.kt b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/ShadowJar.kt new file mode 100644 index 0000000..c370c45 --- /dev/null +++ b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/shadow/ShadowJar.kt @@ -0,0 +1,71 @@ +package xyz.wagyourtail.gradle.shadow + +import org.gradle.api.file.FileCollection +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.TaskAction +import org.gradle.jvm.tasks.Jar +import java.nio.charset.StandardCharsets + +abstract class ShadowJar : Jar() { + + @get:Internal + abstract val shadowContents: ListProperty + + @get:Input + @get:Optional + abstract val relocatePackages: MapProperty + + init { + group = "Shadow" + description = "Shadow the jar with the specified configurations" + + shadowContents.convention(mutableListOf()).finalizeValueOnRead() + relocatePackages.convention(mutableMapOf()).finalizeValueOnRead() + archiveClassifier.convention("all") + } + + fun relocate(from: String, to: String) { + relocatePackages.put(from, to) + } + + @TaskAction + fun runTask() { + for (fileCollection in shadowContents.get()) { + for (file in fileCollection) { + if (!file.exists()) continue + if (file.isDirectory) { + // copy directory + from(file) + } else { + // copy file + from(project.zipTree(file)) + } + } + } + + filteringCharset = StandardCharsets.ISO_8859_1.name() + includeEmptyDirs = false + + if (relocatePackages.getOrElse(emptyMap()).isNotEmpty()) { + val map = relocatePackages.get() + .mapKeys { it.key.replace('.', '/') } + .mapKeys { if (!it.key.endsWith("/")) it.key + "/" else it.key } + .mapValues { it.value.replace('.', '/') } + .mapValues { if (!it.value.endsWith("/")) it.value + "/" else it.value } + val rel = PackageRelocator(map) + eachFile { + if (!it.path.endsWith(".class")) return@eachFile + it.path = rel.map(it.path) + it.filter(mapOf("remapper" to rel), PackageRelocateReader::class.java) + } + } + + // call super + copy() + } + +} diff --git a/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/utils/MustSet.kt b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/utils/MustSet.kt new file mode 100644 index 0000000..1c0f4e7 --- /dev/null +++ b/buildSrc/src/main/kotlin/xyz/wagyourtail/gradle/utils/MustSet.kt @@ -0,0 +1,27 @@ +package xyz.wagyourtail.gradle.utils + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +class MustSet : ReadWriteProperty { + + @Suppress("ClassName") + private object UNINITIALIZED_VALUE + + private var prop: Any? = UNINITIALIZED_VALUE + + @Suppress("UNCHECKED_CAST") + override fun getValue(thisRef: Any?, property: KProperty<*>): T { + return if (prop == UNINITIALIZED_VALUE) { + synchronized(this) { + return if (prop == UNINITIALIZED_VALUE) throw IllegalStateException("Property ${property.name} must be set before use") else prop as T + } + } else prop as T + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) { + synchronized(this) { + prop = value + } + } +} \ No newline at end of file diff --git a/expect-platform-test/build.gradle b/expect-platform-test/build.gradle index f16d789..c481720 100644 --- a/expect-platform-test/build.gradle +++ b/expect-platform-test/build.gradle @@ -82,6 +82,14 @@ tasks.register('runC', JavaExec) { group = 'ept' } +tasks.register('runAgentA', JavaExec) { + classpath = sourceSets.a.runtimeClasspath + sourceSets.main.runtimeClasspath + mainClass = 'xyz.wagyourtail.ept.Main' + group = 'ept' + + expectPlatform.insertAgent(delegate as JavaExecSpec, "a") +} + tasks.register('jarA', ExpectPlatformJar) { platformName = "a" inputFiles = sourceSets.main.output diff --git a/src/agent/java/xyz/wagyourtail/unimined/expect/ExpectPlatformAgent.java b/src/agent/java/xyz/wagyourtail/unimined/expect/ExpectPlatformAgent.java new file mode 100644 index 0000000..14c7663 --- /dev/null +++ b/src/agent/java/xyz/wagyourtail/unimined/expect/ExpectPlatformAgent.java @@ -0,0 +1,48 @@ +package xyz.wagyourtail.unimined.expect; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.tree.ClassNode; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.security.ProtectionDomain; + +public class ExpectPlatformAgent { + private static final String EXPECT_PLATFORM = "expect.platform"; + private static final String REMAP = "expect.remap"; + private static final String platform = System.getProperty(EXPECT_PLATFORM); + private static final String remap = System.getProperty(REMAP); + + + public static void premain(String args, Instrumentation inst) { + if (platform == null) { + throw new IllegalStateException("-D" + EXPECT_PLATFORM + " not set"); + } + inst.addTransformer(new ExpectPlatformTransformer()); + } + + public static void agentmain(String args, Instrumentation inst) { + premain(args, inst); + } + + public static class ExpectPlatformTransformer implements ClassFileTransformer { + TransformPlatform transformPlatform = new TransformPlatform(platform, remap); + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { + ClassReader reader = new ClassReader(classfileBuffer); + ClassNode classNode = new ClassNode(); + reader.accept(classNode, 0); + + transformPlatform.transform(classNode); + + ClassWriter writer = new ClassWriter(reader, 0); + classNode.accept(writer); + + return writer.toByteArray(); + } + + } + +} diff --git a/src/annotations/java/xyz/wagyourtail/unimined/expect/annotation/Environment.java b/src/annotations/java/xyz/wagyourtail/unimined/expect/annotation/Environment.java new file mode 100644 index 0000000..618ed83 --- /dev/null +++ b/src/annotations/java/xyz/wagyourtail/unimined/expect/annotation/Environment.java @@ -0,0 +1,19 @@ +package xyz.wagyourtail.unimined.expect.annotation; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.PACKAGE}) +public @interface Environment { + + EnvType value(); + + enum EnvType { + CLIENT, + SERVER, + COMBINED + } +} diff --git a/src/main/kotlin/xyz/wagyourtail/unimined/expect/ExpectPlatformExtension.kt b/src/main/kotlin/xyz/wagyourtail/unimined/expect/ExpectPlatformExtension.kt index 34d6c0a..468f944 100644 --- a/src/main/kotlin/xyz/wagyourtail/unimined/expect/ExpectPlatformExtension.kt +++ b/src/main/kotlin/xyz/wagyourtail/unimined/expect/ExpectPlatformExtension.kt @@ -1,9 +1,13 @@ package xyz.wagyourtail.unimined.expect +import groovy.lang.Closure +import groovy.lang.DelegatesTo import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.attributes.Attribute import org.gradle.api.tasks.Internal +import org.gradle.process.JavaExecSpec +import xyz.wagyourtail.unimined.expect.transform.ExpectPlatformParams import xyz.wagyourtail.unimined.expect.transform.ExpectPlatformTransform abstract class ExpectPlatformExtension(val project: Project) { @@ -11,8 +15,10 @@ abstract class ExpectPlatformExtension(val project: Project) { val version = ExpectPlatformExtension::class.java.`package`.implementationVersion ?: "1.0.0-SNAPSHOT" val annotationsDep = "xyz.wagyourtail.unimined.expect-platform:expect-platform:$version:annotations" + val agentDep = "xyz.wagyourtail.unimined.expect-platform:expect-platform:$version:agent" - fun platform(platformName: String, configuration: Configuration) { + @JvmOverloads + fun platform(platformName: String, configuration: Configuration, action: ExpectPlatformParams.() -> Unit = {}) { val expectPlatformAttribute = Attribute.of("expectPlatform.${configuration.name}", Boolean::class.javaObjectType) project.dependencies.apply { @@ -33,6 +39,7 @@ abstract class ExpectPlatformExtension(val project: Project) { spec.parameters { it.platformName.set(platformName) + it.action() } } } @@ -42,4 +49,29 @@ abstract class ExpectPlatformExtension(val project: Project) { } } + fun platform( + platformName: String, + configuration: Configuration, + @DelegatesTo( + ExpectPlatformParams::class, + strategy = Closure.DELEGATE_FIRST + ) action: Closure<*> + ) { + platform(platformName, configuration) { + action.delegate = this + action.resolveStrategy = Closure.DELEGATE_FIRST + action.call() + } + } + +// @JvmOverloads +// fun insertAgent(spec: JavaExecSpec, platformName: String, remap: Map = emptyMap()) { +// spec.jvmArgs("-javaagent:${agentJar.absolutePath}", "-Dexpect.platform=${platformName}", "-Dexpect.remap=${TransformPlatform.mapToString(remap)}") +// } + + val agentJar by lazy { + val config = project.configurations.detachedConfiguration(project.dependencies.create(agentDep)) + config.resolve().first { it.extension == "jar" } + } + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/wagyourtail/unimined/expect/TransformPlatform.kt b/src/main/kotlin/xyz/wagyourtail/unimined/expect/TransformPlatform.kt deleted file mode 100644 index 7830b84..0000000 --- a/src/main/kotlin/xyz/wagyourtail/unimined/expect/TransformPlatform.kt +++ /dev/null @@ -1,117 +0,0 @@ -package xyz.wagyourtail.unimined.expect - -import org.objectweb.asm.* -import org.objectweb.asm.tree.* -import xyz.wagyourtail.unimined.expect.utils.toByteArray -import java.nio.file.Path -import kotlin.io.path.* -import kotlin.math.max - -class TransformPlatform(val platformName: String) { - - @OptIn(ExperimentalPathApi::class) - fun transform(inputRoot: Path, outputRoot: Path) { - for (path in inputRoot.walk()) { - if (path.isDirectory()) { - outputRoot.resolve(inputRoot.relativize(path).toString()).createDirectories() - continue - } - - if (path.extension != "class") { - outputRoot.resolve(inputRoot.relativize(path).toString()).writeBytes(path.readBytes()) - continue - } - - val output = outputRoot.resolve(inputRoot.relativize(path).toString()) - - val classNode = ClassNode().also { ClassReader(path.readBytes()).accept(it, 0) } - val epMethods = mutableMapOf() - val poMethods = mutableMapOf() - classNode.methods.forEach { - it.invisibleAnnotations?.forEach { annotation -> - if (annotation.desc == "Lxyz/wagyourtail/unimined/expect/annotation/ExpectPlatform;") { - epMethods[it] = annotation - } else if (annotation.desc == "Lxyz/wagyourtail/unimined/expect/annotation/PlatformOnly;") { - poMethods[it] = annotation - } - } - } - - epMethods.forEach { (method, annotation) -> expectPlatform(method, classNode, annotation) } - poMethods.forEach { (method, annotation) -> platformOnly(method, classNode, annotation) } - - getCurrentTarget(classNode) - - output.createParentDirectories() - output.writeBytes(classNode.toByteArray()) - } - } - - private fun expectPlatform(method: MethodNode, classNode: ClassNode, annotation: AnnotationNode) { - if ((method.access and Opcodes.ACC_PUBLIC) == 0 || (method.access and Opcodes.ACC_STATIC) == 0) { - error("Method annotated with @ExpectPlatform must be public static: ${classNode.name.replace('/', '.')}.${method.name}") - } - - @Suppress("UNCHECKED_CAST") - val platforms = annotation.values?.get(1) as? List - - var platformClass: String? = null - - for (platform in platforms ?: emptyList()) { - val name = platform.values[1] as String - val clazz = platform.values[3] as String - if(name == platformName) { - platformClass = clazz - break - } - } - - if(platformClass == null) { - val packag = classNode.name.substringBeforeLast('/') - val name = classNode.name.substringAfterLast('/') - platformClass = "$packag/$platformName/${name}Impl" - } - - method.instructions.clear() - val type: Type = Type.getMethodType(method.desc) - - var stackIndex = 0 - for (argumentType in type.argumentTypes) { - method.instructions.add(VarInsnNode(argumentType.getOpcode(Opcodes.ILOAD), stackIndex)) - stackIndex += argumentType.size - } - - method.instructions.add( - MethodInsnNode(Opcodes.INVOKESTATIC, platformClass, method.name, method.desc) - ) - method.instructions.add(InsnNode(type.returnType.getOpcode(Opcodes.IRETURN))) - - method.maxStack = max(type.returnType.size, stackIndex) - method.maxLocals = stackIndex - } - - private fun platformOnly(method: MethodNode, classNode: ClassNode, annotation: AnnotationNode) { - @Suppress("UNCHECKED_CAST") - val platforms = annotation.values[1] as List - - if(platformName !in platforms) { - classNode.methods.remove(method) - } - } - - private fun getCurrentTarget(node: ClassNode) { - // transform all calls to xyz.wagyourtail.unimined.expect.Target.getCurrentTarget() to return the current platform - - for (method in node.methods) { - val instructions = method.instructions.iterator() - while (instructions.hasNext()) { - val insn = instructions.next() - if (insn is MethodInsnNode && insn.owner == "xyz/wagyourtail/unimined/expect/Target" && insn.name == "getCurrentTarget") { - instructions.remove() - instructions.add(LdcInsnNode(platformName)) - } - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformFiles.kt b/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformFiles.kt index f10629e..73a2c28 100644 --- a/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformFiles.kt +++ b/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformFiles.kt @@ -73,7 +73,7 @@ abstract class ExpectPlatformFiles : ConventionTask(), ExpectPlatformParams { for (i in toTransform.indices) { val input = toTransform[i] val output = transformed[i] - TransformPlatform(platformName.get()).transform(input, output) + TransformPlatform(platformName.get(), remap.get()).transform(input, output) } } finally { fileSystems.forEach { it.close() } diff --git a/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformJar.kt b/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformJar.kt index 662b563..27a52cf 100644 --- a/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformJar.kt +++ b/src/main/kotlin/xyz/wagyourtail/unimined/expect/task/ExpectPlatformJar.kt @@ -27,13 +27,13 @@ abstract class ExpectPlatformJar : Jar(), ExpectPlatformParams { for (input in inputFiles) { if (input.isDirectory) { val output = temporaryDir.resolve(input.name + "-expect-platform") - TransformPlatform(platformName.get()).transform(input.toPath(), output.toPath()) + TransformPlatform(platformName.get(), remap.get()).transform(input.toPath(), output.toPath()) from(output) } else if (input.extension == "jar") { val output = temporaryDir.resolve(input.nameWithoutExtension + "-expect-platform." + input.extension) input.toPath().openZipFileSystem().use { inputFs -> output.toPath().openZipFileSystem(mapOf("create" to true)).use { outputFs -> - TransformPlatform(platformName.get()).transform( + TransformPlatform(platformName.get(), remap.get()).transform( inputFs.getPath("/"), outputFs.getPath("/") ) diff --git a/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformParams.kt b/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformParams.kt index 5309702..fd31b5b 100644 --- a/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformParams.kt +++ b/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformParams.kt @@ -1,12 +1,22 @@ package xyz.wagyourtail.unimined.expect.transform import org.gradle.api.artifacts.transform.TransformParameters +import org.gradle.api.provider.MapProperty import org.gradle.api.provider.Property import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Optional interface ExpectPlatformParams : TransformParameters { @get:Input val platformName: Property + /** + * same values as with SimpleRemapper, since that's what it's passed to. + * This is necessary to set for hooking up @Environment. + */ + @get:Input + @get:Optional + val remap: MapProperty + } \ No newline at end of file diff --git a/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformTransform.kt b/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformTransform.kt index 87b7f2d..e03a908 100644 --- a/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformTransform.kt +++ b/src/main/kotlin/xyz/wagyourtail/unimined/expect/transform/ExpectPlatformTransform.kt @@ -15,15 +15,16 @@ abstract class ExpectPlatformTransform : TransformAction { override fun transform(outputs: TransformOutputs) { val platformName = parameters.platformName.get() + val remap = parameters.remap.get() val input = inputArtifact.get().asFile if (input.isDirectory) { val output = outputs.dir(input.name + "-expect-platform") - TransformPlatform(platformName).transform(input.toPath(), output.toPath()) + TransformPlatform(platformName, remap).transform(input.toPath(), output.toPath()) } else if (input.extension == "jar") { val output = outputs.file(input.nameWithoutExtension + "-expect-platform." + input.extension) input.toPath().openZipFileSystem().use { inputFs -> output.toPath().openZipFileSystem(mapOf("create" to true)).use { outputFs -> - TransformPlatform(platformName).transform(inputFs.getPath("/"), outputFs.getPath("/")) + TransformPlatform(platformName, remap).transform(inputFs.getPath("/"), outputFs.getPath("/")) } } } else { diff --git a/src/shared/java/xyz/wagyourtail/unimined/expect/TransformPlatform.java b/src/shared/java/xyz/wagyourtail/unimined/expect/TransformPlatform.java new file mode 100644 index 0000000..e654706 --- /dev/null +++ b/src/shared/java/xyz/wagyourtail/unimined/expect/TransformPlatform.java @@ -0,0 +1,203 @@ +package xyz.wagyourtail.unimined.expect; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; +import org.objectweb.asm.Opcodes; +import org.objectweb.asm.Type; +import org.objectweb.asm.commons.ClassRemapper; +import org.objectweb.asm.commons.SimpleRemapper; +import org.objectweb.asm.tree.*; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.*; +import java.util.*; +import java.util.stream.Stream; + +public class TransformPlatform { + private final String platformName; + private final Map remap = new HashMap<>(); + + public TransformPlatform(String platformName, String map) { + this.platformName = platformName; + stringMapParser(map); + } + + public TransformPlatform(String platformName, Map map) { + this.platformName = platformName; + remap.putAll(map); + } + + public void transform(Path inputRoot, Path outputRoot) throws IOException { + try (Stream files = Files.walk(inputRoot)) { + files.parallel().forEach(path -> { + try { + if (Files.isDirectory(path)) return; + Path parent = path.getParent(); + if (parent != null) { + Files.createDirectories(outputRoot.resolve(inputRoot.relativize(parent).toString())); + } + ClassReader reader = new ClassReader(Files.newInputStream(path)); + ClassNode classNode = new ClassNode(); + reader.accept(classNode, 0); + + classNode = transform(classNode); + + Path output = outputRoot.resolve(inputRoot.relativize(path).toString()); + ClassWriter writer = new ClassWriter(reader, 0); + classNode.accept(writer); + + Files.write(output, writer.toByteArray(), StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING); + } catch (IOException exception) { + throw new UncheckedIOException(exception); + } + }); + } + } + + public ClassNode transform(ClassNode classNode) { + Map expectPlatform = new HashMap<>(); + Map platformOnly = new HashMap<>(); + + ClassNode target = new ClassNode(); + ClassRemapper remapper = new ClassRemapper(target, new SimpleRemapper(remap)); + classNode.accept(remapper); + + classNode = target; + + if (classNode.methods == null) return classNode; + for (MethodNode method : classNode.methods) { + if (method.invisibleAnnotations == null) continue; + for (AnnotationNode annotation : method.invisibleAnnotations) { + if (annotation.desc.equals("Lxyz/wagyourtail/unimined/expect/annotation/ExpectPlatform;")) { + expectPlatform.put(method, annotation); + } else if (annotation.desc.equals("Lxyz/wagyourtail/unimined/expect/annotation/PlatformOnly;")) { + platformOnly.put(method, annotation); + } + } + } + + for (Map.Entry entry : expectPlatform.entrySet()) { + expectPlatform(entry.getKey(), classNode, entry.getValue()); + } + + for (Map.Entry entry : platformOnly.entrySet()) { + platformOnly(entry.getKey(), classNode, entry.getValue()); + } + + getCurrentTarget(classNode); + + return classNode; + } + + private void expectPlatform(MethodNode methodNode, ClassNode classNode, AnnotationNode annotationNode) { + if (((methodNode.access & Opcodes.ACC_PUBLIC) == 0) || ((methodNode.access & Opcodes.ACC_STATIC) == 0)) { + throw new RuntimeException("ExpectPlatform methods must be public and static"); + } + + + String platformClass = null; + if (annotationNode.values != null) { + for (AnnotationNode platform : (List) annotationNode.values.get(1)) { + String name = null; + String target = null; + for (int i = 0; i < platform.values.size(); i += 2) { + String key = (String) platform.values.get(i); + Object value = platform.values.get(i + 1); + if (key.equals("name")) { + name = (String) value; + } else if (key.equals("target")) { + target = (String) value; + } + } + assert name != null; + if (name.equals(platformName)) { + platformClass = target; + break; + } + } + } + if (platformClass == null) { + int lastSlash = classNode.name.lastIndexOf('/'); + String pkg; + if (lastSlash == -1) { + pkg = ""; + } else { + pkg = classNode.name.substring(0, lastSlash); + } + String className = classNode.name.substring(lastSlash + 1); + if (pkg.isEmpty()) { + platformClass = platformName + "/" + className + "Impl"; + } else { + platformClass = pkg + "/" + platformName + "/" + className + "Impl"; + } + } + + methodNode.instructions.clear(); + Type type = Type.getMethodType(methodNode.desc); + int stackIndex = 0; + for (Type arg : type.getArgumentTypes()) { + methodNode.instructions.add(new VarInsnNode(arg.getOpcode(Opcodes.ILOAD), stackIndex)); + stackIndex += arg.getSize(); + } + + methodNode.instructions.add( + new MethodInsnNode( + Opcodes.INVOKESTATIC, + platformClass, + methodNode.name, + methodNode.desc, + false + ) + ); + + methodNode.instructions.add(new InsnNode(type.getReturnType().getOpcode(Opcodes.IRETURN))); + + // recalculate proper maxStack and maxLocals manually so we don't have to recompute anything + methodNode.maxStack = Math.max(type.getReturnType().getSize(), stackIndex); + methodNode.maxLocals = stackIndex; + } + + private void platformOnly(MethodNode methodNode, ClassNode classNode, AnnotationNode annotationNode) { + List platforms = (List) annotationNode.values.get(1); + + if (!platforms.contains(platformName)) { + classNode.methods.remove(methodNode); + } + } + + private void getCurrentTarget(ClassNode classNode) { + for (MethodNode methodNode : classNode.methods) { + InsnList instructions = methodNode.instructions; + if (instructions == null) continue; + ListIterator iterator = instructions.iterator(); + while (iterator.hasNext()) { + AbstractInsnNode insnNode = iterator.next(); + if (insnNode.getOpcode() == Opcodes.INVOKESTATIC) { + MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; + if (methodInsnNode.owner.equals("xyz/wagyourtail/unimined/expect/Target") && methodInsnNode.name.equals("getCurrentTarget")) { + iterator.set(new LdcInsnNode(platformName)); + } + } + } + } + } + + private void stringMapParser(String str) { + if (str.isEmpty()) return; + String[] split = str.split(";[=|];"); + for (int i = 0; i < split.length; i += 2) { + remap.put(split[i], split[i + 1]); + } + } + + public static String mapToString(Map map) { + StringBuilder sb = new StringBuilder(); + for (Map.Entry entry : map.entrySet()) { + sb.append(entry.getKey()).append(";=;").append(entry.getValue()).append(";|;"); + } + sb.setLength(sb.length() - 3); + return sb.toString(); + } + +}