Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
- Support manifest header relocation via configurable `attributesToRelocate` property.
- Allow disabling default ProGuard rules in R8 minimization with `R8Spec.useDefaultRules`. ([#2252](https://github.com/GradleUp/shadow/pull/2252))
- Allow passing classpath files to R8 minimization with `R8Spec.classpath`. ([#2255](https://github.com/GradleUp/shadow/pull/2255))
- Keep implementations when service interface is used in minimize. ([#2275](https://github.com/GradleUp/shadow/pull/2275))

### Changed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,178 @@ class MinimizeTest : BasePluginTest() {
}
}

@Test
fun keepServiceImplementationsWhenServiceInterfaceIsUsed() {
settingsScript.appendText(
"""
|include 'used-service', 'unused-service', 'dependency-service', 'app'
|"""
.trimMargin()
)
projectScript.deleteExisting()

path("unused-service/src/main/java/unused/UnusedServiceInterface.java")
.writeText(
"""
|package unused;
|public interface UnusedServiceInterface {}
|"""
.trimMargin()
)
path("unused-service/src/main/java/unused/UnusedServiceClass.java")
.writeText(
"""
|package unused;
|public class UnusedServiceClass implements UnusedServiceInterface {}
|"""
.trimMargin()
)
path("unused-service/src/main/resources/META-INF/services/unused.UnusedServiceInterface")
.writeText("unused.UnusedServiceClass\n")
path("unused-service/build.gradle")
.writeText(
"""
|plugins {
| id 'java'
|}
|"""
.trimMargin()
)

path("dependency-service/src/main/java/dep/DependencyServiceInterface.java")
.writeText(
"""
|package dep;
|public interface DependencyServiceInterface {}
|"""
.trimMargin()
)
path("dependency-service/src/main/java/dep/DependencyServiceClass.java")
.writeText(
"""
|package dep;
|public class DependencyServiceClass implements DependencyServiceInterface {}
|"""
.trimMargin()
)
path("dependency-service/src/main/java/dep/DependencyUnreferencedClass.java")
.writeText(
"""
|package dep;
|public class DependencyUnreferencedClass {}
|"""
.trimMargin()
)
path("dependency-service/src/main/resources/META-INF/services/dep.DependencyServiceInterface")
.writeText("dep.DependencyServiceClass\n")
path("dependency-service/build.gradle")
.writeText(
"""
|plugins {
| id 'java'
|}
|"""
.trimMargin()
)

path("used-service/src/main/java/used/SomeServiceInterface.java")
.writeText(
"""
|package used;
|public interface SomeServiceInterface {}
|"""
.trimMargin()
)
path("used-service/src/main/java/used/SomeServiceClass.java")
.writeText(
"""
|package used;
|import dep.DependencyServiceInterface;
|public class SomeServiceClass implements SomeServiceInterface {
| private final Class<?> dep = DependencyServiceInterface.class;
|}
|"""
.trimMargin()
)
path("used-service/src/main/java/used/SomeUnreferencedClass.java")
.writeText(
"""
|package used;
|public class SomeUnreferencedClass {}
|"""
.trimMargin()
)
path("used-service/src/main/resources/META-INF/services/used.SomeServiceInterface")
.writeText("used.SomeServiceClass\n")
path("used-service/build.gradle")
.writeText(
"""
|plugins {
| id 'java'
|}
|dependencies {
| implementation project(':dependency-service')
|}
|"""
.trimMargin()
)

path("app/src/main/java/app/Main.java")
.writeText(
"""
|package app;
|import used.SomeServiceInterface;
|public class Main {
| private final Class<?> service = SomeServiceInterface.class;
|}
|"""
.trimMargin()
)
path("app/build.gradle")
.writeText(
"""
|${getDefaultProjectBuildScript("java")}
|dependencies {
| implementation project(':used-service')
| implementation project(':unused-service')
|}
|$shadowJarTask {
| minimize()
|}
|"""
.trimMargin()
)

runWithSuccess(":app:$SHADOW_JAR_TASK_NAME")

val outputAppShadowedJar = jarPath("app/build/libs/app-1.0-all.jar")
assertThat(outputAppShadowedJar).useAll {
containsAtLeast(
"app/Main.class",
"used/SomeServiceInterface.class",
"used/SomeServiceClass.class",
"dep/DependencyServiceInterface.class",
"dep/DependencyServiceClass.class",
"META-INF/services/used.SomeServiceInterface",
"META-INF/services/dep.DependencyServiceInterface",
*manifestEntries,
)
containsNone(
"used/SomeUnreferencedClass.class",
"dep/DependencyUnreferencedClass.class",
"unused/UnusedServiceInterface.class",
"unused/UnusedServiceClass.class",
)
classLoader {
loadClass("app.Main")
loadClass("used.SomeServiceInterface")
loadClass("used.SomeServiceClass")
loadClass("dep.DependencyServiceInterface")
loadClass("dep.DependencyServiceClass")
}
}
}

private fun writeApiLibAndImplModules() {
settingsScript.appendText(
"""
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package com.github.jengelman.gradle.plugins.shadow.internal

import java.io.File
import java.io.IOException
import org.gradle.api.Project
import org.gradle.api.artifacts.component.ModuleComponentIdentifier
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.plugins.JavaPlugin.API_CONFIGURATION_NAME
import org.gradle.api.provider.Provider
import org.vafer.jdependency.Clazz
import org.vafer.jdependency.Clazzpath

internal fun Project.getApiJars(): Provider<List<File>> {
Expand Down Expand Up @@ -42,12 +44,20 @@ internal fun Project.getApiJars(): Provider<List<File>> {
}
}

/** Finds unused classes in the project classpath. */
/**
* Finds unused classes in the project classpath.
*
* Modified from
* [org.apache.maven.plugins.shade.filter.MinijarFilter.java](https://github.com/apache/maven-shade-plugin/blob/master/src/main/java/org/apache/maven/plugins/shade/filter/MinijarFilter.java).
*
* Related to MSHADE-313.
*/
internal fun findUnusedClasses(
sourceSetsClassesDirs: Iterable<File>,
classJars: Iterable<File>,
toMinimize: Iterable<File>,
dependencies: Iterable<File>,
resourcesDirs: Iterable<File>,
): Set<String> {
val cp = Clazzpath()
val projectUnits =
Expand All @@ -65,5 +75,93 @@ internal fun findUnusedClasses(
unused.removeAll(cpu.clazzes)
unused.removeAll(cpu.transitiveDependencies)
}

removeServices(cp, unused, dependencies + sourceSetsClassesDirs + resourcesDirs)

return unused.map { it.name }.toSet()
}

private const val SERVICES_PATH = "META-INF/services/"

private fun removeServices(
cp: Clazzpath,
unused: MutableSet<Clazz>,
files: Iterable<File>,
) {
val services = mutableMapOf<String, MutableSet<String>>()
for (file in files) {
collectServices(file, services)
}

if (services.isEmpty()) return

do {
var repeatScan = false
for ((serviceName, providers) in services) {
val serviceClazz = cp.getClazz(serviceName) ?: continue
if (serviceClazz !in unused) {
for (providerName in providers) {
val providerClazz = cp.getClazz(providerName) ?: continue
if (providerClazz in unused) {
unused.remove(providerClazz)
unused.removeAll(providerClazz.transitiveDependencies)
repeatScan = true
}
}
}
}
} while (repeatScan)
}

private fun collectServices(file: File, services: MutableMap<String, MutableSet<String>>) {
when {
file.isDirectory -> {
val servicesDir = file.resolve(SERVICES_PATH)
if (servicesDir.isDirectory) {
servicesDir
.listFiles()
?.filter { it.isFile }
?.forEach { serviceFile ->
val serviceName = serviceFile.name
if (serviceName.isNotEmpty() && !serviceName.contains('/')) {
try {
serviceFile.useLines { lines ->
lines.forEach { line ->
val provider = line.substringBefore('#').trim()
if (provider.isNotEmpty()) {
services.getOrPut(serviceName) { mutableSetOf() }.add(provider)
}
}
}
} catch (_: IOException) {
// Ignore unreadable files.
}
}
}
}
}
file.isFile -> {
try {
file.useZip {
entries().asSequence().forEach { entry ->
if (!entry.isDirectory && entry.name.startsWith(SERVICES_PATH)) {
val serviceName = entry.name.removePrefix(SERVICES_PATH)
if (serviceName.isNotEmpty() && !serviceName.contains('/')) {
getInputStream(entry).bufferedReader().useLines { lines ->
lines.forEach { line ->
val provider = line.substringBefore('#').trim()
if (provider.isNotEmpty()) {
services.getOrPut(serviceName) { mutableSetOf() }.add(provider)
}
}
}
}
}
}
}
} catch (_: Exception) {
// Ignore unreadable or non-zip files.
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,7 @@ public abstract class ShadowJar : Jar() {
classJars = apiJars,
toMinimize = toMinimize,
dependencies = includedDependencies,
resourcesDirs = sourceSetsResourcesDirs,
)
} else {
emptySet()
Expand Down Expand Up @@ -606,6 +607,16 @@ public abstract class ShadowJar : Jar() {
private val _minimizeJar
get() = @Suppress("DEPRECATION") minimizeJar

@get:InputFiles
@get:PathSensitive(PathSensitivity.RELATIVE)
private val sourceSetsResourcesDirs: ConfigurableFileCollection = objectFactory.fileCollection {
project.provider {
project.sourceSetsOrNull?.map { sourceSet ->
sourceSet.output.resourcesDir?.takeIf(File::isDirectory)
} ?: emptySet<File>()
}
}

private val isR8Enabled: Boolean
get() = _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.R8

Expand Down