diff --git a/api/shadow.api b/api/shadow.api index d6efc05c3..566c51870 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -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 (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 (Lorg/gradle/api/model/ObjectFactory;)V public fun (Lorg/gradle/api/model/ObjectFactory;Lorg/gradle/api/tasks/util/PatternSet;)V diff --git a/docs/changes/README.md b/docs/changes/README.md index aab2caa98..8bab3ab11 100644 --- a/docs/changes/README.md +++ b/docs/changes/README.md @@ -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 @@ -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 diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index cccd20bc1..008e19ffd 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -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 @@ -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( + 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() diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Relocators.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Relocators.kt index 9f65e93b1..c5eaaac3f 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Relocators.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/Relocators.kt @@ -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 = + """(?.mapName( name: String, @@ -30,7 +37,7 @@ private fun Set.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 = "" diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt index 514ce36ad..6e71ebf92 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestResourceTransformer.kt @@ -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 @@ -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 /** @@ -37,8 +42,17 @@ constructor(final override val objectFactory: ObjectFactory) : ResourceTransform @get:Input public open val mainClass: Property = 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 = objectFactory.mapProperty() + @get:Input + public open val relocateAttributes: SetProperty = + objectFactory.setProperty(DEFAULT_RELOCATE_ATTRIBUTES) + override fun canTransformResource(element: FileTreeElement): Boolean { return MANIFEST_NAME.equals(element.path, ignoreCase = true).also { flag -> checkDupStrategy(flag, element) @@ -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) @@ -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) { 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 + } } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardFilesResourceTransformer.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardFilesResourceTransformer.kt index f5034fb06..4e9f81ae6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardFilesResourceTransformer.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardFilesResourceTransformer.kt @@ -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 @@ -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 = - """(?.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) } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformerTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformerTest.kt index f8d86fffb..602eedb66 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformerTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ManifestAppenderTransformerTest.kt @@ -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 @@ -53,7 +54,7 @@ class ManifestAppenderTransformerTest : BaseTransformerTest() { + + @Test + fun canTransformResource() = + with(transformer) { + assertThat(canTransformResource(MANIFEST_NAME)).isTrue() + assertThat(canTransformResource(MANIFEST_NAME.lowercase())).isTrue() + assertThat(canTransformResource("META-INF/OTHER.MF")).isFalse() + } + + @Test + fun hasTransformedResource() = + with(transformer) { + assertThat(hasTransformedResource()).isTrue() + } + + @Test + fun mainClassAndManifestEntries() = + with(transformer) { + mainClass.set("com.example.Main") + manifestEntries.put("Custom-Key", "CustomValue") + + transform(textContext(MANIFEST_NAME, "Manifest-Version: 1.0\r\n\r\n")) + + transformToJar().use { jarPath -> + assertThat(jarPath.getMainAttr("Main-Class")).isEqualTo("com.example.Main") + assertThat(jarPath.getMainAttr("Custom-Key")).isEqualTo("CustomValue") + } + } + + @Test + fun rewriteDefaultAttributes() = + with(transformer) { + val manifest = createTestManifest() + val relocator = SimpleRelocator("javax", "jakarta") + + transform(createManifestContext(manifest, relocator)) + + transformToJar().use { jarPath -> + assertThat(jarPath.getMainAttr("Export-Package")) + .isEqualTo( + "jakarta.decorator;version=\"2.0\";uses:=\"jakarta.enterprise.inject\"," + + "jakarta.enterprise.context;version=\"2.0\";uses:=\"jakarta.enterprise.util,jakarta.inject\"" + ) + assertThat(jarPath.getMainAttr("Import-Package")) + .isEqualTo("jakarta.el,jakarta.enterprise.context;version=\"[2.0,3)\"") + assertThat(jarPath.getMainAttr("Provide-Capability")) + .isEqualTo( + "osgi.contract;osgi.contract=JavaCDI;" + + "uses:=\"jakarta.enterprise.context,jakarta.enterprise.context.spi,jakarta.enterprise.context.control," + + "jakarta.enterprise.util,jakarta.enterprise.inject,jakarta.enterprise.inject.spi," + + "jakarta.enterprise.inject.spi.configurator,jakarta.enterprise.inject.literal," + + "jakarta.enterprise.inject.se,jakarta.enterprise.event," + + "jakarta.decorator\";version:List=\"2.0,1.2,1.1,1.0\"" + ) + assertThat(jarPath.getMainAttr("Require-Capability")) + .isEqualTo( + "osgi.serviceloader;" + + "filter:=\"(osgi.serviceloader=jakarta.enterprise.inject.se.SeContainerInitializer)\";" + + "cardinality:=multiple,osgi.serviceloader;" + + "filter:=\"(osgi.serviceloader=jakarta.enterprise.inject.spi.CDIProvider)\";" + + "cardinality:=multiple,osgi.extender;" + + "filter:=\"(osgi.extender=osgi.serviceloader.processor)\"," + + "osgi.contract;osgi.contract=JavaEL;filter:=\"(&(osgi.contract=JavaEL)(version=2.2.0))\"," + + "osgi.contract;osgi.contract=JavaInterceptor;" + + "filter:=\"(&(osgi.contract=JavaInterceptor)(version=1.2.0))\"," + + "osgi.contract;osgi.contract=JavaInject;" + + "filter:=\"(&(osgi.contract=JavaInject)(version=1.0.0))\"," + + "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"" + ) + } + } + + @Test + fun rewriteDefaultAttributesWithSameSuffix() = + with(transformer) { + val manifest = createTestManifest() + val relocator = SimpleRelocator("javax", "shaded.javax") + + transform(createManifestContext(manifest, relocator)) + + transformToJar().use { jarPath -> + assertThat(jarPath.getMainAttr("Export-Package")) + .isEqualTo( + "shaded.javax.decorator;version=\"2.0\";uses:=\"shaded.javax.enterprise.inject\"," + + "shaded.javax.enterprise.context;version=\"2.0\";uses:=\"shaded.javax.enterprise.util,shaded.javax.inject\"" + ) + assertThat(jarPath.getMainAttr("Import-Package")) + .isEqualTo("shaded.javax.el,shaded.javax.enterprise.context;version=\"[2.0,3)\"") + assertThat(jarPath.getMainAttr("Provide-Capability")) + .isEqualTo( + "osgi.contract;osgi.contract=JavaCDI;" + + "uses:=\"shaded.javax.enterprise.context,shaded.javax.enterprise.context.spi,shaded.javax.enterprise.context.control," + + "shaded.javax.enterprise.util,shaded.javax.enterprise.inject,shaded.javax.enterprise.inject.spi," + + "shaded.javax.enterprise.inject.spi.configurator,shaded.javax.enterprise.inject.literal," + + "shaded.javax.enterprise.inject.se,shaded.javax.enterprise.event," + + "shaded.javax.decorator\";version:List=\"2.0,1.2,1.1,1.0\"" + ) + assertThat(jarPath.getMainAttr("Require-Capability")) + .isEqualTo( + "osgi.serviceloader;" + + "filter:=\"(osgi.serviceloader=shaded.javax.enterprise.inject.se.SeContainerInitializer)\";" + + "cardinality:=multiple,osgi.serviceloader;" + + "filter:=\"(osgi.serviceloader=shaded.javax.enterprise.inject.spi.CDIProvider)\";" + + "cardinality:=multiple,osgi.extender;" + + "filter:=\"(osgi.extender=osgi.serviceloader.processor)\"," + + "osgi.contract;osgi.contract=JavaEL;filter:=\"(&(osgi.contract=JavaEL)(version=2.2.0))\"," + + "osgi.contract;osgi.contract=JavaInterceptor;" + + "filter:=\"(&(osgi.contract=JavaInterceptor)(version=1.2.0))\"," + + "osgi.contract;osgi.contract=JavaInject;" + + "filter:=\"(&(osgi.contract=JavaInject)(version=1.0.0))\"," + + "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"" + ) + } + } + + @Test + fun rewriteRelocateAttributes() = + with(transformer) { + val source = + """ + |Manifest-Version: 1.0 + |description-custom: This jar uses javax packages + |""" + .trimMargin() + .crlfEolString + + val relocator = SimpleRelocator("javax", "jakarta") + relocateAttributes.addAll("description-custom", "attribute-unknown") + + transform(textContext(MANIFEST_NAME, source, relocator)) + + transformToJar().use { jarPath -> + assertThat(jarPath.getMainAttr("description-custom")) + .isEqualTo("This jar uses jakarta packages") + } + } + + @Test + fun removeAttributeUsingNullConstant() = + with(transformer) { + val source = + """ + |Manifest-Version: 1.0 + |Header-To-Remove: Value1 + |Header-To-Keep: Value2 + |""" + .trimMargin() + .crlfEolString + + manifestEntries.put("Header-To-Remove", ManifestResourceTransformer.NULL) + + transform(textContext(MANIFEST_NAME, source)) + + transformToJar().use { jarPath -> + assertThat(jarPath.getMainAttr("Header-To-Remove")).isNull() + assertThat(jarPath.getMainAttr("Header-To-Keep")).isEqualTo("Value2") + } + } + + private companion object { + fun createManifestContext( + manifest: Manifest, + vararg relocators: Relocator, + ): TransformerContext { + val baos = ByteArrayOutputStream() + manifest.write(baos) + return TransformerContext( + path = MANIFEST_NAME, + inputStream = ByteArrayInputStream(baos.toByteArray()), + relocators = relocators.toSet(), + ) + } + + private fun createTestManifest() = + Manifest().apply { + with(mainAttributes) { + put(Attributes.Name.MANIFEST_VERSION, "1.0") + putValue( + "Export-Package", + "javax.decorator;version=\"2.0\";uses:=\"javax.enterprise.inject\"," + + "javax.enterprise.context;version=\"2.0\";uses:=\"javax.enterprise.util,javax.inject\"", + ) + putValue( + "Import-Package", + "javax.el,javax.enterprise.context;version=\"[2.0,3)\"", + ) + putValue( + "Provide-Capability", + "osgi.contract;osgi.contract=JavaCDI;uses:=\"" + + "javax.enterprise.context,javax.enterprise.context.spi,javax.enterprise.context.control," + + "javax.enterprise.util,javax.enterprise.inject,javax.enterprise.inject.spi," + + "javax.enterprise.inject.spi.configurator,javax.enterprise.inject.literal," + + "javax.enterprise.inject.se,javax.enterprise.event,javax.decorator\";" + + "version:List=\"2.0,1.2,1.1,1.0\"", + ) + putValue( + "Require-Capability", + "osgi.serviceloader;" + + "filter:=\"(osgi.serviceloader=javax.enterprise.inject.se.SeContainerInitializer)\";" + + "cardinality:=multiple," + + "osgi.serviceloader;" + + "filter:=\"(osgi.serviceloader=javax.enterprise.inject.spi.CDIProvider)\";" + + "cardinality:=multiple,osgi.extender;" + + "filter:=\"(osgi.extender=osgi.serviceloader.processor)\"," + + "osgi.contract;osgi.contract=JavaEL;filter:=\"(&(osgi.contract=JavaEL)(version=2.2.0))\"," + + "osgi.contract;osgi.contract=JavaInterceptor;" + + "filter:=\"(&(osgi.contract=JavaInterceptor)(version=1.2.0))\"," + + "osgi.contract;osgi.contract=JavaInject;" + + "filter:=\"(&(osgi.contract=JavaInject)(version=1.0.0))\"," + + "osgi.ee;filter:=\"(&(osgi.ee=JavaSE)(version=1.8))\"", + ) + } + } + } +} diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/Strings.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/Strings.kt index 0444763ab..7a7482616 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/Strings.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/Strings.kt @@ -5,6 +5,9 @@ import java.nio.file.FileSystems val String.invariantEolString: String get() = replace(System.lineSeparator(), "\n") +val String.crlfEolString: String + get() = replace("\r\n", "\n").replace("\n", "\r\n") + val String.variantSeparatorsPathString: String get() = replace("/", fileSystem.separator)