Repository: GradleUp/shadow (kotlin)

Issue:
Relocation Duplicates kotlin.kotlin_builtins
Please check the (User Guide)[http://imperceptiblethoughts.com/shadow] before submitting "how do I do 'x'?" questions!
I'm still a little new to Gradle, so I'm not sure if this is an issue or just me misusing everything. Thanks!

### Shadow Version
1.2.4
### Gradle Version
3.5
### Expected Behavior
`kotlin.kotlin_builtins is moved to cafe.neso.bouncer.minecraft.lib.kotlin.kotlin_builtins`
I'm not sure if this is what I want or what would fix this issue. I'm getting this exception specifically:
`java.lang.IllegalStateException: Resource not found in classpath: cafe/neso/minecraft/bouncer/lib/kotlin/cafe.neso.minecraft.bouncer.lib.kotlin.cafe.neso.minecraft.bouncer.lib.kotlin_builtins`
### Actual Behavior
`kotlin.kotlin_builtins` is moved from `kotlin` to `cafe/neso/bouncer/minecraft/lib/kotlin` but the package remains as `kotlin` in the file, while other files appear to be referencing `cafe.neso.bouncer.minecraft.lib.kotlin.cafe.neso.bouncer.minecraft.lib.kotlin.kotlin_builtins` (note the duplication)
### Gradle Build Script(s)
https://gist.github.com/moltendorf/2735da9fa626b4fc2aab49a0eb3eca6e
### Content of Shadow JAR (`jar tf <jar file>` - post link to GIST if too long)
https://gist.github.com/moltendorf/2735da9fa626b4fc2aab49a0eb3eca6e#file-jar-files

Pull request description:
- Closes #295.
- Closes #562.
- Closes #884.

---

- [x] [CHANGELOG](https://github.com/GradleUp/shadow/blob/main/docs/changes/README.md)'s "Unreleased" section has been updated, if applicable.

Patch:
diff --git a/docs/changes/README.md b/docs/changes/README.md
index 46b16e976..e26e172c2 100644
--- a/docs/changes/README.md
+++ b/docs/changes/README.md
@@ -15,6 +15,10 @@
   - Overload `relocate`, `transform` and things for better usability in Kotlin.
 - **BREAKING CHANGE:** Remove redundant types from function returning. ([#1308](https://github.com/GradleUp/shadow/pull/1308))
 
+**Fixed**
+
+- Fix relocation exclusion for file patterns like `kotlin/kotlin.kotlin_builtins`. ([#1313](https://github.com/GradleUp/shadow/pull/1313))
+
 
 ## [v9.0.0-beta10] (2025-03-05)
 
diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt
index 9ca8297ae..c5ee0e436 100644
--- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt
+++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt
@@ -1,6 +1,7 @@
 package com.github.jengelman.gradle.plugins.shadow.relocation
 
 import java.util.regex.Pattern
+import org.apache.commons.io.FilenameUtils
 import org.codehaus.plexus.util.SelectorUtils
 import org.gradle.api.tasks.Input
 
@@ -199,12 +200,19 @@ public open class SimpleRelocator @JvmOverloads constructor(
           continue
         }
 
-        val classPattern = pattern.replace('.', '/')
-        add(classPattern)
+        val fileName = FilenameUtils.getName(pattern)
+        val fileParent = FilenameUtils.getPathNoEndSeparator(pattern)
+        val filePattern = if (!fileParent.isNullOrEmpty() && fileName.isNotEmpty()) {
+          // It's a file pattern like `kotlin/kotlin.kotlin_builtins`, so we don't need to normalize it.
+          pattern
+        } else {
+          pattern.replace('.', '/')
+        }
+        add(filePattern)
         // Actually, class patterns should just use 'foo.bar.*' ending with a single asterisk, but some users
         // mistake them for path patterns like 'my/path/**', so let us be a bit more lenient here.
-        if (classPattern.endsWith("/*") || classPattern.endsWith("/**")) {
-          val packagePattern = classPattern.substring(0, classPattern.lastIndexOf('/'))
+        if (filePattern.endsWith("/*") || filePattern.endsWith("/**")) {
+          val packagePattern = filePattern.substring(0, filePattern.lastIndexOf('/'))
           add(packagePattern)
         }
       }


Test patch:
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 6a0f6177b..99feaacf0 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
@@ -432,6 +432,41 @@ class RelocationTest : BasePluginTest() {
     }
   }
 
+  @Issue(
+    "https://github.com/GradleUp/shadow/issues/295",
+    "https://github.com/GradleUp/shadow/issues/562",
+    "https://github.com/GradleUp/shadow/issues/884",
+  )
+  @Test
+  fun preserveKotlinBuiltins() {
+    val kotlinJar = buildJar("kotlin.jar") {
+      insert("kotlin/kotlin.kotlin_builtins", "This is a Kotlin builtins file.")
+    }
+    projectScriptPath.appendText(
+      """
+        dependencies {
+          ${implementationFiles(kotlinJar)}
+        }
+        $shadowJar {
+          relocate('kotlin.', 'foo.kotlin.') {
+            exclude('kotlin/kotlin.kotlin_builtins')
+          }
+        }
+      """.trimIndent(),
+    )
+
+    run(shadowJarTask)
+
+    assertThat(outputShadowJar).useAll {
+      containsEntries(
+        "kotlin/kotlin.kotlin_builtins",
+      )
+      doesNotContainEntries(
+        "foo/kotlin/kotlin.kotlin_builtins",
+      )
+    }
+  }
+
   private companion object {
     @JvmStatic
     fun prefixProvider() = listOf(
diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt
index 53f391e0e..11fd55e46 100644
--- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt
+++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt
@@ -67,6 +67,14 @@ class SimpleRelocatorTest {
     assertThat(relocator.canRelocatePath("test")).isFalse() // shorter than path pattern with /
     assertThat(relocator.canRelocatePath("org/f")).isTrue() // equal to path pattern
     assertThat(relocator.canRelocatePath("/org/f")).isTrue() // equal to path pattern with /
+
+    relocator = SimpleRelocator("foo.")
+    assertThat(relocator.canRelocatePath("foo/foo.bar")).isTrue()
+    relocator.exclude("foo/foo.bar")
+    assertThat(relocator.canRelocatePath("foo/foo.bar")).isFalse() // Don't handle file path pattern.
+    assertThat(relocator.canRelocatePath("foo/foobar")).isTrue()
+    relocator.exclude("foo/foobar")
+    assertThat(relocator.canRelocatePath("foo/foobar")).isFalse() // File without extension.
   }
 
   @Test