Skip to content
Open
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
10 changes: 10 additions & 0 deletions api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -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 <init> (Lorg/gradle/api/model/ObjectFactory;)V
public fun <init> (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 <init> ()V
public fun <init> (Lorg/gradle/api/tasks/util/PatternSet;)V
Expand Down
2 changes: 2 additions & 0 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 24 additions & 0 deletions docs/kotlin-plugins/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
),
)
Comment on lines +177 to +184
@get:Input
@get:Option(
option = "enable-kotlin-module-remapping",
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, ByteArray>()

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
Comment thread
Goooler marked this conversation as resolved.
}

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()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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++ }
Expand All @@ -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
}
Expand Down