diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 30b5f19a8..47e7cd50b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,7 +5,7 @@ help you get started. ## Prerequisites -- **Java Development Kit (JDK)**: JDK 17 or higher is required to build the project. JDK 25 is recommended. +- **Java Development Kit (JDK)**: JDK 25 or higher is required to build the project. - **Gradle**: The project uses the Gradle Wrapper, so you do not need to install Gradle globally. Run command-line tasks using `./gradlew` (or `gradlew.bat` on Windows). diff --git a/build.gradle.kts b/build.gradle.kts index 7c04025a1..bf14ee14d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,10 +4,13 @@ import org.gradle.api.plugins.JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.JAVADOC_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME +import org.gradle.language.base.plugins.LifecycleBasePlugin.VERIFICATION_GROUP import org.gradle.plugin.compatibility.compatibility import org.jetbrains.kotlin.gradle.dsl.JvmDefaultMode import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinJvmCompilerOptions import org.jetbrains.kotlin.gradle.dsl.KotlinVersion +import org.jetbrains.kotlin.gradle.dsl.abi.BinariesSource import org.jetbrains.kotlin.gradle.dsl.abi.ExperimentalAbiValidation plugins { @@ -30,7 +33,11 @@ dokka { dokkaPublications.html { outputDirectory = rootDir.resolve("docs/api") } kotlin { explicitApi() - @OptIn(ExperimentalAbiValidation::class) abiValidation() + @OptIn(ExperimentalAbiValidation::class) + abiValidation { + // Check APIs under main and versioned source sets. + binariesSource = BinariesSource.NON_TEST_COMPILATIONS + } val jdkRelease = "17" compilerOptions { allWarningsAsErrors = true @@ -46,6 +53,8 @@ kotlin { } } +addMultiReleaseSourceSet(24) + lint { baseline = file("gradle/lint-baseline.xml") ignoreTestFixturesSources = true @@ -246,6 +255,10 @@ buildConfig { } } +tasks.jar { + manifest.attributes("Multi-Release" to true) +} + tasks.pluginUnderTestMetadata { pluginClasspath.from(testPluginClasspath) } tasks.check { dependsOn(tasks.withType()) } @@ -260,3 +273,63 @@ tasks.clean { rootDir.resolve("site"), ) } + +fun addMultiReleaseSourceSet(version: Int) { + kotlin.target.compilations.create("java${version}") { + associateWith(kotlin.target.compilations.getByName("main")) + compileTaskProvider { + compilerOptions { + this@compilerOptions as KotlinJvmCompilerOptions + jvmTarget = JvmTarget.fromTarget(version.toString()) + freeCompilerArgs.add("-Xjdk-release=$version") + } + } + compileJavaTaskProvider { options.release = version } + + tasks.jar { from(output.allOutputs) { into("META-INF/versions/$version") } } + + val versionedTest = + tasks.register("testJava${version}", Test::class) { + useJUnitPlatform() + group = VERIFICATION_GROUP + description = "Runs test suite using Java $version toolchain." + val testSourceSet = sourceSets.test.get() + testClassesDirs = testSourceSet.output.classesDirs + val testCpWithoutMainOutput = testSourceSet.runtimeClasspath - sourceSets.main.get().output + // Prefer MR classes on the classpath, so remove main output. + classpath = files(tasks.jar.flatMap { it.archiveFile }) + testCpWithoutMainOutput + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(version) + } + } + + val validateMultiReleaseJar = + with(tasks) { + if ("validateMultiReleaseJar" in names) { + named("validateMultiReleaseJar") + } else { + register("validateMultiReleaseJar", JavaExec::class) { + group = VERIFICATION_GROUP + description = "Validates the multi-release JAR." + mainModule = "jdk.jartool" + mainClass = "sun.tools.jar.Main" + val jarFile = jar.flatMap { it.archiveFile } + inputs.file(jarFile) + argumentProviders.add( + CommandLineArgumentProvider { + // https://docs.oracle.com/en/java/javase/25/docs/specs/man/jar.html + listOf("--validate", "--file", jarFile.get().asFile.absolutePath) + } + ) + } + } + } + + tasks.check { + dependsOn( + versionedTest, + validateMultiReleaseJar, + ) + } + } +} diff --git a/gradle.properties b/gradle.properties index 977f7d500..1763c2b7d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,6 +2,11 @@ # https://kotlinlang.org/docs/gradle-configure-project.html#dependency-on-the-standard-library kotlin.stdlib.default.dependency=false +# Keep associated Kotlin compilations from depending on archive tasks (e.g., jar), which can create circular task graphs in multi-release setups. +# https://kotlinlang.org/docs/gradle-configure-project.html#disable-use-of-artifact-in-compilation-task +# https://kotlinlang.org/docs/whatsnew2020.html#added-task-dependency-for-rare-cases-when-the-compile-task-lacks-one-on-an-artifact +kotlin.build.archivesTaskOutputAsFriendModule=false + org.gradle.caching=true org.gradle.configuration-cache=true org.gradle.configuration-cache.parallel=true diff --git a/settings.gradle.kts b/settings.gradle.kts index b7bcd8de6..14939c62e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -12,7 +12,10 @@ pluginManagement { } } -plugins { id("com.gradle.develocity") version "4.5.0" } +plugins { + id("com.gradle.develocity") version "4.5.0" + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} develocity { buildScan { @@ -42,3 +45,7 @@ rootProject.name = "shadow" enableFeaturePreview("NO_IMPLICIT_LOOKUP_IN_PARENT_PROJECTS") enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +check(JavaVersion.current() >= JavaVersion.VERSION_25) { + "Needs Java 25 or above to run, the current is ${JavaVersion.current()}" +} diff --git a/src/java24/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt b/src/java24/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt new file mode 100644 index 000000000..b93f6af57 --- /dev/null +++ b/src/java24/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/RelocatorRemapper.kt @@ -0,0 +1,588 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import java.lang.classfile.AccessFlags +import java.lang.classfile.Annotation +import java.lang.classfile.AnnotationElement +import java.lang.classfile.AnnotationValue +import java.lang.classfile.ClassFile +import java.lang.classfile.ClassFileVersion +import java.lang.classfile.ClassSignature +import java.lang.classfile.ClassTransform +import java.lang.classfile.CodeModel +import java.lang.classfile.CodeTransform +import java.lang.classfile.CustomAttribute +import java.lang.classfile.FieldModel +import java.lang.classfile.FieldTransform +import java.lang.classfile.Interfaces +import java.lang.classfile.MethodModel +import java.lang.classfile.MethodSignature +import java.lang.classfile.MethodTransform +import java.lang.classfile.Signature +import java.lang.classfile.Superclass +import java.lang.classfile.TypeAnnotation +import java.lang.classfile.attribute.AnnotationDefaultAttribute +import java.lang.classfile.attribute.CompilationIDAttribute +import java.lang.classfile.attribute.ConstantValueAttribute +import java.lang.classfile.attribute.DeprecatedAttribute +import java.lang.classfile.attribute.EnclosingMethodAttribute +import java.lang.classfile.attribute.ExceptionsAttribute +import java.lang.classfile.attribute.InnerClassInfo +import java.lang.classfile.attribute.InnerClassesAttribute +import java.lang.classfile.attribute.ModuleAttribute +import java.lang.classfile.attribute.ModuleExportInfo +import java.lang.classfile.attribute.ModuleHashesAttribute +import java.lang.classfile.attribute.ModuleMainClassAttribute +import java.lang.classfile.attribute.ModuleOpenInfo +import java.lang.classfile.attribute.ModulePackagesAttribute +import java.lang.classfile.attribute.ModuleProvideInfo +import java.lang.classfile.attribute.ModuleResolutionAttribute +import java.lang.classfile.attribute.ModuleTargetAttribute +import java.lang.classfile.attribute.NestHostAttribute +import java.lang.classfile.attribute.NestMembersAttribute +import java.lang.classfile.attribute.PermittedSubclassesAttribute +import java.lang.classfile.attribute.RecordAttribute +import java.lang.classfile.attribute.RecordComponentInfo +import java.lang.classfile.attribute.RuntimeInvisibleAnnotationsAttribute +import java.lang.classfile.attribute.RuntimeInvisibleParameterAnnotationsAttribute +import java.lang.classfile.attribute.RuntimeInvisibleTypeAnnotationsAttribute +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute +import java.lang.classfile.attribute.RuntimeVisibleParameterAnnotationsAttribute +import java.lang.classfile.attribute.RuntimeVisibleTypeAnnotationsAttribute +import java.lang.classfile.attribute.SignatureAttribute +import java.lang.classfile.attribute.SourceDebugExtensionAttribute +import java.lang.classfile.attribute.SourceFileAttribute +import java.lang.classfile.attribute.SourceIDAttribute +import java.lang.classfile.attribute.SyntheticAttribute +import java.lang.classfile.attribute.UnknownAttribute +import java.lang.classfile.constantpool.StringEntry +import java.lang.classfile.instruction.ConstantInstruction +import java.lang.classfile.instruction.ExceptionCatch +import java.lang.classfile.instruction.FieldInstruction +import java.lang.classfile.instruction.InvokeDynamicInstruction +import java.lang.classfile.instruction.InvokeInstruction +import java.lang.classfile.instruction.LocalVariable +import java.lang.classfile.instruction.LocalVariableType +import java.lang.classfile.instruction.NewMultiArrayInstruction +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.DirectMethodHandleDesc +import java.lang.constant.DynamicCallSiteDesc +import java.lang.constant.DynamicConstantDesc +import java.lang.constant.MethodHandleDesc +import java.lang.constant.MethodTypeDesc +import java.lang.constant.PackageDesc +import kotlin.jvm.optionals.getOrNull +import org.gradle.api.GradleException +import org.gradle.api.file.FileCopyDetails + +/** + * Applies remapping to the given class file using the provided relocators and returns the + * (possibly) remapped class bytes. If no remapping is required, the original bytes are returned. + */ +@Suppress("unused") // Used by Multi-Release JARs for Java 24+. +internal fun FileCopyDetails.remapClass(relocators: Set): ByteArray = + file.readBytes().let { bytes -> + var modified = false + val remapper = ClassFileRelocatorRemapper(relocators) { modified = true } + + val newBytes = + try { + val classFile = ClassFile.of() + val classModel = classFile.parse(bytes) + val newClassDesc = remapper.mapClassDesc(classModel.thisClass().asSymbol()) + classFile.transformClass(classModel, newClassDesc, remapper.asClassTransform()) + } catch (t: Throwable) { + throw GradleException("Error in Class-File API processing class $path", t) + } + + // If we didn't need to change anything, keep the original bytes as-is. + if (modified) newBytes else bytes + } + +private class ClassFileRelocatorRemapper( + private val relocators: Set, + private val onModified: () -> Unit, +) { + fun asClassTransform(): ClassTransform = ClassTransform { clb, cle -> + when (cle) { + is FieldModel -> + clb.withField( + cle.fieldName().stringValue(), + mapClassDesc(ClassDesc.ofDescriptor(cle.fieldType().stringValue())), + ) { fb -> + fb.withFlags(cle.flags().flagsMask()).transform(cle, asFieldTransform()) + } + is MethodModel -> + clb.withMethod( + cle.methodName().stringValue(), + cle.methodTypeSymbol().remap(), + cle.flags().flagsMask(), + ) { mb -> + mb.transform(cle, asMethodTransform()) + } + is Superclass -> clb.withSuperclass(mapClassDesc(cle.superclassEntry().asSymbol())) + is Interfaces -> + clb.withInterfaceSymbols(cle.interfaces().map { mapClassDesc(it.asSymbol()) }) + is SignatureAttribute -> clb.with(SignatureAttribute.of(cle.asClassSignature().remap())) + is InnerClassesAttribute -> + clb.with( + InnerClassesAttribute.of( + cle.classes().map { ici -> + val innerClassName = ici.innerClass().asSymbol().internalName() + val mappedInnerClass = mapClassDesc(ici.innerClass().asSymbol()) + InnerClassInfo.of( + mappedInnerClass, + ici.outerClass().map { mapClassDesc(it.asSymbol()) }, + ici.innerName().map { + mapInnerClassName( + innerClassName, + mappedInnerClass.internalName(), + it.stringValue(), + ) + }, + ici.flagsMask(), + ) + } + ) + ) + is EnclosingMethodAttribute -> + clb.with( + EnclosingMethodAttribute.of( + mapClassDesc(cle.enclosingClass().asSymbol()), + cle.enclosingMethodName().map { it.stringValue() }, + cle + .enclosingMethodType() + .map { MethodTypeDesc.ofDescriptor(it.stringValue()) } + .map { it.remap() }, + ) + ) + is RecordAttribute -> clb.with(RecordAttribute.of(cle.components().map { it.remap() })) + is ModuleAttribute -> + clb.with( + ModuleAttribute.of( + cle.moduleName(), + cle.moduleFlagsMask(), + cle.moduleVersion().getOrNull(), + cle.requires(), + cle.exports().map { mei -> + ModuleExportInfo.of( + clb + .constantPool() + .packageEntry( + PackageDesc.ofInternalName(map(mei.exportedPackage().asSymbol().internalName())) + ), + mei.exportsFlagsMask(), + mei.exportsTo(), + ) + }, + cle.opens().map { moi -> + ModuleOpenInfo.of( + clb + .constantPool() + .packageEntry( + PackageDesc.ofInternalName(map(moi.openedPackage().asSymbol().internalName())) + ), + moi.opensFlagsMask(), + moi.opensTo(), + ) + }, + cle.uses().map { clb.constantPool().classEntry(mapClassDesc(it.asSymbol())) }, + cle.provides().map { mp -> + ModuleProvideInfo.of( + mapClassDesc(mp.provides().asSymbol()), + mp.providesWith().map { mapClassDesc(it.asSymbol()) }, + ) + }, + ) + ) + is ModuleMainClassAttribute -> + clb.with(ModuleMainClassAttribute.of(mapClassDesc(cle.mainClass().asSymbol()))) + // ModulePackages is stored separately from Module, so it must be relocated independently. + is ModulePackagesAttribute -> + clb.with( + ModulePackagesAttribute.ofNames( + cle.packages().map { PackageDesc.ofInternalName(map(it.asSymbol().internalName())) } + ) + ) + is NestHostAttribute -> + clb.with(NestHostAttribute.of(mapClassDesc(cle.nestHost().asSymbol()))) + is NestMembersAttribute -> + clb.with( + NestMembersAttribute.ofSymbols(cle.nestMembers().map { mapClassDesc(it.asSymbol()) }) + ) + is PermittedSubclassesAttribute -> + clb.with( + PermittedSubclassesAttribute.ofSymbols( + cle.permittedSubclasses().map { mapClassDesc(it.asSymbol()) } + ) + ) + is RuntimeVisibleAnnotationsAttribute -> + clb.with(RuntimeVisibleAnnotationsAttribute.of(cle.annotations().map { it.remap() })) + is RuntimeInvisibleAnnotationsAttribute -> + clb.with(RuntimeInvisibleAnnotationsAttribute.of(cle.annotations().map { it.remap() })) + is RuntimeVisibleTypeAnnotationsAttribute -> + clb.with(RuntimeVisibleTypeAnnotationsAttribute.of(cle.annotations().map { it.remap() })) + is RuntimeInvisibleTypeAnnotationsAttribute -> + clb.with(RuntimeInvisibleTypeAnnotationsAttribute.of(cle.annotations().map { it.remap() })) + // Preserve standard elements without relocatable references and opaque custom attributes. + is AccessFlags, + is ClassFileVersion, + is CustomAttribute<*>, + is CompilationIDAttribute, + is DeprecatedAttribute, + is ModuleHashesAttribute, + is ModuleResolutionAttribute, + is ModuleTargetAttribute, + is SourceDebugExtensionAttribute, + is SourceFileAttribute, + is SourceIDAttribute, + is SyntheticAttribute, + is UnknownAttribute -> clb.with(cle) + } + } + + fun mapClassDesc(desc: ClassDesc): ClassDesc { + when { + desc.isArray -> return mapClassDesc(desc.componentType()).arrayType() + desc.isPrimitive -> return desc + } + val internalName = desc.descriptorString().let { it.substring(1, it.length - 1) } + val mappedInternalName = map(internalName) + return if (internalName == mappedInternalName) desc + else ClassDesc.ofDescriptor("L$mappedInternalName;") + } + + // Keep InnerClasses.inner_name consistent with ASM's Remapper.mapInnerClassName behavior. + private fun mapInnerClassName(name: String, mappedName: String, innerName: String): String { + if (name == mappedName) return innerName + val namePackageIndex = name.lastIndexOf('/') + val mappedPackageIndex = mappedName.lastIndexOf('/') + if ( + namePackageIndex != -1 && + mappedPackageIndex != -1 && + name.substring(namePackageIndex) == mappedName.substring(mappedPackageIndex) + ) { + return innerName + } + val innerClassIndex = mappedName.lastIndexOf('$') + if (innerClassIndex == -1) return innerName + var innerNameIndex = innerClassIndex + 1 + while (innerNameIndex < mappedName.length && mappedName[innerNameIndex].isDigit()) { + innerNameIndex++ + } + return mappedName.substring(innerNameIndex) + } + + private fun ClassDesc.internalName(): String = + descriptorString().let { it.substring(1, it.length - 1) } + + private fun asFieldTransform() = FieldTransform { fb, fe -> + when (fe) { + is ConstantValueAttribute -> { + val constant = fe.constant() + if (constant is StringEntry) { + val remapped = map(constant.stringValue(), true) + fb.with(ConstantValueAttribute.of(fb.constantPool().stringEntry(remapped))) + } else { + fb.with(fe) + } + } + is SignatureAttribute -> fb.with(SignatureAttribute.of(fe.asTypeSignature().remap())) + is RuntimeVisibleAnnotationsAttribute -> + fb.with(RuntimeVisibleAnnotationsAttribute.of(fe.annotations().map { it.remap() })) + is RuntimeInvisibleAnnotationsAttribute -> + fb.with(RuntimeInvisibleAnnotationsAttribute.of(fe.annotations().map { it.remap() })) + is RuntimeVisibleTypeAnnotationsAttribute -> + fb.with(RuntimeVisibleTypeAnnotationsAttribute.of(fe.annotations().map { it.remap() })) + is RuntimeInvisibleTypeAnnotationsAttribute -> + fb.with(RuntimeInvisibleTypeAnnotationsAttribute.of(fe.annotations().map { it.remap() })) + else -> fb.with(fe) + } + } + + private fun asMethodTransform() = MethodTransform { mb, me -> + when (me) { + is AnnotationDefaultAttribute -> + mb.with(AnnotationDefaultAttribute.of(me.defaultValue().remap())) + is CodeModel -> mb.transformCode(me, asCodeTransform()) + is ExceptionsAttribute -> + mb.with(ExceptionsAttribute.ofSymbols(me.exceptions().map { mapClassDesc(it.asSymbol()) })) + is SignatureAttribute -> mb.with(SignatureAttribute.of(me.asMethodSignature().remap())) + is RuntimeVisibleAnnotationsAttribute -> + mb.with(RuntimeVisibleAnnotationsAttribute.of(me.annotations().map { it.remap() })) + is RuntimeInvisibleAnnotationsAttribute -> + mb.with(RuntimeInvisibleAnnotationsAttribute.of(me.annotations().map { it.remap() })) + is RuntimeVisibleParameterAnnotationsAttribute -> + mb.with( + RuntimeVisibleParameterAnnotationsAttribute.of( + me.parameterAnnotations().map { pas -> pas.map { it.remap() } } + ) + ) + is RuntimeInvisibleParameterAnnotationsAttribute -> + mb.with( + RuntimeInvisibleParameterAnnotationsAttribute.of( + me.parameterAnnotations().map { pas -> pas.map { it.remap() } } + ) + ) + is RuntimeVisibleTypeAnnotationsAttribute -> + mb.with(RuntimeVisibleTypeAnnotationsAttribute.of(me.annotations().map { it.remap() })) + is RuntimeInvisibleTypeAnnotationsAttribute -> + mb.with(RuntimeInvisibleTypeAnnotationsAttribute.of(me.annotations().map { it.remap() })) + else -> mb.with(me) + } + } + + private fun asCodeTransform() = CodeTransform { cob, coe -> + when (coe) { + is FieldInstruction -> + cob.fieldAccess( + coe.opcode(), + mapClassDesc(coe.owner().asSymbol()), + coe.name().stringValue(), + mapClassDesc(coe.typeSymbol()), + ) + is InvokeInstruction -> + cob.invoke( + coe.opcode(), + mapClassDesc(coe.owner().asSymbol()), + coe.name().stringValue(), + coe.typeSymbol().remap(), + coe.isInterface, + ) + is InvokeDynamicInstruction -> + cob.invokedynamic( + DynamicCallSiteDesc.of( + coe.bootstrapMethod().remap(), + coe.name().stringValue(), + coe.typeSymbol().remap(), + *coe.bootstrapArgs().map { it.remap() }.toTypedArray(), + ) + ) + is NewObjectInstruction -> cob.new_(mapClassDesc(coe.className().asSymbol())) + is NewReferenceArrayInstruction -> cob.anewarray(mapClassDesc(coe.componentType().asSymbol())) + is NewMultiArrayInstruction -> + cob.multianewarray(mapClassDesc(coe.arrayType().asSymbol()), coe.dimensions()) + is TypeCheckInstruction -> + cob.with(TypeCheckInstruction.of(coe.opcode(), mapClassDesc(coe.type().asSymbol()))) + is ExceptionCatch -> + cob.exceptionCatch( + coe.tryStart(), + coe.tryEnd(), + coe.handler(), + coe.catchType().map { cob.constantPool().classEntry(mapClassDesc(it.asSymbol())) }, + ) + is LocalVariable -> + cob.localVariable( + coe.slot(), + coe.name().stringValue(), + mapClassDesc(coe.typeSymbol()), + coe.startScope(), + coe.endScope(), + ) + is LocalVariableType -> + cob.localVariableType( + coe.slot(), + coe.name().stringValue(), + coe.signatureSymbol().remap(), + coe.startScope(), + coe.endScope(), + ) + is ConstantInstruction.LoadConstantInstruction -> { + val value = coe.constantEntry().constantValue() + val name = value.javaClass.name + @Suppress("CAST_NEVER_SUCCEEDS") + when (name) { + "java.lang.String" -> { + val s = value.toString() + cob.ldc(cob.constantPool().stringEntry(map(s, mapLiterals = true))) + } + "java.lang.Integer" -> cob.ldc(cob.constantPool().intEntry(value as Int)) + "java.lang.Float" -> cob.ldc(cob.constantPool().floatEntry(value as Float)) + "java.lang.Long" -> cob.ldc(cob.constantPool().longEntry(value as Long)) + "java.lang.Double" -> cob.ldc(cob.constantPool().doubleEntry(value as Double)) + else -> cob.ldc((value as ConstantDesc).remap()) + } + } + is RuntimeVisibleTypeAnnotationsAttribute -> + cob.with(RuntimeVisibleTypeAnnotationsAttribute.of(coe.annotations().map { it.remap() })) + is RuntimeInvisibleTypeAnnotationsAttribute -> + cob.with(RuntimeInvisibleTypeAnnotationsAttribute.of(coe.annotations().map { it.remap() })) + else -> cob.with(coe) + } + } + + private fun MethodTypeDesc.remap(): MethodTypeDesc { + return MethodTypeDesc.of( + mapClassDesc(returnType()), + *parameterList().map { mapClassDesc(it) }.toTypedArray(), + ) + } + + private fun ClassSignature.remap(): ClassSignature { + val superclassSignature = superclassSignature()?.remap() + return ClassSignature.of( + typeParameters().map { it.remap() }, + superclassSignature, + *superinterfaceSignatures().map { it.remap() }.toTypedArray(), + ) + } + + private fun MethodSignature.remap(): MethodSignature { + return MethodSignature.of( + typeParameters().map { it.remap() }, + throwableSignatures().map { it.remap() }, + result().remap(), + *arguments().map { it.remap() }.toTypedArray(), + ) + } + + private fun RecordComponentInfo.remap(): RecordComponentInfo { + return RecordComponentInfo.of( + name().stringValue(), + mapClassDesc(descriptorSymbol()), + attributes().map { atr -> + when (atr) { + is SignatureAttribute -> SignatureAttribute.of(atr.asTypeSignature().remap()) + is RuntimeVisibleAnnotationsAttribute -> + RuntimeVisibleAnnotationsAttribute.of(atr.annotations().map { it.remap() }) + is RuntimeInvisibleAnnotationsAttribute -> + RuntimeInvisibleAnnotationsAttribute.of(atr.annotations().map { it.remap() }) + is RuntimeVisibleTypeAnnotationsAttribute -> + RuntimeVisibleTypeAnnotationsAttribute.of(atr.annotations().map { it.remap() }) + is RuntimeInvisibleTypeAnnotationsAttribute -> + RuntimeInvisibleTypeAnnotationsAttribute.of(atr.annotations().map { it.remap() }) + else -> atr + } + }, + ) + } + + private fun DirectMethodHandleDesc.remap(): DirectMethodHandleDesc { + return when (kind()) { + DirectMethodHandleDesc.Kind.GETTER, + DirectMethodHandleDesc.Kind.SETTER, + DirectMethodHandleDesc.Kind.STATIC_GETTER, + DirectMethodHandleDesc.Kind.STATIC_SETTER -> + MethodHandleDesc.ofField( + kind(), + mapClassDesc(owner()), + methodName(), + mapClassDesc(ClassDesc.ofDescriptor(lookupDescriptor())), + ) + else -> + MethodHandleDesc.ofMethod( + kind(), + mapClassDesc(owner()), + methodName(), + MethodTypeDesc.ofDescriptor(lookupDescriptor()).remap(), + ) + } + } + + private fun ConstantDesc.remap(): ConstantDesc { + return when (this) { + is ClassDesc -> mapClassDesc(this) + is DynamicConstantDesc<*> -> remap() + is DirectMethodHandleDesc -> remap() + is MethodTypeDesc -> remap() + else -> { + @Suppress("CAST_NEVER_SUCCEEDS") + if (javaClass.name == "java.lang.String") { + map(toString(), mapLiterals = true) as ConstantDesc + } else { + this + } + } + } + } + + private fun DynamicConstantDesc<*>.remap(): DynamicConstantDesc<*> { + return DynamicConstantDesc.ofNamed( + bootstrapMethod().remap(), + constantName(), + mapClassDesc(constantType()), + *bootstrapArgsList().map { it.remap() }.toTypedArray(), + ) + } + + @Suppress("UNCHECKED_CAST") + private fun S.remap(): S { + return when (this) { + is Signature.ArrayTypeSig -> Signature.ArrayTypeSig.of(componentSignature().remap()) as S + is Signature.ClassTypeSig -> { + val mappedOuter = outerType().getOrNull()?.remap() + val mappedClass = mapClassDesc(classDesc()) + val mappedInternalName = + mappedClass.descriptorString().let { it.substring(1, it.length - 1) } + val className = + mappedOuter?.let { outer -> + val mappedOuterDesc = outer.classDesc().descriptorString() + val mappedOuterName = mappedOuterDesc.substring(1, mappedOuterDesc.length - 1) + // With an outer type, ClassTypeSig expects only the nested class segment. Passing the + // full internal name would produce an invalid signature such as + // Lpkg/Outer.pkg/Outer$Inner;. + mappedInternalName.removePrefix("$mappedOuterName$").takeIf { + it != mappedInternalName + } ?: mappedInternalName.substringAfterLast('$', className()) + } ?: mappedInternalName + Signature.ClassTypeSig.of( + mappedOuter, + className, + *typeArgs() + .map { ta -> + when (ta) { + is Signature.TypeArg.Unbounded -> ta + is Signature.TypeArg.Bounded -> + Signature.TypeArg.bounded(ta.wildcardIndicator(), ta.boundType().remap()) + } + } + .toTypedArray(), + ) as S + } + else -> this + } + } + + private fun Annotation.remap() = + Annotation.of( + mapClassDesc(classSymbol()), + elements().map { el -> AnnotationElement.of(el.name(), el.value().remap()) }, + ) + + private fun AnnotationValue.remap(): AnnotationValue { + return when (this) { + is AnnotationValue.OfAnnotation -> AnnotationValue.ofAnnotation(annotation().remap()) + is AnnotationValue.OfArray -> AnnotationValue.ofArray(values().map { it.remap() }) + is AnnotationValue.OfConstant -> { + if (this is AnnotationValue.OfString) { + val str = stringValue() + // mapLiterals=true enables the skipStringConstants check in each relocator. + val mapped = map(str, mapLiterals = true) + if (mapped != str) AnnotationValue.ofString(mapped) else this + } else { + this + } + } + is AnnotationValue.OfClass -> AnnotationValue.ofClass(mapClassDesc(classSymbol())) + is AnnotationValue.OfEnum -> + AnnotationValue.ofEnum(mapClassDesc(classSymbol()), constantName().stringValue()) + } + } + + private fun TypeAnnotation.remap() = + TypeAnnotation.of(targetInfo(), targetPath(), annotation().remap()) + + private fun Signature.TypeParam.remap() = + Signature.TypeParam.of( + identifier(), + classBound().getOrNull()?.remap(), + *interfaceBounds().map { it.remap() }.toTypedArray(), + ) + + private fun map(name: String, mapLiterals: Boolean = false): String = + relocators.mapName(name = name, mapLiterals = mapLiterals, onModified = onModified) +} 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 c9daa0df9..7d7b92194 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 @@ -50,4 +50,7 @@ private class RelocatorRemapper( override fun map(internalName: String): String = relocators.mapName(name = internalName, onModified = onModified) + + // Module attributes encode package names separately, so ASM remaps them through this hook. + override fun mapPackageName(name: String): String = map(name) } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt index 0c7931078..d7d1a0e56 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest.kt @@ -2,8 +2,13 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.containsMatch import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo +import assertk.assertions.isNotNull +import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext +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.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.util.noOpDelegate @@ -23,11 +28,14 @@ import org.vafer.jdeb.shaded.objectweb.asm.AnnotationVisitor import org.vafer.jdeb.shaded.objectweb.asm.ClassReader import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor import org.vafer.jdeb.shaded.objectweb.asm.ClassWriter +import org.vafer.jdeb.shaded.objectweb.asm.ConstantDynamic import org.vafer.jdeb.shaded.objectweb.asm.FieldVisitor +import org.vafer.jdeb.shaded.objectweb.asm.Handle import org.vafer.jdeb.shaded.objectweb.asm.Label import org.vafer.jdeb.shaded.objectweb.asm.MethodVisitor import org.vafer.jdeb.shaded.objectweb.asm.ModuleVisitor import org.vafer.jdeb.shaded.objectweb.asm.Opcodes +import org.vafer.jdeb.shaded.objectweb.asm.Type /** * The cases reflect the cases in @@ -77,7 +85,9 @@ class BytecodeRemappingTest { val failure = assertThrows { details.remapClass(relocators) } - assertThat(failure.message).isEqualTo("Error in ASM processing class $path") + assertThat(failure.message) + .isNotNull() + .containsMatch("Error in (ASM|Class-File API) processing class $path".toRegex()) } @Test @@ -227,18 +237,7 @@ class BytecodeRemappingTest { fun moduleMainClassIsRelocated() { val originalMainClass = $$"com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase" - val writer = ClassWriter(0) - writer.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null) - writer.visitModule("example.module", 0, null).apply { visitMainClass(originalMainClass) } - writer.visitEnd() - val file = - tempDir.resolve("module-info.class").toFile().apply { writeBytes(writer.toByteArray()) } - val details = - object : FileCopyDetails by noOpDelegate() { - override fun getPath(): String = file.name - - override fun getFile(): File = file - } + val details = moduleInfoDetails { visitMainClass(originalMainClass) } val result = details.remapClass(relocators) var remappedMainClass: String? = null @@ -262,6 +261,317 @@ class BytecodeRemappingTest { assertThat(remappedMainClass).isEqualTo(relocatedFixtureBase) } + @Test + fun modulePackagesAreRelocated() { + val originalPackage = "com/github/jengelman/gradle/plugins/shadow/internal" + val relocatedPackage = "com/example/relocated" + val details = moduleInfoDetails { + visitPackage(originalPackage) + visitExport(originalPackage, 0) + visitOpen(originalPackage, 0) + } + + val result = details.remapClass(relocators) + val packages = mutableListOf() + val exports = mutableListOf() + val opens = mutableListOf() + ClassReader(result) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visitModule( + name: String, + access: Int, + version: String?, + ): ModuleVisitor = + object : ModuleVisitor(Opcodes.ASM9) { + override fun visitPackage(packaze: String) { + packages += packaze + } + + override fun visitExport( + packaze: String, + access: Int, + modules: Array?, + ) { + exports += packaze + } + + override fun visitOpen( + packaze: String, + access: Int, + modules: Array?, + ) { + opens += packaze + } + } + }, + 0, + ) + + assertThat(packages).isEqualTo(listOf(relocatedPackage)) + assertThat(exports).isEqualTo(listOf(relocatedPackage)) + assertThat(opens).isEqualTo(listOf(relocatedPackage)) + } + + @Test + fun moduleClassReferencesAreRelocatedAndMetadataIsPreserved() { + val originalService = + $$"com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureInterface" + val originalProvider = + $$"com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureSubject" + val relocatedService = $$"com/example/relocated/BytecodeRemappingTest$FixtureInterface" + val relocatedProvider = $$"com/example/relocated/BytecodeRemappingTest$FixtureSubject" + val details = + moduleInfoDetails(Opcodes.ACC_OPEN, "1.0") { + visitRequire("java.base", Opcodes.ACC_MANDATED, "25") + visitExport("com/example/api", Opcodes.ACC_SYNTHETIC, "consumer.module") + visitOpen("com/example/internal", Opcodes.ACC_MANDATED, "reflective.module") + visitUse(originalService) + visitProvide(originalService, originalProvider) + } + var moduleHeader: Triple? = null + val requires = mutableListOf() + val exports = mutableListOf() + val opens = mutableListOf() + val uses = mutableListOf() + val provides = mutableListOf>>() + + ClassReader(details.remapClass(relocators)) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visitModule( + name: String, + access: Int, + version: String?, + ): ModuleVisitor { + moduleHeader = Triple(name, access, version) + return object : ModuleVisitor(Opcodes.ASM9) { + override fun visitRequire(module: String, access: Int, version: String?) { + requires += "$module:$access:$version" + } + + override fun visitExport( + packaze: String, + access: Int, + modules: Array?, + ) { + exports += "$packaze:$access:${modules?.joinToString()}" + } + + override fun visitOpen( + packaze: String, + access: Int, + modules: Array?, + ) { + opens += "$packaze:$access:${modules?.joinToString()}" + } + + override fun visitUse(service: String) { + uses += service + } + + override fun visitProvide(service: String, providers: Array) { + provides += service to providers.toList() + } + } + } + }, + 0, + ) + + assertThat(moduleHeader).isEqualTo(Triple("example.module", Opcodes.ACC_OPEN, "1.0")) + assertThat(requires).isEqualTo(listOf("java.base:${Opcodes.ACC_MANDATED}:25")) + assertThat(exports) + .isEqualTo(listOf("com/example/api:${Opcodes.ACC_SYNTHETIC}:consumer.module")) + assertThat(opens) + .isEqualTo(listOf("com/example/internal:${Opcodes.ACC_MANDATED}:reflective.module")) + assertThat(uses).isEqualTo(listOf(relocatedService)) + assertThat(provides).isEqualTo(listOf(relocatedService to listOf(relocatedProvider))) + } + + @Test + fun innerClassSimpleNameIsRelocated() { + val originalInnerClass = "example/Outer\$Old" + val relocatedInnerClass = "example/Outer\$New" + val details = + classDetails(originalInnerClass) { + visitInnerClass(originalInnerClass, "example/Outer", "Old", Opcodes.ACC_PUBLIC) + } + val exactClassRelocator = setOf(exactPathRelocator(originalInnerClass, relocatedInnerClass)) + var innerClass: Triple? = null + + val result = details.remapClass(exactClassRelocator) + ClassReader(result) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visitInnerClass( + name: String, + outerName: String?, + innerName: String?, + access: Int, + ) { + innerClass = Triple(name, outerName, innerName) + } + }, + 0, + ) + + assertThat(ClassReader(result).className).isEqualTo(relocatedInnerClass) + assertThat(innerClass).isEqualTo(Triple(relocatedInnerClass, "example/Outer", "New")) + } + + @ParameterizedTest + @ValueSource(booleans = [false, true]) + fun fieldStringConstantHonorsSkipSetting(skipStringConstants: Boolean) { + val originalValue = + $$"com.github.jengelman.gradle.plugins.shadow.internal.BytecodeRemappingTest$FixtureBase" + val relocatedValue = $$"com.example.relocated.BytecodeRemappingTest$FixtureBase" + val details = + classDetails("example/Constants") { + visitField( + Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC or Opcodes.ACC_FINAL, + "VALUE", + "Ljava/lang/String;", + null, + originalValue, + ) + .visitEnd() + } + val stringRelocators = + setOf( + SimpleRelocator( + "com.github.jengelman.gradle.plugins.shadow.internal", + "com.example.relocated", + skipStringConstants = skipStringConstants, + ) + ) + var fieldValue: Any? = null + + ClassReader(details.remapClass(stringRelocators)) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visitField( + access: Int, + name: String, + descriptor: String, + signature: String?, + value: Any?, + ): FieldVisitor? { + if (name == "VALUE") fieldValue = value + return null + } + }, + 0, + ) + + assertThat(fieldValue).isEqualTo(if (skipStringConstants) originalValue else relocatedValue) + } + + @Test + fun constantPoolAndInvokeDynamicReferencesAreRelocated() { + val originalType = + $$"com/github/jengelman/gradle/plugins/shadow/internal/BytecodeRemappingTest$FixtureBase" + val relocatedType = $$"com/example/relocated/BytecodeRemappingTest$FixtureBase" + val originalDescriptor = "L$originalType;" + val relocatedDescriptor = "L$relocatedType;" + val methodHandle = + Handle(Opcodes.H_INVOKESTATIC, originalType, "factory", "()$originalDescriptor", false) + val constantBootstrap = + Handle( + Opcodes.H_INVOKESTATIC, + "java/lang/invoke/ConstantBootstraps", + "nullConstant", + "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;", + false, + ) + val callSiteBootstrap = + Handle( + Opcodes.H_INVOKESTATIC, + originalType, + "bootstrap", + "(Ljava/lang/invoke/MethodHandles\$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;)Ljava/lang/invoke/CallSite;", + false, + ) + val callSiteBootstrapDescriptor = callSiteBootstrap.desc + val details = + classDetails("example/Constants") { + visitMethod(Opcodes.ACC_PUBLIC or Opcodes.ACC_STATIC, "test", "()V", null, null).apply { + visitCode() + visitLdcInsn(Type.getType(originalDescriptor)) + visitInsn(Opcodes.POP) + visitLdcInsn(Type.getMethodType("()$originalDescriptor")) + visitInsn(Opcodes.POP) + visitLdcInsn(methodHandle) + visitInsn(Opcodes.POP) + visitLdcInsn(ConstantDynamic("constant", originalDescriptor, constantBootstrap)) + visitInsn(Opcodes.POP) + visitInvokeDynamicInsn( + "call", + "()$originalDescriptor", + callSiteBootstrap, + Type.getType(originalDescriptor), + methodHandle, + ) + visitInsn(Opcodes.POP) + visitInsn(Opcodes.RETURN) + visitMaxs(1, 0) + visitEnd() + } + } + val references = mutableListOf() + + fun recordReference(value: Any) { + when (value) { + is Type -> references += "type:${value.descriptor}" + is Handle -> references += "handle:${value.owner}:${value.desc}" + is ConstantDynamic -> { + references += "constant:${value.descriptor}" + recordReference(value.bootstrapMethod) + repeat(value.bootstrapMethodArgumentCount) { + recordReference(value.getBootstrapMethodArgument(it)) + } + } + } + } + + ClassReader(details.remapClass(relocators)) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visitMethod( + access: Int, + name: String, + descriptor: String, + signature: String?, + exceptions: Array?, + ): MethodVisitor = + object : MethodVisitor(Opcodes.ASM9) { + override fun visitLdcInsn(value: Any) { + recordReference(value) + } + + override fun visitInvokeDynamicInsn( + name: String, + descriptor: String, + bootstrapMethodHandle: Handle, + vararg bootstrapMethodArguments: Any, + ) { + references += "invokedynamic:$descriptor" + recordReference(bootstrapMethodHandle) + bootstrapMethodArguments.forEach(::recordReference) + } + } + }, + 0, + ) + + assertThat(references).contains("type:$relocatedDescriptor") + assertThat(references).contains("type:()$relocatedDescriptor") + assertThat(references).contains("handle:$relocatedType:()$relocatedDescriptor") + assertThat(references).contains("handle:$relocatedType:$callSiteBootstrapDescriptor") + assertThat(references).contains("constant:$relocatedDescriptor") + assertThat(references).contains("invokedynamic:()$relocatedDescriptor") + } + @Test fun localVariableIsRelocated() { val result = fixtureSubjectDetails.remapClass(relocators) @@ -294,6 +604,51 @@ class BytecodeRemappingTest { override fun getFile(): File = _file } + private fun moduleInfoDetails( + access: Int = 0, + version: String? = null, + configure: ModuleVisitor.() -> Unit, + ): FileCopyDetails { + val writer = ClassWriter(0) + writer.visit(Opcodes.V9, Opcodes.ACC_MODULE, "module-info", null, null, null) + writer.visitModule("example.module", access, version).apply(configure).visitEnd() + writer.visitEnd() + return bytecodeDetails("module-info.class", writer.toByteArray()) + } + + private fun classDetails( + internalName: String, + configure: ClassWriter.() -> Unit, + ): FileCopyDetails { + val writer = ClassWriter(0) + writer.visit(Opcodes.V17, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null) + writer.configure() + writer.visitEnd() + return bytecodeDetails("$internalName.class", writer.toByteArray()) + } + + private fun bytecodeDetails(path: String, bytes: ByteArray): FileCopyDetails { + val file = tempDir.resolve(path).createParentDirectories().toFile().apply { writeBytes(bytes) } + return object : FileCopyDetails by noOpDelegate() { + override fun getPath(): String = path + + override fun getFile(): File = file + } + } + + private fun exactPathRelocator(original: String, relocated: String): Relocator = + object : Relocator { + override fun canRelocatePath(path: String): Boolean = path == original + + override fun relocatePath(context: RelocatePathContext): String = relocated + + override fun canRelocateClass(className: String): Boolean = false + + override fun relocateClass(context: RelocateClassContext): String = context.className + + override fun applyToSourceContent(sourceContent: String): String = sourceContent + } + // --------------------------------------------------------------------------- // Fixture classes – declared as nested classes so their bytecode is compiled // into the test output directory and can be fetched via requireResourceAsPath.