diff --git a/build.gradle.kts b/build.gradle.kts index 256d1186b..cb22bec14 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -26,6 +26,8 @@ description = providers.gradleProperty("POM_DESCRIPTION").get() dokka { dokkaPublications.html { outputDirectory = rootDir.resolve("docs/api") } } +java { toolchain { languageVersion = JavaLanguageVersion.of(25) } } + kotlin { explicitApi() @OptIn(ExperimentalAbiValidation::class) abiValidation { enabled = true } @@ -219,6 +221,7 @@ kotlin.target.compilations { tasks.withType().configureEach { options.release = libs.versions.jdkRelease.get().toInt() + options.compilerArgs.add("--enable-preview") } tasks.pluginUnderTestMetadata { pluginClasspath.from(testPluginClasspath) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 58d9b4779..e28e0c4b4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -jdkRelease = "17" +jdkRelease = "24" minGradle = "9.0.0" kotlin = "2.3.20-Beta1" moshi = "1.15.2" diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 01b1a3d42..b29d70d4c 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -380,7 +380,10 @@ abstract class BasePluginTest { return gradleRunner( projectDir = projectRoot, arguments = commonGradleArgs + arguments, - block = block, + block = { + forwardOutput() + block() + }, ) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ClassFileHelper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ClassFileHelper.kt new file mode 100644 index 000000000..546e7fff4 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ClassFileHelper.kt @@ -0,0 +1,304 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import java.lang.classfile.ClassBuilder +import java.lang.classfile.ClassElement +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassSignature +import java.lang.classfile.ClassTransform +import java.lang.classfile.CodeBuilder +import java.lang.classfile.CodeElement +import java.lang.classfile.CodeTransform +import java.lang.classfile.FieldModel +import java.lang.classfile.Interfaces +import java.lang.classfile.MethodModel +import java.lang.classfile.MethodSignature +import java.lang.classfile.Opcode +import java.lang.classfile.Signature +import java.lang.classfile.Superclass +import java.lang.classfile.attribute.CodeAttribute +import java.lang.classfile.attribute.SignatureAttribute +import java.lang.classfile.constantpool.ClassEntry +import java.lang.classfile.constantpool.StringEntry +import java.lang.classfile.instruction.ConstantInstruction +import java.lang.classfile.instruction.FieldInstruction +import java.lang.classfile.instruction.InvokeInstruction +import java.lang.classfile.instruction.NewObjectInstruction +import java.lang.classfile.instruction.NewReferenceArrayInstruction +import java.lang.classfile.instruction.TypeCheckInstruction +import java.lang.constant.ClassDesc +import java.lang.constant.ConstantDesc +import java.lang.constant.MethodTypeDesc +import java.util.function.Function + +internal object ClassFileHelper { + + fun remapClass( + classBytes: ByteArray, + mapFunction: Function, + mapValueFunction: Function, + ): ByteArray { + val cc = ClassFile.of() + val classModel = cc.parse(classBytes) + return cc.transformClass(classModel, SimpleClassRemapper(mapFunction, mapValueFunction)) + } + + private class SimpleClassRemapper( + private val mapFunction: Function, + private val mapValueFunction: Function, + ) : ClassTransform { + override fun accept(builder: ClassBuilder, element: ClassElement) { + when (element) { + is FieldModel -> { + // Remap field descriptor + val oldDesc = element.fieldTypeSymbol() + val newDesc = mapFunction.apply(oldDesc) + builder.withField(element.fieldName().stringValue(), newDesc) { fb -> + fb.withFlags(element.flags().flagsMask()) + for (elem in element) { + if (elem is SignatureAttribute) { + // Field Signature -> TypeSignature + fb.with( + SignatureAttribute.of( + remapSignature(Signature.parseFrom(elem.signature().stringValue()), mapFunction) + ) + ) + } else { + fb.with(elem) + } + } + } + } + is MethodModel -> { + // Remap method descriptor + val oldDesc = element.methodTypeSymbol() + val newDesc = mapMethodDesc(oldDesc, mapFunction) + builder.withMethod( + element.methodName().stringValue(), + newDesc, + element.flags().flagsMask(), + ) { mb -> + for (elem in element) { + if (elem is CodeAttribute) { + mb.transformCode(elem, SimpleCodeRemapper(mapFunction, mapValueFunction)) + } else if (elem is SignatureAttribute) { + // Method Signature -> MethodSignature + try { + val ms = MethodSignature.parseFrom(elem.signature().stringValue()) + mb.with(SignatureAttribute.of(remapMethodSignature(ms, mapFunction))) + } catch (e: Exception) { + e.printStackTrace() + // Fallback or rethrow? Usually parseFrom shouldn't fail on valid class. + // If it's not a MethodSignature (e.g. malformed), copy original + mb.with(elem) + } + } else { + mb.with(elem) + } + } + } + } + is Superclass -> { + // Remap superclass + val newSc = mapFunction.apply(element.superclassEntry().asSymbol()) + builder.withSuperclass(builder.constantPool().classEntry(newSc)) + } + is Interfaces -> { + val newInterfaces = ArrayList() + for (iface in element.interfaces()) { + newInterfaces.add( + builder.constantPool().classEntry(mapFunction.apply(iface.asSymbol())) + ) + } + builder.withInterfaces(newInterfaces) + } + is SignatureAttribute -> { + // Class Signature + try { + val cs = ClassSignature.parseFrom(element.signature().stringValue()) + builder.with(SignatureAttribute.of(remapClassSignature(cs, mapFunction))) + } catch (e: Exception) { + e.printStackTrace() + builder.with(element) + } + } + else -> { + builder.with(element) + } + } + } + } + + private class SimpleCodeRemapper( + private val mapFunction: Function, + private val mapValueFunction: Function, + ) : CodeTransform { + + override fun accept(builder: CodeBuilder, element: CodeElement) { + when (element) { + is TypeCheckInstruction -> { // CHECKCAST, INSTANCEOF + val newType = mapFunction.apply(element.type().asSymbol()) + if (element.opcode() == Opcode.CHECKCAST) { + builder.checkcast(newType) + } else if (element.opcode() == Opcode.INSTANCEOF) { + builder.instanceOf(newType) + } else { + builder.with(element) // fallback + } + } + is NewObjectInstruction -> { + builder.new_(mapFunction.apply(element.className().asSymbol())) + } + is NewReferenceArrayInstruction -> { + builder.anewarray(mapFunction.apply(element.componentType().asSymbol())) + } + is FieldInstruction -> { + val newOwner = mapFunction.apply(element.owner().asSymbol()) + val newType = mapFunction.apply(element.typeSymbol()) + val name = element.name().stringValue() + val op = element.opcode() + if (op == Opcode.GETFIELD) builder.getfield(newOwner, name, newType) + else if (op == Opcode.PUTFIELD) builder.putfield(newOwner, name, newType) + else if (op == Opcode.GETSTATIC) builder.getstatic(newOwner, name, newType) + else if (op == Opcode.PUTSTATIC) builder.putstatic(newOwner, name, newType) + else builder.with(element) + } + is InvokeInstruction -> { + val newOwner = mapFunction.apply(element.owner().asSymbol()) + val newDesc = mapMethodDesc(element.typeSymbol(), mapFunction) + val name = element.name().stringValue() + val isInterface = element.isInterface() + val op = element.opcode() + if (op == Opcode.INVOKEVIRTUAL) builder.invokevirtual(newOwner, name, newDesc) + else if (op == Opcode.INVOKESPECIAL) + builder.invokespecial(newOwner, name, newDesc, isInterface) + else if (op == Opcode.INVOKESTATIC) + builder.invokestatic(newOwner, name, newDesc, isInterface) + else if (op == Opcode.INVOKEINTERFACE) builder.invokeinterface(newOwner, name, newDesc) + else builder.with(element) + } + is ConstantInstruction.LoadConstantInstruction -> { + if (element.constantEntry() is ClassEntry) { + val ce = element.constantEntry() as ClassEntry + builder.ldc(mapFunction.apply(ce.asSymbol())) + } else if (element.constantEntry() is StringEntry) { + val se = element.constantEntry() as StringEntry + builder.ldc(mapValueFunction.apply(se.stringValue()) as ConstantDesc) + } else { + builder.with(element) + } + } + else -> { + builder.with(element) + } + } + } + } + + private fun mapMethodDesc( + desc: MethodTypeDesc, + mapFunction: Function, + ): MethodTypeDesc { + val newReturnType = mapFunction.apply(desc.returnType()) + val newParamTypes = + desc.parameterList().stream().map(mapFunction).toArray { size -> + arrayOfNulls(size) + } + return MethodTypeDesc.of(newReturnType, *newParamTypes) + } + + private fun remapClassSignature( + cs: ClassSignature, + mapFunction: Function, + ): ClassSignature { + return ClassSignature.of( + remapTypeParams(cs.typeParameters(), mapFunction), // Expecting List + remapSignature(cs.superclassSignature(), mapFunction) as Signature.ClassTypeSig, + *cs + .superinterfaceSignatures() + .stream() + .map { s -> remapSignature(s, mapFunction) as Signature.ClassTypeSig } + .toArray { size -> arrayOfNulls(size) }, + ) + } + + private fun remapMethodSignature( + ms: MethodSignature, + mapFunction: Function, + ): MethodSignature { + return MethodSignature.of( + remapTypeParams(ms.typeParameters(), mapFunction), // Expecting List + ms + .throwableSignatures() + .stream() + .map { s -> remapSignature(s, mapFunction) as Signature.ThrowableSig } + .toList(), + remapSignature(ms.result(), mapFunction), + *ms + .arguments() + .stream() + .map { s -> remapSignature(s, mapFunction) } + .toArray { size -> arrayOfNulls(size) }, + ) + } + + private fun remapSignature( + sig: Signature, + mapFunction: Function, + ): Signature { + System.err.println("DEBUG: remapSignature " + sig.javaClass.simpleName) + if (sig is Signature.ClassTypeSig) { + val newDesc = mapFunction.apply(sig.classDesc()) + val newArgs = ArrayList() + for (arg in sig.typeArgs()) { + if (arg is Signature.TypeArg.Bounded) { + val newBound = remapSignature(arg.boundType(), mapFunction) as Signature.RefTypeSig + val indicator = arg.wildcardIndicator() + if (indicator == Signature.TypeArg.Bounded.WildcardIndicator.NONE) { + newArgs.add(Signature.TypeArg.of(newBound)) + } else if (indicator == Signature.TypeArg.Bounded.WildcardIndicator.EXTENDS) { + newArgs.add(Signature.TypeArg.extendsOf(newBound)) + } else if (indicator == Signature.TypeArg.Bounded.WildcardIndicator.SUPER) { + newArgs.add(Signature.TypeArg.superOf(newBound)) + } + } else if (arg is Signature.TypeArg.Unbounded) { + newArgs.add(arg) + } + } + val outer = sig.outerType().orElse(null) + val newOuter = + if (outer != null) remapSignature(outer, mapFunction) as Signature.ClassTypeSig else null + if (newOuter != null) { + return Signature.ClassTypeSig.of(newOuter, newDesc, *newArgs.toTypedArray()) + } else { + return Signature.ClassTypeSig.of(newDesc, *newArgs.toTypedArray()) + } + } else if (sig is Signature.ArrayTypeSig) { + return Signature.ArrayTypeSig.of(remapSignature(sig.componentSignature(), mapFunction)) + } else if (sig is Signature.TypeVarSig) { + return sig // Keep type var name + } else if (sig is Signature.BaseTypeSig) { + return sig + } + return sig + } + + private fun remapTypeParams( + params: List, + mapFunction: Function, + ): List { + val newParams = ArrayList() + for (p in params) { + val newClassBound = + if (p.classBound().isPresent) + remapSignature(p.classBound().get(), mapFunction) as Signature.RefTypeSig + else null + val newInterfaceBounds = + p.interfaceBounds() + .stream() + .map { s -> remapSignature(s, mapFunction) as Signature.RefTypeSig } + .toArray { size -> arrayOfNulls(size) } + newParams.add(Signature.TypeParam.of(p.identifier(), newClassBound, *newInterfaceBounds)) + } + return newParams + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt index c10dddab5..f652e22f2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt @@ -3,9 +3,8 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath +import java.lang.constant.ClassDesc import java.util.regex.Pattern -import org.vafer.jdeb.shaded.objectweb.asm.Opcodes -import org.vafer.jdeb.shaded.objectweb.asm.commons.Remapper /** * Modified from @@ -18,17 +17,27 @@ import org.vafer.jdeb.shaded.objectweb.asm.commons.Remapper internal class RelocatorRemapper( private val relocators: Set, private val onModified: () -> Unit = {}, -) : Remapper(Opcodes.ASM9) { +) { - override fun mapValue(value: Any): Any { - return if (value is String) { - mapName(value, mapLiterals = true) + fun map(desc: ClassDesc): ClassDesc { + val descriptor = desc.descriptorString() + // We only map class types (L...;), not primitives. + if (descriptor.length < 3 || descriptor[0] != 'L') return desc + + // Extract internal name: Lcom/example/Foo; -> com/example/Foo + val internalName = descriptor.substring(1, descriptor.length - 1) + val newInternalName = mapName(internalName, mapLiterals = false) + + return if (newInternalName != internalName) { + ClassDesc.ofDescriptor("L$newInternalName;") } else { - super.mapValue(value) + desc } } - override fun map(internalName: String): String = mapName(internalName) + fun mapValue(value: String): String { + return mapName(value, true) + } private fun mapName(name: String, mapLiterals: Boolean = false): String { // Maybe a list of types. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt index 9b8c0c933..ca9f41c9a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.kt @@ -4,6 +4,7 @@ package com.github.jengelman.gradle.plugins.shadow.tasks +import com.github.jengelman.gradle.plugins.shadow.internal.ClassFileHelper 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 @@ -32,9 +33,6 @@ import org.gradle.api.internal.file.copy.FileCopyDetailsInternal import org.gradle.api.logging.Logging import org.gradle.api.tasks.WorkResult import org.gradle.api.tasks.WorkResults -import org.vafer.jdeb.shaded.objectweb.asm.ClassReader -import org.vafer.jdeb.shaded.objectweb.asm.ClassWriter -import org.vafer.jdeb.shaded.objectweb.asm.commons.ClassRemapper /** * Modified from @@ -222,19 +220,16 @@ constructor( // 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 cv = ClassRemapper(cw, remapper) - - try { - cr.accept(cv, ClassReader.EXPAND_FRAMES) - } catch (t: Throwable) { - throw GradleException("Error in ASM processing class $path", t) - } - val newBytes = + try { + ClassFileHelper.remapClass(bytes, remapper::map, remapper::mapValue) + } catch (t: Throwable) { + throw GradleException("Error in Class-File API processing class $path", t) + } + + val resultBytes = if (modified) { - cw.toByteArray() + newBytes } else { // If we didn't need to change anything, keep the original bytes as-is bytes @@ -251,7 +246,7 @@ constructor( } // Now we put it back on so the class file is written out with the right extension. zipOutStr.putNextEntry(entry) - zipOutStr.write(newBytes) + zipOutStr.write(resultBytes) zipOutStr.closeEntry() } catch (_: ZipException) { logger.warn("We have a duplicate $relocatedPath in source project") diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt index 5cc83d4ea..9c7f3fed5 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import assertk.assertThat import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.internal.RelocatorRemapper +import java.lang.constant.ClassDesc import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource @@ -13,7 +14,7 @@ class RelocatorRemapperTest { fun relocateSignaturePatterns(input: String, expected: String) { val relocator = RelocatorRemapper(relocators = setOf(SimpleRelocator("org.package", "shadow.org.package"))) - assertThat(relocator.map(input)).isEqualTo(expected) + assertThat(relocator.map(ClassDesc.of(input))).isEqualTo(expected) } private companion object {