Skip to content

Commit

Permalink
Add KSP convention with relocation tasks,
Browse files Browse the repository at this point in the history
  • Loading branch information
770grappenmaker committed Aug 2, 2024
1 parent 314cb08 commit 053b931
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 14 deletions.
4 changes: 4 additions & 0 deletions conventions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
191 changes: 191 additions & 0 deletions conventions/src/main/kotlin/relocator-convention.gradle.kts
Original file line number Diff line number Diff line change
@@ -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, "<clinit>", "()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 = */ "<init>",
/* 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) }
}
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
5 changes: 1 addition & 4 deletions lib/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
plugins {
id("kotlin-convention")
id("published-library")
id("relocator-convention")
alias(libs.plugins.dokka)
alias(libs.plugins.ksp)
}

repositories {
Expand All @@ -18,7 +18,4 @@ dependencies {
api(libs.bundles.asm)
api(libs.coroutines.core)
testImplementation(kotlin("test"))

implementation(projects.relocator)
ksp(projects.relocator)
}
14 changes: 13 additions & 1 deletion lib/src/test/kotlin/com/grappenmaker/mappings/TestAW.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 6 additions & 0 deletions lib/src/test/kotlin/com/grappenmaker/mappings/TestMappings.kt
Original file line number Diff line number Diff line change
@@ -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.*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<KSAnnotated> {
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 : Annotatable.Builder<T>> T.deprecate(
Expand Down Expand Up @@ -213,6 +248,8 @@ class MappingsRelocatorProcessor(private val generator: CodeGenerator) : SymbolP
}
}

private const val originalMonoPackage = "com.grappenmaker.mappings"

@Retention
@Target(
AnnotationTarget.FILE,
Expand All @@ -222,4 +259,7 @@ class MappingsRelocatorProcessor(private val generator: CodeGenerator) : SymbolP
AnnotationTarget.FUNCTION,
AnnotationTarget.PROPERTY,
)
annotation class Relocated(val level: DeprecationLevel = DeprecationLevel.WARNING)
annotation class Relocated(
val originalPackage: String = originalMonoPackage,
val level: DeprecationLevel = DeprecationLevel.WARNING
)

0 comments on commit 053b931

Please sign in to comment.