diff --git a/conventions/build.gradle.kts b/conventions/build.gradle.kts index 851d088..cbcf7c0 100644 --- a/conventions/build.gradle.kts +++ b/conventions/build.gradle.kts @@ -8,5 +8,9 @@ repositories { dependencies { implementation(libs.dokka) + implementation(libs.ksp) implementation(libs.kotlin.jvm) + implementation(libs.kotlin.metadata) + implementation(libs.asm) + implementation(libs.asm.commons) } \ No newline at end of file diff --git a/conventions/src/main/kotlin/relocator-convention.gradle.kts b/conventions/src/main/kotlin/relocator-convention.gradle.kts new file mode 100644 index 0000000..c9626f8 --- /dev/null +++ b/conventions/src/main/kotlin/relocator-convention.gradle.kts @@ -0,0 +1,191 @@ +import com.google.devtools.ksp.gradle.KspTaskJvm +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.objectweb.asm.ClassReader +import org.objectweb.asm.ClassVisitor +import org.objectweb.asm.ClassWriter +import org.objectweb.asm.Opcodes +import org.objectweb.asm.Type +import org.objectweb.asm.commons.Remapper +import org.objectweb.asm.commons.SimpleRemapper +import org.objectweb.asm.tree.ClassNode +import kotlin.io.path.createDirectories +import kotlin.io.path.writeBytes +import kotlin.metadata.KmClass +import kotlin.metadata.Visibility +import kotlin.metadata.jvm.JvmMetadataVersion +import kotlin.metadata.jvm.KotlinClassMetadata +import kotlin.metadata.visibility + +plugins { + id("com.google.devtools.ksp") + id("kotlin-convention") +} + +repositories { + mavenCentral() +} + +dependencies { + implementation(project(":relocator")) + ksp(project(":relocator")) +} + +tasks { + afterEvaluate { + val main by sourceSets.getting + val kspKotlin by getting(KspTaskJvm::class) + val compileKotlin by getting(KotlinCompile::class) + + val generateStubs by registering(GenerateStubs::class) { + dependsOn(compileKotlin) + mapping = kspKotlin.destination.map { it.resolve("resources").resolve("incompatible-types.txt") } + compilerOutput = compileKotlin.destinationDirectory + output = layout.buildDirectory.get().dir("classes").dir("stubs").dir(main.name) + } + + (main.output.classesDirs as ConfigurableFileCollection).from(generateStubs) + } +} + +abstract class GenerateStubs : DefaultTask() { + @get:InputFile + abstract val mapping: RegularFileProperty + + @get:InputDirectory + abstract val compilerOutput: DirectoryProperty + + @get:OutputDirectory + abstract val output: DirectoryProperty + + @TaskAction + fun generate() { + val file = mapping.get().asFile + if (!file.exists()) return + + val compilerOutputDir = compilerOutput.get().asFile + val outputDir = output.get().asFile + + val mapping = file.useLines { lines -> + lines.map { it.trim() }.filter { it.isNotEmpty() }.associate { + val parts = it.split('=') + parts[1] to parts[0] + } + } + + val remapper = SimpleRemapper(mapping) + + for ((internalOriginalName, internalStubName) in mapping) { + val stubOutput = outputDir.resolveClass(internalStubName).toPath() + val originalClassFile = compilerOutputDir.resolveClass(internalOriginalName) + + if (!originalClassFile.exists()) error("Relocated member was never compiled: $internalOriginalName") + stubOutput.also { it.parent.createDirectories() }.writeBytes( + generateStub(internalStubName, internalOriginalName, originalClassFile, remapper) + ) + } + } + + private fun generateStub( + internalStubName: String, + internalOriginalName: String, + originalClassFile: File, + remapper: Remapper, + ): ByteArray { + // old-fashioned bytecode weaving baby + val reader = ClassReader(originalClassFile.inputStream()) + val node = ClassNode() + reader.accept(node, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG) + + return with(ClassWriter(0)) { + visit( + /* version = */ Opcodes.V1_8, + /* access = */ node.access or Opcodes.ACC_SYNTHETIC, + /* name = */ internalStubName, + /* signature = */ null, + /* superName = */ remapper.map(node.superName) ?: node.superName, + /* interfaces = */ node.interfaces.map { remapper.map(it) ?: it } + .toTypedArray().takeIf { it.isNotEmpty() } + ) + + visitMetadata(KotlinClassMetadata.Class( + KmClass().apply { + name = "${internalStubName}_Compat" + visibility = Visibility.INTERNAL + }, + JvmMetadataVersion.LATEST_STABLE_SUPPORTED, + flags = 0 + ).write(), 3) + + for (field in node.fields) visitField( + /* access = */ field.access, + /* name = */ field.name, + /* descriptor = */ remapper.mapDesc(field.desc), + /* signature = */ null, + /* value = */ null + ).visitEnd() + + for (method in node.methods) with( + visitMethod( + /* access = */ method.access, + /* name = */ method.name, + /* descriptor = */ remapper.mapMethodDesc(method.desc), + /* signature = */ null, + /* exceptions = */ null + ) + ) { + var locals = Type.getArgumentCount(method.desc) + if (method.access and Opcodes.ACC_STATIC == 0) locals++ + + visitCode() + visitInsn(Opcodes.ACONST_NULL) + visitInsn(Opcodes.ATHROW) + visitMaxs(1, locals) + visitEnd() + } + + with(visitMethod(Opcodes.ACC_STATIC, "", "()V", null, null)) { + visitCode() + + visitTypeInsn(Opcodes.NEW, "java/lang/UnsupportedOperationException") + visitInsn(Opcodes.DUP) + visitLdcInsn( + "This class is deprecated in favour of $internalOriginalName, please update your dependencies!" + ) + + visitMethodInsn( + /* opcode = */ Opcodes.INVOKESPECIAL, + /* owner = */ "java/lang/UnsupportedOperationException", + /* name = */ "", + /* descriptor = */ "(Ljava/lang/String;)V", + /* isInterface = */ false + ) + + visitInsn(Opcodes.ATHROW) + + visitMaxs(3, 0) + visitEnd() + } + + visitEnd() + toByteArray() + } + } + + private fun ClassVisitor.visitMetadata( + annotation: Metadata, + kind: Int = annotation.kind, + ) = with(visitAnnotation("Lkotlin/Metadata;", true)) { + visit("mv", annotation.metadataVersion) + visit("xi", annotation.extraInt) + visit("xs", annotation.extraString) + visit("k", kind) + visit("bv", annotation.bytecodeVersion) + visit("pn", annotation.packageName) + with(visitArray("d1")) { annotation.data1.forEach { visit(null, it) }; visitEnd() } + with(visitArray("d2")) { annotation.data2.forEach { visit(null, it) }; visitEnd() } + visitEnd() + } + + private fun File.resolveClass(internalName: String) = + "$internalName.class".split('/').fold(this) { acc, curr -> acc.resolve(curr) } +} \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8703bf6..ba89f93 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,6 +13,7 @@ dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dok dokka-versioning = { module = "org.jetbrains.dokka:versioning-plugin", version.ref = "dokka" } coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlin-jvm = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } +kotlin-metadata = { module = "org.jetbrains.kotlin:kotlin-metadata-jvm", version.ref = "kotlin" } tiny-remapper = { module = "net.fabricmc:tiny-remapper", version = "0.9.0" } ksp = { module = "com.google.devtools.ksp:com.google.devtools.ksp.gradle.plugin", version.ref = "ksp" } ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } diff --git a/lib/build.gradle.kts b/lib/build.gradle.kts index f52c85a..843c37d 100644 --- a/lib/build.gradle.kts +++ b/lib/build.gradle.kts @@ -1,8 +1,8 @@ plugins { id("kotlin-convention") id("published-library") + id("relocator-convention") alias(libs.plugins.dokka) - alias(libs.plugins.ksp) } repositories { @@ -18,7 +18,4 @@ dependencies { api(libs.bundles.asm) api(libs.coroutines.core) testImplementation(kotlin("test")) - - implementation(projects.relocator) - ksp(projects.relocator) } \ No newline at end of file diff --git a/lib/src/test/kotlin/com/grappenmaker/mappings/TestAW.kt b/lib/src/test/kotlin/com/grappenmaker/mappings/TestAW.kt index 983ec89..4754fdc 100644 --- a/lib/src/test/kotlin/com/grappenmaker/mappings/TestAW.kt +++ b/lib/src/test/kotlin/com/grappenmaker/mappings/TestAW.kt @@ -1,6 +1,18 @@ package com.grappenmaker.mappings -import com.grappenmaker.mappings.aw.* +import com.grappenmaker.mappings.aw.AccessMask +import com.grappenmaker.mappings.aw.AccessWidener +import com.grappenmaker.mappings.aw.AccessWidenerVisitor +import com.grappenmaker.mappings.aw.AccessedMember +import com.grappenmaker.mappings.aw.AccessedClass +import com.grappenmaker.mappings.aw.AccessWidenerTree +import com.grappenmaker.mappings.aw.MemberIdentifier +import com.grappenmaker.mappings.aw.loadAccessWidener +import com.grappenmaker.mappings.aw.remap +import com.grappenmaker.mappings.aw.toTree +import com.grappenmaker.mappings.aw.applyWidener +import com.grappenmaker.mappings.aw.plus +import com.grappenmaker.mappings.aw.write import org.objectweb.asm.Opcodes.* import org.objectweb.asm.tree.ClassNode import kotlin.test.Test diff --git a/lib/src/test/kotlin/com/grappenmaker/mappings/TestMappings.kt b/lib/src/test/kotlin/com/grappenmaker/mappings/TestMappings.kt index bb87a7d..ba0230b 100644 --- a/lib/src/test/kotlin/com/grappenmaker/mappings/TestMappings.kt +++ b/lib/src/test/kotlin/com/grappenmaker/mappings/TestMappings.kt @@ -1,5 +1,11 @@ package com.grappenmaker.mappings +import com.grappenmaker.mappings.format.CSRGMappingsFormat +import com.grappenmaker.mappings.format.RecafMappingsFormat +import com.grappenmaker.mappings.format.ProguardMappingsFormat +import com.grappenmaker.mappings.format.TinyMappings +import com.grappenmaker.mappings.format.CompactedMappingsFormat +import com.grappenmaker.mappings.format.write import com.grappenmaker.mappings.remap.MappingsRemapper import com.grappenmaker.mappings.remap.remap import org.objectweb.asm.Opcodes.* diff --git a/relocator/src/main/kotlin/com/grappenmaker/mappings/MappingsRelocator.kt b/relocator/src/main/kotlin/com/grappenmaker/mappings/MappingsRelocator.kt index 38a8b3c..d57a93e 100644 --- a/relocator/src/main/kotlin/com/grappenmaker/mappings/MappingsRelocator.kt +++ b/relocator/src/main/kotlin/com/grappenmaker/mappings/MappingsRelocator.kt @@ -15,35 +15,70 @@ class MappingsRelocatorProvider : SymbolProcessorProvider { override fun create(environment: SymbolProcessorEnvironment) = MappingsRelocatorProcessor(environment.codeGenerator) } -private const val originalMonoPackage = "com.grappenmaker.mappings" - class MappingsRelocatorProcessor(private val generator: CodeGenerator) : SymbolProcessor { - private val KSAnnotated.level - get() = getAnnotationsByType(Relocated::class).singleOrNull()?.level ?: DeprecationLevel.WARNING + private var ranOnce = false + get() { + val temp = field + field = true + return temp + } + private val KSAnnotated.relocated get() = getAnnotationsByType(Relocated::class).singleOrNull() private fun maxOf(lhs: DeprecationLevel, rhs: DeprecationLevel) = if (lhs > rhs) lhs else rhs + private val KSDeclaration.internalName: String + get() = parentDeclaration?.let { "${it.internalName}$${simpleName.asString()}" } + ?: "${packageName.asString().replace('.', '/')}/${simpleName.asString()}" + override fun process(resolver: Resolver): List { + if (ranOnce) return emptyList() + val symbols = resolver.getSymbolsWithAnnotation(Relocated::class.java.name) val toProcess = symbols.toMutableList() for (symbol in symbols) if (symbol is KSFile) toProcess += symbol.declarations + if (toProcess.isEmpty()) return emptyList() val groups = toProcess.groupBy { it.containingFile } + val incompatibleWriter = generator.createNewFile( + dependencies = Dependencies(false, *groups.keys.filterNotNull().toTypedArray()), + packageName = "", + fileName = "incompatible-types", + extensionName = "txt" + ).writer() + + fun KSClassDeclaration.recurse(prefix: String, sep: Char = '/') { + if (classKind == ClassKind.ENUM_ENTRY) return + + val newPrefix = prefix + sep + simpleName.asString() + incompatibleWriter.appendLine("$newPrefix=$internalName") + for (member in declarations) if (member is KSClassDeclaration) member.recurse(newPrefix, '$') + } + for ((file, children) in groups) { - if (file == null || file.packageName.asString() == originalMonoPackage) continue + if (file == null) continue + + val baseAnnotation = file.relocated + val originalBasePackage = baseAnnotation?.originalPackage ?: originalMonoPackage + if (file.packageName.asString() == originalBasePackage) continue - val baseDeprecation = file.level + val baseDeprecation = baseAnnotation?.level ?: DeprecationLevel.WARNING val builder = FileSpec.builder(originalMonoPackage, file.fileName.removeSuffix(".kt")) .indent(" ") .addFileComment("This file contains auto-generated binary compatibility-preserving delegates\n") .addFileComment("Do not modify!") + val internalBasePackage = originalBasePackage.replace('.', '/') + children.fold(builder) { acc, curr -> - curr.process(acc, maxOf(baseDeprecation, curr.level)) + if (curr is KSClassDeclaration) curr.recurse(internalBasePackage) + + curr.process(acc, maxOf(baseDeprecation, curr.relocated?.level ?: DeprecationLevel.WARNING)) }.build().writeTo(generator, aggregating = false) } - return (groups[null] ?: emptyList()).filterNot { it is KSFile } + incompatibleWriter.close() + + return emptyList() } private fun > T.deprecate( @@ -213,6 +248,8 @@ class MappingsRelocatorProcessor(private val generator: CodeGenerator) : SymbolP } } +private const val originalMonoPackage = "com.grappenmaker.mappings" + @Retention @Target( AnnotationTarget.FILE, @@ -222,4 +259,7 @@ class MappingsRelocatorProcessor(private val generator: CodeGenerator) : SymbolP AnnotationTarget.FUNCTION, AnnotationTarget.PROPERTY, ) -annotation class Relocated(val level: DeprecationLevel = DeprecationLevel.WARNING) \ No newline at end of file +annotation class Relocated( + val originalPackage: String = originalMonoPackage, + val level: DeprecationLevel = DeprecationLevel.WARNING +) \ No newline at end of file