Close #3 Allow the use of @ExpectPlatform without api

This commit is contained in:
shedaniel
2021-01-11 11:07:28 +08:00
parent 4f3b528a49
commit d7d8f5a1a0
7 changed files with 193 additions and 8 deletions

View File

@@ -0,0 +1,22 @@
package me.shedaniel.architect.plugin.callsite;
public class PlatformExpectedError extends Error {
public PlatformExpectedError() {
}
public PlatformExpectedError(String message) {
super(message);
}
public PlatformExpectedError(String message, Throwable cause) {
super(message, cause);
}
public PlatformExpectedError(Throwable cause) {
super(cause);
}
public PlatformExpectedError(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}

View File

@@ -0,0 +1,64 @@
package me.shedaniel.architect.plugin.callsite;
import java.lang.invoke.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
public class PlatformMethods {
public static CallSite platform(MethodHandles.Lookup lookup, String name, MethodType type) {
Class<?> lookupClass = lookup.lookupClass();
String lookupType = lookupClass.getName().replace("$", "") + "Impl";
String platformExpectedClass = lookupType.substring(0, lookupType.lastIndexOf('.')) + "." + getModLoader() + "." +
lookupType.substring(lookupType.lastIndexOf('.') + 1);
Class<?> newClass;
try {
newClass = Class.forName(platformExpectedClass, false, lookupClass.getClassLoader());
} catch (ClassNotFoundException exception) {
throw new PlatformExpectedError(lookupClass.getName() + "#" + name + " expected platform implementation in " + platformExpectedClass +
"#" + name + ", but the class doesn't exist!", exception);
}
MethodHandle platformMethod;
try {
platformMethod = lookup.findStatic(newClass, name, type);
} catch (NoSuchMethodException exception) {
throw new PlatformExpectedError(lookupClass.getName() + "#" + name + " expected platform implementation in " + platformExpectedClass +
"#" + name + ", but the method doesn't exist!", exception);
} catch (IllegalAccessException exception) {
throw new PlatformExpectedError(lookupClass.getName() + "#" + name + " expected platform implementation in " + platformExpectedClass +
"#" + name + ", but the method's modifier doesn't match the access requirements!", exception);
}
return new ConstantCallSite(platformMethod);
}
private static String modLoader = null;
public static String getModLoader() {
try {
return (String) Class.forName("me.shedaniel.architectury.platform.Platform").getDeclaredMethod("getModLoader").invoke(null);
} catch (Throwable ignored) {
}
if (modLoader == null) {
List<String> loader = new ArrayList<>();
HashMap<String, String> MOD_LOADERS = new HashMap<>();
MOD_LOADERS.put("net.fabricmc.loader.FabricLoader", "fabric");
MOD_LOADERS.put("net.minecraftforge.fml.common.Mod", "forge");
for (Map.Entry<String, String> entry : MOD_LOADERS.entrySet()) {
try {
Class.forName(entry.getKey(), false, null);
loader.add(entry.getValue());
break;
} catch (ClassNotFoundException ignored) {
}
}
if (loader.isEmpty())
throw new IllegalStateException("No detected mod loader!");
if (loader.size() >= 2)
System.err.println("Detected multiple mod loaders! Something is wrong on the classpath! " + String.join(", ", loader));
modLoader = loader.get(0);
}
return modLoader;
}
}

View File

@@ -21,6 +21,10 @@ open class ArchitectPluginExtension(val project: Project) {
}
fun common(forgeEnabled: Boolean) {
with(project.dependencies) {
add("compileOnly", "me.shedaniel:architectury-annotations:+")
}
if (forgeEnabled) {
project.configurations.create("mcp")
project.configurations.create("mcpGenerateMod")

View File

@@ -1,17 +1,91 @@
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 transformExpectPlatform(): (ClassNode, (String, ByteArray) -> Unit) -> ClassNode = { clazz, classAdder ->
clazz.methods.filter { method -> method?.visibleAnnotations?.any { it.desc == expectPlatform } == true }
.forEach { method ->
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 = false
return { clazz, classAdder ->
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 -> {
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()
})
}
}
}
}
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 {
@@ -49,7 +123,7 @@ fun transformExpectPlatform(): (ClassNode, (String, ByteArray) -> Unit) -> Class
val handle = Handle(
Opcodes.H_INVOKESTATIC,
"me/shedaniel/architectury/PlatformMethods",
platformMethodsClass,
"platform",
methodType.toMethodDescriptorString(),
false
@@ -68,7 +142,8 @@ fun transformExpectPlatform(): (ClassNode, (String, ByteArray) -> Unit) -> Class
}
}
clazz
clazz
}
}
private fun InsnList.addLoad(type: Char, index: Int) {

View File

@@ -70,7 +70,7 @@ open class TransformTask : Jar() {
}
project.logger.lifecycle(":transforming " + input.fileName + " => " + intermediate.fileName)
Transform.transform(intermediate, output, transformExpectPlatform())
Transform.transform(intermediate, output, transformExpectPlatform(project))
Files.deleteIfExists(intermediate)

View File

@@ -12,8 +12,10 @@ import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
import java.util.zip.ZipOutputStream
typealias ClassTransformer = (clazz: ClassNode, classAdder: (String, ByteArray) -> Unit) -> ClassNode
object Transform {
inline fun transform(input: Path, output: Path, transform: (ClassNode, (String, ByteArray) -> Unit) -> ClassNode) {
inline fun transform(input: Path, output: Path, transform: ClassTransformer) {
output.toFile().delete()
if (!Files.exists(input)) {
@@ -50,7 +52,7 @@ object Transform {
reader.accept(node, ClassReader.EXPAND_FRAMES)
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)
transform(node) { name, bytes ->
zipOutputStream.putNextEntry(ZipEntry(name))
zipOutputStream.putNextEntry(ZipEntry("$name.class"))
zipOutputStream.write(bytes)
zipOutputStream.closeEntry()
}.accept(writer)