From 95a0d1cbda589d12a2a2e1477145896d8bc2eb72 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:05:46 +0800 Subject: [PATCH 01/24] Add AGENTS.md --- AGENTS.md | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000000..10e8b8a653 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,10 @@ +# Agent Guidelines + +To understand how to contribute code, format commits, and run tests for this repository, +please refer to the [CONTRIBUTING.md](./CONTRIBUTING.md) file. + +**Instructions for AI Agents:** +1. Adhere to the coding standards and PR templates defined therein. +2. Make an atomic commit after completing each task. +3. Pass `--non-interactive` to Gradle commands to disable interactive console prompts. +4. Prefer using the `gh` CLI for GitHub-related tasks. From e0fb6a7c5cc346ef14795c6bbf503e266a97e2d6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:16:36 +0800 Subject: [PATCH 02/24] Validate ZIP entry names to prevent Zip Slip path traversal Original commit on main: 0c2744fcc050e34a084f5131d5e167b13c342bfd --- src/docs/changes/README.md | 4 ++ .../plugins/shadow/internal/ZipUtils.groovy | 14 +++++++ .../shadow/tasks/ShadowCopyAction.groovy | 13 +++--- .../internal/ZipEntryValidationSpec.groovy | 42 +++++++++++++++++++ 4 files changed, 67 insertions(+), 6 deletions(-) create mode 100644 src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipUtils.groovy create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationSpec.groovy diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 40440f60e5..9a1c9b8b8a 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -3,6 +3,10 @@ ## [Unreleased] +**Fixed** + +- Validate ZIP entry names to prevent Zip Slip path traversal. + ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipUtils.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipUtils.groovy new file mode 100644 index 0000000000..1cc1ec9632 --- /dev/null +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipUtils.groovy @@ -0,0 +1,14 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import org.apache.tools.zip.ZipEntry +import org.gradle.api.GradleException + +class ZipUtils { + + static ZipEntry zipEntry(String name) { + if (name.split("[/\\\\]").any { it == ".." }) { + throw new GradleException("Malicious ZIP entry containing path traversal sequence: $name") + } + return new ZipEntry(name) + } +} diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 90ade6def9..05e3ccdf88 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -7,6 +7,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.ZipCompressor import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.transformers.Transformer import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext +import com.github.jengelman.gradle.plugins.shadow.internal.ZipUtils import groovy.util.logging.Slf4j import org.apache.commons.io.FilenameUtils import org.apache.commons.io.IOUtils @@ -227,7 +228,7 @@ class ShadowCopyAction implements CopyAction { if (!remapper.hasRelocators() || !isClass) { if (!isTransformable(fileDetails)) { String mappedPath = remapper.map(fileDetails.relativePath.pathString) - ZipEntry archiveEntry = new ZipEntry(mappedPath) + ZipEntry archiveEntry = ZipUtils.zipEntry(mappedPath) archiveEntry.setTime(getArchiveTimeFor(fileDetails.lastModified)) archiveEntry.unixMode = (UnixStat.FILE_FLAG | fileDetails.permissions.toUnixNumeric()) zipOutStr.putNextEntry(archiveEntry) @@ -313,7 +314,7 @@ class ShadowCopyAction implements CopyAction { private void remapClass(RelativeArchivePath file, ZipFile archive) { if (file.classFile) { - ZipEntry zipEntry = setArchiveTimes(new ZipEntry(remapper.mapPath(file) + '.class')) + ZipEntry zipEntry = setArchiveTimes(ZipUtils.zipEntry(remapper.mapPath(file) + '.class')) addParentDirectories(new RelativeArchivePath(zipEntry)) remapClass(archive.getInputStream(file.entry), file.pathString, file.entry.time) } @@ -366,7 +367,7 @@ class ShadowCopyAction implements CopyAction { InputStream bis = new ByteArrayInputStream(renamedClass) try { // Now we put it back on so the class file is written out with the right extension. - ZipEntry archiveEntry = new ZipEntry(mappedName + ".class") + ZipEntry archiveEntry = ZipUtils.zipEntry(mappedName + ".class") archiveEntry.setTime(getArchiveTimeFor(lastModified)) zipOutStr.putNextEntry(archiveEntry) IOUtils.copyLarge(bis, zipOutStr) @@ -380,7 +381,7 @@ class ShadowCopyAction implements CopyAction { private void copyArchiveEntry(RelativeArchivePath archiveFile, ZipFile archive) { String mappedPath = remapper.map(archiveFile.entry.name) - ZipEntry entry = new ZipEntry(mappedPath) + ZipEntry entry = ZipUtils.zipEntry(mappedPath) entry.setTime(getArchiveTimeFor(archiveFile.entry.time)) RelativeArchivePath mappedFile = new RelativeArchivePath(entry) addParentDirectories(mappedFile) @@ -399,7 +400,7 @@ class ShadowCopyAction implements CopyAction { try { // Trailing slash in name indicates that entry is a directory String path = dirDetails.relativePath.pathString + '/' - ZipEntry archiveEntry = new ZipEntry(path) + ZipEntry archiveEntry = ZipUtils.zipEntry(path) archiveEntry.setTime(getArchiveTimeFor(dirDetails.lastModified)) archiveEntry.unixMode = (UnixStat.DIR_FLAG | dirDetails.permissions.toUnixNumeric()) zipOutStr.putNextEntry(archiveEntry) @@ -460,7 +461,7 @@ class ShadowCopyAction implements CopyAction { } else { //Parent is always a directory so add / to the end of the path String path = segments.take(segments.length - 1).join('/') + '/' - return new RelativeArchivePath(setArchiveTimes(new ZipEntry(path))) + return new RelativeArchivePath(setArchiveTimes(ZipUtils.zipEntry(path))) } } } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationSpec.groovy new file mode 100644 index 0000000000..436425e21e --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/ZipEntryValidationSpec.groovy @@ -0,0 +1,42 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import org.gradle.api.GradleException +import spock.lang.Specification + +class ZipEntryValidationSpec extends Specification { + + def "valid zip entry names do not throw"() { + expect: + ZipUtils.zipEntry(name).name == name + + where: + name << [ + "com/example/MyClass.class", + "META-INF/MANIFEST.MF", + "assets/icon..png", + "foo/bar/baz.txt", + "relative/path/to/resource" + ] + } + + def "malicious zip entry names with path traversal throw exception"() { + when: + ZipUtils.zipEntry(name) + + then: + def e = thrown(GradleException) + e.message == "Malicious ZIP entry containing path traversal sequence: $name" + + where: + name << [ + "../../../../tmp/pwned.txt", + "foo/../../bar", + "foo\\..\\..\\bar", + "..", + "../file.txt", + "..\\file.txt", + "foo/bar/..", + "foo/bar/../baz" + ] + } +} From 60cb77a0ea16db423e2cf0f1960b1fcd4a39d842 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:17:02 +0800 Subject: [PATCH 03/24] Stop catching ZipException when writing entries (#1970) Original commit on main: d73d94ad5986fb6d9366e37598494d8f7c02c896 --- src/docs/changes/README.md | 4 ++++ .../gradle/plugins/shadow/tasks/ShadowCopyAction.groovy | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 9a1c9b8b8a..6fdd525a07 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -3,6 +3,10 @@ ## [Unreleased] +**Changed** + +- Stop catching ZipException when writing entries. ([#1970](https://github.com/GradleUp/shadow/pull/1970)) + **Fixed** - Validate ZIP entry names to prevent Zip Slip path traversal. diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index 05e3ccdf88..cb641d977e 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -372,8 +372,6 @@ class ShadowCopyAction implements CopyAction { zipOutStr.putNextEntry(archiveEntry) IOUtils.copyLarge(bis, zipOutStr) zipOutStr.closeEntry() - } catch (ZipException ignored) { - log.warn("We have a duplicate " + mappedName + " in source project") } finally { bis.close() } From 7653fce1fd12367df1b1b56cba47be0dc43fc828 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:18:15 +0800 Subject: [PATCH 04/24] Fix failing for non-existent class directories (#1976) Original commit on main: 85ba389ed222770bca0f5de51b6af75532c5d92f --- src/docs/changes/README.md | 1 + .../gradle/plugins/shadow/tasks/ShadowJar.java | 8 +++++++- .../gradle/plugins/shadow/ShadowPluginSpec.groovy | 15 +++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 6fdd525a07..ad65e448f8 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -10,6 +10,7 @@ **Fixed** - Validate ZIP entry names to prevent Zip Slip path traversal. +- Fix failing for non-existent class directories. ([#1976](https://github.com/GradleUp/shadow/pull/1976)) ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index bc25af4f7d..e22b8b011c 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -182,7 +182,13 @@ protected void copy() { if (enableRelocation) { RelocationUtil.configureRelocation(this, relocationPrefix); } - from(getIncludedDependencies()); + for (File file : getIncludedDependencies().getFiles()) { + if (!file.exists()) { + getLogger().info("Skipping non-existent dependency: {}", file); + } else { + from(file); + } + } super.copy(); getLogger().info(shadowStats.toString()); } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy index bed7ca1514..8116ff9bc6 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPluginSpec.groovy @@ -1154,4 +1154,19 @@ class ShadowPluginSpec extends PluginSpecification { "META-INF/MANIFEST.MF" ]) } + + def "skip non-existent dependency directory"() { + given: + buildFile << """ + dependencies { + implementation files('non-existent-dir') + } + """.stripIndent() + + when: + def result = runWithSuccess('shadowJar') + + then: + result.task(':shadowJar').outcome == org.gradle.testkit.runner.TaskOutcome.SUCCESS + } } From c3f342125cc14daaa53af077f14264e1da7ff20d Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:19:19 +0800 Subject: [PATCH 05/24] Use GradleException for expected build failures (#2113) Original commit on main: ff1809f351e567133f2a17b655127f484893c009 --- src/docs/changes/README.md | 1 + .../gradle/plugins/shadow/ShadowPlugin.groovy | 7 +++++++ .../transformers/PropertiesFileTransformer.groovy | 3 ++- .../PropertiesFileTransformerSpec.groovy | 13 +++++++++++++ 4 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index ad65e448f8..276f3d376c 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -6,6 +6,7 @@ **Changed** - Stop catching ZipException when writing entries. ([#1970](https://github.com/GradleUp/shadow/pull/1970)) +- Use GradleException for expected build failures. ([#2113](https://github.com/GradleUp/shadow/pull/2113)) **Fixed** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy index b3e8b88ea1..c0d93ff268 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowPlugin.groovy @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow import com.github.jengelman.gradle.plugins.shadow.legacy.LegacyShadowPlugin import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar +import org.gradle.api.GradleException import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.plugins.ApplicationPlugin @@ -19,6 +20,12 @@ class ShadowPlugin implements Plugin { plugins.withType(ApplicationPlugin) { plugins.apply(ShadowApplicationPlugin) } + plugins.withId('com.android.base') { + throw new GradleException( + "Shadow does not support being used with AGP. You may need the Android Fused Library plugin instead. " + + "See https://developer.android.com/build/publish-library/fused-library" + ) + } // Apply the legacy plugin last // Because we apply the ShadowJavaPlugin/ShadowApplication plugin in a withType callback for the // respective JavaPlugin/ApplicationPlugin, it may still apply before the shadowJar task is created and diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy index a8e5dc58c0..f78372ec16 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy @@ -23,6 +23,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.CleanProperties import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream import org.codehaus.plexus.util.IOUtil +import org.gradle.api.GradleException import org.gradle.api.file.FileTreeElement import org.gradle.api.tasks.Input import org.gradle.api.tasks.Internal @@ -196,7 +197,7 @@ class PropertiesFileTransformer implements Transformer { private Properties transformKeys(Properties properties) { if (keyTransformer == null) { - throw new IllegalStateException("keyTransformer must not be null.") + throw new GradleException("keyTransformer must not be null.") } if (keyTransformer == IDENTITY) { return properties diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy index 578d4a1dfa..0678f46ad4 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformerSpec.groovy @@ -161,4 +161,17 @@ class PropertiesFileTransformerSpec extends TransformerSpecSupport { path | charset | input || output 'utf8.properties' | 'utf-8' | ['foo': '传傳磨宿说説'] || ['foo': '传傳磨宿说説'] } + + void "throws GradleException when keyTransformer is null"() { + given: + PropertiesFileTransformer transformer = new PropertiesFileTransformer() + transformer.keyTransformer = null + + when: + transformer.transform(context("test.properties", "foo=bar")) + + then: + def e = thrown(org.gradle.api.GradleException) + e.message == "keyTransformer must not be null." + } } From 1dead140d9cdf5d1cf841f970ff8b808561f38df Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:21:15 +0800 Subject: [PATCH 06/24] Update RelocatorRemapper class pattern to cover more Java method descriptors (#1731) Original commit on main: 5497d138557b6851be85fbd64fc3d1d768d04edc --- src/docs/changes/README.md | 1 + .../shadow/impl/RelocatorRemapper.groovy | 68 +++++++++++-------- .../relocation/RelocatorRemapperSpec.groovy | 28 ++++++++ 3 files changed, 69 insertions(+), 28 deletions(-) create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 276f3d376c..bb4ad5e66a 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -7,6 +7,7 @@ - Stop catching ZipException when writing entries. ([#1970](https://github.com/GradleUp/shadow/pull/1970)) - Use GradleException for expected build failures. ([#2113](https://github.com/GradleUp/shadow/pull/2113)) +- Update `RelocatorRemapper` class pattern to cover more Java method descriptors. ([#1731](https://github.com/GradleUp/shadow/pull/1731)) **Fixed** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy index f47ecf1048..9ea2d70324 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy @@ -38,7 +38,7 @@ import java.util.regex.Pattern @CompileStatic class RelocatorRemapper extends Remapper { - private final Pattern classPattern = Pattern.compile("(\\[*)?L(.+)") + private final Pattern classPattern = Pattern.compile("([\\[()BCDFIJSZ]*)?L([^;]+)(;?)") List relocators ShadowStats stats @@ -58,26 +58,28 @@ class RelocatorRemapper extends Remapper { String name = (String) object String value = name - String prefix = "" - String suffix = "" - Matcher m = classPattern.matcher(name) - if (m.matches()) { - prefix = m.group(1) + "L" - suffix = "" - name = m.group(2) - } - - for (Relocator r : relocators) { - if (r.canRelocateClass(name)) { - RelocateClassContext classContext = RelocateClassContext.builder().className(name).stats(stats).build() - value = prefix + r.relocateClass(classContext) + suffix - break - } else if (r.canRelocatePath(name)) { - RelocatePathContext pathContext = RelocatePathContext.builder().path(name).stats(stats).build() - value = prefix + r.relocatePath(pathContext) + suffix - break - } + if (m.find()) { + StringBuffer sb = new StringBuffer() + do { + String prefix = (m.group(1) ?: "") + "L" + String className = m.group(2) + String relocated = className + for (Relocator r : relocators) { + if (r.canRelocateClass(className)) { + RelocateClassContext classContext = RelocateClassContext.builder().className(className).stats(stats).build() + relocated = r.relocateClass(classContext) + break + } else if (r.canRelocatePath(className)) { + RelocatePathContext pathContext = RelocatePathContext.builder().path(className).stats(stats).build() + relocated = r.relocatePath(pathContext) + break + } + } + m.appendReplacement(sb, Matcher.quoteReplacement(prefix + relocated + ";")) + } while (m.find()) + m.appendTail(sb) + value = sb.toString() } return value @@ -90,20 +92,30 @@ class RelocatorRemapper extends Remapper { String map(String name) { String value = name - String prefix = "" - String suffix = "" - Matcher m = classPattern.matcher(name) - if (m.matches()) { - prefix = m.group(1) + "L" - suffix = "" - name = m.group(2) + if (m.find()) { + StringBuffer sb = new StringBuffer() + do { + String prefix = (m.group(1) ?: "") + "L" + String className = m.group(2) + String relocated = className + for (Relocator r : relocators) { + if (r.canRelocatePath(className)) { + RelocatePathContext pathContext = RelocatePathContext.builder().path(className).stats(stats).build() + relocated = r.relocatePath(pathContext) + break + } + } + m.appendReplacement(sb, Matcher.quoteReplacement(prefix + relocated + ";")) + } while (m.find()) + m.appendTail(sb) + value = sb.toString() } for (Relocator r : relocators) { if (r.canRelocatePath(name)) { RelocatePathContext pathContext = RelocatePathContext.builder().path(name).stats(stats).build() - value = prefix + r.relocatePath(pathContext) + suffix + value = r.relocatePath(pathContext) break } } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy new file mode 100644 index 0000000000..5357b394ac --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy @@ -0,0 +1,28 @@ +package com.github.jengelman.gradle.plugins.shadow.relocation + +import com.github.jengelman.gradle.plugins.shadow.ShadowStats +import com.github.jengelman.gradle.plugins.shadow.impl.RelocatorRemapper +import spock.lang.Specification +import spock.lang.Unroll + +@Unroll +class RelocatorRemapperSpec extends Specification { + + def "mapValue relocates #descriptor correctly"() { + given: + Relocator relocator = new SimpleRelocator('org.package', 'shadow.org.package', [], []) + RelocatorRemapper remapper = new RelocatorRemapper([relocator], new ShadowStats()) + + expect: + remapper.mapValue(descriptor) == expected + + where: + descriptor || expected + 'Lorg/package/ClassA;' || 'Lshadow/org/package/ClassA;' + '[Lorg/package/ClassA;' || '[Lshadow/org/package/ClassA;' + '(Lorg/package/ClassA;)V' || '(Lshadow/org/package/ClassA;)V' + '(ZLorg/package/ClassA;)V' || '(ZLshadow/org/package/ClassA;)V' + '(ILorg/package/ClassA;Lorg/package/ClassB;)V' || '(ILshadow/org/package/ClassA;Lshadow/org/package/ClassB;)V' + '()Lorg/package/ClassA;' || '()Lshadow/org/package/ClassA;' + } +} From 833d9313290dc98e0824c0a55baf3176587657a1 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:22:23 +0800 Subject: [PATCH 07/24] Fix skipStringConstants per-relocator behavior: use continue instead of return (#1968) Original commit on main: 0564cae7dc936922aac5f273ddd778320f1608c8 --- src/docs/changes/README.md | 1 + .../plugins/shadow/impl/RelocatorRemapper.groovy | 4 ++++ .../shadow/relocation/RelocatorRemapperSpec.groovy | 11 +++++++++++ 3 files changed, 16 insertions(+) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index bb4ad5e66a..3aac719d22 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -13,6 +13,7 @@ - Validate ZIP entry names to prevent Zip Slip path traversal. - Fix failing for non-existent class directories. ([#1976](https://github.com/GradleUp/shadow/pull/1976)) +- Fix skipStringConstants per-relocator behavior in mapName. ([#1968](https://github.com/GradleUp/shadow/pull/1968)) ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy index 9ea2d70324..57e9acc5ed 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy @@ -23,6 +23,7 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowStats import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction.RelativeArchivePath import groovy.transform.CompileStatic import org.vafer.jdeb.shaded.objectweb.asm.commons.Remapper @@ -66,6 +67,9 @@ class RelocatorRemapper extends Remapper { String className = m.group(2) String relocated = className for (Relocator r : relocators) { + if (r instanceof SimpleRelocator && ((SimpleRelocator) r).rawString) { + continue + } if (r.canRelocateClass(className)) { RelocateClassContext classContext = RelocateClassContext.builder().className(className).stats(stats).build() relocated = r.relocateClass(classContext) diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy index 5357b394ac..9b03673c92 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy @@ -25,4 +25,15 @@ class RelocatorRemapperSpec extends Specification { '(ILorg/package/ClassA;Lorg/package/ClassB;)V' || '(ILshadow/org/package/ClassA;Lshadow/org/package/ClassB;)V' '()Lorg/package/ClassA;' || '()Lshadow/org/package/ClassA;' } + + def "skipStringConstants is per relocator in mapValue"() { + given: + Relocator skippingRelocator = new SimpleRelocator('org.package', 'shadow.org.package', [], [], true) + Relocator normalRelocator = new SimpleRelocator('com.example', 'shadow.com.example', [], []) + RelocatorRemapper remapper = new RelocatorRemapper([skippingRelocator, normalRelocator], new ShadowStats()) + + expect: + remapper.mapValue('Lcom/example/Foo;') == 'Lshadow/com/example/Foo;' + remapper.mapValue('Lorg/package/Bar;') == 'Lorg/package/Bar;' + } } From 8fee4b1a7dfeb34808b9cb484506da3575397028 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:23:08 +0800 Subject: [PATCH 08/24] Do not write modified class files for no-op relocations (#1694) Original commit on main: 74651fbbdee4579ce49271bf7f57d823a03d8c62 --- src/docs/changes/README.md | 1 + .../shadow/tasks/ShadowCopyAction.groovy | 48 +++++++++++++------ 2 files changed, 34 insertions(+), 15 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 3aac719d22..fd2219f72a 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -8,6 +8,7 @@ - Stop catching ZipException when writing entries. ([#1970](https://github.com/GradleUp/shadow/pull/1970)) - Use GradleException for expected build failures. ([#2113](https://github.com/GradleUp/shadow/pull/2113)) - Update `RelocatorRemapper` class pattern to cover more Java method descriptors. ([#1731](https://github.com/GradleUp/shadow/pull/1731)) +- Do not write modified class files for no-op relocations. ([#1694](https://github.com/GradleUp/shadow/pull/1694)) **Fixed** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy index cb641d977e..4f52bb4a45 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyAction.groovy @@ -337,34 +337,52 @@ class ShadowCopyAction implements CopyAction { * See #364 and #408. */ private void remapClass(InputStream classInputStream, String path, long lastModified) { - InputStream is = classInputStream - ClassReader cr = new ClassReader(is) - - // We don't pass the ClassReader here. This forces the ClassWriter to rebuild the constant pool. - // Copying the original constant pool should be avoided because it would keep references - // to the original class names. This is not a problem at runtime (because these entries in the - // constant pool are never used), but confuses some tools such as Felix' maven-bundle-plugin - // that use the constant pool to determine the dependencies of a class. - ClassWriter cw = new ClassWriter(0) + byte[] bytes + try { + bytes = IOUtils.toByteArray(classInputStream) + } finally { + classInputStream.close() + } + + boolean modified = false + RelocatorRemapper modifiedRemapper = new RelocatorRemapper(remapper.relocators, remapper.stats) { + @Override + Object mapValue(Object object) { + Object result = super.mapValue(object) + if (result != object) { + modified = true + } + return result + } - ClassVisitor cv = new ClassRemapper(cw, remapper) + @Override + String map(String name) { + String result = super.map(name) + if (result != name) { + modified = true + } + return result + } + } + + ClassReader cr = new ClassReader(bytes) + ClassWriter cw = new ClassWriter(0) + ClassVisitor cv = new ClassRemapper(cw, modifiedRemapper) try { cr.accept(cv, ClassReader.EXPAND_FRAMES) } catch (Throwable ise) { throw new GradleException("Error in ASM processing class " + path, ise) - } finally { - is.close() } - byte[] renamedClass = cw.toByteArray() + byte[] finalClass = modified ? cw.toByteArray() : bytes // Temporarily remove the multi-release prefix. String multiReleasePrefix = path.find("^META-INF/versions/\\d+/") ?: "" path = path.replace(multiReleasePrefix, "") - String mappedName = multiReleasePrefix + remapper.mapPath(path) + String mappedName = multiReleasePrefix + modifiedRemapper.mapPath(path) - InputStream bis = new ByteArrayInputStream(renamedClass) + InputStream bis = new ByteArrayInputStream(finalClass) try { // Now we put it back on so the class file is written out with the right extension. ZipEntry archiveEntry = ZipUtils.zipEntry(mappedName + ".class") From 62a2f9dfbec9ac56b5eeee9656ed93926a8d5e1a Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:23:31 +0800 Subject: [PATCH 09/24] Support relocating list of types in RelocatorRemapper (#1714) Original commit on main: 0fc59cccb9a99df4dc737937beb7822daec61346 --- src/docs/changes/README.md | 1 + .../plugins/shadow/relocation/RelocatorRemapperSpec.groovy | 1 + 2 files changed, 2 insertions(+) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index fd2219f72a..2063b26ee7 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -9,6 +9,7 @@ - Use GradleException for expected build failures. ([#2113](https://github.com/GradleUp/shadow/pull/2113)) - Update `RelocatorRemapper` class pattern to cover more Java method descriptors. ([#1731](https://github.com/GradleUp/shadow/pull/1731)) - Do not write modified class files for no-op relocations. ([#1694](https://github.com/GradleUp/shadow/pull/1694)) +- Support relocating list of types in `RelocatorRemapper`. ([#1714](https://github.com/GradleUp/shadow/pull/1714)) **Fixed** diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy index 9b03673c92..1b7ff33df3 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy @@ -23,6 +23,7 @@ class RelocatorRemapperSpec extends Specification { '(Lorg/package/ClassA;)V' || '(Lshadow/org/package/ClassA;)V' '(ZLorg/package/ClassA;)V' || '(ZLshadow/org/package/ClassA;)V' '(ILorg/package/ClassA;Lorg/package/ClassB;)V' || '(ILshadow/org/package/ClassA;Lshadow/org/package/ClassB;)V' + 'Lorg/package/ClassA;Lorg/package/ClassB;' || 'Lshadow/org/package/ClassA;Lshadow/org/package/ClassB;' '()Lorg/package/ClassA;' || '()Lshadow/org/package/ClassA;' } From 0f8d9cd238b73fe9256f371f5eb9e8149d24df88 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:24:10 +0800 Subject: [PATCH 10/24] Fix excluding dependencies whose versions contain + (#1597) Original commit on main: 8b6090e2589f3d7349dbb6b9e96adb9121bbdea9 --- src/docs/changes/README.md | 1 + .../internal/AbstractDependencyFilter.groovy | 6 ++-- .../AbstractDependencyFilterSpec.groovy | 30 +++++++++++++++++++ 3 files changed, 34 insertions(+), 3 deletions(-) create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilterSpec.groovy diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 2063b26ee7..2e5fda2a91 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -16,6 +16,7 @@ - Validate ZIP entry names to prevent Zip Slip path traversal. - Fix failing for non-existent class directories. ([#1976](https://github.com/GradleUp/shadow/pull/1976)) - Fix skipStringConstants per-relocator behavior in mapName. ([#1968](https://github.com/GradleUp/shadow/pull/1968)) +- Fix excluding dependencies whose versions contain `+`. ([#1597](https://github.com/GradleUp/shadow/pull/1597)) ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.groovy index 07f01e0891..36c5e0fd0a 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilter.groovy @@ -106,9 +106,9 @@ abstract class AbstractDependencyFilter implements DependencyFilter { @Override Spec dependency(Dependency dependency) { this.dependency({ ResolvedDependency it -> - (!dependency.group || it.moduleGroup.matches(dependency.group)) && - (!dependency.name || it.moduleName.matches(dependency.name)) && - (!dependency.version || it.moduleVersion.matches(dependency.version)) + (!dependency.group || it.moduleGroup == dependency.group || it.moduleGroup.matches(dependency.group)) && + (!dependency.name || it.moduleName == dependency.name || it.moduleName.matches(dependency.name)) && + (!dependency.version || it.moduleVersion == dependency.version || it.moduleVersion.matches(dependency.version)) }) } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilterSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilterSpec.groovy new file mode 100644 index 0000000000..037cf7284c --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/AbstractDependencyFilterSpec.groovy @@ -0,0 +1,30 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import org.gradle.api.Project +import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.specs.Spec +import org.gradle.testfixtures.ProjectBuilder +import spock.lang.Specification + +class AbstractDependencyFilterSpec extends Specification { + + def "matches dependency whose version contains plus sign"() { + given: + Project project = ProjectBuilder.builder().build() + AbstractDependencyFilter filter = new DefaultDependencyFilter(project) + Dependency dep = project.dependencies.create("com.example:foo:1.0.0+1") + + ResolvedDependency resolvedDep = Stub(ResolvedDependency) { + getModuleGroup() >> "com.example" + getModuleName() >> "foo" + getModuleVersion() >> "1.0.0+1" + } + + when: + Spec spec = filter.dependency(dep) + + then: + spec.isSatisfiedBy(resolvedDep) + } +} From 7edb3a29c40e130aad40b61e47a196e9c1efc7ab Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:24:45 +0800 Subject: [PATCH 11/24] Fix relocation exclusion for file patterns like kotlin/kotlin.kotlin_builtins (#1313) Original commit on main: 6f1f666f927c799eee413b5faf3b982991ed42c4 --- src/docs/changes/README.md | 1 + .../plugins/shadow/relocation/SimpleRelocator.groovy | 11 +++++++---- .../shadow/relocation/SimpleRelocatorTest.groovy | 8 ++++++++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 2e5fda2a91..6bdd8b9e8a 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -17,6 +17,7 @@ - Fix failing for non-existent class directories. ([#1976](https://github.com/GradleUp/shadow/pull/1976)) - Fix skipStringConstants per-relocator behavior in mapName. ([#1968](https://github.com/GradleUp/shadow/pull/1968)) - Fix excluding dependencies whose versions contain `+`. ([#1597](https://github.com/GradleUp/shadow/pull/1597)) +- Fix relocation exclusion for file patterns like `kotlin/kotlin.kotlin_builtins`. ([#1313](https://github.com/GradleUp/shadow/pull/1313)) ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy index 90e3e72347..afeafa33aa 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy @@ -19,6 +19,7 @@ package com.github.jengelman.gradle.plugins.shadow.relocation +import org.apache.commons.io.FilenameUtils import org.codehaus.plexus.util.SelectorUtils import org.gradle.api.tasks.Input import org.gradle.api.tasks.Optional @@ -112,12 +113,14 @@ class SimpleRelocator implements Relocator { continue } - String classPattern = pattern.replace('.', '/') + String fileName = FilenameUtils.getName(pattern) + String fileParent = FilenameUtils.getPathNoEndSeparator(pattern) + String filePattern = (fileParent && fileName) ? pattern : pattern.replace('.', '/') - normalized.add(classPattern) + normalized.add(filePattern) - if (classPattern.endsWith("/*")) { - String packagePattern = classPattern.substring(0, classPattern.lastIndexOf('/')) + if (filePattern.endsWith("/*") || filePattern.endsWith("/**")) { + String packagePattern = filePattern.substring(0, filePattern.lastIndexOf('/')) normalized.add(packagePattern) } } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy index ba04bfb516..dea283d821 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.groovy @@ -89,6 +89,14 @@ class SimpleRelocatorTest { assertEquals(false, relocator.canRelocatePath("org/foo/recurse/sub/Class")) assertEquals(false, relocator.canRelocatePath("org/foo/recurse/sub/Class.class")) + relocator = new SimpleRelocator("foo.", null, null, null) + assertEquals(true, relocator.canRelocatePath("foo/foo.bar")) + relocator.exclude("foo/foo.bar") + assertEquals(false, relocator.canRelocatePath("foo/foo.bar")) + assertEquals(true, relocator.canRelocatePath("foo/foobar")) + relocator.exclude("foo/foobar") + assertEquals(false, relocator.canRelocatePath("foo/foobar")) + // Verify edge cases relocator = new SimpleRelocator("org.f", null, null, null) assertEquals(false, relocator.canRelocatePath("")) // Empty path From 6b2a2544d285bbf6b250df6c0a2a517c3244fa6b Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:25:26 +0800 Subject: [PATCH 12/24] Make the output of PropertiesFileTransformer reproducible (#1861) Original commit on main: 90b0b3e8e0fd23d62cd1900b7d470cc8dc1f103d --- src/docs/changes/README.md | 1 + .../shadow/internal/CleanProperties.groovy | 59 +++++++++++-------- .../PropertiesFileTransformer.groovy | 16 +---- .../internal/CleanPropertiesSpec.groovy | 25 ++++++++ 4 files changed, 64 insertions(+), 37 deletions(-) create mode 100644 src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanPropertiesSpec.groovy diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 6bdd8b9e8a..57dd4e5268 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -10,6 +10,7 @@ - Update `RelocatorRemapper` class pattern to cover more Java method descriptors. ([#1731](https://github.com/GradleUp/shadow/pull/1731)) - Do not write modified class files for no-op relocations. ([#1694](https://github.com/GradleUp/shadow/pull/1694)) - Support relocating list of types in `RelocatorRemapper`. ([#1714](https://github.com/GradleUp/shadow/pull/1714)) +- Reproducible `PropertiesFileTransformer` output. ([#1861](https://github.com/GradleUp/shadow/pull/1861)) **Fixed** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy index b94a779ccc..4f017f16b5 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanProperties.groovy @@ -2,36 +2,47 @@ package com.github.jengelman.gradle.plugins.shadow.internal import groovy.transform.CompileStatic +import java.nio.charset.Charset + @CompileStatic class CleanProperties extends Properties { - private static class StripCommentsWithTimestampBufferedWriter extends BufferedWriter { - - private final int lengthOfExpectedTimestamp - - StripCommentsWithTimestampBufferedWriter(final Writer out) { - super(out) - - lengthOfExpectedTimestamp = ("#" + new Date().toString()).length() - } - - @Override - void write(final String str) throws IOException { - if (couldBeCommentWithTimestamp(str)) { - return + @Override + Set> entrySet() { + Set> sorted = new TreeSet<>(new Comparator>() { + @Override + int compare(Map.Entry o1, Map.Entry o2) { + return String.valueOf(o1.key).compareTo(String.valueOf(o2.key)) } - super.write(str) - } - - private boolean couldBeCommentWithTimestamp(final String str) { - return str != null && - str.startsWith("#") && - str.length() == lengthOfExpectedTimestamp - } + }) + sorted.addAll(super.entrySet()) + return sorted } @Override - void store(final Writer writer, final String comments) throws IOException { - super.store(new StripCommentsWithTimestampBufferedWriter(writer), comments) + Enumeration keys() { + Vector sortedKeys = new Vector<>(super.keySet()) + Collections.sort(sortedKeys, new Comparator() { + @Override + int compare(Object o1, Object o2) { + return String.valueOf(o1).compareTo(String.valueOf(o2)) + } + }) + return sortedKeys.elements() + } + + void writeWithoutComments(Charset charset, OutputStream os) { + StringWriter writer = new StringWriter() + super.store(writer, null) + BufferedReader reader = new BufferedReader(new StringReader(writer.toString())) + BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(os, charset)) + String line + while ((line = reader.readLine()) != null) { + if (!line.startsWith("#")) { + bw.write(line) + bw.newLine() + } + } + bw.flush() } } diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy index f78372ec16..89ce4b1750 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy @@ -20,6 +20,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import com.github.jengelman.gradle.plugins.shadow.internal.CleanProperties +import java.nio.charset.Charset import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream import org.codehaus.plexus.util.IOUtil @@ -242,23 +243,12 @@ class PropertiesFileTransformer implements Transformer { @Override void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { - // cannot close the writer as the OutputStream needs to remain open - def zipWriter = new OutputStreamWriter(os, charset) - propertiesEntries.each { String path, Properties props -> + propertiesEntries.each { String path, CleanProperties props -> ZipEntry entry = new ZipEntry(path) entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) os.putNextEntry(entry) - IOUtil.copy(readerFor(props, charset), zipWriter) - zipWriter.flush() + props.writeWithoutComments(Charset.forName(getCharset()), os) os.closeEntry() } } - - private static InputStreamReader readerFor(Properties props, String charset) { - ByteArrayOutputStream baos = new ByteArrayOutputStream() - baos.withWriter(charset) { w -> - props.store(w, '') - } - new InputStreamReader(new ByteArrayInputStream(baos.toByteArray()), charset) - } } diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanPropertiesSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanPropertiesSpec.groovy new file mode 100644 index 0000000000..362c23fb72 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/internal/CleanPropertiesSpec.groovy @@ -0,0 +1,25 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import spock.lang.Specification +import java.nio.charset.StandardCharsets + +class CleanPropertiesSpec extends Specification { + + def "writes sorted properties without comments"() { + given: + CleanProperties props = new CleanProperties() + props.put("key", "value") + props.put("key2", "value2") + props.put("a", "b") + props.put("0", "1") + + ByteArrayOutputStream os = new ByteArrayOutputStream() + + when: + props.writeWithoutComments(StandardCharsets.UTF_8, os) + + then: + String result = os.toString("UTF-8").replace("\r\n", "\n") + result == "0=1\na=b\nkey=value\nkey2=value2\n" + } +} From ebaead09581c15f8421e9a6048b4690008903fa7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:30:38 +0800 Subject: [PATCH 13/24] Merge Gradle Module descriptors into the modern META-INF path (#1706) Original commit on main: d6347798dbfcb9f17c8fe6a94e78c88171a8cd67 --- src/docs/changes/README.md | 1 + .../shadow/impl/RelocatorRemapper.groovy | 90 +++++++------------ .../GroovyExtensionModuleTransformer.groovy | 9 +- .../plugins/shadow/TransformerSpec.groovy | 4 +- 4 files changed, 38 insertions(+), 66 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 57dd4e5268..204638b3f2 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -11,6 +11,7 @@ - Do not write modified class files for no-op relocations. ([#1694](https://github.com/GradleUp/shadow/pull/1694)) - Support relocating list of types in `RelocatorRemapper`. ([#1714](https://github.com/GradleUp/shadow/pull/1714)) - Reproducible `PropertiesFileTransformer` output. ([#1861](https://github.com/GradleUp/shadow/pull/1861)) +- Merge Groovy Module descriptors into modern `META-INF` path. ([#1706](https://github.com/GradleUp/shadow/pull/1706)) **Fixed** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy index 57e9acc5ed..875ca12cb2 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy @@ -56,75 +56,51 @@ class RelocatorRemapper extends Remapper { @Override Object mapValue(Object object) { if (object instanceof String) { - String name = (String) object - String value = name - - Matcher m = classPattern.matcher(name) - if (m.find()) { - StringBuffer sb = new StringBuffer() - do { - String prefix = (m.group(1) ?: "") + "L" - String className = m.group(2) - String relocated = className - for (Relocator r : relocators) { - if (r instanceof SimpleRelocator && ((SimpleRelocator) r).rawString) { - continue - } - if (r.canRelocateClass(className)) { - RelocateClassContext classContext = RelocateClassContext.builder().className(className).stats(stats).build() - relocated = r.relocateClass(classContext) - break - } else if (r.canRelocatePath(className)) { - RelocatePathContext pathContext = RelocatePathContext.builder().path(className).stats(stats).build() - relocated = r.relocatePath(pathContext) - break - } - } - m.appendReplacement(sb, Matcher.quoteReplacement(prefix + relocated + ";")) - } while (m.find()) - m.appendTail(sb) - value = sb.toString() - } - - return value + return mapName((String) object, true) } - return super.mapValue(object) } @Override String map(String name) { - String value = name - - Matcher m = classPattern.matcher(name) - if (m.find()) { - StringBuffer sb = new StringBuffer() - do { - String prefix = (m.group(1) ?: "") + "L" - String className = m.group(2) - String relocated = className - for (Relocator r : relocators) { - if (r.canRelocatePath(className)) { - RelocatePathContext pathContext = RelocatePathContext.builder().path(className).stats(stats).build() - relocated = r.relocatePath(pathContext) - break - } - } - m.appendReplacement(sb, Matcher.quoteReplacement(prefix + relocated + ";")) - } while (m.find()) - m.appendTail(sb) - value = sb.toString() + return mapName(name, false) + } + + String mapName(String name, boolean mapLiterals) { + String[] parts = name.split(";", -1) + List mapped = new ArrayList<>(parts.length) + for (String part : parts) { + mapped.add(realMap(part, mapLiterals)) + } + return mapped.join(";") + } + + private String realMap(String name, boolean mapLiterals) { + String newName = name + String prefix = "" + String suffix = "" + + Matcher m = classPattern.matcher(newName) + if (m.matches()) { + prefix = (m.group(1) ?: "") + "L" + suffix = m.group(3) ?: "" + newName = m.group(2) } for (Relocator r : relocators) { - if (r.canRelocatePath(name)) { - RelocatePathContext pathContext = RelocatePathContext.builder().path(name).stats(stats).build() - value = r.relocatePath(pathContext) - break + if (mapLiterals && r instanceof SimpleRelocator && ((SimpleRelocator) r).rawString) { + continue + } + if (r.canRelocateClass(newName)) { + RelocateClassContext classContext = RelocateClassContext.builder().className(newName).stats(stats).build() + return prefix + r.relocateClass(classContext) + suffix + } else if (r.canRelocatePath(newName)) { + RelocatePathContext pathContext = RelocatePathContext.builder().path(newName).stats(stats).build() + return prefix + r.relocatePath(pathContext) + suffix } } - return value + return name } String mapPath(String path) { diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy index d378a9cd33..a4f5844f96 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/GroovyExtensionModuleTransformer.groovy @@ -56,16 +56,11 @@ class GroovyExtensionModuleTransformer implements Transformer { private static final MERGED_MODULE_VERSION = '1.0.0' private final Properties module = new Properties() - private boolean legacy = true // default to Groovy 2.4 or earlier @Override boolean canTransformResource(FileTreeElement element) { def path = element.relativePath.pathString - if (path == GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH) { - legacy = false // Groovy 2.5+ - return true - } - return path == GROOVY_LEGACY_EXTENSION_MODULE_DESCRIPTOR_PATH + return path == GROOVY_LEGACY_EXTENSION_MODULE_DESCRIPTOR_PATH || path == GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH } @Override @@ -110,7 +105,7 @@ class GroovyExtensionModuleTransformer implements Transformer { @Override void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { - ZipEntry entry = new ZipEntry(legacy ? GROOVY_LEGACY_EXTENSION_MODULE_DESCRIPTOR_PATH : GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH) + ZipEntry entry = new ZipEntry(GROOVY_EXTENSION_MODULE_DESCRIPTOR_PATH) entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) os.putNextEntry(entry) IOUtil.copy(toInputStream(module), os) diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy index 1ba44e918e..cb253fac58 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/TransformerSpec.groovy @@ -634,7 +634,7 @@ staticExtensionClasses=com.acme.bar.SomeStaticExtension'''.stripIndent()).write( assert output.exists() and: - def text = getJarFileContents(output, 'META-INF/services/org.codehaus.groovy.runtime.ExtensionModule') + def text = getJarFileContents(output, 'META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule') def props = new Properties() props.load(new StringReader(text)) assert props.getProperty('moduleName') == 'MergedByShadowJar' @@ -721,7 +721,7 @@ staticExtensionClasses=com.acme.bar.SomeStaticExtension'''.stripIndent()).write( assert output.exists() and: - def text = getJarFileContents(output, 'META-INF/services/org.codehaus.groovy.runtime.ExtensionModule') + def text = getJarFileContents(output, 'META-INF/groovy/org.codehaus.groovy.runtime.ExtensionModule') def props = new Properties() props.load(new StringReader(text)) assert props.getProperty('moduleName') == 'MergedByShadowJar' From 978cd2f90dbb3bc2fbe6db3fa0e2b6bb70a95e7a Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:45:55 +0800 Subject: [PATCH 14/24] Migrate build configuration to Version Catalog and upgrade Gradle Wrapper to 9.7.0 - Introduce gradle/libs.versions.toml to manage dependencies and plugins. - Refactor build.gradle.kts and settings.gradle.kts to use Version Catalog aliases. - Upgrade Gradle Wrapper to version 9.7.0. --- build.gradle.kts | 24 +-- gradle/libs.versions.toml | 18 ++ gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew.bat | 164 +++++++++--------- settings.gradle.kts | 4 +- .../shadow/relocation/SimpleRelocator.groovy | 4 +- .../plugins/shadow/ApplicationSpec.groovy | 2 +- 7 files changed, 119 insertions(+), 99 deletions(-) create mode 100644 gradle/libs.versions.toml diff --git a/build.gradle.kts b/build.gradle.kts index c68048012a..d2319d3028 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,9 +7,9 @@ import org.gradle.plugin.compatibility.compatibility plugins { groovy `java-gradle-plugin` - id("com.gradle.plugin-publish") version "2.1.0" - id("com.vanniktech.maven.publish") version "0.36.0" - id("com.diffplug.spotless") version "8.2.1" + alias(libs.plugins.pluginPublish) + alias(libs.plugins.mavenPublish) + alias(libs.plugins.spotless) } version = providers.gradleProperty("VERSION_NAME").get() @@ -73,22 +73,22 @@ publishing.publications.withType().configureEach { } dependencies { - api("org.apache.ant:ant:1.10.15") // Types from Ant are exposed in the public API. - implementation("org.jdom:jdom2:2.0.6.1") - implementation("commons-io:commons-io:2.19.0") - implementation("org.codehaus.plexus:plexus-utils:4.0.2") - implementation("org.codehaus.plexus:plexus-xml:4.1.1") - implementation("org.apache.logging.log4j:log4j-core:2.25.3") - implementation("org.vafer:jdependency:2.16") + api(libs.apache.ant) // Types from Ant are exposed in the public API. + implementation(libs.commons.io) + implementation(libs.jdom2) + implementation(libs.plexus.utils) + implementation(libs.plexus.xml) + implementation(libs.apache.log4j) + implementation(libs.jdependency) testImplementation("org.spockframework:spock-core:2.4-groovy-4.0") { exclude(group = "org.codehaus.groovy") exclude(group = "org.hamcrest") } - testImplementation("org.xmlunit:xmlunit-legacy:2.11.0") + testImplementation(libs.xmlunit) testImplementation("org.apache.commons:commons-lang3:3.17.0") testImplementation("com.google.guava:guava:33.3.1-jre") - testImplementation(platform("org.junit:junit-bom:5.13.1")) + testImplementation(platform(libs.junit.bom)) testImplementation("org.junit.jupiter:junit-jupiter") testImplementation("org.junit.platform:junit-platform-suite-engine") } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000000..384f5efa75 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,18 @@ +[versions] +pluginPublish = "2.1.1" + +[libraries] +apache-ant = "org.apache.ant:ant:1.10.17" +apache-log4j = "org.apache.logging.log4j:log4j-core:2.26.1" +commons-io = "commons-io:commons-io:2.19.0" +jdependency = "org.vafer:jdependency:2.16" +jdom2 = "org.jdom:jdom2:2.0.6.1" +junit-bom = "org.junit:junit-bom:5.14.0" +plexus-utils = "org.codehaus.plexus:plexus-utils:4.0.3" +plexus-xml = "org.codehaus.plexus:plexus-xml:4.1.1" +xmlunit = "org.xmlunit:xmlunit-legacy:2.13.0" + +[plugins] +mavenPublish = "com.vanniktech.maven.publish:0.37.0" +pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "pluginPublish" } +spotless = "com.diffplug.spotless:8.9.0" diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index df6a6ad763..69dd0d0404 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.7.0-bin.zip networkTimeout=10000 retries=0 retryBackOffMs=500 diff --git a/gradlew.bat b/gradlew.bat index 24c62d56f2..aa5f10b069 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,82 +1,82 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables, and ensure extensions are enabled -setlocal EnableExtensions - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -"%COMSPEC%" /c exit 1 - -:execute -@rem Setup the command line - - - -@rem Execute Gradle -@rem endlocal doesn't take effect until after the line is parsed and variables are expanded -@rem which allows us to clear the local environment before executing the java command -endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel - -:exitWithErrorLevel -@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts -"%COMSPEC%" /c exit %ERRORLEVEL% +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables, and ensure extensions are enabled +setlocal EnableExtensions + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +"%COMSPEC%" /c exit 1 + +:execute +@rem Setup the command line + + + +@rem Execute Gradle +@rem endlocal doesn't take effect until after the line is parsed and variables are expanded +@rem which allows us to clear the local environment before executing the java command +endlocal & "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* & call :exitWithErrorLevel + +:exitWithErrorLevel +@rem Use "%COMSPEC%" /c exit to allow operators to work properly in scripts +"%COMSPEC%" /c exit %ERRORLEVEL% diff --git a/settings.gradle.kts b/settings.gradle.kts index 2eefdfe891..6efdaabe51 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,12 +6,12 @@ pluginManagement { } plugins { - id("com.gradle.develocity") version "4.2" + id("com.gradle.develocity") version "4.5.0" } develocity { buildScan { - termsOfUseUrl = "https://gradle.com/terms-of-service" + termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" termsOfUseAgree = "yes" // TODO: workaround for https://github.com/gradle/gradle/issues/22879. val isCI = providers.environmentVariable("CI").isPresent diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy index afeafa33aa..c57e8c0141 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy @@ -115,7 +115,9 @@ class SimpleRelocator implements Relocator { String fileName = FilenameUtils.getName(pattern) String fileParent = FilenameUtils.getPathNoEndSeparator(pattern) - String filePattern = (fileParent && fileName) ? pattern : pattern.replace('.', '/') + boolean hasParent = fileParent != null && !fileParent.isEmpty() + boolean hasName = fileName != null && !fileName.isEmpty() + String filePattern = (hasParent && hasName) ? pattern : pattern.replace('.', '/') normalized.add(filePattern) diff --git a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy index 13d35e39f2..3c48312c04 100644 --- a/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/ApplicationSpec.groovy @@ -116,7 +116,7 @@ class ApplicationSpec extends PluginSpecification { settingsFile.write """ plugins { // https://docs.gradle.org/8.0.1/userguide/toolchains.html#sub:download_repositories - id("org.gradle.toolchains.foojay-resolver-convention") version("0.7.0") + id("org.gradle.toolchains.foojay-resolver-convention") version("1.0.0") } rootProject.name = 'myapp' From d3dae0a2a53e79f6c38014e7ff04256952073060 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 14:55:44 +0800 Subject: [PATCH 15/24] Remove rootProject name --- settings.gradle.kts | 2 -- src/docs/changes/README.md | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/settings.gradle.kts b/settings.gradle.kts index 6efdaabe51..68bc4b102b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -25,6 +25,4 @@ dependencyResolutionManagement { } } -rootProject.name = "shadow" - enableFeaturePreview("STABLE_CONFIGURATION_CACHE") diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 204638b3f2..27ef1afbdd 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -10,8 +10,8 @@ - Update `RelocatorRemapper` class pattern to cover more Java method descriptors. ([#1731](https://github.com/GradleUp/shadow/pull/1731)) - Do not write modified class files for no-op relocations. ([#1694](https://github.com/GradleUp/shadow/pull/1694)) - Support relocating list of types in `RelocatorRemapper`. ([#1714](https://github.com/GradleUp/shadow/pull/1714)) -- Reproducible `PropertiesFileTransformer` output. ([#1861](https://github.com/GradleUp/shadow/pull/1861)) -- Merge Groovy Module descriptors into modern `META-INF` path. ([#1706](https://github.com/GradleUp/shadow/pull/1706)) +- Make the output of `PropertiesFileTransformer` reproducible. ([#1861](https://github.com/GradleUp/shadow/pull/1861)) +- Merge Gradle Module descriptors into the modern `META-INF` path. ([#1706](https://github.com/GradleUp/shadow/pull/1706)) **Fixed** From f1af3b2ceb3acaed8f5f953c88d2e9fa1872cd0d Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 15:17:41 +0800 Subject: [PATCH 16/24] Use ZipUtils.zipEntry in transformers to prevent Zip Slip --- .../shadow/transformers/PropertiesFileTransformer.groovy | 3 ++- .../plugins/shadow/transformers/ServiceFileTransformer.groovy | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy index 89ce4b1750..0c9976a7ef 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer.groovy @@ -20,6 +20,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers import com.github.jengelman.gradle.plugins.shadow.internal.CleanProperties +import com.github.jengelman.gradle.plugins.shadow.internal.ZipUtils import java.nio.charset.Charset import org.apache.tools.zip.ZipEntry import org.apache.tools.zip.ZipOutputStream @@ -244,7 +245,7 @@ class PropertiesFileTransformer implements Transformer { @Override void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { propertiesEntries.each { String path, CleanProperties props -> - ZipEntry entry = new ZipEntry(path) + ZipEntry entry = ZipUtils.zipEntry(path) entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) os.putNextEntry(entry) props.writeWithoutComments(Charset.forName(getCharset()), os) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy index 7c19c1ae55..24814e951c 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ServiceFileTransformer.groovy @@ -19,6 +19,7 @@ package com.github.jengelman.gradle.plugins.shadow.transformers +import com.github.jengelman.gradle.plugins.shadow.internal.ZipUtils import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction import org.apache.tools.zip.ZipEntry @@ -92,7 +93,7 @@ class ServiceFileTransformer implements Transformer, PatternFilterable { @Override void modifyOutputStream(ZipOutputStream os, boolean preserveFileTimestamps) { serviceEntries.each { String path, ServiceStream stream -> - ZipEntry entry = new ZipEntry(path) + ZipEntry entry = ZipUtils.zipEntry(path) entry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, entry.time) os.putNextEntry(entry) IOUtil.copy(stream.toInputStream(), os) From aee1556eb39bce6e18b49ff1ef852b0216720a4e Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 21:43:40 +0800 Subject: [PATCH 17/24] Deprecate InheritManifest (#1722) Original commit on main: f7d8d1b3ace73db99d428f3d47b1a11ebdfec711 --- src/docs/changes/README.md | 4 ++++ .../gradle/plugins/shadow/tasks/InheritManifest.groovy | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 27ef1afbdd..cc4e956b48 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -22,6 +22,10 @@ - Fix relocation exclusion for file patterns like `kotlin/kotlin.kotlin_builtins`. ([#1313](https://github.com/GradleUp/shadow/pull/1313)) +**Deprecated** + +- Deprecate `InheritManifest`. ([#1722](https://github.com/GradleUp/shadow/pull/1722)) + ## [v8.3.11] (2026-05-28) > [!WARNING] diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy index 1cfed3c4c1..babb2920a7 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy @@ -2,6 +2,10 @@ package com.github.jengelman.gradle.plugins.shadow.tasks import org.gradle.api.java.archives.Manifest +/** + * @deprecated inheritFrom should be replaced by from. + */ +@Deprecated interface InheritManifest extends Manifest { InheritManifest inheritFrom(Object... inheritPaths) From f64927db0749bdd371f6b2ac3ace9a7d1c048f98 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 21:44:03 +0800 Subject: [PATCH 18/24] Deprecate minimizeJar property (#2124) Original commit on main: 49a203d198be9f37c1a3d499fd0615c4e8a4c1a9 --- src/docs/changes/README.md | 1 + .../shadow/tasks/DefaultInheritManifest.groovy | 1 + .../gradle/plugins/shadow/tasks/ShadowJar.java | 18 +++++++++++++++++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index cc4e956b48..69f84cdad9 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -25,6 +25,7 @@ **Deprecated** - Deprecate `InheritManifest`. ([#1722](https://github.com/GradleUp/shadow/pull/1722)) +- Deprecate `minimizeJar` property. ([#2124](https://github.com/GradleUp/shadow/pull/2124)) ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy index 7f3ad7d378..fbe0d178f9 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/DefaultInheritManifest.groovy @@ -10,6 +10,7 @@ import org.gradle.api.java.archives.ManifestMergeSpec import org.gradle.api.java.archives.internal.DefaultManifest import org.gradle.api.java.archives.internal.DefaultManifestMergeSpec +@SuppressWarnings("deprecation") class DefaultInheritManifest implements InheritManifest { private List inheritMergeSpecs = [] diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index e22b8b011c..06947ab6a3 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -81,7 +81,6 @@ public ShadowJar() { relocators = new ArrayList<>(); configurations = new ArrayList<>(); - this.getInputs().property("minimize", (Callable) () -> minimizeJar); this.getOutputs().doNotCacheIf("Has one or more transforms or relocators that are not cacheable", task -> { for (Transformer transformer : transformers) { if (!isCacheableTransform(transformer.getClass())) { @@ -476,4 +475,21 @@ public String getRelocationPrefix() { public void setRelocationPrefix(String relocationPrefix) { this.relocationPrefix = relocationPrefix; } + + /** + * @deprecated Use {@link #minimize()} instead. + */ + @Deprecated + @Input + public boolean getMinimizeJar() { + return minimizeJar; + } + + /** + * @deprecated Use {@link #minimize()} instead. + */ + @Deprecated + public void setMinimizeJar(boolean minimizeJar) { + this.minimizeJar = minimizeJar; + } } From 6d4fdb7513edda7f935572cbe0f108b42383f508 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 21:44:44 +0800 Subject: [PATCH 19/24] Deprecate TransformerContext.Builder (#2184) Original commit on main: 2f0f2d9d56d811b385c234798351ad22a79598d2 --- src/docs/changes/README.md | 1 + .../transformers/TransformerContext.groovy | 31 +++++++++++++++++-- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 69f84cdad9..9b00ab0f04 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -26,6 +26,7 @@ - Deprecate `InheritManifest`. ([#1722](https://github.com/GradleUp/shadow/pull/1722)) - Deprecate `minimizeJar` property. ([#2124](https://github.com/GradleUp/shadow/pull/2124)) +- Deprecate `TransformerContext.Builder`. ([#2184](https://github.com/GradleUp/shadow/pull/2184)) ## [v8.3.11] (2026-05-28) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy index b951dfa668..6785a8054e 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext.groovy @@ -4,11 +4,8 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowStats import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowCopyAction import groovy.transform.Canonical -import groovy.transform.builder.Builder - @Canonical -@Builder class TransformerContext { String path @@ -16,6 +13,34 @@ class TransformerContext { List relocators ShadowStats stats + /** + * @deprecated Use TransformerContext constructor instead. + */ + @Deprecated + static class Builder { + private String path + private InputStream is + private List relocators + private ShadowStats stats + + Builder path(String path) { this.path = path; return this } + Builder is(InputStream is) { this.is = is; return this } + Builder relocators(List relocators) { this.relocators = relocators; return this } + Builder stats(ShadowStats stats) { this.stats = stats; return this } + + TransformerContext build() { + return new TransformerContext(path, is, relocators, stats) + } + } + + /** + * @deprecated Use TransformerContext constructor instead. + */ + @Deprecated + static Builder builder() { + return new Builder() + } + static long getEntryTimestamp(boolean preserveFileTimestamps, long entryTime) { preserveFileTimestamps ? entryTime : ShadowCopyAction.CONSTANT_TIME_FOR_ZIP_ENTRIES } From b02da5e939e54284a32d250828076a9180fad192 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Thu, 13 Aug 2026 21:53:11 +0800 Subject: [PATCH 20/24] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../gradle/plugins/shadow/tasks/InheritManifest.groovy | 2 +- .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.java | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy index babb2920a7..6e200df8b3 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy @@ -3,7 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow.tasks import org.gradle.api.java.archives.Manifest /** - * @deprecated inheritFrom should be replaced by from. + * @deprecated Use {@link Manifest#from(Object...)} on the standard Gradle {@link Manifest} instead. */ @Deprecated interface InheritManifest extends Manifest { diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 06947ab6a3..1d3c26fb7f 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -477,7 +477,8 @@ public void setRelocationPrefix(String relocationPrefix) { } /** - * @deprecated Use {@link #minimize()} instead. + * @deprecated This legacy boolean property is deprecated. Use {@link #minimize()} to enable minimization; + * omitting the call keeps minimization disabled. */ @Deprecated @Input @@ -486,7 +487,8 @@ public boolean getMinimizeJar() { } /** - * @deprecated Use {@link #minimize()} instead. + * @deprecated This legacy boolean property is deprecated. Use {@link #minimize()} when enabling minimization; + * setting {@code false} is equivalent to not calling {@code minimize()}. */ @Deprecated public void setMinimizeJar(boolean minimizeJar) { From a49a41b356e41b1383addba278b43d37f8771436 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 21:29:12 +0800 Subject: [PATCH 21/24] Fix formatting and copyright handling in ApacheNoticeResourceTransformer (#1623, #1627, #1628) Original commit on main: 2c104019ffcd8a9e7df4234d0dc28b84100b6496 --- src/docs/changes/README.md | 1 + .../ApacheNoticeResourceTransformer.groovy | 38 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 9b00ab0f04..cccf806964 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -20,6 +20,7 @@ - Fix skipStringConstants per-relocator behavior in mapName. ([#1968](https://github.com/GradleUp/shadow/pull/1968)) - Fix excluding dependencies whose versions contain `+`. ([#1597](https://github.com/GradleUp/shadow/pull/1597)) - Fix relocation exclusion for file patterns like `kotlin/kotlin.kotlin_builtins`. ([#1313](https://github.com/GradleUp/shadow/pull/1313)) +- Fix formatting and copyright handling in `ApacheNoticeResourceTransformer`. ([#1623](https://github.com/GradleUp/shadow/pull/1623), [#1627](https://github.com/GradleUp/shadow/pull/1627), [#1628](https://github.com/GradleUp/shadow/pull/1628)) **Deprecated** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy index f7a8add395..f28ef5a426 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy @@ -71,6 +71,8 @@ class ApacheNoticeResourceTransformer implements Transformer { @Input String copyright + private String fallbackCopyright + /** * The file encoding of the NOTICE file. */ @@ -148,7 +150,7 @@ class ApacheNoticeResourceTransformer implements Transformer { } else { String ent = sb.toString() if (ent.startsWith(projectName) && ent.indexOf("Copyright ") != -1) { - copyright = ent + fallbackCopyright = ent } if (currentOrg == null) { entries.add(ent) @@ -183,42 +185,40 @@ class ApacheNoticeResourceTransformer implements Transformer { zipEntry.time = TransformerContext.getEntryTimestamp(preserveFileTimestamps, zipEntry.time) os.putNextEntry(zipEntry) - Writer pow - if (StringUtils.isNotEmpty(encoding)) { - pow = new OutputStreamWriter(os, encoding) - } else { - pow = new OutputStreamWriter(os) - } - PrintWriter writer = new PrintWriter(pow) + String actualCopyright = copyright ?: fallbackCopyright + StringBuilder sb = new StringBuilder() int count = 0 for (String line : entries) { ++count - if (line == copyright && count != 2) { + if (line == actualCopyright && count != 2) { continue } - if (count == 2 && copyright != null) { - writer.print(copyright) - writer.print('\n') + if (count == 2 && actualCopyright != null) { + sb.append(actualCopyright).append('\n') } else { - writer.print(line) - writer.print('\n') + sb.append(line).append('\n') } if (count == 3) { //do org stuff for (Map.Entry> entry : organizationEntries.entrySet()) { - writer.print(entry.getKey()) - writer.print('\n') + sb.append(entry.getKey()).append('\n') for (String l : entry.getValue()) { - writer.print(l) + sb.append(l) } - writer.print('\n') + sb.append('\n') } } } - writer.flush() + byte[] bytes + if (StringUtils.isNotEmpty(encoding)) { + bytes = sb.toString().trim().getBytes(encoding) + } else { + bytes = sb.toString().trim().getBytes() + } + os.write(bytes) entries.clear() } From c4acdd9fe229512cdf405c12b9ac7052a2b5e55c Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 21:29:41 +0800 Subject: [PATCH 22/24] Fix ShadowJar not being successful after includes or excludes are changed (#1200) Original commit on main: 450b83108c8c058259919991111a88f00043a414 --- src/docs/changes/README.md | 1 + .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.java | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index cccf806964..0ff919f319 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -21,6 +21,7 @@ - Fix excluding dependencies whose versions contain `+`. ([#1597](https://github.com/GradleUp/shadow/pull/1597)) - Fix relocation exclusion for file patterns like `kotlin/kotlin.kotlin_builtins`. ([#1313](https://github.com/GradleUp/shadow/pull/1313)) - Fix formatting and copyright handling in `ApacheNoticeResourceTransformer`. ([#1623](https://github.com/GradleUp/shadow/pull/1623), [#1627](https://github.com/GradleUp/shadow/pull/1627), [#1628](https://github.com/GradleUp/shadow/pull/1628)) +- Fix ShadowJar not being re-executed after includes or excludes are changed. ([#1200](https://github.com/GradleUp/shadow/pull/1200)) **Deprecated** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java index 1d3c26fb7f..dcc522a49a 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.java @@ -44,6 +44,7 @@ import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.List; +import java.util.Set; import java.util.concurrent.Callable; @CacheableTask @@ -81,6 +82,9 @@ public ShadowJar() { relocators = new ArrayList<>(); configurations = new ArrayList<>(); + this.getInputs().property("minimize", (Callable) () -> minimizeJar); + this.getInputs().property("includes", (Callable>) () -> getIncludes()); + this.getInputs().property("excludes", (Callable>) () -> getExcludes()); this.getOutputs().doNotCacheIf("Has one or more transforms or relocators that are not cacheable", task -> { for (Transformer transformer : transformers) { if (!isCacheableTransform(transformer.getClass())) { From 62dba81ca1a7615fd377b28f5f6219c9a801dd33 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 13 Aug 2026 21:30:26 +0800 Subject: [PATCH 23/24] Honor executableDir in ShadowApplicationPlugin (#1738) Original commit on main: c2cd2ced073e990174d57e33f0555c95a489dd46 --- src/docs/changes/README.md | 1 + .../gradle/plugins/shadow/ShadowApplicationPlugin.groovy | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/docs/changes/README.md b/src/docs/changes/README.md index 0ff919f319..de09fb3c43 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -22,6 +22,7 @@ - Fix relocation exclusion for file patterns like `kotlin/kotlin.kotlin_builtins`. ([#1313](https://github.com/GradleUp/shadow/pull/1313)) - Fix formatting and copyright handling in `ApacheNoticeResourceTransformer`. ([#1623](https://github.com/GradleUp/shadow/pull/1623), [#1627](https://github.com/GradleUp/shadow/pull/1627), [#1628](https://github.com/GradleUp/shadow/pull/1628)) - Fix ShadowJar not being re-executed after includes or excludes are changed. ([#1200](https://github.com/GradleUp/shadow/pull/1200)) +- Respect `executableDir` in application extension when configuring start scripts and distribution. ([#1738](https://github.com/GradleUp/shadow/pull/1738)) **Deprecated** diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy index 9fdc54bc82..3e449f9472 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy @@ -94,7 +94,7 @@ class ShadowApplicationPlugin implements Plugin { project.tasks.named(SHADOW_INSTALL_TASK_NAME, Sync).configure { task -> task.doFirst { if (task.destinationDir.directory) { - if (task.destinationDir.listFiles().size() != 0 && (!new File(task.destinationDir, 'lib').directory || !new File(task.destinationDir, 'bin').directory)) { + if (task.destinationDir.listFiles().size() != 0 && (!new File(task.destinationDir, 'lib').directory || !new File(task.destinationDir, 'bin').directory || !new File(task.destinationDir, javaApplication.executableDir).directory)) { throw new GradleException("The specified installation directory '${task.destinationDir}' is neither empty nor does it contain an installation for '${javaApplication.applicationName}'.\n" + "If you really want to install to this directory, delete it and run the install task again.\n" + "Alternatively, choose a different installation directory." @@ -104,7 +104,7 @@ class ShadowApplicationPlugin implements Plugin { } task.doLast { task.eachFile { - if (it.path == "bin/${javaApplication.applicationName}") { + if (it.path == "${javaApplication.executableDir}/${javaApplication.applicationName}") { it.mode = 0x755 } } @@ -122,7 +122,7 @@ class ShadowApplicationPlugin implements Plugin { from(jar) from(project.configurations.shadow) } - into("bin") { + into({ javaApplication.executableDir }) { from(startScripts) filePermissions { it.unix(493) } } From 9bd08dffcf4f3f35c6e175174a75632c0776a250 Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Thu, 13 Aug 2026 21:56:52 +0800 Subject: [PATCH 24/24] Apply suggestions from code review Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../gradle/plugins/shadow/ShadowApplicationPlugin.groovy | 2 +- .../transformers/ApacheNoticeResourceTransformer.groovy | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy index 3e449f9472..e5d7dfd43d 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy @@ -94,7 +94,7 @@ class ShadowApplicationPlugin implements Plugin { project.tasks.named(SHADOW_INSTALL_TASK_NAME, Sync).configure { task -> task.doFirst { if (task.destinationDir.directory) { - if (task.destinationDir.listFiles().size() != 0 && (!new File(task.destinationDir, 'lib').directory || !new File(task.destinationDir, 'bin').directory || !new File(task.destinationDir, javaApplication.executableDir).directory)) { + if (task.destinationDir.listFiles().size() != 0 && (!new File(task.destinationDir, 'lib').directory || !new File(task.destinationDir, javaApplication.executableDir).directory)) { throw new GradleException("The specified installation directory '${task.destinationDir}' is neither empty nor does it contain an installation for '${javaApplication.applicationName}'.\n" + "If you really want to install to this directory, delete it and run the install task again.\n" + "Alternatively, choose a different installation directory." diff --git a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy index f28ef5a426..11e22fbde8 100644 --- a/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy +++ b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/transformers/ApacheNoticeResourceTransformer.groovy @@ -214,9 +214,9 @@ class ApacheNoticeResourceTransformer implements Transformer { byte[] bytes if (StringUtils.isNotEmpty(encoding)) { - bytes = sb.toString().trim().getBytes(encoding) + bytes = sb.toString().stripTrailing().getBytes(encoding) } else { - bytes = sb.toString().trim().getBytes() + bytes = sb.toString().stripTrailing().getBytes() } os.write(bytes)