Repository: GradleUp/shadow (kotlin)

Issue:
Feature Request: Fine-Grained Content Filtering for Dependencies
### What feature do you want to see added?

We are currently facing an issue where our project (JAR A) depends on a shaded JAR (JAR B). This shaded JAR B includes an older, vulnerable version of a library (Package C), including its META-INF files.

Our project A also brings in a newer, non-vulnerable version of Package C as a transitive dependency. However, during the final shading process for JAR A, the older META-INF files from Package C, which are bundled inside the shaded JAR B, are being included. This is causing our vulnerability scanner to flag the final artifact as having a known vulnerability, even though the newer classes of Package C are correctly used at runtime.

We need a mechanism to precisely exclude the problematic META-INF directory of the old Package C that resides within the shaded JAR B.

---

We would like the Gradle Shadow plugin to implement a feature for fine-grained content filtering on a per-dependency basis. This functionality is already available in the Apache Maven Shade Plugin (https://maven.apache.org/plugins/maven-shade-plugin/examples/includes-excludes.html) and is extremely powerful for resolving these types of conflicts. 

Here is an example from their documentation of how they achieve this:

```xml
<filters>
  <filter>
    <artifact>junit:junit</artifact>
    <includes>
      <include>junit/framework/**</include>
    </includes>
    <excludes>
      <exclude>org/junit/runners/**</exclude>
    </excludes>
  </filter>
  <filter>
    <artifact>*:*</artifact>
    <excludes>
      <exclude>META-INF/*.SF</exclude>
      <exclude>META-INF/*.DSA</exclude>
      <exclude>META-INF/*.RSA</exclude>
    </excludes>
  </filter>
</filters>
```

For our specific use case in gradle, it might look something like this:

```groovy
// Hypothetical DSL
shadowJar {
  dependencies {
    // ... other configurations
    include(dependency('group:shaded-jar-B')) {
      exclude('META-INF/maven/vulnerable/package-c/**')
    }
}
```

This would instruct the Shadow plugin to remove all files matching the exclude pattern, but only from the contents of the shaded-jar-B dependency before merging them into the final uber JAR.


Thanks for your consideration!


### Upstream changes

n/a

### Are you interested in contributing this feature?

Yes if the proposal is sound.

Pull request description:
Closes #1677.

Patch:
diff --git a/docs/configuration/merging/README.md b/docs/configuration/merging/README.md
index 3a695752d..f332acd8a 100644
--- a/docs/configuration/merging/README.md
+++ b/docs/configuration/merging/README.md
@@ -77,20 +77,117 @@ Different strategies will lead to different results for `foo/bar` files in the J
     ```
 
 The [`ResourceTransformer`][ResourceTransformer]s like [`ServiceFileTransformer`][ServiceFileTransformer] will not work
-as expected as the duplicate resource files fed for them are excluded beforehand. However, this behavior might be what you expected for duplicate `foo/bar` files, preventing them from being included.
+as expected as the duplicate resource files fed for them are excluded beforehand. However, this behavior might be what
+you expected for duplicate `foo/bar` files, preventing them from being included.
 
-Want [`ResourceTransformer`][ResourceTransformer]s and `duplicatesStrategy` to work together? There are several steps
-to take:
+Want [`ResourceTransformer`][ResourceTransformer]s and `duplicatesStrategy` to work together? There are several common
+steps to take:
 
-1. Set the strategy to `INCLUDE` or `WARN`.
+1. Set the default strategy to `INCLUDE` or `WARN`.
 2. Apply your [`ResourceTransformer`][ResourceTransformer]s.
 3. Remove duplicate entries by
-    - overriding the default strategy for specific files using [`filesMatching`][Jar.filesMatching]
+    - overriding the default strategy for specific files to `EXCLUDE` or `FAIL` using
+    [`filesMatching`][Jar.filesMatching], [`filesNotMatching`][Jar.filesNotMatching], or [`eachFile`][Jar.eachFile] functions
     - or applying [`PreserveFirstFoundResourceTransformer`][PreserveFirstFoundResourceTransformer] for specific files
     - or write your own [`ResourceTransformer`][ResourceTransformer] to handle duplicates
     - or mechanism similar.
-4. Optionally, enable [`ShadowJar.failOnDuplicateEntries`][ShadowJar.failOnDuplicateEntries] to check duplicate entries in the final JAR.
-5. Optionally, use [Diffuse](https://github.com/JakeWharton/diffuse) to diff the JARs.
+
+Alternatively, you can follow these steps:
+
+1. Set the default strategy to `EXCLUDE` or `FAIL`.
+2. Apply your [`ResourceTransformer`][ResourceTransformer]s.
+3. Bypass the duplicate entries which should be handled by the [`ResourceTransformer`][ResourceTransformer]s using
+    [`filesMatching`][Jar.filesMatching], [`filesNotMatching`][Jar.filesNotMatching], or [`eachFile`][Jar.eachFile] functions
+    to set their `duplicatesStrategy` to `INCLUDE` or `WARN`.
+
+Optional steps:
+
+- Enable [`ShadowJar.failOnDuplicateEntries`][ShadowJar.failOnDuplicateEntries] to check duplicate entries in the final JAR.
+- Use [Diffuse](https://github.com/JakeWharton/diffuse) to diff the JARs.
+
+Here are some examples:
+
+=== "Kotlin"
+
+    ```kotlin
+    tasks.shadowJar {
+      // Step 1.
+      duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or WARN.
+      // Step 2.
+      mergeServiceFiles()
+      // Step 3. Using `filesNotMatching`:
+      filesNotMatching("META-INF/services/**") {
+        duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Or FAIL.
+      }
+      // Step 3. Using `PreserveFirstFoundResourceTransformer`:
+      transform<com.github.jengelman.gradle.plugins.shadow.transformers.PreserveFirstFoundResourceTransformer>() {
+        resources.add("META-INF/foo/**") // Or something else where the first occurrence should be preserved.
+      }
+    }
+
+    tasks.shadowJar {
+      // Step 1.
+      duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Or FAIL.
+      // Step 2.
+      mergeServiceFiles()
+      // Step 3. Using `filesMatching`:
+      filesMatching("META-INF/services/**") {
+        duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or WARN.
+      }
+      // Step 3. Using `eachFile`:
+      eachFile {
+        if (path.startsWith("META-INF/services/")) {
+          duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or WARN.
+        }
+      }
+    }
+
+    tasks.shadowJar {
+      // Optional step.
+      failOnDuplicateEntries = true
+    }
+    ```
+
+=== "Groovy"
+
+    ```groovy
+    tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+      // Step 1.
+      duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or WARN.
+      // Step 2.
+      mergeServiceFiles()
+      // Step 3. Using `filesNotMatching`:
+      filesNotMatching('META-INF/services/**') {
+        duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Or FAIL.
+      }
+      // Step 3. Using `PreserveFirstFoundResourceTransformer`:
+      transform(com.github.jengelman.gradle.plugins.shadow.transformers.PreserveFirstFoundResourceTransformer) {
+        resources.add('META-INF/foo/**') // Or something else where the first occurrence should be preserved.
+      }
+    }
+
+    tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+      // Step 1.
+      duplicatesStrategy = DuplicatesStrategy.EXCLUDE // Or FAIL.
+      // Step 2.
+      mergeServiceFiles()
+      // Step 3. Using `filesMatching`:
+      filesMatching('META-INF/services/**') {
+        duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or WARN.
+      }
+      // Step 3. Using `eachFile`:
+      eachFile {
+        if (it.path.startsWith('META-INF/services/')) {
+          it.duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or WARN.
+        }
+      }
+    }
+
+    tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) {
+      // Optional step.
+      failOnDuplicateEntries = true
+    }
+    ```
 
 ## Basic ResourceTransformer Usage
 
@@ -451,7 +548,9 @@ It must be added using the [`transform`][ShadowJar.transform] methods.
 
 
 [AbstractCopyTask]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.AbstractCopyTask.html
+[Jar.eachFile]: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar:eachFile(org.gradle.api.Action)
 [Jar.filesMatching]: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar:filesMatching(java.lang.Iterable,%20org.gradle.api.Action)
+[Jar.filesNotMatching]: https://docs.gradle.org/current/dsl/org.gradle.jvm.tasks.Jar.html#org.gradle.jvm.tasks.Jar:filesNotMatching(java.lang.Iterable,%20org.gradle.api.Action)
 [AppendingTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-appending-transformer/index.html
 [DuplicatesStrategy]: https://docs.gradle.org/current/javadoc/org/gradle/api/file/DuplicatesStrategy.html
 [GroovyExtensionModuleTransformer]: ../../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-groovy-extension-module-transformer/index.html
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 be28cf93c..cbe130cc9 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
@@ -219,19 +219,12 @@ public abstract class ShadowJar : Jar() {
    * **NOTE:** The strategy takes precedence over transforming and relocating.
    * Some [ResourceTransformer]s like [ServiceFileTransformer] will not work as expected with setting the strategy to
    * [EXCLUDE] (the default), as the duplicate resource files fed for them are excluded beforehand.
-   * Want [ResourceTransformer]s and the strategy to work together? There are several steps to take:
-   *
-   * 1. Set the strategy to [INCLUDE] or [WARN].
-   * 2. Apply your [ResourceTransformer]s.
-   * 3. Remove duplicate entries by
-   *     - overriding the default strategy for specific files using [filesMatching]
-   *     - or applying `PreserveFirstFoundResourceTransformer` for specific files
-   *     - or write your own `ResourceTransformer`s to handle duplicates
-   *     - or mechanism similar.
-   * 4. Optionally, enable [failOnDuplicateEntries] to check duplicate entries in the final JAR.
-   * 5. Optionally, use [Diffuse](https://github.com/JakeWharton/diffuse) to diff the JARs.
+   * Want [ResourceTransformer]s and the strategy to work together? See more details in the
+   * [Handling Duplicates Strategy](https://gradleup.com/shadow/configuration/merging/#handling-duplicates-strategy) section.
    *
+   * @see [eachFile]
    * @see [filesMatching]
+   * @see [filesNotMatching]
    * @see [DuplicatesStrategy]
    * @see [CopySpec.duplicatesStrategy]
    */


Test patch:
diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt
index edc224d9e..6b1d2fe41 100644
--- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt
+++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformerTest.kt
@@ -8,6 +8,11 @@ import com.github.jengelman.gradle.plugins.shadow.util.getContent
 import kotlin.io.path.appendText
 import kotlin.io.path.writeText
 import org.gradle.api.file.DuplicatesStrategy
+import org.gradle.api.file.DuplicatesStrategy.EXCLUDE
+import org.gradle.api.file.DuplicatesStrategy.FAIL
+import org.gradle.api.file.DuplicatesStrategy.INCLUDE
+import org.gradle.api.file.DuplicatesStrategy.INHERIT
+import org.gradle.api.file.DuplicatesStrategy.WARN
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.Arguments
@@ -221,8 +226,8 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
   }
 
   @Test
-  fun strategyExcludeCanBeOverriddenByFilesMatching() {
-    writeDuplicatesStrategy(DuplicatesStrategy.EXCLUDE)
+  fun strategyCanBeOverriddenByFilesMatching() {
+    writeDuplicatesStrategy(EXCLUDE)
     projectScript.appendText(
       """
         $shadowJarTask {
@@ -242,8 +247,8 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
   }
 
   @Test
-  fun strategyIncludeCanBeOverriddenByFilesNotMatching() {
-    writeDuplicatesStrategy(DuplicatesStrategy.INCLUDE)
+  fun strategyCanBeOverriddenByFilesNotMatching() {
+    writeDuplicatesStrategy(INCLUDE)
     projectScript.appendText(
       """
         $shadowJarTask {
@@ -262,6 +267,34 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
     }
   }
 
+  @ParameterizedTest
+  @MethodSource("eachFileStrategyProvider")
+  fun strategyCanBeOverriddenByEachFile(
+    default: DuplicatesStrategy,
+    override: DuplicatesStrategy,
+    matchPath: String,
+  ) {
+    writeDuplicatesStrategy(default)
+    projectScript.appendText(
+      """
+        $shadowJarTask {
+          eachFile {
+            if (path == '$matchPath') {
+              duplicatesStrategy = DuplicatesStrategy.$override
+            }
+          }
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarPath)
+
+    assertThat(outputShadowedJar).useAll {
+      getContent(ENTRY_SERVICES_SHADE).isEqualTo(CONTENT_ONE_TWO)
+      getContent(ENTRY_SERVICES_FOO).isEqualTo("one")
+    }
+  }
+
   private fun writeDuplicatesStrategy(strategy: DuplicatesStrategy) {
     projectScript.appendText(
       """
@@ -279,21 +312,21 @@ class ServiceFileTransformerTest : BaseTransformerTest() {
   private companion object {
     @JvmStatic
     fun withThrowingProvider() = listOf(
-      Arguments.of(
-        DuplicatesStrategy.FAIL,
-        "Cannot copy zip entry .* to .* because zip entry .* has already been copied there",
-      ),
-      Arguments.of(
-        DuplicatesStrategy.INHERIT,
-        "Entry .* is a duplicate but no duplicate handling strategy has been set",
-      ),
+      Arguments.of(FAIL, "Cannot copy zip entry .* to .* because zip entry .* has already been copied there"),
+      Arguments.of(INHERIT, "Entry .* is a duplicate but no duplicate handling strategy has been set"),
     )
 
     @JvmStatic
     fun withoutThrowingProvider() = listOf(
-      Arguments.of(DuplicatesStrategy.EXCLUDE, CONTENT_ONE, "one"),
-      Arguments.of(DuplicatesStrategy.INCLUDE, CONTENT_ONE_TWO, "one\ntwo"),
-      Arguments.of(DuplicatesStrategy.WARN, CONTENT_ONE_TWO, "one\ntwo"),
+      Arguments.of(EXCLUDE, CONTENT_ONE, "one"),
+      Arguments.of(INCLUDE, CONTENT_ONE_TWO, "one\ntwo"),
+      Arguments.of(WARN, CONTENT_ONE_TWO, "one\ntwo"),
+    )
+
+    @JvmStatic
+    fun eachFileStrategyProvider() = listOf(
+      Arguments.of(EXCLUDE, INCLUDE, ENTRY_SERVICES_SHADE),
+      Arguments.of(INCLUDE, EXCLUDE, ENTRY_SERVICES_FOO),
     )
   }
 }