Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package com.github.jengelman.gradle.plugins.shadow.internal

import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext
import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
import java.lang.reflect.Field
import org.objectweb.asm.AnnotationVisitor
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassVisitor
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.Opcodes

internal class RelocationClassWriter(
classReader: ClassReader,
relocators: Set<Relocator>,
flags: Int = 0,
) : ClassWriter(classReader, flags) {
internal var isRelocated = false

init {
// If the class is a Kotlin class, we need to apply relocations to the symbol table.
if (classReader.isKotlinClass()) {

Copilot AI Aug 21, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isKotlinClass() method creates a ClassVisitor and processes the entire class file to check for a single annotation. This could be expensive for large classes. Consider using ClassReader.readUnsignedShort() and parsing annotations more efficiently, or caching this result to avoid repeated scanning.

Copilot uses AI. Check for mistakes.
val symbolTable = symbolTableField.get(this)

@Suppress("UNCHECKED_CAST")
val entries = entriesField.get(symbolTable) as Array<Any?>
entries.forEach { entryObj ->
if (entryObj != null) {
(symbolValueField.get(entryObj) as? String)?.let { value ->
val newValue = relocators.applyClassRelocation(value)
if (value != newValue) {
symbolValueField.set(entryObj, newValue)
isRelocated = true
}
}
}
}
}
}

private fun ClassReader.isKotlinClass(): Boolean {
var flag = false
accept(
object : ClassVisitor(Opcodes.ASM9) {
override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor? {
if (desc == "Lkotlin/Metadata;") {
flag = true
}
return super.visitAnnotation(desc, visible)
}
},
ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG or ClassReader.SKIP_FRAMES,
)
return flag
Comment thread
Goooler marked this conversation as resolved.
}

companion object Companion {
private val classWriterClass = ClassWriter::class.java
private val symbolTableClass: Class<*> = Class.forName("org.objectweb.asm.SymbolTable") // Package private.
private val symbolClass: Class<*> = Class.forName("org.objectweb.asm.Symbol") // Package private.

private val symbolTableField: Field = classWriterClass.getDeclaredField("symbolTable")
.apply { isAccessible = true }
private val entriesField: Field = symbolTableClass.getDeclaredField("entries")
.apply { isAccessible = true }
private val symbolValueField: Field = symbolClass.getDeclaredField("value")
.apply { isAccessible = true }

fun Iterable<Relocator>.applyClassRelocation(value: String): String = fold(value) { string, relocator ->
relocator.relocateClass(RelocateClassContext(string))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
package com.github.jengelman.gradle.plugins.shadow.tasks

import com.github.jengelman.gradle.plugins.shadow.internal.RelocationClassWriter
import com.github.jengelman.gradle.plugins.shadow.internal.RelocationClassWriter.Companion.applyClassRelocation
import com.github.jengelman.gradle.plugins.shadow.internal.RelocatorRemapper
import com.github.jengelman.gradle.plugins.shadow.internal.cast
import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext
import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext
import java.io.File
import java.util.GregorianCalendar
import java.util.zip.ZipException
import kotlin.metadata.jvm.JvmMetadataVersion
import kotlin.metadata.jvm.KmModule
import kotlin.metadata.jvm.KmPackageParts
import kotlin.metadata.jvm.KotlinModuleMetadata
import kotlin.metadata.jvm.UnstableMetadataApi
import org.apache.tools.zip.UnixStat
import org.apache.tools.zip.Zip64RequiredException
import org.apache.tools.zip.ZipEntry
Expand All @@ -23,7 +31,6 @@ import org.gradle.api.logging.Logging
import org.gradle.api.tasks.WorkResult
import org.gradle.api.tasks.WorkResults
import org.objectweb.asm.ClassReader
import org.objectweb.asm.ClassWriter
import org.objectweb.asm.commons.ClassRemapper

/**
Expand Down Expand Up @@ -156,17 +163,29 @@ public open class ShadowCopyAction(

private fun visitFile(fileDetails: FileCopyDetails) {
val path = fileDetails.path
if (path.endsWith(".class")) {
if (isUnused(path)) return
if (relocators.isEmpty()) {
fileDetails.writeToZip(path)
return
when {
path.endsWith(".class") -> {
if (isUnused(path)) return
if (relocators.isEmpty()) {
fileDetails.writeToZip(path)
} else {
fileDetails.remapClass(relocators)
}
}

path.endsWith(".kotlin_module") -> {
if (relocators.isEmpty()) {
fileDetails.writeToZip(path)
} else {
fileDetails.remapKotlinModule(relocators)
}
}

else -> {
val mapped = RelocatorRemapper(relocators).map(path)
if (transform(fileDetails, mapped)) return
fileDetails.writeToZip(mapped)
}
fileDetails.remapClass()
} else {
val mapped = RelocatorRemapper(relocators).map(path)
if (transform(fileDetails, mapped)) return
fileDetails.writeToZip(mapped)
}
}

Expand All @@ -183,7 +202,7 @@ public open class ShadowCopyAction(
* Applies remapping to the given class with the specified relocation path. The remapped class is then written
* to the zip file.
*/
private fun FileCopyDetails.remapClass() = file.readBytes().let { bytes ->
private fun FileCopyDetails.remapClass(relocators: Set<Relocator>) = file.readBytes().let { bytes ->
var modified = false
val remapper = RelocatorRemapper(relocators) { modified = true }

Expand All @@ -192,8 +211,8 @@ public open class ShadowCopyAction(
// to the original class names. This is not a problem at runtime (because these entries in the
// constant pool are never used), but confuses some tools such as Felix's maven-bundle-plugin
// that use the constant pool to determine the dependencies of a class.
val cw = ClassWriter(0)
val cr = ClassReader(bytes)
val cw = RelocationClassWriter(classReader = cr, relocators = relocators)
val cv = ClassRemapper(cw, remapper)

try {
Expand Down Expand Up @@ -226,6 +245,39 @@ public open class ShadowCopyAction(
}
}

/**
* Applies remapping to the given kotlin module with the specified relocation path.
* The remapped module is then written to the zip file.
*/
@OptIn(UnstableMetadataApi::class)
private fun FileCopyDetails.remapKotlinModule(relocators: Set<Relocator>) {
val mappedPath = RelocatorRemapper(relocators).mapPath(path)
file.inputStream().use { ins ->
val metadata = KotlinModuleMetadata.read(ins.readBytes())
val result = KmModule()
for ((pkg, parts) in metadata.kmModule.packageParts) {
result.packageParts[relocators.applyPathRelocation(pkg)] =
KmPackageParts(
parts.fileFacades.mapTo(mutableListOf()) {
relocators.applyPathRelocation(it)
},
parts.multiFileClassParts.entries.associateTo(mutableMapOf()) { (name, facade) ->
relocators.applyClassRelocation(name) to relocators.applyPathRelocation(facade)
},
)
}
Comment thread
Goooler marked this conversation as resolved.

val newMetadata = KotlinModuleMetadata(result, JvmMetadataVersion.LATEST_STABLE_SUPPORTED)

val entry = zipEntry(mappedPath, preserveFileTimestamps, lastModified) {
unixMode = UnixStat.FILE_FLAG or permissions.toUnixNumeric()
}
zipOutStr.putNextEntry(entry)
zipOutStr.write(newMetadata.write())
zipOutStr.closeEntry()
}
}

private fun transform(fileDetails: FileCopyDetails, mapped: String): Boolean {
val transformer = transformers.find { it.canTransformResource(fileDetails) } ?: return false
fileDetails.file.inputStream().use { inputStream ->
Expand Down Expand Up @@ -256,6 +308,10 @@ public open class ShadowCopyAction(
private val ZipOutputStream.entries: List<ZipEntry>
get() = this::class.java.getDeclaredField("entries").apply { isAccessible = true }.get(this).cast()

internal fun Iterable<Relocator>.applyPathRelocation(value: String): String = fold(value) { string, relocator ->
relocator.relocatePath(RelocatePathContext(string))
}

/**
* A copy of [org.gradle.api.internal.file.archive.ZipEntryConstants.CONSTANT_TIME_FOR_ZIP_ENTRIES].
*
Expand Down
131 changes: 0 additions & 131 deletions src/main/kotlin/com/xpdustry/ksr/KotlinRelocation.kt

This file was deleted.

43 changes: 0 additions & 43 deletions src/main/kotlin/com/xpdustry/ksr/KotlinShadowRelocatorPlugin.kt

This file was deleted.

Loading