Skip to content
Draft
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
6 changes: 6 additions & 0 deletions api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -450,17 +450,23 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/ManifestApp
}

public class com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer {
public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer$Companion;
public static final field NULL Ljava/lang/Object;
public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
public fun attributes (Ljava/util/Map;)V
public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z
public fun getMainClass ()Lorg/gradle/api/provider/Property;
public fun getManifestEntries ()Lorg/gradle/api/provider/MapProperty;
public final fun getObjectFactory ()Lorg/gradle/api/model/ObjectFactory;
public fun getRelocateAttributes ()Lorg/gradle/api/provider/SetProperty;
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 final class com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer$Companion {
}

public class com/github/jengelman/gradle/plugins/shadow/transformers/MergeLicenseResourceTransformer : 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
Expand Down
5 changes: 5 additions & 0 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

- Allow configuring the final R8 configuration file with `R8Spec.configurationFile`. ([#2133](https://github.com/GradleUp/shadow/pull/2133))
- Add `ProGuardFilesResourceTransformer` to merge R8/ProGuard rule files. ([#2196](https://github.com/GradleUp/shadow/pull/2196))
- Improvements for `ManifestResourceTransformer`. ([#2200](https://github.com/GradleUp/shadow/pull/2200))
- Support removing manifest attributes using `NULL`.
- Support manifest header relocation via configurable `relocateAttributes` property.

### Changed

Expand Down Expand Up @@ -37,6 +40,8 @@
Use `ShadowJar.exclude` or `ShadowJar.from` instead. The classes will be removed in Shadow 10.
- Deprecate `TransformerContext.Builder`. ([#2184](https://github.com/GradleUp/shadow/pull/2184))
Use `TransformerContext` constructor instead. The Builder API will be removed in Shadow 10.
- Deprecate `ManifestResourceTransformer.attributes(Map)`. ([#2200](https://github.com/GradleUp/shadow/pull/2200))
Calling `manifestEntries` instead. The method will be removed in Shadow 10.

## [9.6.1](https://github.com/GradleUp/shadow/releases/tag/9.6.1) - 2026-07-22

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ package com.github.jengelman.gradle.plugins.shadow.transformers

import assertk.all
import assertk.assertThat
import assertk.assertions.contains
import assertk.assertions.isEqualTo
import assertk.assertions.isNotEqualTo
import assertk.assertions.isNotNull
import assertk.assertions.isNull
import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey
import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly
import com.github.jengelman.gradle.plugins.shadow.testkit.getContent
Expand Down Expand Up @@ -79,6 +79,35 @@ class TransformersTest : BaseTransformerTest() {
}
}

@Test
fun manifestResourceTransformerRemoveAttributes() {
writeClass()
projectScript.appendText(
"""
|$jarTask {
| manifest {
| attributes 'Header-To-Remove-1': 'Value1', 'Header-To-Keep': 'Value2'
| }
|}
|${transform<ManifestResourceTransformer>(
transformerBlock =
"""
|manifestEntries.put('Header-To-Remove-1', ${ManifestResourceTransformer::class.java.name}.NULL)
"""
.trimMargin()
)}
"""
.trimMargin()
)

runWithSuccess(shadowJarPath)

commonAssertions {
assertThat(getValue("Header-To-Remove-1")).isNull()
assertThat(getValue("Header-To-Keep")).isEqualTo("Value2")
}
}

@Test // #427
fun mergeLog4j2PluginCacheFiles() {
val content = requireResourceAsPath(PLUGIN_CACHE_FILE).readText()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,15 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass
import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath
import java.util.regex.Pattern

/**
* Matches Java class names, package wildcards (e.g. `com.foo.**`), single-segment packages, and
* inner classes (`com.foo.Bar$Inner`).
*/
internal val classNamePattern: Regex =
"""(?<![a-zA-Z0-9_$.])([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z0-9_$*?]+)*)""".toRegex()

/** https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-4.html */
private val classPattern: Pattern = Pattern.compile("([\\[()BCDFIJSZ]*)?L([^;]+);?")
private val typeDescriptorPattern = Pattern.compile("([\\[()BCDFIJSZ]*)?L([^;]+);?")

internal fun Set<Relocator>.mapName(
name: String,
Expand All @@ -30,7 +37,7 @@ private fun Set<Relocator>.realMap(name: String, mapLiterals: Boolean): String {
var prefix = ""
var suffix = ""

val matcher = classPattern.matcher(newName)
val matcher = typeDescriptorPattern.matcher(newName)
if (matcher.matches()) {
prefix = matcher.group(1) + "L"
suffix = ""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
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.classNamePattern
import com.github.jengelman.gradle.plugins.shadow.internal.mapProperty
import com.github.jengelman.gradle.plugins.shadow.internal.property
import com.github.jengelman.gradle.plugins.shadow.internal.setProperty
import com.github.jengelman.gradle.plugins.shadow.internal.writeEntry
import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass
import java.io.IOException
import java.io.Serializable
import java.util.jar.Attributes.Name as JarAttributeName
import java.util.jar.JarFile.MANIFEST_NAME
import java.util.jar.Manifest
Expand All @@ -15,6 +19,7 @@ import org.gradle.api.logging.Logging
import org.gradle.api.model.ObjectFactory
import org.gradle.api.provider.MapProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.api.tasks.Input

/**
Expand All @@ -37,8 +42,17 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform

@get:Input public open val mainClass: Property<String> = objectFactory.property("")

/**
* Additional manifest entries to add to or remove from `MANIFEST.MF`.
*
* Setting an entry's value to [NULL] removes the corresponding attribute from the manifest.
*/
@get:Input public open val manifestEntries: MapProperty<String, Any> = objectFactory.mapProperty()

@get:Input
public open val relocateAttributes: SetProperty<String> =
objectFactory.setProperty(DEFAULT_RELOCATE_ATTRIBUTES)

override fun canTransformResource(element: FileTreeElement): Boolean {
return MANIFEST_NAME.equals(element.path, ignoreCase = true).also { flag ->
checkDupStrategy(flag, element)
Expand All @@ -51,7 +65,21 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform
// passed in with the processing so we cannot tell what artifact is being processed.
if (!manifestDiscovered) {
try {
manifest = Manifest(context.inputStream)
val loadedManifest = Manifest(context.inputStream)
if (context.relocators.isNotEmpty()) {
val attributes = loadedManifest.mainAttributes
for (attribute in relocateAttributes.get()) {
val attributeValue = attributes.getValue(attribute)
if (attributeValue != null) {
val newValue =
classNamePattern.replace(attributeValue) { matchResult ->
context.relocators.relocateClass(matchResult.value)
}
attributes.putValue(attribute, newValue)
}
}
}
manifest = loadedManifest
manifestDiscovered = true
} catch (e: IOException) {
logger.warn("Failed to read MANIFEST.MF", e)
Expand All @@ -71,22 +99,49 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform
mainClass.get().takeIf(CharSequence::isNotEmpty)?.let {
attributes[JarAttributeName.MAIN_CLASS] = it
}
manifestEntries.get().forEach { (key, value) -> attributes.putValue(key, value.toString()) }
manifestEntries.get().forEach { (key, value) ->
if (value == NULL) {
attributes.remove(JarAttributeName(key))
} else {
attributes.putValue(key, value.toString())
}
}

os.writeEntry(MANIFEST_NAME, preserveFileTimestamps) {
manifest!!.write(this)
}
}

/**
* Adds the given attributes to [manifestEntries].
*
* If a value is `null`, it will be mapped to [NULL] to remove the attribute from the manifest.
*/
@Deprecated(
"Use manifestEntries instead. This method will be removed in Shadow 10.",
replaceWith = ReplaceWith("manifestEntries.putAll(attributes)"),
)
public open fun attributes(attributes: Map<String, *>) {
attributes.forEach { (key, value) ->
if (value != null) {
manifestEntries.put(key, value)
}
manifestEntries.put(key, value ?: NULL)
}
}

private companion object {
public companion object {
private val DEFAULT_RELOCATE_ATTRIBUTES =
setOf("Export-Package", "Import-Package", "Provide-Capability", "Require-Capability")

private val logger = Logging.getLogger(ManifestResourceTransformer::class.java)

/**
* A sentinel object used in [manifestEntries] or [attributes] to indicate that the specified
* manifest attribute should be removed from the merged `MANIFEST.MF`.
*/
@JvmField
public val NULL: Any =
object : Serializable {
@Suppress("unused") // For JavaIoSerializableObjectMustHaveReadResolve.
private fun readResolve(): Any = NULL
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
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.classNamePattern
import com.github.jengelman.gradle.plugins.shadow.internal.writeEntry
import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator
import com.github.jengelman.gradle.plugins.shadow.relocation.relocateClass
Expand Down Expand Up @@ -50,18 +51,11 @@ constructor(patternSet: PatternSet = PatternSet().include("META-INF/proguard/**"
}

internal companion object {
/**
* Matches Java class names, fully qualified class names, package wildcards (e.g. `com.foo.**`),
* and inner classes (`com.foo.Bar$Inner`).
*/
private val CLASS_PATTERN =
"""(?<![a-zA-Z0-9_$.])([a-zA-Z_$][a-zA-Z0-9_$]*(?:\.[a-zA-Z0-9_$*?]+)+)""".toRegex()

fun Iterable<Relocator>.relocateRuleLine(line: String): String {
return when {
line.isBlank() || line.trimStart().startsWith("#") -> line
else ->
CLASS_PATTERN.replace(line) { matchResult ->
classNamePattern.replace(line) { matchResult ->
relocateClass(matchResult.value)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isFalse
import assertk.assertions.isTrue
import com.github.jengelman.gradle.plugins.shadow.testkit.crlfEolString
import com.github.jengelman.gradle.plugins.shadow.testkit.getContent
import java.util.jar.JarFile.MANIFEST_NAME
import org.junit.jupiter.api.Test
Expand Down Expand Up @@ -53,7 +54,7 @@ class ManifestAppenderTransformerTest : BaseTransformerTest<ManifestAppenderTran
|
|"""
.trimMargin()
.replace("\n", "\r\n")
.crlfEolString
)
}

Expand Down
Loading