Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions api/shadow.api
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,15 @@ public class com/github/jengelman/gradle/plugins/shadow/transformers/PreserveFir
public fun getResources ()Lorg/gradle/api/provider/SetProperty;
}

public class com/github/jengelman/gradle/plugins/shadow/transformers/ProGuardTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/PatternFilterableResourceTransformer {
public fun <init> ()V
public fun <init> (Lorg/gradle/api/tasks/util/PatternSet;)V
public synthetic fun <init> (Lorg/gradle/api/tasks/util/PatternSet;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun hasTransformedResource ()Z
public fun modifyOutputStream (Lorg/apache/tools/zip/ZipOutputStream;Z)V
public fun transform (Lcom/github/jengelman/gradle/plugins/shadow/transformers/TransformerContext;)V
}

public class com/github/jengelman/gradle/plugins/shadow/transformers/PropertiesFileTransformer : com/github/jengelman/gradle/plugins/shadow/transformers/ResourceTransformer {
public fun <init> (Lorg/gradle/api/model/ObjectFactory;)V
public fun canTransformResource (Lorg/gradle/api/file/FileTreeElement;)Z
Expand Down
3 changes: 3 additions & 0 deletions docs/changes/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

## [Unreleased](https://github.com/GradleUp/shadow/compare/9.4.1...HEAD) - 2026-xx-xx

### Added

- Add `ProGuardTransformer` to merge `META-INF/proguard/*.pro` files. ([#1997](https://github.com/GradleUp/shadow/pull/1997))

## [9.4.1](https://github.com/GradleUp/shadow/releases/tag/9.4.1) - 2026-03-27

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.github.jengelman.gradle.plugins.shadow.transformers

import assertk.assertThat
import assertk.assertions.isEqualTo
import com.github.jengelman.gradle.plugins.shadow.testkit.getContent
import kotlin.io.path.appendText
import org.junit.jupiter.api.Test

class ProGuardTransformerTest : BaseTransformerTest() {
@Test
fun mergeProGuardFiles() {
val proGuardEntry = "META-INF/proguard/app.pro"
val content1 = "-keep class com.foo.Bar { *; }"
val content2 = "-keep class com.foo.Baz { *; }"
val one = buildJarOne { insert(proGuardEntry, content1) }
val two = buildJarTwo { insert(proGuardEntry, content2) }
val config = transform<ProGuardTransformer>(dependenciesBlock = implementationFiles(one, two))
projectScript.appendText(config)

runWithSuccess(shadowJarPath)

val content = outputShadowedJar.use { it.getContent(proGuardEntry) }
assertThat(content).isEqualTo("$content1\n$content2")
}

@Test
fun relocateProGuardFiles() {
val proGuardEntry = "META-INF/proguard/app.pro"
val content = "-keep class org.foo.Service { *; }\n-keep class org.foo.exclude.OtherService"
val one = buildJarOne { insert(proGuardEntry, content) }
val config =
"""
dependencies {
${implementationFiles(one)}
}
$shadowJarTask {
relocate('org.foo', 'borg.foo') {
exclude 'org.foo.exclude.*'
}
transform(com.github.jengelman.gradle.plugins.shadow.transformers.ProGuardTransformer)
}
"""
.trimIndent()
projectScript.appendText(config)

runWithSuccess(shadowJarPath)

val transformedContent = outputShadowedJar.use { it.getContent(proGuardEntry) }
assertThat(transformedContent)
.isEqualTo("-keep class borg.foo.Service { *; }\n-keep class org.foo.exclude.OtherService")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,22 @@ class TransformersTest : BaseTransformerTest() {
}
}

@Test
fun mergeProGuardFiles() {
val proGuardEntry = "META-INF/proguard/app.pro"
val content1 = "-keep class com.foo.Bar { *; }"
val content2 = "-keep class com.foo.Baz { *; }"
val one = buildJarOne { insert(proGuardEntry, content1) }
val two = buildJarTwo { insert(proGuardEntry, content2) }
val config = transform<ProGuardTransformer>(dependenciesBlock = implementationFiles(one, two))
projectScript.appendText(config)

runWithSuccess(shadowJarPath)

val content = outputShadowedJar.use { it.getContent(proGuardEntry) }
assertThat(content).isEqualTo("$content1\n$content2")
}

@ParameterizedTest
@MethodSource("transformerConfigProvider")
fun otherTransformers(pair: Pair<String, KClass<*>>) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,31 +68,40 @@ constructor(
this.excludes.addAll(excludes)
}

if (!rawString) {
// Create exclude pattern sets for sources.
for (exclude in this.excludes) {
// Excludes should be subpackages of the global pattern.
if (exclude.startsWith(this.pattern)) {
sourcePackageExcludes.add(
exclude.substring(this.pattern.length).replaceFirst("[.][*]$".toRegex(), "")
)
}
// Excludes should be subpackages of the global pattern.
if (exclude.startsWith(pathPattern)) {
sourcePathExcludes.add(
exclude.substring(pathPattern.length).replaceFirst("/[*]$".toRegex(), "")
)
}
updateSourceExcludes()
}

private fun updateSourceExcludes() {
if (rawString) return
sourcePackageExcludes.clear()
sourcePathExcludes.clear()
// Create exclude pattern sets for sources.
for (exclude in this.excludes) {
// Excludes should be subpackages of the global pattern.
if (exclude.startsWith(this.pattern)) {
sourcePackageExcludes.add(
exclude.substring(this.pattern.length).replaceFirst("[.][*]$".toRegex(), "")
)
}
// Excludes should be subpackages of the global pattern.
if (exclude.startsWith(pathPattern)) {
sourcePathExcludes.add(
exclude.substring(pathPattern.length).replaceFirst("/[*]$".toRegex(), "")
)
}
}
}

public open fun include(pattern: String) {
includes.addAll(normalizePatterns(listOf(pattern)))
includes.add(pattern)
updateSourceExcludes()
}

public open fun exclude(pattern: String) {
excludes.addAll(normalizePatterns(listOf(pattern)))
excludes.add(pattern)
updateSourceExcludes()
}

override fun canRelocatePath(path: String): Boolean {
Expand Down Expand Up @@ -206,7 +215,7 @@ constructor(
*/
val RX_ENDS_WITH_JAVA_KEYWORD: Pattern =
Pattern.compile(
"\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" +
"\\b(import|package|class|interface|enum|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws|-keep) $" +
"|" +
"\\{@link( \\*)* $" +
"|" +
Expand Down Expand Up @@ -255,9 +264,7 @@ constructor(
// Make sure that search pattern starts at word boundary and that we look for literal ".", not
// regex jokers.
val snippets =
sourceContent
.split(("\\b" + patternFrom.replace(".", "[.]") + "\\b").toRegex())
.filter(CharSequence::isNotEmpty)
sourceContent.split(("\\b" + patternFrom.replace(".", "[.]") + "\\b").toRegex())
snippets.forEachIndexed { i, snippet ->
val isFirstSnippet = i == 0
val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1]
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.github.jengelman.gradle.plugins.shadow.transformers

import com.github.jengelman.gradle.plugins.shadow.internal.zipEntry
import org.apache.tools.zip.ZipOutputStream
import org.gradle.api.tasks.Internal
import org.gradle.api.tasks.util.PatternSet

/**
* Resources transformer that merges entries in `META-INF/proguard` resources into a single
* resource. For example, if there are several `META-INF/proguard/app.pro` resources spread across
* many JARs the individual entries will all be concatenated into a single
* `META-INF/proguard/app.pro` resource packaged into the resultant JAR produced by the shading
* process.
*/
@CacheableTransformer
public open class ProGuardTransformer
@JvmOverloads
constructor(patternSet: PatternSet = PatternSet().include(PROGUARD_PATTERN)) :
PatternFilterableResourceTransformer(patternSet = patternSet) {
@get:Internal internal val proGuardEntries = mutableMapOf<String, MutableList<String>>()

override fun transform(context: TransformerContext) {
val lines = proGuardEntries.getOrPut(context.path) { mutableListOf() }
context.inputStream
.bufferedReader()
.use { it.readLines() }
.forEach { line ->
var relocatedLine = line
context.relocators.forEach { relocator ->
relocatedLine = relocator.applyToSourceContent(relocatedLine)
}
lines.add(relocatedLine)
}
}

override fun hasTransformedResource(): Boolean = proGuardEntries.isNotEmpty()

override fun modifyOutputStream(os: ZipOutputStream, preserveFileTimestamps: Boolean) {
proGuardEntries.forEach { (path, lines) ->
os.putNextEntry(zipEntry(path, preserveFileTimestamps))
os.write(lines.joinToString(System.lineSeparator()).toByteArray())
os.closeEntry()
}
}

private companion object {
private const val PROGUARD_PATTERN = "META-INF/proguard/**"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,12 @@ class SimpleRelocatorTest {
assertThat(relocator.canRelocatePath("org/foo/Class.class")).isFalse()
}

@Test
fun relocateSourceAtStart() {
val relocator = SimpleRelocator("org.foo", "borg.foo")
assertThat(relocator.applyToSourceContent("org.foo.Bar")).isEqualTo("borg.foo.Bar")
}

@Test
fun relocateSourceWithExcludesRaw() {
val relocator =
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.github.jengelman.gradle.plugins.shadow.transformers

import assertk.assertThat
import assertk.assertions.isEqualTo
import assertk.assertions.isTrue
import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator
import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath
import com.github.jengelman.gradle.plugins.shadow.testkit.getContent
import com.github.jengelman.gradle.plugins.shadow.util.zipOutputStream
import java.nio.file.Path
import kotlin.io.path.createTempFile
import kotlin.io.path.deleteExisting
import kotlin.io.path.outputStream
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.Arguments
import org.junit.jupiter.params.provider.MethodSource

class ProGuardTransformerTest : BaseTransformerTest<ProGuardTransformer>() {
private lateinit var tempJar: Path

@BeforeEach
override fun beforeEach() {
super.beforeEach()
tempJar = createTempFile("shade.", ".jar")
}

@AfterEach
fun afterEach() {
tempJar.deleteExisting()
}

@ParameterizedTest
@MethodSource("resourceProvider")
fun canTransformResource(path: String, expected: Boolean) {
assertThat(transformer.canTransformResource(path)).isEqualTo(expected)
}

@ParameterizedTest
@MethodSource("proGuardFileProvider")
fun transformProGuardFile(path: String, input1: String, input2: String, output: String) {
if (transformer.canTransformResource(path)) {
transformer.transform(textContext(path, input1))
transformer.transform(textContext(path, input2))
}

assertThat(transformer.hasTransformedResource()).isTrue()
val entry = transformer.proGuardEntries.getValue(path).joinToString("\n")
assertThat(entry).isEqualTo(output)
}

@Test
fun mergesMultipleFiles() {
val path = "META-INF/proguard/app.pro"
val content1 = "-keep class com.foo.Bar { *; }"
val content2 = "-keep class com.foo.Baz { *; }"

transformer.transform(textContext(path, content1))
transformer.transform(textContext(path, content2))

tempJar.outputStream().zipOutputStream().use { zos ->
transformer.modifyOutputStream(zos, false)
}

val transformedContent = JarPath(tempJar).use { it.getContent(path) }
assertThat(transformedContent)
.isEqualTo("-keep class com.foo.Bar { *; }\n-keep class com.foo.Baz { *; }")
}

@Test
fun canTransformAlternatePath() {
transformer.include("META-INF/custom/**")
assertThat(transformer.canTransformResource("META-INF/proguard/rules.pro")).isTrue()
assertThat(transformer.canTransformResource("META-INF/custom/rules.pro")).isTrue()
}

@Test
fun relocatedClasses() {
val relocator = SimpleRelocator("org.foo", "borg.foo", excludes = listOf("org.foo.exclude.*"))
val content = "-keep class org.foo.Service { *; }\n-keep class org.foo.exclude.OtherService"
val path = "META-INF/proguard/app.pro"

transformer.transform(textContext(path, content, relocator))

tempJar.outputStream().zipOutputStream().use { zos ->
transformer.modifyOutputStream(zos, false)
}

val transformedContent = JarPath(tempJar).use { it.getContent(path) }
assertThat(transformedContent)
.isEqualTo("-keep class borg.foo.Service { *; }\n-keep class org.foo.exclude.OtherService")
}

@Test
fun mergeRelocatedFiles() {
val relocator = SimpleRelocator("org.foo", "borg.foo", excludes = listOf("org.foo.exclude.*"))
val content1 = "-keep class org.foo.Service { *; }"
val content2 = "-keep class org.foo.exclude.OtherService"
val path = "META-INF/proguard/app.pro"

transformer.transform(textContext(path, content1, relocator))
transformer.transform(textContext(path, content2, relocator))

tempJar.outputStream().zipOutputStream().use { zos ->
transformer.modifyOutputStream(zos, false)
}

val transformedContent = JarPath(tempJar).use { it.getContent(path) }
assertThat(transformedContent)
.isEqualTo("-keep class borg.foo.Service { *; }\n-keep class org.foo.exclude.OtherService")
}

private companion object {
@JvmStatic
fun resourceProvider() =
listOf(
// path, expected
Arguments.of("META-INF/proguard/app.pro", true),
Arguments.of("META-INF/proguard/rules.pro", true),
Arguments.of("META-INF/services/com.acme.Foo", false),
Arguments.of("foo/bar.properties", false),
)

@JvmStatic
fun proGuardFileProvider() =
listOf(
// path, input1, input2, output
Arguments.of(
"META-INF/proguard/app.pro",
"-keep class com.foo.Bar",
"-keep class com.foo.Baz",
"-keep class com.foo.Bar\n-keep class com.foo.Baz",
),
Arguments.of(
"META-INF/proguard/rules.pro",
"-keep class com.foo.**",
"-dontwarn com.foo.**",
"-keep class com.foo.**\n-dontwarn com.foo.**",
),
)
}
}
Loading