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. 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..68bc4b102b 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 @@ -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 40440f60e5..de09fb3c43 100644 --- a/src/docs/changes/README.md +++ b/src/docs/changes/README.md @@ -3,6 +3,33 @@ ## [Unreleased] +**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)) +- 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)) +- 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** + +- 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)) +- 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** + +- 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/ShadowApplicationPlugin.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.groovy index 9fdc54bc82..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)) { + 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." @@ -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) } } 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/impl/RelocatorRemapper.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/impl/RelocatorRemapper.groovy index f47ecf1048..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 @@ -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 @@ -38,7 +39,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 @@ -55,60 +56,51 @@ class RelocatorRemapper extends Remapper { @Override Object mapValue(Object object) { if (object instanceof String) { - 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 - } - } - - return value + return mapName((String) object, true) } - return super.mapValue(object) } @Override String map(String name) { - String value = name + 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(name) + Matcher m = classPattern.matcher(newName) if (m.matches()) { - prefix = m.group(1) + "L" - suffix = "" - name = m.group(2) + 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 = prefix + r.relocatePath(pathContext) + suffix - 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/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/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/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/relocation/SimpleRelocator.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.groovy index 90e3e72347..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 @@ -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,16 @@ class SimpleRelocator implements Relocator { continue } - String classPattern = pattern.replace('.', '/') + String fileName = FilenameUtils.getName(pattern) + String fileParent = FilenameUtils.getPathNoEndSeparator(pattern) + boolean hasParent = fileParent != null && !fileParent.isEmpty() + boolean hasName = fileName != null && !fileName.isEmpty() + String filePattern = (hasParent && hasName) ? 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/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/InheritManifest.groovy b/src/main/groovy/com/github/jengelman/gradle/plugins/shadow/tasks/InheritManifest.groovy index 1cfed3c4c1..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 @@ -2,6 +2,10 @@ package com.github.jengelman.gradle.plugins.shadow.tasks import org.gradle.api.java.archives.Manifest +/** + * @deprecated Use {@link Manifest#from(Object...)} on the standard Gradle {@link Manifest} instead. + */ +@Deprecated interface InheritManifest extends Manifest { InheritManifest inheritFrom(Object... inheritPaths) 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..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 @@ -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) } @@ -336,43 +337,59 @@ 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 = new ZipEntry(mappedName + ".class") + ZipEntry archiveEntry = ZipUtils.zipEntry(mappedName + ".class") archiveEntry.setTime(getArchiveTimeFor(lastModified)) 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() } @@ -380,7 +397,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 +416,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 +477,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/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..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 @@ -82,6 +83,8 @@ public ShadowJar() { 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())) { @@ -182,7 +185,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()); } @@ -470,4 +479,23 @@ public String getRelocationPrefix() { public void setRelocationPrefix(String relocationPrefix) { this.relocationPrefix = relocationPrefix; } + + /** + * @deprecated This legacy boolean property is deprecated. Use {@link #minimize()} to enable minimization; + * omitting the call keeps minimization disabled. + */ + @Deprecated + @Input + public boolean getMinimizeJar() { + return minimizeJar; + } + + /** + * @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) { + this.minimizeJar = minimizeJar; + } } 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..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 @@ -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().stripTrailing().getBytes(encoding) + } else { + bytes = sb.toString().stripTrailing().getBytes() + } + os.write(bytes) entries.clear() } 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/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..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,9 +20,12 @@ 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 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 +199,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 @@ -241,23 +244,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 -> - ZipEntry entry = new ZipEntry(path) + propertiesEntries.each { String path, CleanProperties props -> + ZipEntry entry = ZipUtils.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/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) 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 } 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' 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 + } } 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' 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) + } +} 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" + } +} 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" + ] + } +} 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..1b7ff33df3 --- /dev/null +++ b/src/test/groovy/com/github/jengelman/gradle/plugins/shadow/relocation/RelocatorRemapperSpec.groovy @@ -0,0 +1,40 @@ +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;Lorg/package/ClassB;' || 'Lshadow/org/package/ClassA;Lshadow/org/package/ClassB;' + '()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;' + } +} 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 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." + } }