diff --git a/api/shadow.api b/api/shadow.api index 4a67b24a3..89c8f875a 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -389,6 +389,16 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/IncludeReso public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V } +public class com/github/jengelman/gradle/plugins/shadow/transformers/KotlinModuleMetadataTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/PatternFilterableResourceTransformer { + public fun (Lorg/gradle/api/model/ObjectFactory;)V + public fun (Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/tasks/util/PatternSet;)V + public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z + public final fun getObjectFactory ()Lorg/gradle/api/model/ObjectFactory; + public fun hasTransformedResource ()Z + public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V + public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V +} + public class com/github/jengelman/gradle/plugins/shadow/transformers/Log4j2PluginsCacheFileTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/PatternFilterableResourceTransformer { public fun ()V public fun (Lorg/gradle/api/tasks/util/PatternSet;)V diff --git a/docs/changes/README.md b/docs/changes/README.md index ee5e54667..e939b0b03 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -17,6 +17,8 @@ - Bump min Gradle requirement to 9.2.0. ([#2057](https://github.com/GradleUp/shadow/pull/2057)) - Remove `afterEvaluate` when adding variants. ([#2056](https://github.com/GradleUp/shadow/pull/2056)) +- Deprecate `enableKotlinModuleRemapping` for `ShadowJar`. ([#2073](https://github.com/GradleUp/shadow/pull/2073)) + Apply `KotlinModuleMetadataTransformer` explicitly to support relocating inside Kotlin module metadata files. ### Fixed diff --git a/docs/kotlin-plugins/README.md b/docs/kotlin-plugins/README.md index 339865136..9539dbaad 100644 --- a/docs/kotlin-plugins/README.md +++ b/docs/kotlin-plugins/README.md @@ -144,6 +144,30 @@ automatically configure additional tasks for bundling the shadowed JAR for its ` } ``` +## Kotlin Module Metadata Remapping +Kotlin module metadata (`.kotlin_module`) files contain information about package parts and facades. When relocating +classes, Shadow automatically relocates the packages and facades inside the Kotlin module metadata files. This feature +is enabled by default via the deprecated `enableKotlinModuleRemapping` property. + +To explicitly apply this remapping (recommended for future compatibility), add +[`KotlinModuleMetadataTransformer`][KotlinModuleMetadataTransformer] to your task configuration: + +=== "Kotlin" + + ```kotlin + tasks.shadowJar { + transform(com.github.jengelman.gradle.plugins.shadow.transformers.KotlinModuleMetadataTransformer::class.java) + } + ``` + +=== "Groovy" + + ```groovy + tasks.shadowJar { + transform(com.github.jengelman.gradle.plugins.shadow.transformers.KotlinModuleMetadataTransformer) + } + ``` [org.jetbrains.kotlin.multiplatform]: https://kotlinlang.org/docs/multiplatform-intro.html +[KotlinModuleMetadataTransformer]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-kotlin-module-metadata-transformer/index.html diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 1a713a0b7..cdc899de6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -683,6 +683,86 @@ class RelocationTest : BasePluginTest() { } } + @Issue("https://github.com/GradleUp/shadow/issues/843") + @OptIn(UnstableMetadataApi::class) + @Test + fun relocateKotlinModuleFilesExplicitly() { + val originalModuleFilePath = "META-INF/kotlin-stdlib.kotlin_module" + val originalModuleFileBytes = requireResourceAsPath(originalModuleFilePath).readBytes() + val stdlibJar = + buildJar("stdlib.jar") { insert(originalModuleFilePath, originalModuleFileBytes) } + projectScript.appendText( + """ + dependencies { + ${implementationFiles(stdlibJar)} + } + $shadowJarTask { + relocate('kotlin', 'my.kotlin') + enableKotlinModuleRemapping = false + transform(com.github.jengelman.gradle.plugins.shadow.transformers.KotlinModuleMetadataTransformer) + } + """ + .trimIndent() + ) + + runWithSuccess(shadowJarPath) + + val relocatedModuleFilePath = "META-INF/kotlin-stdlib.shadow.kotlin_module" + + assertThat(outputShadowedJar).useAll { + containsOnly(relocatedModuleFilePath, *manifestEntries) + } + + val originalModule = + KotlinModuleMetadata.read(requireResourceAsStream(originalModuleFilePath).readBytes()) + val relocatedModule = outputShadowedJar.use { + KotlinModuleMetadata.read(it.getBytes(relocatedModuleFilePath)) + } + + assertThat(relocatedModule.version.toString()).isEqualTo("2.2.0") + assertThat(originalModule.version.toString()).isEqualTo("2.2.0") + + // No implementation for writing the optionalAnnotationClasses property yet. + // https://github.com/JetBrains/kotlin/blob/81502985ae0a2f5b21e121ffc180c3f4dd467e17/libraries/kotlinx-metadata/jvm/src/kotlin/metadata/jvm/KotlinModuleMetadata.kt#L71 + assertThat(relocatedModule.kmModule.optionalAnnotationClasses).isEmpty() + + val originalPkgParts = originalModule.kmModule.packageParts.entries + val relocatedPkgParts = relocatedModule.kmModule.packageParts.entries + // They are not empty and different. + assertThat(originalPkgParts).isNotEqualTo(relocatedPkgParts) + assertThat(originalPkgParts.size).isEqualTo(relocatedPkgParts.size) + + relocatedPkgParts.forEachIndexed { index, (relocatedPkg, relocatedParts) -> + val (originalPkg, originalParts) = originalPkgParts.elementAt(index) + assertThat(relocatedPkg).isNotEqualTo(originalPkg) + assertThat(relocatedPkg).isEqualTo(originalPkg.replace("kotlin", "my.kotlin")) + + if (originalParts.fileFacades.isEmpty()) { + assertThat(relocatedParts.fileFacades).isEmpty() + } else { + assertThat(relocatedParts.fileFacades).isNotEmpty() + assertThat(relocatedParts.fileFacades).isNotEqualTo(originalParts.fileFacades) + assertThat(relocatedParts.fileFacades) + .isEqualTo(originalParts.fileFacades.map { it.replace("kotlin/", "my/kotlin/") }) + } + + if (originalParts.multiFileClassParts.isEmpty()) { + assertThat(relocatedParts.multiFileClassParts).isEmpty() + } else { + assertThat(relocatedParts.multiFileClassParts).isNotEmpty() + assertThat(relocatedParts.multiFileClassParts) + .isNotEqualTo(originalParts.multiFileClassParts) + assertThat(relocatedParts.multiFileClassParts) + .isEqualTo( + originalParts.multiFileClassParts.entries.associateTo(mutableMapOf()) { (name, facade) + -> + name.replace("kotlin/", "my/kotlin/") to facade.replace("kotlin/", "my/kotlin/") + } + ) + } + } + } + private fun writeClassWithStringRef() { writeClass { """ 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 e59b24df4..c9efde71c 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 @@ -8,16 +8,11 @@ import com.github.jengelman.gradle.plugins.shadow.internal.cast import com.github.jengelman.gradle.plugins.shadow.internal.remapClass import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry 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 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 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 @@ -185,13 +180,6 @@ constructor( } } } - enableKotlinModuleRemapping && path.endsWith(".kotlin_module") -> { - if (relocators.isEmpty()) { - fileDetails.writeToZip(path) - } else { - fileDetails.remapKotlinModule() - } - } else -> { val relocated = relocators.relocatePath(path) if (transform(fileDetails, relocated)) return @@ -209,49 +197,6 @@ constructor( } } - /** - * 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() = - file.readBytes().let { bytes -> - val kmMetadata = KotlinModuleMetadata.read(bytes) - val newKmModule = - KmModule().apply { - // We don't need to relocate the nested properties in `optionalAnnotationClasses`, there - // is a very special use case for Kotlin Multiplatform. - optionalAnnotationClasses += kmMetadata.kmModule.optionalAnnotationClasses - packageParts += - kmMetadata.kmModule.packageParts.map { (pkg, parts) -> - val relocatedPkg = relocators.relocateClass(pkg) - val relocatedParts = - KmPackageParts( - parts.fileFacades.mapTo(mutableListOf()) { relocators.relocatePath(it) }, - parts.multiFileClassParts.entries.associateTo(mutableMapOf()) { (name, facade) - -> - relocators.relocatePath(name) to relocators.relocatePath(facade) - }, - ) - relocatedPkg to relocatedParts - } - } - val newKmMetadata = KotlinModuleMetadata(newKmModule, kmMetadata.version) - - val newBytes = newKmMetadata.write() - val relocatedPath = relocators.relocatePath(path) - val entryName = - when { - relocatedPath != path -> relocatedPath - // Nothing changed, so keep the original path. - newBytes.contentEquals(bytes) -> path - // Content changed but path didn't, so rename to avoid name clash. The filename does not - // matter to the compiler. - else -> path.replace(".kotlin_module", ".shadow.kotlin_module") - } - writeToZip(entryName = entryName, bytes = newBytes) - } - private fun transform(fileDetails: FileCopyDetails, path: String): Boolean { val transformer = transformers.find { it.canTransformResource(fileDetails) } ?: return false fileDetails.file.inputStream().use { inputStream -> diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 956d679e4..ca20e0012 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -20,6 +20,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer +import com.github.jengelman.gradle.plugins.shadow.transformers.KotlinModuleMetadataTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer.Companion.create import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer @@ -173,6 +174,14 @@ public abstract class ShadowJar : Jar() { * * Defaults to `true`. */ + @Deprecated( + "Use `KotlinModuleMetadataTransformer` explicitly instead. This will be removed in Shadow 10.", + replaceWith = + ReplaceWith( + "transform(KotlinModuleMetadataTransformer::class.java)", + "com.github.jengelman.gradle.plugins.shadow.transformers.KotlinModuleMetadataTransformer", + ), + ) @get:Input @get:Option( option = "enable-kotlin-module-remapping", @@ -517,14 +526,25 @@ public abstract class ShadowJar : Jar() { } else { emptySet() } + val actualTransformers = + transformers.get().let { set -> + @Suppress("DEPRECATION") + if ( + enableKotlinModuleRemapping.get() && set.none { it is KotlinModuleMetadataTransformer } + ) { + set + KotlinModuleMetadataTransformer::class.java.create(objectFactory) + } else { + set + } + } @Suppress("DEPRECATION") return ShadowCopyAction( zipFile = archiveFile.get().asFile, zosProvider = zosProvider, - transformers = transformers.get(), + transformers = actualTransformers, relocators = relocators.get() + packageRelocators, unusedClasses = unusedClasses, - enableKotlinModuleRemapping = enableKotlinModuleRemapping.get(), + enableKotlinModuleRemapping = false, // Unused param. preserveFileTimestamps = isPreserveFileTimestamps, failOnDuplicateEntries = failOnDuplicateEntries.get(), metadataCharset, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/KotlinModuleMetadataTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/KotlinModuleMetadataTransformer.kt new file mode 100644 index 000000000..106536cb2 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/KotlinModuleMetadataTransformer.kt @@ -0,0 +1,91 @@ +package com.github.jengelman.gradle.plugins.shadow.transformers + +import com.github.jengelman.gradle.plugins.shadow.internal.checkDupStrategy +import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry +import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass +import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath +import javax.inject.Inject +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.ZipOutputStream +import org.gradle.api.file.FileTreeElement +import org.gradle.api.model.ObjectFactory +import org.gradle.api.tasks.util.PatternSet + +/** + * A resource transformer that relocates package parts within Kotlin module metadata files + * (`.kotlin_module`). + */ +@CacheableTransformer +public open class KotlinModuleMetadataTransformer( + final override val objectFactory: ObjectFactory, + patternSet: PatternSet, +) : PatternFilterableResourceTransformer(patternSet) { + + @Inject + public constructor( + objectFactory: ObjectFactory + ) : this( + objectFactory, + PatternSet().include("**/*.kotlin_module"), + ) + + private val moduleEntries = mutableMapOf() + + override fun canTransformResource(element: FileTreeElement): Boolean { + return super.canTransformResource(element).also { flag -> checkDupStrategy(flag, element) } + } + + @OptIn(UnstableMetadataApi::class) + override fun transform(context: TransformerContext) { + val bytes = context.inputStream.readBytes() + if (context.relocators.isEmpty()) { + moduleEntries[context.path] = bytes + return + } + val kmMetadata = KotlinModuleMetadata.read(bytes) + val newKmModule = + KmModule().apply { + // We don't need to relocate the nested properties in `optionalAnnotationClasses`, there + // is a very special use case for Kotlin Multiplatform. + optionalAnnotationClasses += kmMetadata.kmModule.optionalAnnotationClasses + packageParts += + kmMetadata.kmModule.packageParts.map { (pkg, parts) -> + val relocatedPkg = context.relocators.relocateClass(pkg) + val relocatedParts = + KmPackageParts( + parts.fileFacades.mapTo(mutableListOf()) { context.relocators.relocatePath(it) }, + parts.multiFileClassParts.entries.associateTo(mutableMapOf()) { (name, facade) -> + context.relocators.relocatePath(name) to context.relocators.relocatePath(facade) + }, + ) + relocatedPkg to relocatedParts + } + } + val newKmMetadata = KotlinModuleMetadata(newKmModule, kmMetadata.version) + val newBytes = newKmMetadata.write() + + val entryName = + when { + // Nothing changed, so keep the original (already relocated) path. + newBytes.contentEquals(bytes) -> context.path + // Content changed but path didn't, so rename to avoid name clash. The filename does not + // matter to the compiler. + else -> context.path.replace(".kotlin_module", ".shadow.kotlin_module") + } + + moduleEntries[entryName] = newBytes + } + + override fun hasTransformedResource(): Boolean = moduleEntries.isNotEmpty() + + override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) { + moduleEntries.forEach { (path, bytes) -> + os.putNextEntry(zipEntry(path, preserveFileTimestamps)) + os.write(bytes) + os.closeEntry() + } + } +} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index d5bf5e8f4..0c43b29a5 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -148,7 +148,7 @@ class ShadowPropertiesTest { with(shadowJarTask) { assertThat(addMultiReleaseAttribute.get()).isTrue() assertThat(enableAutoRelocation.get()).isFalse() - assertThat(enableKotlinModuleRemapping.get()).isTrue() + @Suppress("DEPRECATION") assertThat(enableKotlinModuleRemapping.get()).isTrue() assertThat(failOnDuplicateEntries.get()).isFalse() assertThat(minimizeJar.get()).isFalse() assertThat(mainClass.orNull).isNull() diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt index 6696d3628..6ab7c8456 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DuplicatesStrategyCheckerTest.kt @@ -29,7 +29,7 @@ class DuplicatesStrategyCheckerTest { getTransformerClasses().map { it.create(testObjectFactory) } - assertThat(allResourceTransformers.size).isEqualTo(17) + assertThat(allResourceTransformers.size).isEqualTo(18) var invocationCount = 0 onCheckDupStrategyInvoked = { invocationCount++ } @@ -38,7 +38,7 @@ class DuplicatesStrategyCheckerTest { val file = createTempFile(directory = tempDir).toFile() it.canTransformResource(path = file.path, file = file) } - assertThat(invocationCount).isEqualTo(14) + assertThat(invocationCount).isEqualTo(15) } finally { onCheckDupStrategyInvoked = null }