From 0ea96b4d57e34dd7ac7b5683149719f9d1810013 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 16:29:23 +0800 Subject: [PATCH 01/68] Add includedSourcesJars --- .../internal/DefaultDependencyFilter.kt | 35 ++++++++++++++++++- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 8 +++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index c743b14b84..6350def1da 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -2,9 +2,16 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.tasks.DependencyFilter import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.file.FileCollection +import org.gradle.jvm.JvmLibrary +import org.gradle.language.base.artifact.SourcesArtifact -internal class DefaultDependencyFilter(project: Project) : +internal class DefaultDependencyFilter(private val project: Project) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, @@ -19,4 +26,30 @@ internal class DefaultDependencyFilter(project: Project) : } } } + + fun resolveSourcesJars(configurations: Collection): FileCollection { + return configurations + .map { resolveSourcesJars(it) } + .reduceOrNull { acc, fileCollection -> acc + fileCollection } ?: project.files() + } + + private fun resolveSourcesJars(configuration: Configuration): FileCollection { + val componentIds = + configuration.incoming.resolutionResult.allDependencies + .filterIsInstance() + .map { it.selected.id } + .filterIsInstance() + .toSet() + val files = + project.dependencies + .createArtifactResolutionQuery() + .forComponents(componentIds) + .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java) + .execute() + .resolvedComponents + .flatMap { it.getArtifacts(SourcesArtifact::class.java) } + .filterIsInstance() + .map { it.file } + return project.files(files) + } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index e39d04eafe..e776ddb5a3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -188,6 +188,14 @@ public abstract class ShadowJar : Jar() { dependencyFilter.zip(configurations) { df, cs -> df.resolve(cs) } } + @get:Classpath + internal val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { + dependencyFilter.zip(configurations) { df, cs -> + df as DefaultDependencyFilter + df.resolveSourcesJars(cs) + } + } + /** * Enables auto relocation of packages in the dependencies. * From a312982948d179c290312ce91a02727fe692c603 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:08:28 +0800 Subject: [PATCH 02/68] Generate shadowed sources jar when includedSourcesJars is present --- .../gradle/plugins/shadow/BasePluginTest.kt | 7 + .../gradle/plugins/shadow/RelocationTest.kt | 76 ++++++++++ .../shadow/util/AppendableMavenRepository.kt | 23 +++ .../internal/DefaultDependencyFilter.kt | 2 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 137 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 02c3b5feb9..df71e569f4 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -73,6 +73,9 @@ abstract class BasePluginTest { open val outputShadowedJar: JarPath get() = jarPath("build/libs/my-1.0-all.jar") + val outputShadowedSourcesJar: JarPath + get() = jarPath("build/libs/my-1.0-all-sources.jar") + val outputServerShadowedJar: JarPath get() = jarPath("server/build/libs/server-1.0-all.jar") @@ -90,6 +93,10 @@ abstract class BasePluginTest { insert("a.properties", "a") insert("a2.properties", "a2") } + buildSourcesJar { + insert("a/A.java", "package a;\npublic class A {}") + insert("a.properties", "a") + } } val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index bc589d716b..c014de5885 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo +import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -10,11 +11,13 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONS import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getBytes +import com.github.jengelman.gradle.plugins.shadow.testkit.getContent import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import kotlin.io.path.appendText +import kotlin.io.path.exists import kotlin.io.path.readBytes import kotlin.io.path.writeText import kotlin.time.Duration.Companion.seconds @@ -753,6 +756,79 @@ class RelocationTest : BasePluginTest() { } } + @Test + fun generateShadowedSourcesJarWithRelocation() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main { + | String a = "a.A"; + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:a:1.0' + |} + |$shadowJarTask { + | relocate('a', 'shadow.a') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/a/", + "shadow/a/A.java", + "shadow/a.properties", + ) + getContent("my/Main.java") + .isEqualTo( + """ + |package my; + |public class Main { + | String a = "shadow.a.A"; + |} + """ + .trimMargin() + ) + getContent("shadow/a/A.java") + .isEqualTo( + """ + |package shadow.a; + |public class A {} + """ + .trimMargin() + ) + } + } + + @Test + fun skipShadowedSourcesJarWhenNoIncludedSourcesJars() { + writeClass() + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + """ + .trimMargin() + ) + + runWithSuccess("clean", shadowJarPath) + + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + } + private companion object { @JvmStatic fun preserveLastModifiedProvider() = diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt index fa7eda57bc..0643d431aa 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt @@ -97,9 +97,16 @@ class AppendableMavenRepository(val root: Path) { """ .trimMargin() } + val sourcesArtifactLine = + if (module.sourcesArtifactPath != null) { + "artifact('${module.sourcesArtifactPath}') { classifier = 'sources' }" + } else { + "" + } module.createMavenPublication( """ |artifact '${module.artifactPath}' + |$sourcesArtifactLine |pom.withXml { xml -> | def dependenciesNode = xml.asNode().get('dependencies') ?: xml.asNode().appendNode('dependencies') | $nodes @@ -199,6 +206,7 @@ class AppendableMavenRepository(val root: Path) { inner class JarModule(groupId: String, artifactId: String, version: String) : Module(groupId, artifactId, version) { private var existingJar: Path? = null + private var existingSourcesJar: Path? = null val artifactPath: String get() = @@ -210,6 +218,16 @@ class AppendableMavenRepository(val root: Path) { } ?.invariantSeparatorsPathString ?: error("No jar file provided for $coordinate") + val sourcesArtifactPath: String? + get() = + existingSourcesJar + ?.also { + check(it.exists() && it.isRegularFile()) { + "Sources jar file does not exist or is not a regular file: $it" + } + } + ?.invariantSeparatorsPathString + fun useJar(existingJar: Path) { this.existingJar = existingJar } @@ -218,6 +236,11 @@ class AppendableMavenRepository(val root: Path) { val jarPath = jarsDir.resolve("${coordinate.replace(':', '-')}.jar") existingJar = JarBuilder(jarPath).apply(builder).write() } + + fun buildSourcesJar(builder: JarBuilder.() -> Unit) { + val jarPath = jarsDir.resolve("${coordinate.replace(':', '-')}-sources.jar") + existingSourcesJar = JarBuilder(jarPath).apply(builder).write() + } } class BomModule(groupId: String, artifactId: String, version: String) : diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 6350def1da..475788ff2d 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -11,7 +11,7 @@ import org.gradle.api.file.FileCollection import org.gradle.jvm.JvmLibrary import org.gradle.language.base.artifact.SourcesArtifact -internal class DefaultDependencyFilter(private val project: Project) : +internal class DefaultDependencyFilter(@Transient private val project: Project) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index e776ddb5a3..4fde242c4b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,8 +7,10 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec +import com.github.jengelman.gradle.plugins.shadow.internal.UnixMode import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream +import com.github.jengelman.gradle.plugins.shadow.internal.entries import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars @@ -17,13 +19,16 @@ import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.minimizeWithR8 import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey +import com.github.jengelman.gradle.plugins.shadow.internal.parentDirectoryEntries import com.github.jengelman.gradle.plugins.shadow.internal.property import com.github.jengelman.gradle.plugins.shadow.internal.setProperty import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets import com.github.jengelman.gradle.plugins.shadow.internal.useZip +import com.github.jengelman.gradle.plugins.shadow.internal.writeEntry import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator 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.relocation.relocatePath import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer @@ -33,6 +38,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException +import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -62,6 +68,7 @@ import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider @@ -196,6 +203,17 @@ public abstract class ShadowJar : Jar() { } } + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection { + val sourceSets = project.extensions.findByType(SourceSetContainer::class.java) + if (sourceSets != null) { + sourceSets.named("main").map { it.allSource.srcDirs } + } else { + emptySet() + } + } + /** * Enables auto relocation of packages in the dependencies. * @@ -539,6 +557,7 @@ public abstract class ShadowJar : Jar() { injectManifestAttributes() super.copy() runR8Minimization() + generateShadowedSourcesJar() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. @@ -747,6 +766,124 @@ public abstract class ShadowJar : Jar() { ) } + private fun generateShadowedSourcesJar() { + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } + if (sourcesJars.isEmpty) return + + val archive = archiveFile.get().asFile + val sourcesJarFile = + archive.parentFile.resolve("${archive.nameWithoutExtension}-sources.${archive.extension}") + + val actualRelocators = relocators.get() + packageRelocators + val visitedFiles = mutableSetOf() + val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 + + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + for (srcDir in sourceSetsSourceDirs.files) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + if (visitedFiles.add(relPath)) { + val relocatedPath = actualRelocators.relocatePath(relPath) + val bytes = + if (isSourceFile(relPath)) { + var text = file.readText(charset) + for (relocator in actualRelocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + file.readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = isPreserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val relocatedPath = actualRelocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = + if (isSourceFile(name)) { + var text = getInputStream(entry).bufferedReader(charset).readText() + for (relocator in actualRelocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + getInputStream(entry).readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = isPreserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = isPreserveFileTimestamps, + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) + } + } + } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e + } + } + + private fun isSourceFile(path: String): Boolean { + return path.endsWith(".java") || + path.endsWith(".kt") || + path.endsWith(".groovy") || + path.endsWith(".scala") + } + public companion object { public const val SHADOW_JAR_TASK_NAME: String = "shadowJar" From 24fba42e2ee7a6e1d0cc14a43f54d01c5b8625ad Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:10:38 +0800 Subject: [PATCH 03/68] Extract generateShadowedSourcesJar logic to internal package --- .../shadow/internal/ShadowSourcesJar.kt | 134 ++++++++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 131 ++--------------- 2 files changed, 145 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt new file mode 100644 index 0000000000..79ca417994 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -0,0 +1,134 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath +import java.io.File +import java.nio.charset.Charset +import org.gradle.api.tasks.bundling.ZipEntryCompression + +internal fun generateShadowedSourcesJar( + archiveFile: File, + sourceSetsSourceDirs: Iterable, + includedSourcesJars: Iterable, + relocators: Iterable, + entryCompression: ZipEntryCompression, + isZip64: Boolean, + metadataCharset: String?, + preserveFileTimestamps: Boolean, +) { + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } + if (sourcesJars.isEmpty()) return + + val sourcesJarFile = + archiveFile.parentFile.resolve( + "${archiveFile.nameWithoutExtension}-sources.${archiveFile.extension}" + ) + + val visitedFiles = mutableSetOf() + val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 + + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + for (srcDir in sourceSetsSourceDirs) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + if (visitedFiles.add(relPath)) { + val relocatedPath = relocators.relocatePath(relPath) + val bytes = + if (isSourceFile(relPath)) { + var text = file.readText(charset) + for (relocator in relocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + file.readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = + if (isSourceFile(name)) { + var text = getInputStream(entry).bufferedReader(charset).readText() + for (relocator in relocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + getInputStream(entry).readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = preserveFileTimestamps, + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) + } + } + } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e + } +} + +private fun isSourceFile(path: String): Boolean { + return path.endsWith(".java") || + path.endsWith(".kt") || + path.endsWith(".groovy") || + path.endsWith(".scala") +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 4fde242c4b..961b9b3ac8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,28 +7,25 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec -import com.github.jengelman.gradle.plugins.shadow.internal.UnixMode import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.entries import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses +import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.minimizeWithR8 import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey -import com.github.jengelman.gradle.plugins.shadow.internal.parentDirectoryEntries import com.github.jengelman.gradle.plugins.shadow.internal.property import com.github.jengelman.gradle.plugins.shadow.internal.setProperty import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets import com.github.jengelman.gradle.plugins.shadow.internal.useZip -import com.github.jengelman.gradle.plugins.shadow.internal.writeEntry import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator 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.relocation.relocatePath import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer @@ -38,7 +35,6 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException -import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -767,121 +763,16 @@ public abstract class ShadowJar : Jar() { } private fun generateShadowedSourcesJar() { - val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } - if (sourcesJars.isEmpty) return - - val archive = archiveFile.get().asFile - val sourcesJarFile = - archive.parentFile.resolve("${archive.nameWithoutExtension}-sources.${archive.extension}") - - val actualRelocators = relocators.get() + packageRelocators - val visitedFiles = mutableSetOf() - val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 - - try { - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - for (srcDir in sourceSetsSourceDirs.files) { - if (!srcDir.exists()) continue - srcDir - .walkTopDown() - .filter { it.isFile } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - if (visitedFiles.add(relPath)) { - val relocatedPath = actualRelocators.relocatePath(relPath) - val bytes = - if (isSourceFile(relPath)) { - var text = file.readText(charset) - for (relocator in actualRelocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - file.readBytes() - } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = isPreserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } - } - - sourcesJars.forEach { jarFile -> - jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val relocatedPath = actualRelocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = - if (isSourceFile(name)) { - var text = getInputStream(entry).bufferedReader(charset).readText() - for (relocator in actualRelocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - getInputStream(entry).readBytes() - } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = isPreserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } - } - } - - val entries = zos.entries.map { it.name } - val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() - entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> - if (!added.add(entryName)) return@forEach - zos.writeEntry( - name = entryName, - preserveLastModified = isPreserveFileTimestamps, - lastModified = currentTimeMillis, - unixMode = UnixMode.directory(), - ) - } - } - } - } catch (e: Exception) { - sourcesJarFile.delete() - throw e - } - } - - private fun isSourceFile(path: String): Boolean { - return path.endsWith(".java") || - path.endsWith(".kt") || - path.endsWith(".groovy") || - path.endsWith(".scala") + generateShadowedSourcesJar( + archiveFile = archiveFile.get().asFile, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + entryCompression = entryCompression, + isZip64 = isZip64, + metadataCharset = metadataCharset, + preserveFileTimestamps = isPreserveFileTimestamps, + ) } public companion object { From 5d4f2c3cc14b28238217cf2da8d49953808b7a6e Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:17:50 +0800 Subject: [PATCH 04/68] Use module g for shadowed sources jar functional test --- .../gradle/plugins/shadow/BasePluginTest.kt | 23 +++++++++++++++---- .../gradle/plugins/shadow/RelocationTest.kt | 21 +++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index df71e569f4..9e40ddce29 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -61,6 +61,9 @@ abstract class BasePluginTest { lateinit var artifactBJar: Path private set + lateinit var artifactGJar: Path + private set + val projectScript: Path get() = path("build.gradle") @@ -93,10 +96,6 @@ abstract class BasePluginTest { insert("a.properties", "a") insert("a2.properties", "a2") } - buildSourcesJar { - insert("a/A.java", "package a;\npublic class A {}") - insert("a.properties", "a") - } } val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } @@ -118,6 +117,20 @@ abstract class BasePluginTest { // Circular dependency with e. addDependency(e) } + val g = + jarModule("my", "g", "1.0") { + buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } + buildSourcesJar { + insert( + "g/G.java", + """ + |package g; + |public class G {} + """ + .trimMargin(), + ) + } + } bomModule("my", "bom", "1.0") { addDependency(a) addDependency(b) @@ -125,12 +138,14 @@ abstract class BasePluginTest { addDependency(d) addDependency(e) addDependency(f) + addDependency(g) } } localRepo.publish() artifactAJar = path("my/a/1.0/a-1.0.jar", parent = localRepo.root) artifactBJar = path("my/b/1.0/b-1.0.jar", parent = localRepo.root) + artifactGJar = path("my/g/1.0/g-1.0.jar", parent = localRepo.root) } @BeforeEach diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index c014de5885..9728befb38 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -762,8 +762,9 @@ class RelocationTest : BasePluginTest() { .writeText( """ |package my; + |import g.G; |public class Main { - | String a = "a.A"; + | G g; |} """ .trimMargin() @@ -771,10 +772,10 @@ class RelocationTest : BasePluginTest() { projectScript.appendText( """ |dependencies { - | implementation 'my:a:1.0' + | implementation 'my:g:1.0' |} |$shadowJarTask { - | relocate('a', 'shadow.a') + | relocate('g', 'shadow.g') |} """ .trimMargin() @@ -787,25 +788,25 @@ class RelocationTest : BasePluginTest() { "my/", "my/Main.java", "shadow/", - "shadow/a/", - "shadow/a/A.java", - "shadow/a.properties", + "shadow/g/", + "shadow/g/G.java", ) getContent("my/Main.java") .isEqualTo( """ |package my; + |import shadow.g.G; |public class Main { - | String a = "shadow.a.A"; + | G g; |} """ .trimMargin() ) - getContent("shadow/a/A.java") + getContent("shadow/g/G.java") .isEqualTo( """ - |package shadow.a; - |public class A {} + |package shadow.g; + |public class G {} """ .trimMargin() ) From 1d383df53c62744e2d3d92e6521b27a7ca2297b7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:25:48 +0800 Subject: [PATCH 05/68] Configure sourceSetsSourceDirs in ShadowJavaPlugin and ShadowKmpPlugin --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 4 +++- .../jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt | 3 +++ .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 10 +--------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index b97b267838..a1f8d8d8c2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -36,9 +36,11 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } protected open fun Project.configureShadowJar() { + val mainSourceSet = sourceSets.named("main") val taskProvider = registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> - task.from(sourceSets.named("main").map { it.output }) + task.from(mainSourceSet.map { it.output }) + task.sourceSetsSourceDirs.convention(mainSourceSet.map { it.allSource.srcDirs }) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index c8bb3a257e..68c1d389f0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -36,6 +36,9 @@ public abstract class ShadowKmpPlugin : Plugin { val kotlinJvmMain = target.compilations.named("main") registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) + task.sourceSetsSourceDirs.convention( + kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + ) task.configurations.convention( kotlinJvmMain .flatMap { configurations.named(it.runtimeDependencyConfigurationName) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 961b9b3ac8..fcf5e71fd3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -64,7 +64,6 @@ import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider @@ -201,14 +200,7 @@ public abstract class ShadowJar : Jar() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) - internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection { - val sourceSets = project.extensions.findByType(SourceSetContainer::class.java) - if (sourceSets != null) { - sourceSets.named("main").map { it.allSource.srcDirs } - } else { - emptySet() - } - } + internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() /** * Enables auto relocation of packages in the dependencies. From 59bcafa54abf95ec177eaf9cf5d82757de14c1a8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:32:15 +0800 Subject: [PATCH 06/68] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc196d0de..1c5717d28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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)) +- Support shadowed sources JAR. ([#2265](https://github.com/GradleUp/shadow/pull/2265)) ### Changed From 04118a4ae03257f372e06c7eb0cafc08067db79a Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:40:57 +0800 Subject: [PATCH 07/68] Allow generating shadowed sources jar when project sources are present even without dependency sources --- .../gradle/plugins/shadow/RelocationTest.kt | 25 +++++++++++++++++-- .../shadow/internal/ShadowSourcesJar.kt | 5 +++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 9728befb38..544bb674a4 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -814,7 +814,7 @@ class RelocationTest : BasePluginTest() { } @Test - fun skipShadowedSourcesJarWhenNoIncludedSourcesJars() { + fun generateShadowedSourcesJarWhenNoIncludedSourcesJars() { writeClass() projectScript.appendText( """ @@ -825,7 +825,28 @@ class RelocationTest : BasePluginTest() { .trimMargin() ) - runWithSuccess("clean", shadowJarPath) + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + ) + } + } + + @Test + fun skipShadowedSourcesJarWhenNoSources() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 79ca417994..60d5343f04 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -17,7 +17,10 @@ internal fun generateShadowedSourcesJar( preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } - if (sourcesJars.isEmpty()) return + val hasProjectSources = sourceSetsSourceDirs.any { + it.exists() && it.walkTopDown().any(File::isFile) + } + if (!hasProjectSources && sourcesJars.isEmpty()) return val sourcesJarFile = archiveFile.parentFile.resolve( From 8d99b4144985a717e632ec84c1edbb6fd811b4a7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 18:04:55 +0800 Subject: [PATCH 08/68] Support publishing shadow sources jar --- api/shadow.api | 3 + .../gradle/plugins/shadow/PublishingTest.kt | 69 +++++++++++++++++-- .../gradle/plugins/shadow/RelocationTest.kt | 6 +- .../shadow/util/GradleModuleMetadata.kt | 4 ++ .../plugins/shadow/ShadowApplicationPlugin.kt | 6 +- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 47 +++++++++++++ .../shadow/internal/ShadowSourcesJar.kt | 12 +--- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 24 ++++++- 8 files changed, 148 insertions(+), 23 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 5ce64b4d71..b7118a4f6b 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -58,6 +58,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugi public static final field COMPONENT_NAME Ljava/lang/String; public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin$Companion; public static final field SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME Ljava/lang/String; + public static final field SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME Ljava/lang/String; public fun (Lorg/gradle/api/component/SoftwareComponentFactory;)V public synthetic fun apply (Ljava/lang/Object;)V public fun apply (Lorg/gradle/api/Project;)V @@ -69,6 +70,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugi public final class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin$Companion { public final synthetic fun getShadowRuntimeElements (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider; + public final synthetic fun getShadowSourcesElements (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider; } public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin : org/gradle/api/Plugin { @@ -259,6 +261,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getAddMultiReleaseAttribute ()Lorg/gradle/api/provider/Property; public fun getApiJars ()Lorg/gradle/api/file/ConfigurableFileCollection; protected abstract fun getArchiveOperations ()Lorg/gradle/api/file/ArchiveOperations; + public final fun getArchiveSourcesFile ()Lorg/gradle/api/file/RegularFileProperty; public fun getConfigurations ()Lorg/gradle/api/provider/SetProperty; public fun getDependencyFilter ()Lorg/gradle/api/provider/Property; public fun getDuplicatesStrategy ()Lorg/gradle/api/file/DuplicatesStrategy; diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index ea6d376d7d..6018cbb55a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -9,6 +9,7 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath @@ -35,6 +36,7 @@ import org.apache.maven.model.io.xpp3.MavenXpp3Reader import org.gradle.api.JavaVersion import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType import org.gradle.api.attributes.LibraryElements import org.gradle.api.attributes.Usage import org.gradle.api.attributes.java.TargetJvmVersion @@ -279,10 +281,17 @@ class PublishingTest : BasePluginTest() { "maven-1.0.jar.sha512", "maven-1.0.module.sha512", "maven-1.0.pom.sha512", + "maven-1.0-sources.jar", + "maven-1.0-sources.jar.md5", + "maven-1.0-sources.jar.sha1", + "maven-1.0-sources.jar.sha256", + "maven-1.0-sources.jar.sha512", ) assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) - assertShadowVariantCommon(gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module"))) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } @Test @@ -413,11 +422,18 @@ class PublishingTest : BasePluginTest() { "my-artifact-2.0-my-classifier.my-ext.md5", "my-artifact-2.0.pom.md5", "my-artifact-2.0.pom.sha1", + "my-artifact-2.0-sources.my-ext", + "my-artifact-2.0-sources.my-ext.md5", + "my-artifact-2.0-sources.my-ext.sha1", + "my-artifact-2.0-sources.my-ext.sha256", + "my-artifact-2.0-sources.my-ext.sha512", ) assertShadowJarCommon(repoJarPath("$artifactRoot/my-artifact-2.0-my-classifier.my-ext")) assertPomCommon(repoPath("$artifactRoot/my-artifact-2.0.pom")) - assertShadowVariantCommon(gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module"))) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module")) + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } @Test @@ -471,6 +487,12 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", + // Entries of maven-1.0-sources.jar + "maven-1.0-sources.jar", + "maven-1.0-sources.jar.md5", + "maven-1.0-sources.jar.sha1", + "maven-1.0-sources.jar.sha256", + "maven-1.0-sources.jar.sha512", ) assertThat(repoPath("my/maven-all/1.0").entries) .containsOnly( @@ -489,6 +511,12 @@ class PublishingTest : BasePluginTest() { "maven-all-1.0-all.jar.sha512", "maven-all-1.0.module.sha512", "maven-all-1.0.pom.sha512", + // Entries of maven-all-1.0-sources.jar + "maven-all-1.0-sources.jar", + "maven-all-1.0-sources.jar.md5", + "maven-all-1.0-sources.jar.sha1", + "maven-all-1.0-sources.jar.sha256", + "maven-all-1.0-sources.jar.sha512", ) assertThat(repoJarPath("my/maven/1.0/maven-1.0.jar")).useAll { containsOnly(*manifestEntries) } @@ -498,12 +526,13 @@ class PublishingTest : BasePluginTest() { assertPomCommon(repoPath("my/maven/1.0/maven-1.0.pom"), arrayOf("my:a:1.0", "my:b:1.0")) gmmAdapter.fromJson(repoPath("my/maven/1.0/maven-1.0.module")).let { gmm -> - // apiElements, runtimeElements, shadowRuntimeElements + // apiElements, runtimeElements, shadowRuntimeElements, shadowSourcesElements assertThat(gmm.variantNames) .containsOnly( API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertThat(gmm.apiElementsVariant).all { transform { it.attributes } @@ -524,12 +553,18 @@ class PublishingTest : BasePluginTest() { transform { it.coordinates }.containsOnly("my:a:1.0", "my:b:1.0") } assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } assertPomCommon(repoPath("my/maven-all/1.0/maven-all-1.0.pom")) gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")).let { gmm -> - assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + assertThat(gmm.variantNames) + .containsOnly( + SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, + ) assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } } @@ -622,6 +657,11 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", + "maven-1.0-sources.jar", + "maven-1.0-sources.jar.md5", + "maven-1.0-sources.jar.sha1", + "maven-1.0-sources.jar.sha256", + "maven-1.0-sources.jar.sha512", *entriesCommon, ) assertThat(gmm.variantNames) @@ -629,9 +669,11 @@ class PublishingTest : BasePluginTest() { API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertVariantsCommon(gmm) assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) assertThat(pomDependencies).containsOnly("my:a:1.0" to "runtime", "my:b:1.0" to "compile") } else { assertThat(artifactEntries).containsOnly(*entriesCommon) @@ -722,6 +764,17 @@ class PublishingTest : BasePluginTest() { } } + private fun assertShadowSourcesVariantCommon( + gmm: GradleModuleMetadata, + variantAttrs: Array> = shadowSourcesVariantAttrs, + body: Assert.() -> Unit = {}, + ) { + assertThat(gmm.shadowSourcesElementsVariant).all { + transform { it.attributes }.containsOnly(*variantAttrs) + body() + } + } + private fun assertShadowJarCommon(jarPath: JarPath) { assertThat(jarPath).useAll { containsAtLeast(*entriesInA) @@ -752,6 +805,14 @@ class PublishingTest : BasePluginTest() { Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, ) + val shadowSourcesVariantAttrs = + arrayOf( + Category.CATEGORY_ATTRIBUTE.name to Category.DOCUMENTATION, + Bundling.BUNDLING_ATTRIBUTE.name to Bundling.SHADOWED, + DocsType.DOCS_TYPE_ATTRIBUTE.name to DocsType.SOURCES, + Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, + ) + fun MavenXpp3Reader.read(path: Path): Model = path.inputStream().use { read(it) } fun JsonAdapter.fromJson(path: Path): T = checkNotNull(fromJson(path.readText())) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 544bb674a4..98eac49ead 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -3,7 +3,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo -import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -17,7 +16,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import kotlin.io.path.appendText -import kotlin.io.path.exists import kotlin.io.path.readBytes import kotlin.io.path.writeText import kotlin.time.Duration.Companion.seconds @@ -836,7 +834,7 @@ class RelocationTest : BasePluginTest() { } @Test - fun skipShadowedSourcesJarWhenNoSources() { + fun generateEmptyShadowedSourcesJarWhenNoSources() { projectScript.appendText( """ |dependencies { @@ -848,7 +846,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + assertThat(outputShadowedSourcesJar).useAll { containsOnly() } } private companion object { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt index fef15ab379..4e888c1c2c 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt @@ -1,6 +1,7 @@ package com.github.jengelman.gradle.plugins.shadow.util import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME @@ -18,6 +19,9 @@ data class GradleModuleMetadata(private val variants: List) { val shadowRuntimeElementsVariant: Variant get() = variants.single { it.name == SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME } + val shadowSourcesElementsVariant: Variant + get() = variants.single { it.name == SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME } + val variantNames: List get() = variants.map { it.name } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt index 3221fe1887..3d1a28537c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt @@ -46,7 +46,7 @@ public abstract class ShadowApplicationPlugin : Plugin { task.description = "Runs this project as a JVM application using the shadow jar" task.group = ApplicationPlugin.APPLICATION_GROUP - task.classpath = files(tasks.shadowJar) + task.classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) with(applicationExtension) { task.mainModule.convention(mainModule) @@ -63,7 +63,7 @@ public abstract class ShadowApplicationPlugin : Plugin { task.description = "Creates OS specific scripts to run the project as a JVM application using the shadow jar" - task.classpath = files(tasks.shadowJar) + task.classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) @Suppress("InternalGradleApiUsage") // TODO: replace usages of conventionMapping. with(applicationExtension) { @@ -118,7 +118,7 @@ public abstract class ShadowApplicationPlugin : Plugin { dist.contents { distSpec -> distSpec.from(file("src/dist")) distSpec.into("lib") { lib -> - lib.from(tasks.shadowJar) + lib.from(tasks.shadowJar.flatMap { it.archiveFile }) // Reflects the value of the `Class-Path` attribute in the JAR manifest. lib.from(configurations.shadow) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index a1f8d8d8c2..144882372b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -15,6 +15,7 @@ import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType import org.gradle.api.attributes.LibraryElements import org.gradle.api.attributes.Usage import org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE @@ -77,6 +78,35 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl shadowRuntimeElements.outgoing.artifact(tasks.shadowJar) } + val shadowSourcesElements = + configurations.consumable(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME) { shadowSourcesElements + -> + shadowSourcesElements.attributes { attrs -> + attrs.attribute( + Usage.USAGE_ATTRIBUTE, + objects.named(Usage::class.java, Usage.JAVA_RUNTIME), + ) + attrs.attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attrs.attribute( + Bundling.BUNDLING_ATTRIBUTE, + objects.named(Bundling::class.java, Bundling.SHADOWED), + ) + attrs.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + objects.named(DocsType::class.java, DocsType.SOURCES), + ) + } + val sourcesJarFile = tasks.shadowJar.flatMap { it.archiveSourcesFile } + shadowSourcesElements.outgoing.artifact(sourcesJarFile) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.classifier = "sources" + artifact.type = "jar" + } + } + // See more details in #2086. afterEvaluate { if (shadow.addTargetJvmVersionAttribute.get()) { @@ -113,11 +143,13 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl protected open fun Project.configureComponents() { val shadowRuntimeElements = configurations.shadowRuntimeElements + val shadowSourcesElements = configurations.shadowSourcesElements val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) shadowComponent.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> variant.mapToMavenScope("runtime") } + shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) {} components.named("java", AdhocComponentWithVariants::class.java) { component -> component.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> variant.mapToOptional() @@ -128,6 +160,15 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl variant.skip() } } + component.addVariantsFromConfiguration(shadowSourcesElements) { variant -> + variant.mapToOptional() + if (shadow.addShadowVariantIntoJavaComponent.get()) { + logger.info("Adding {} variant to Java component.", shadowSourcesElements.name) + } else { + logger.info("Skipping adding {} variant to Java component.", shadowSourcesElements.name) + variant.skip() + } + } } } @@ -137,10 +178,16 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl public companion object { public const val COMPONENT_NAME: String = SHADOW public const val SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME: String = "shadowRuntimeElements" + public const val SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME: String = "shadowSourcesElements" @get:JvmSynthetic public inline val ConfigurationContainer.shadowRuntimeElements: NamedDomainObjectProvider get() = named(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) + + @get:JvmSynthetic + public inline val ConfigurationContainer.shadowSourcesElements: + NamedDomainObjectProvider + get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 60d5343f04..c4422a08aa 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -7,7 +7,7 @@ import java.nio.charset.Charset import org.gradle.api.tasks.bundling.ZipEntryCompression internal fun generateShadowedSourcesJar( - archiveFile: File, + sourcesJarFile: File, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, @@ -17,15 +17,7 @@ internal fun generateShadowedSourcesJar( preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } - val hasProjectSources = sourceSetsSourceDirs.any { - it.exists() && it.walkTopDown().any(File::isFile) - } - if (!hasProjectSources && sourcesJars.isEmpty()) return - - val sourcesJarFile = - archiveFile.parentFile.resolve( - "${archiveFile.nameWithoutExtension}-sources.${archiveFile.extension}" - ) + if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index fcf5e71fd3..b937fcdae7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -52,7 +52,7 @@ import org.gradle.api.file.DuplicatesStrategy.EXCLUDE import org.gradle.api.file.DuplicatesStrategy.FAIL import org.gradle.api.file.DuplicatesStrategy.INCLUDE import org.gradle.api.file.DuplicatesStrategy.INHERIT -import org.gradle.api.file.DuplicatesStrategy.WARN +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.CacheableTask @@ -62,6 +62,7 @@ import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction @@ -198,6 +199,24 @@ public abstract class ShadowJar : Jar() { } } + @get:Optional + @get:OutputFile + public val archiveSourcesFile: RegularFileProperty = + objectFactory + .fileProperty() + .convention( + destinationDirectory.file( + archiveFileName.map { name -> + val idx = name.lastIndexOf('.') + if (idx != -1) { + "${name.substring(0, idx)}-sources${name.substring(idx)}" + } else { + "$name-sources" + } + } + ) + ) + @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() @@ -755,8 +774,9 @@ public abstract class ShadowJar : Jar() { } private fun generateShadowedSourcesJar() { + if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( - archiveFile = archiveFile.get().asFile, + sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs.files, includedSourcesJars = includedSourcesJars.files, relocators = relocators.get() + packageRelocators, From 85f1aa5611d6910efeaf915ce5e14ee1df88f003 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 18:20:51 +0800 Subject: [PATCH 09/68] Remove outdated comment for applyToSourceContent --- .../gradle/plugins/shadow/relocation/SimpleRelocator.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 85384f0699..c9677d0d69 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -127,10 +127,6 @@ constructor( return if (rawString) clazz else clazz.replaceFirst(pattern.toRegex(), shadedPattern) } - /** - * We don't call this function now, so we don't have to expose [sourcePackageExcludes] and - * [sourcePathExcludes] as inputs. - */ override fun applyToSourceContent(sourceContent: String): String { if (rawString) return sourceContent val content = From 93c8d73550eb75a302e3507a4aba3224a7bb789f Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 18:32:10 +0800 Subject: [PATCH 10/68] Add documentation and test for generating Javadoc/Dokka from shadowed sources --- build.gradle.kts | 1 + docs/publishing/README.md | 74 +++++++++++++++++++ gradle/libs.versions.toml | 4 +- .../gradle/plugins/shadow/JavaPluginsTest.kt | 42 +++++++++++ .../plugins/shadow/KotlinPluginsTest.kt | 52 +++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index bb05feec52..b5de68abaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -140,6 +140,7 @@ dependencies { testPluginRuntimeOnly(libs.foojayResolver) testPluginRuntimeOnly(libs.pluginPublish) + testPluginRuntimeOnly(libs.dokka) lintChecks(libs.androidx.gradlePluginLints) } diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 2ec088dfd5..bc6c4eb699 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -515,6 +515,79 @@ customizable properties listed in [Configuring Output Name][configuring-output-n We modified `archiveClassifier`, `archiveExtension` and `archiveBaseName` in this example, the published artifact will be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. +## Generating Javadoc or Dokka from Shadowed Sources + +When creating fat / shadowed libraries, you may want to generate a complete Javadoc or Dokka JAR covering both your +project sources and shadowed dependency sources with relocated packages. + +Because `shadowJar` outputs the shadowed sources archive at `archiveSourcesFile` (where relocated packages and +source contents have already been transformed), you can configure the `javadoc` task (or Dokka task) to consume the +shadowed sources and classes directly from `shadowJar`. The generated documentation will reflect the relocated package +names (e.g. `shadow.g.G` instead of `g.G`). + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.javadoc { + source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) + classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('javadoc', Javadoc) { + source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + } + ``` + +If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed sources and configure `sourceRoots`: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + kotlin("jvm") + id("com.gradleup.shadow") + id("org.jetbrains.dokka") + } + + val extractShadowedSources = tasks.register("extractShadowedSources") { + from(zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile })) + into(layout.buildDirectory.dir("extracted-shadowed-sources")) + } + + dokka { + dokkaSourceSets.configureEach { + sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) + classpath.setFrom(tasks.shadowJar.flatMap { it.archiveFile }) + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'org.jetbrains.kotlin.jvm' + id 'com.gradleup.shadow' + id 'org.jetbrains.dokka' + } + + tasks.register('extractShadowedSources', Sync) { + from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + into layout.buildDirectory.dir('extracted-shadowed-sources') + } + + dokka { + dokkaSourceSets.configureEach { + sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + } + } + ``` [Jar]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) @@ -522,3 +595,4 @@ be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name +[dokka]: https://kotlinlang.org/docs/dokka-introduction.html diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62a63a1088..323d4df3f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ minGradle = "9.4.0" kotlin = "2.4.10" moshi = "1.15.2" pluginPublish = "2.1.1" +dokka = "2.2.0" [libraries] apache-ant = "org.apache.ant:ant:1.10.17" @@ -22,6 +23,7 @@ foojayResolver = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.to develocity = "com.gradle:develocity-gradle-plugin:4.5.0" kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } pluginPublish = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "pluginPublish" } +dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } androidx-gradlePluginLints = "androidx.lint:lint-gradle:1.0.0" # Dummy to get renovate updates, the version is used in rootProject build.gradle with spotless. @@ -34,7 +36,7 @@ assertk = "com.willowtreeapps.assertk:assertk:0.28.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } android-lint = "com.android.lint:9.4.0" -jetbrains-dokka = "org.jetbrains.dokka:2.2.0" +jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } mavenPublish = "com.vanniktech.maven.publish:0.37.0" pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "pluginPublish" } spotless = "com.diffplug.spotless:8.10.1" diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index b55e52a462..6dbd62a5d3 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -9,6 +9,7 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.assertions.isNull +import assertk.assertions.isTrue import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.Companion.ENABLE_DEVELOCITY_INTEGRATION_PROPERTY import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey @@ -27,6 +28,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.prependText import kotlin.io.path.appendText import kotlin.io.path.deleteExisting +import kotlin.io.path.exists import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.name import kotlin.io.path.outputStream @@ -1326,6 +1328,46 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun generateJavadocFromShadowedSourcesJar() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |/** Main class doc */ + |public class Main { + | /** Main method doc */ + | public static void main(String[] args) {} + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + | shadow 'my:g:1.0' + |} + | + |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + | relocate 'g', 'shadow.g' + |} + | + |tasks.named('javadoc', Javadoc) { + | source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + | classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + |} + """ + .trimMargin() + ) + + runWithSuccess("javadoc") + + val javadocDir = projectRoot.resolve("build/docs/javadoc") + assertThat(javadocDir.resolve("my/Main.html").exists()).isTrue() + assertThat(javadocDir.resolve("shadow/g/G.html").exists()).isTrue() + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 5105912faa..a8f1a72a96 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -12,6 +12,9 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import kotlin.io.path.appendText +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.relativeTo +import kotlin.io.path.walk import kotlin.io.path.writeText import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -279,6 +282,55 @@ class KotlinPluginsTest : BasePluginTest() { ) } + @Test + fun generateDokkaFromShadowedSourcesJar() { + projectScript.writeText( + """ + |plugins { + | id 'org.jetbrains.kotlin.jvm' + | id 'com.gradleup.shadow' + | id 'org.jetbrains.dokka' + |} + |dependencies { + | implementation 'my:g:1.0' + | shadow 'my:g:1.0' + |} + |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + | relocate 'g', 'shadow.g' + |} + |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { + | from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + | into layout.buildDirectory.dir('extracted-shadowed-sources') + |} + |dokka { + | dokkaSourceSets.configureEach { + | sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + | classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + | } + |} + """ + .trimMargin() + ) + path("src/main/kotlin/my/Main.kt") + .writeText( + """ + |package my + |/** Main class doc */ + |class Main + """ + .trimMargin() + ) + + runWithSuccess("dokkaGenerateHtml") + + val dokkaDir = projectRoot.resolve("build/dokka/html") + val dokkaFiles = + dokkaDir.walk().map { it.relativeTo(dokkaDir).invariantSeparatorsPathString }.toList() + assertThat(dokkaFiles).contains("index.html") + assertThat(dokkaFiles).contains("my/my/-main/index.html") + assertThat(dokkaFiles).contains("my/shadow.g/-g/index.html") + } + private fun compileOnlyStdlib(exclude: Boolean): String { return if (exclude) { // Disable the stdlib dependency added via `implementation`. From ecad8e4837bc50837f3f463f81fed4c1e5e7feab Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 09:48:43 +0800 Subject: [PATCH 11/68] Clean up ShadowJavaPlugin --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 144882372b..a59d87fdd7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -21,7 +21,9 @@ import org.gradle.api.attributes.Usage import org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE import org.gradle.api.component.AdhocComponentWithVariants import org.gradle.api.component.SoftwareComponentFactory +import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME +import org.gradle.api.provider.Provider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -54,14 +56,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl compileClasspath.extendsFrom(shadowConfig) } val shadowRuntimeElements = - configurations.consumable(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) { shadowRuntimeElements - -> - shadowRuntimeElements.extendsFrom(shadowConfig) - shadowRuntimeElements.attributes { attrs -> - attrs.attribute( - Usage.USAGE_ATTRIBUTE, - objects.named(Usage::class.java, Usage.JAVA_RUNTIME), - ) + registerConsumableConfiguration(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) { + extendsFrom(shadowConfig) + attributes { attrs -> attrs.attribute( Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY), @@ -70,42 +67,26 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.JAR), ) - attrs.attributeProvider( - Bundling.BUNDLING_ATTRIBUTE, - shadow.bundlingAttribute.map { attr -> objects.named(Bundling::class.java, attr) }, - ) } - shadowRuntimeElements.outgoing.artifact(tasks.shadowJar) + outgoing.artifact(tasks.shadowJar) } - - val shadowSourcesElements = - configurations.consumable(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME) { shadowSourcesElements - -> - shadowSourcesElements.attributes { attrs -> - attrs.attribute( - Usage.USAGE_ATTRIBUTE, - objects.named(Usage::class.java, Usage.JAVA_RUNTIME), - ) - attrs.attribute( - Category.CATEGORY_ATTRIBUTE, - objects.named(Category::class.java, Category.DOCUMENTATION), - ) - attrs.attribute( - Bundling.BUNDLING_ATTRIBUTE, - objects.named(Bundling::class.java, Bundling.SHADOWED), - ) - attrs.attribute( - DocsType.DOCS_TYPE_ATTRIBUTE, - objects.named(DocsType::class.java, DocsType.SOURCES), - ) - } - val sourcesJarFile = tasks.shadowJar.flatMap { it.archiveSourcesFile } - shadowSourcesElements.outgoing.artifact(sourcesJarFile) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.classifier = "sources" - artifact.type = "jar" - } + registerConsumableConfiguration(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME) { + attributes { attrs -> + attrs.attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attrs.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + objects.named(DocsType::class.java, DocsType.SOURCES), + ) } + outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.classifier = "sources" + artifact.type = "jar" + } + } // See more details in #2086. afterEvaluate { @@ -151,27 +132,54 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) {} components.named("java", AdhocComponentWithVariants::class.java) { component -> - component.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> - variant.mapToOptional() - if (shadow.addShadowVariantIntoJavaComponent.get()) { - logger.info("Adding {} variant to Java component.", shadowRuntimeElements.name) - } else { - logger.info("Skipping adding {} variant to Java component.", shadowRuntimeElements.name) - variant.skip() - } - } - component.addVariantsFromConfiguration(shadowSourcesElements) { variant -> - variant.mapToOptional() - if (shadow.addShadowVariantIntoJavaComponent.get()) { - logger.info("Adding {} variant to Java component.", shadowSourcesElements.name) - } else { - logger.info("Skipping adding {} variant to Java component.", shadowSourcesElements.name) - variant.skip() - } + val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent + component.addVariants( + addIntoJavaComponent = addIntoJavaComponent, + outgoingConfiguration = shadowRuntimeElements, + logger = logger, + ) + component.addVariants( + addIntoJavaComponent = addIntoJavaComponent, + outgoingConfiguration = shadowSourcesElements, + logger = logger, + ) + } + } + + private fun AdhocComponentWithVariants.addVariants( + addIntoJavaComponent: Provider, + outgoingConfiguration: NamedDomainObjectProvider, + logger: Logger, + ) { + addVariantsFromConfiguration(outgoingConfiguration) { variant -> + variant.mapToOptional() + if (addIntoJavaComponent.get()) { + logger.info("Adding {} variant to Java component.", outgoingConfiguration.name) + } else { + logger.info("Skipping adding {} variant to Java component.", outgoingConfiguration.name) + variant.skip() } } } + private fun Project.registerConsumableConfiguration( + name: String, + action: ConsumableConfiguration.() -> Unit, + ) = + configurations.consumable(name) { configuration -> + configuration.attributes { attrs -> + attrs.attribute( + Usage.USAGE_ATTRIBUTE, + objects.named(Usage::class.java, Usage.JAVA_RUNTIME), + ) + attrs.attributeProvider( + Bundling.BUNDLING_ATTRIBUTE, + shadow.bundlingAttribute.map { attr -> objects.named(Bundling::class.java, attr) }, + ) + } + configuration.action() + } + @Deprecated("This method will be removed in Shadow 10.") protected open fun Project.configureJavaGradlePlugin() {} From 68c2e1b8cf704da29eef898dc80c03f30a95838b Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 11:16:17 +0800 Subject: [PATCH 12/68] Set Dokka properties via gradle.properties as workaround for IP --- .../gradle/plugins/shadow/SnippetExecutable.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt index 244bc68510..64bf20ce07 100644 --- a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt +++ b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt @@ -51,6 +51,23 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + // TODO: https://github.com/Kotlin/dokka/issues/4488 + projectRoot + .resolve("gradle.properties") + .writeText( + """ + |# Dokka 2.2.0 DGPv2 is the default, but the plugin still looks up these properties dynamically. + |# Setting them here avoids cross-project property lookups that break isolated projects. + |org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled + |org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true + |org.jetbrains.dokka.experimental.gradle.pluginMode.nowarn=true + |org.jetbrains.dokka.experimental.tryK2=true + |org.jetbrains.dokka.experimental.tryK2.noWarn=true + |org.jetbrains.dokka.experimental.tryK2.nowarn=true + |org.jetbrains.dokka.internal.enableWorkaroundKT80551=true + """ + .trimMargin() + ) val pluginsBlock = """ |plugins { From 0f814e5f5bd9d1fc5a484048836274b83d1b6b46 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 17:51:48 +0800 Subject: [PATCH 13/68] Clean docs/publishing/README.md --- docs/publishing/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index bc6c4eb699..1a5dafbbef 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -520,17 +520,17 @@ be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. When creating fat / shadowed libraries, you may want to generate a complete Javadoc or Dokka JAR covering both your project sources and shadowed dependency sources with relocated packages. -Because `shadowJar` outputs the shadowed sources archive at `archiveSourcesFile` (where relocated packages and -source contents have already been transformed), you can configure the `javadoc` task (or Dokka task) to consume the -shadowed sources and classes directly from `shadowJar`. The generated documentation will reflect the relocated package -names (e.g. `shadow.g.G` instead of `g.G`). +Because `shadowJar` outputs the shadowed sources archive at `archiveSourcesFile` (where relocated packages and source +contents have already been transformed), you can configure the `javadoc` task (or Dokka task) to consume the shadowed +sources and classes directly from `shadowJar`. The generated documentation will reflect the relocated package names +(e.g. `shadow.com.Example` instead of `com.Example`). === ":material-language-kotlin: build.gradle.kts" ```kotlin tasks.javadoc { - source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) + source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) } ``` @@ -538,8 +538,8 @@ names (e.g. `shadow.g.G` instead of `g.G`). ```groovy tasks.named('javadoc', Javadoc) { - source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) } ``` @@ -550,8 +550,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source ```kotlin plugins { kotlin("jvm") - id("com.gradleup.shadow") id("org.jetbrains.dokka") + id("com.gradleup.shadow") } val extractShadowedSources = tasks.register("extractShadowedSources") { @@ -561,8 +561,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source dokka { dokkaSourceSets.configureEach { - sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) classpath.setFrom(tasks.shadowJar.flatMap { it.archiveFile }) + sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) } } ``` @@ -572,8 +572,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source ```groovy plugins { id 'org.jetbrains.kotlin.jvm' - id 'com.gradleup.shadow' id 'org.jetbrains.dokka' + id 'com.gradleup.shadow' } tasks.register('extractShadowedSources', Sync) { @@ -583,8 +583,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source dokka { dokkaSourceSets.configureEach { - sourceRoots.from(extractShadowedSources.map { it.destinationDir }) classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + sourceRoots.from(extractShadowedSources.map { it.destinationDir }) } } ``` From f4eab8f3a675125b4ce9919368c6016bbb7c7a2d Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 17:57:49 +0800 Subject: [PATCH 14/68] Clean up tests --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 25 ++++++------ .../plugins/shadow/KotlinPluginsTest.kt | 38 ++++++++++--------- .../gradle/plugins/shadow/RelocationTest.kt | 4 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 1 - .../gradle/plugins/shadow/testkit/JarPath.kt | 2 +- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 6dbd62a5d3..067cf37c46 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -3,13 +3,13 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.all import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.containsAtLeast import assertk.assertions.containsMatch import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.assertions.isNull -import assertk.assertions.isTrue import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.Companion.ENABLE_DEVELOCITY_INTEGRATION_PROPERTY import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey @@ -28,10 +28,11 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.prependText import kotlin.io.path.appendText import kotlin.io.path.deleteExisting -import kotlin.io.path.exists import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.name import kotlin.io.path.outputStream +import kotlin.io.path.relativeTo +import kotlin.io.path.walk import kotlin.io.path.writeText import kotlin.reflect.full.declaredFunctions import kotlin.reflect.jvm.javaMethod @@ -1336,7 +1337,6 @@ class JavaPluginsTest : BasePluginTest() { |package my; |/** Main class doc */ |public class Main { - | /** Main method doc */ | public static void main(String[] args) {} |} """ @@ -1346,16 +1346,13 @@ class JavaPluginsTest : BasePluginTest() { """ |dependencies { | implementation 'my:g:1.0' - | shadow 'my:g:1.0' |} - | - |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + |$shadowJarTask { | relocate 'g', 'shadow.g' |} - | |tasks.named('javadoc', Javadoc) { - | source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) - | classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + | classpath = files($shadowJarTask.flatMap { it.archiveFile }) + | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }) |} """ .trimMargin() @@ -1364,8 +1361,14 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess("javadoc") val javadocDir = projectRoot.resolve("build/docs/javadoc") - assertThat(javadocDir.resolve("my/Main.html").exists()).isTrue() - assertThat(javadocDir.resolve("shadow/g/G.html").exists()).isTrue() + val javadocFiles = + javadocDir.walk().map { it.relativeTo(javadocDir).invariantSeparatorsPathString } + assertThat(javadocFiles) + .containsAtLeast( + "index.html", + "my/Main.html", + "shadow/g/G.html", + ) } private fun dependencies(configuration: String, vararg flags: String): String { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index a8f1a72a96..63f9ccf183 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.containsAtLeast import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME @@ -284,6 +285,15 @@ class KotlinPluginsTest : BasePluginTest() { @Test fun generateDokkaFromShadowedSourcesJar() { + path("src/main/kotlin/my/Main.kt") + .writeText( + """ + |package my + |/** Main class doc */ + |class Main + """ + .trimMargin() + ) projectScript.writeText( """ |plugins { @@ -293,42 +303,34 @@ class KotlinPluginsTest : BasePluginTest() { |} |dependencies { | implementation 'my:g:1.0' - | shadow 'my:g:1.0' |} - |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + |$shadowJarTask { | relocate 'g', 'shadow.g' |} |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { - | from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + | from zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }) | into layout.buildDirectory.dir('extracted-shadowed-sources') |} |dokka { | dokkaSourceSets.configureEach { + | classpath.from($shadowJarTask.flatMap { it.archiveFile }) | sourceRoots.from(extractShadowedSources.map { it.destinationDir }) - | classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) | } |} """ .trimMargin() ) - path("src/main/kotlin/my/Main.kt") - .writeText( - """ - |package my - |/** Main class doc */ - |class Main - """ - .trimMargin() - ) runWithSuccess("dokkaGenerateHtml") val dokkaDir = projectRoot.resolve("build/dokka/html") - val dokkaFiles = - dokkaDir.walk().map { it.relativeTo(dokkaDir).invariantSeparatorsPathString }.toList() - assertThat(dokkaFiles).contains("index.html") - assertThat(dokkaFiles).contains("my/my/-main/index.html") - assertThat(dokkaFiles).contains("my/shadow.g/-g/index.html") + val dokkaFiles = dokkaDir.walk().map { it.relativeTo(dokkaDir).invariantSeparatorsPathString } + assertThat(dokkaFiles) + .containsAtLeast( + "index.html", + "my/my/-main/index.html", + "my/shadow.g/-g/index.html", + ) } private fun compileOnlyStdlib(exclude: Boolean): String { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 98eac49ead..7a4e5f9983 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo @@ -15,6 +16,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain +import com.github.jengelman.gradle.plugins.shadow.testkit.toEntries import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -846,7 +848,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(outputShadowedSourcesJar).useAll { containsOnly() } + assertThat(outputShadowedSourcesJar).useAll { toEntries().isEmpty() } } private companion object { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index b937fcdae7..6ac3176003 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -9,7 +9,6 @@ import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifes import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream -import com.github.jengelman.gradle.plugins.shadow.internal.entries import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index 6ba359b9bd..cb7d8b8e8e 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -122,6 +122,6 @@ fun Assert.runMain( os.toString().invariantEolString } -private fun Assert.toEntries() = transform { actual -> +fun Assert.toEntries() = transform { actual -> actual.entries().toList().map { it.name } } From 935c73b82de782b4b624d297816009f12818c89d Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 18:32:25 +0800 Subject: [PATCH 15/68] Filter unused classes when generating shadowed sources jar --- .../gradle/plugins/shadow/BasePluginTest.kt | 54 +++++++++++++++++++ .../gradle/plugins/shadow/MinimizeTest.kt | 40 ++++++++++++++ .../shadow/internal/ShadowSourcesJar.kt | 38 +++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 22 ++++---- 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 9e40ddce29..98dc1ca93f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -131,6 +131,58 @@ abstract class BasePluginTest { ) } } + val h = + jarModule("my", "h", "1.0") { + buildJar { + insert("h/H.class", createEmptyClassBytes("h/H")) + insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) + } + buildSourcesJar { + insert( + "h/H.java", + """ + |package h; + |public class H {} + """ + .trimMargin(), + ) + insert( + "h/UnusedH.java", + """ + |package h; + |public class UnusedH {} + """ + .trimMargin(), + ) + } + } + val k = + jarModule("my", "k", "1.0") { + buildJar { + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) + insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + } + buildSourcesJar { + insert( + "k/Utils.kt", + """ + |@file:JvmName("CustomUtils") + |package k + |fun util() {} + """ + .trimMargin(), + ) + insert( + "k/UnusedUtils.kt", + """ + |@file:JvmName("CustomUnusedUtils") + |package k + |fun unusedUtil() {} + """ + .trimMargin(), + ) + } + } bomModule("my", "bom", "1.0") { addDependency(a) addDependency(b) @@ -139,6 +191,8 @@ abstract class BasePluginTest { addDependency(e) addDependency(f) addDependency(g) + addDependency(h) + addDependency(k) } } localRepo.publish() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index ebd8e798f5..550c22967f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -126,6 +126,46 @@ class MinimizeTest : BasePluginTest() { } } + @Test + fun minimizeSourcesJar() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |import h.H; + |import k.CustomUtils; + |public class Main { + | H h; + | CustomUtils u; + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:h:1.0' + | implementation 'my:k:1.0' + |} + |$shadowJarTask { + | minimize() + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsAtLeast("my/Main.class", "h/H.class", "k/CustomUtils.class") + containsNone("h/UnusedH.class", "k/CustomUnusedUtils.class") + } + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("my/Main.java", "h/H.java", "k/Utils.kt") + containsNone("h/UnusedH.java", "k/UnusedUtils.kt") + } + } + /** * 'Client', 'Server' and 'junit' are independent. 'junit' is excluded from the minimize step. The * minimize step shall remove 'Client' but not 'junit'. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index c4422a08aa..377ab43a98 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -11,6 +11,7 @@ internal fun generateShadowedSourcesJar( sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, + unusedClasses: Set = emptySet(), entryCompression: ZipEntryCompression, isZip64: Boolean, metadataCharset: String?, @@ -37,6 +38,7 @@ internal fun generateShadowedSourcesJar( .filter { it.isFile } .forEach { file -> val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + if (isUnused(relPath, { file.readText(charset) }, unusedClasses)) return@forEach if (visitedFiles.add(relPath)) { val relocatedPath = relocators.relocatePath(relPath) val bytes = @@ -75,6 +77,15 @@ internal fun generateShadowedSourcesJar( ) { return@forEach } + if ( + isUnused( + name, + { getInputStream(entry).bufferedReader(charset).readText() }, + unusedClasses, + ) + ) { + return@forEach + } val relocatedPath = relocators.relocatePath(name) if (visitedFiles.add(relocatedPath)) { val bytes = @@ -121,6 +132,33 @@ internal fun generateShadowedSourcesJar( } } +private val jvmNameRegex = + Regex( + """@file\s*:\s*(?:\[[^\]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" + ) + +private fun isUnused( + path: String, + sourceContentProvider: () -> String, + unusedClasses: Set, +): Boolean { + if (unusedClasses.isEmpty() || !isSourceFile(path)) return false + val simpleName = path.substringAfterLast('/').substringBeforeLast('.') + val pkg = path.substringBeforeLast('/', "").replace('/', '.') + val className = if (pkg.isEmpty()) simpleName else "$pkg.$simpleName" + if (unusedClasses.contains(className)) return true + + if (path.endsWith(".kt")) { + val text = sourceContentProvider() + val customJvmName = jvmNameRegex.find(text)?.groupValues?.get(1) + val facadeName = customJvmName ?: "${simpleName}Kt" + val facadeClassName = if (pkg.isEmpty()) facadeName else "$pkg.$facadeName" + if (unusedClasses.contains(facadeClassName)) return true + } + + return false +} + private fun isSourceFile(path: String): Boolean { return path.endsWith(".java") || path.endsWith(".kt") || diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 6ac3176003..0299cb4da5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -570,15 +570,16 @@ public abstract class ShadowJar : Jar() { override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { val unusedClasses = if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -772,6 +773,8 @@ public abstract class ShadowJar : Jar() { ) } + private var unusedClasses: Set = emptySet() + private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( @@ -779,6 +782,7 @@ public abstract class ShadowJar : Jar() { sourceSetsSourceDirs = sourceSetsSourceDirs.files, includedSourcesJars = includedSourcesJars.files, relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, entryCompression = entryCompression, isZip64 = isZip64, metadataCharset = metadataCharset, From 476b36d3ba10922d9201c2cbfe4ee22e0ab18aa4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 18:43:18 +0800 Subject: [PATCH 16/68] Extract declared package from source files to determine canonical path and match unused classes --- .../plugins/shadow/KotlinPluginsTest.kt | 30 ++++ .../shadow/internal/ShadowSourcesJar.kt | 136 +++++++++------ .../shadow/internal/ShadowSourcesJarTest.kt | 164 ++++++++++++++++++ 3 files changed, 277 insertions(+), 53 deletions(-) create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 63f9ccf183..852e050f0a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -8,6 +8,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass @@ -333,6 +334,35 @@ class KotlinPluginsTest : BasePluginTest() { ) } + @Test + fun generateShadowedSourcesJarNormalizesPackageDirectory() { + path("src/main/kotlin/FlatFile.kt") + .writeText( + """ + |package my.custom.nested + | + |class FlatClass + """ + .trimMargin() + ) + projectScript.writeText( + """ + |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} + |$shadowJarTask { + | relocate 'my.custom', 'shadow.custom' + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("shadow/custom/nested/FlatFile.kt") + containsNone("FlatFile.kt") + } + } + private fun compileOnlyStdlib(exclude: Boolean): String { return if (exclude) { // Disable the stdlib dependency added via `implementation`. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 377ab43a98..ae5503c278 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -38,26 +38,42 @@ internal fun generateShadowedSourcesJar( .filter { it.isFile } .forEach { file -> val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - if (isUnused(relPath, { file.readText(charset) }, unusedClasses)) return@forEach - if (visitedFiles.add(relPath)) { + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { val relocatedPath = relocators.relocatePath(relPath) - val bytes = - if (isSourceFile(relPath)) { - var text = file.readText(charset) - for (relocator in relocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - file.readBytes() + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) } } } @@ -77,34 +93,42 @@ internal fun generateShadowedSourcesJar( ) { return@forEach } - if ( - isUnused( - name, - { getInputStream(entry).bufferedReader(charset).readText() }, - unusedClasses, - ) - ) { - return@forEach - } - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = - if (isSourceFile(name)) { - var text = getInputStream(entry).bufferedReader(charset).readText() - for (relocator in relocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - getInputStream(entry).readBytes() + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) } } } @@ -132,24 +156,30 @@ internal fun generateShadowedSourcesJar( } } +private val packageRegex = Regex("""(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""") + private val jvmNameRegex = Regex( """@file\s*:\s*(?:\[[^\]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" ) -private fun isUnused( - path: String, - sourceContentProvider: () -> String, +internal fun extractPackage(text: String): String { + val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() + return if (matches.isEmpty()) "" else matches.joinToString(".") +} + +internal fun isUnused( + fileName: String, + pkg: String, + text: String, unusedClasses: Set, ): Boolean { - if (unusedClasses.isEmpty() || !isSourceFile(path)) return false - val simpleName = path.substringAfterLast('/').substringBeforeLast('.') - val pkg = path.substringBeforeLast('/', "").replace('/', '.') + if (unusedClasses.isEmpty()) return false + val simpleName = fileName.substringBeforeLast('.') val className = if (pkg.isEmpty()) simpleName else "$pkg.$simpleName" if (unusedClasses.contains(className)) return true - if (path.endsWith(".kt")) { - val text = sourceContentProvider() + if (fileName.endsWith(".kt")) { val customJvmName = jvmNameRegex.find(text)?.groupValues?.get(1) val facadeName = customJvmName ?: "${simpleName}Kt" val facadeClassName = if (pkg.isEmpty()) facadeName else "$pkg.$facadeName" diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt new file mode 100644 index 0000000000..22806dcf21 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -0,0 +1,164 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.containsAtLeast +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import java.io.File +import java.util.zip.ZipFile +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +class ShadowSourcesJarTest { + + @Test + fun extractPackageStatements() { + assertThat(extractPackage("package com.example.foo;")).isEqualTo("com.example.foo") + assertThat(extractPackage("package com.example.foo")).isEqualTo("com.example.foo") + assertThat(extractPackage(" package com.example.foo.bar ; ")) + .isEqualTo("com.example.foo.bar") + assertThat( + extractPackage( + """ + /* + * Multi-line header comment. + */ + package com.example.license; + public class License {} + """ + .trimIndent() + ) + ) + .isEqualTo("com.example.license") + assertThat( + extractPackage( + """ + @file:JvmName("MyUtils") + package com.example.annotated + fun test() {} + """ + .trimIndent() + ) + ) + .isEqualTo("com.example.annotated") + assertThat( + extractPackage( + """ + package a + package b.c + class Chained + """ + .trimIndent() + ) + ) + .isEqualTo("a.b.c") + assertThat(extractPackage("public class NoPackage {}")).isEqualTo("") + } + + @Test + fun isUnusedMatching() { + val unusedSet = + setOf( + "com.example.UnusedJava", + "com.example.UnusedKtClass", + "com.example.DefaultFacadeKt", + "com.example.CustomFacade", + ) + + assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", unusedSet)) + .isTrue() + assertThat(isUnused("UsedJava.java", "com.example", "class UsedJava {}", unusedSet)).isFalse() + + assertThat(isUnused("UnusedKtClass.kt", "com.example", "class UnusedKtClass", unusedSet)) + .isTrue() + assertThat( + isUnused( + "DefaultFacade.kt", + "com.example", + "fun topLevel() {}", + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "Utils.kt", + "com.example", + """ + @file:JvmName("CustomFacade") + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "Utils.kt", + "com.example", + """ + @file:kotlin.jvm.JvmName(name = "CustomFacade") + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "UsedUtils.kt", + "com.example", + """ + @file:JvmName("UsedFacade") + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isFalse() + + assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", emptySet())) + .isFalse() + assertThat(isUnused("Main.java", "", "class Main {}", setOf("Main"))).isTrue() + assertThat(isUnused("Main.java", "", "class Main {}", setOf("Other"))).isFalse() + } + + @Test + fun generateShadowedSourcesJarNormalizesPackageDirectory(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + val flatMismatchedFile = srcDir.resolve("Mismatched.kt") + flatMismatchedFile.writeText( + """ + package com.example.nested + class Mismatched + """ + .trimIndent() + ) + + val outputJar = tempDir.resolve("output-sources.jar") + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = listOf(SimpleRelocator("com.example", "shadow.example")), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + assertThat(outputJar.exists()).isTrue() + val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } + assertThat(entries).containsAtLeast("shadow/example/nested/Mismatched.kt") + } +} From e07d3be2479e5417f8a5ee6af05385429eaccc7f Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 18:56:02 +0800 Subject: [PATCH 17/68] Extract createDefaultLocalMavenRepository to LocalMavenRepository.kt --- .../gradle/plugins/shadow/BasePluginTest.kt | 115 +---------------- .../shadow/util/LocalMavenRepository.kt | 119 ++++++++++++++++++ 2 files changed, 121 insertions(+), 113 deletions(-) create mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 98dc1ca93f..ab3a9c1131 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -18,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.util.AppendableMavenRepository import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import com.github.jengelman.gradle.plugins.shadow.util.JvmLang +import com.github.jengelman.gradle.plugins.shadow.util.createDefaultLocalMavenRepository import java.io.Closeable import java.nio.file.Path import java.util.Properties @@ -27,7 +28,6 @@ import kotlin.io.path.appendText import kotlin.io.path.createDirectories import kotlin.io.path.createDirectory import kotlin.io.path.createFile -import kotlin.io.path.createTempDirectory import kotlin.io.path.deleteExisting import kotlin.io.path.deleteRecursively import kotlin.io.path.exists @@ -84,118 +84,7 @@ abstract class BasePluginTest { @BeforeAll fun beforeAll() { - localRepo = - AppendableMavenRepository( - root = createTempDirectory().resolve("local-maven-repo").createDirectories() - ) - .apply { - jarModule("junit", "junit", "3.8.2") { useJar(junitJar) } - val a = - jarModule("my", "a", "1.0") { - buildJar { - insert("a.properties", "a") - insert("a2.properties", "a2") - } - } - val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } - val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } - val d = - jarModule("my", "d", "1.0") { - buildJar { insert("d.properties", "d") } - // Depends on c but c does not depend on d. - addDependency(c) - } - val e = - jarModule("my", "e", "1.0") { - buildJar { insert("e.properties", "e") } - // Circular dependency with f. - addDependency("my:f:1.0") - } - val f = - jarModule("my", "f", "1.0") { - buildJar { insert("f.properties", "f") } - // Circular dependency with e. - addDependency(e) - } - val g = - jarModule("my", "g", "1.0") { - buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } - buildSourcesJar { - insert( - "g/G.java", - """ - |package g; - |public class G {} - """ - .trimMargin(), - ) - } - } - val h = - jarModule("my", "h", "1.0") { - buildJar { - insert("h/H.class", createEmptyClassBytes("h/H")) - insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) - } - buildSourcesJar { - insert( - "h/H.java", - """ - |package h; - |public class H {} - """ - .trimMargin(), - ) - insert( - "h/UnusedH.java", - """ - |package h; - |public class UnusedH {} - """ - .trimMargin(), - ) - } - } - val k = - jarModule("my", "k", "1.0") { - buildJar { - insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) - insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) - } - buildSourcesJar { - insert( - "k/Utils.kt", - """ - |@file:JvmName("CustomUtils") - |package k - |fun util() {} - """ - .trimMargin(), - ) - insert( - "k/UnusedUtils.kt", - """ - |@file:JvmName("CustomUnusedUtils") - |package k - |fun unusedUtil() {} - """ - .trimMargin(), - ) - } - } - bomModule("my", "bom", "1.0") { - addDependency(a) - addDependency(b) - addDependency(c) - addDependency(d) - addDependency(e) - addDependency(f) - addDependency(g) - addDependency(h) - addDependency(k) - } - } - localRepo.publish() + localRepo = createDefaultLocalMavenRepository(junitJar).apply { publish() } artifactAJar = path("my/a/1.0/a-1.0.jar", parent = localRepo.root) artifactBJar = path("my/b/1.0/b-1.0.jar", parent = localRepo.root) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt new file mode 100644 index 0000000000..26dfb6d0e4 --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt @@ -0,0 +1,119 @@ +package com.github.jengelman.gradle.plugins.shadow.util + +import com.github.jengelman.gradle.plugins.shadow.BasePluginTest.Companion.createEmptyClassBytes +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.createTempDirectory + +fun createDefaultLocalMavenRepository(junitJar: Path): AppendableMavenRepository { + return AppendableMavenRepository( + root = createTempDirectory().resolve("local-maven-repo").createDirectories() + ) + .apply { + jarModule("junit", "junit", "3.8.2") { useJar(junitJar) } + val a = + jarModule("my", "a", "1.0") { + buildJar { + insert("a.properties", "a") + insert("a2.properties", "a2") + } + } + val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } + val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } + val d = + jarModule("my", "d", "1.0") { + buildJar { insert("d.properties", "d") } + // Depends on c but c does not depend on d. + addDependency(c) + } + val e = + jarModule("my", "e", "1.0") { + buildJar { insert("e.properties", "e") } + // Circular dependency with f. + addDependency("my:f:1.0") + } + val f = + jarModule("my", "f", "1.0") { + buildJar { insert("f.properties", "f") } + // Circular dependency with e. + addDependency(e) + } + val g = + jarModule("my", "g", "1.0") { + buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } + buildSourcesJar { + insert( + "g/G.java", + """ + |package g; + |public class G {} + """ + .trimMargin(), + ) + } + } + val h = + jarModule("my", "h", "1.0") { + buildJar { + insert("h/H.class", createEmptyClassBytes("h/H")) + insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) + } + buildSourcesJar { + insert( + "h/H.java", + """ + |package h; + |public class H {} + """ + .trimMargin(), + ) + insert( + "h/UnusedH.java", + """ + |package h; + |public class UnusedH {} + """ + .trimMargin(), + ) + } + } + val k = + jarModule("my", "k", "1.0") { + buildJar { + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) + insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + } + buildSourcesJar { + insert( + "k/Utils.kt", + """ + |@file:JvmName("CustomUtils") + |package k + |fun util() {} + """ + .trimMargin(), + ) + insert( + "k/UnusedUtils.kt", + """ + |@file:JvmName("CustomUnusedUtils") + |package k + |fun unusedUtil() {} + """ + .trimMargin(), + ) + } + } + bomModule("my", "bom", "1.0") { + addDependency(a) + addDependency(b) + addDependency(c) + addDependency(d) + addDependency(e) + addDependency(f) + addDependency(g) + addDependency(h) + addDependency(k) + } + } +} From 23adce55337387e88813d9c8930b29467a828990 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:01:49 +0800 Subject: [PATCH 18/68] Pass zos for generateShadowedSourcesJar --- .../shadow/internal/ShadowSourcesJar.kt | 237 ++++++++---------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 35 ++- .../shadow/internal/ShadowSourcesJarTest.kt | 25 +- 3 files changed, 148 insertions(+), 149 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ae5503c278..924708df52 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,155 +4,138 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset -import org.gradle.api.tasks.bundling.ZipEntryCompression internal fun generateShadowedSourcesJar( - sourcesJarFile: File, + zos: TrackingZipOutputStream, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, unusedClasses: Set = emptySet(), - entryCompression: ZipEntryCompression, - isZip64: Boolean, - metadataCharset: String?, - preserveFileTimestamps: Boolean, + charset: Charset = Charsets.UTF_8, + preserveFileTimestamps: Boolean = true, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() - val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 - try { - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - for (srcDir in sourceSetsSourceDirs) { - if (!srcDir.exists()) continue - srcDir - .walkTopDown() - .filter { it.isFile } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } + for (srcDir in sourceSetsSourceDirs) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) } - } - - sourcesJars.forEach { jarFile -> - jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val isSource = isSourceFile(name) - if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = getInputStream(entry).readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } } } + } + } - val entries = zos.entries.map { it.name } - val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() - entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> - if (!added.add(entryName)) return@forEach + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() zos.writeEntry( - name = entryName, + name = relocatedPath, preserveLastModified = preserveFileTimestamps, - lastModified = currentTimeMillis, - unixMode = UnixMode.directory(), - ) + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } } } } - } catch (e: Exception) { - sourcesJarFile.delete() - throw e + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = preserveFileTimestamps, + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) + } } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 0299cb4da5..7732a59692 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -34,6 +34,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException +import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -777,17 +778,29 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - generateShadowedSourcesJar( - sourcesJarFile = archiveSourcesFile.get().asFile, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, - includedSourcesJars = includedSourcesJars.files, - relocators = relocators.get() + packageRelocators, - unusedClasses = unusedClasses, - entryCompression = entryCompression, - isZip64 = isZip64, - metadataCharset = metadataCharset, - preserveFileTimestamps = isPreserveFileTimestamps, - ) + val sourcesJarFile = archiveSourcesFile.get().asFile + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + generateShadowedSourcesJar( + zos = zos, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, + charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8, + preserveFileTimestamps = isPreserveFileTimestamps, + ) + } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e + } } public companion object { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt index 22806dcf21..34c5aa036f 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -145,17 +145,20 @@ class ShadowSourcesJarTest { ) val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = listOf(srcDir), - includedSourcesJars = emptyList(), - relocators = listOf(SimpleRelocator("com.example", "shadow.example")), - unusedClasses = emptySet(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = null, - preserveFileTimestamps = true, - ) + outputJar + .createZipOutputStream( + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + encoding = null, + ) + .use { zos -> + generateShadowedSourcesJar( + zos = zos, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = listOf(SimpleRelocator("com.example", "shadow.example")), + ) + } assertThat(outputJar.exists()).isTrue() val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } From 30b2c88714b390c72c3d4b6b27f0b8f84326a128 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:01:52 +0800 Subject: [PATCH 19/68] Revert "Pass zos for generateShadowedSourcesJar" This reverts commit 23adce55337387e88813d9c8930b29467a828990. --- .../shadow/internal/ShadowSourcesJar.kt | 237 ++++++++++-------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 35 +-- .../shadow/internal/ShadowSourcesJarTest.kt | 25 +- 3 files changed, 149 insertions(+), 148 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 924708df52..ae5503c278 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,138 +4,155 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset +import org.gradle.api.tasks.bundling.ZipEntryCompression internal fun generateShadowedSourcesJar( - zos: TrackingZipOutputStream, + sourcesJarFile: File, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, unusedClasses: Set = emptySet(), - charset: Charset = Charsets.UTF_8, - preserveFileTimestamps: Boolean = true, + entryCompression: ZipEntryCompression, + isZip64: Boolean, + metadataCharset: String?, + preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() + val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 - for (srcDir in sourceSetsSourceDirs) { - if (!srcDir.exists()) continue - srcDir - .walkTopDown() - .filter { it.isFile } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + for (srcDir in sourceSetsSourceDirs) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } } - } } - } - } - sourcesJars.forEach { jarFile -> - jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val isSource = isSourceFile(name) - if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } } } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = getInputStream(entry).readBytes() + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach zos.writeEntry( - name = relocatedPath, + name = entryName, preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) } } } - } - } - - val entries = zos.entries.map { it.name } - val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() - entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> - if (!added.add(entryName)) return@forEach - zos.writeEntry( - name = entryName, - preserveLastModified = preserveFileTimestamps, - lastModified = currentTimeMillis, - unixMode = UnixMode.directory(), - ) - } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 7732a59692..0299cb4da5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -34,7 +34,6 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException -import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -778,29 +777,17 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - val sourcesJarFile = archiveSourcesFile.get().asFile - try { - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - generateShadowedSourcesJar( - zos = zos, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, - includedSourcesJars = includedSourcesJars.files, - relocators = relocators.get() + packageRelocators, - unusedClasses = unusedClasses, - charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8, - preserveFileTimestamps = isPreserveFileTimestamps, - ) - } - } catch (e: Exception) { - sourcesJarFile.delete() - throw e - } + generateShadowedSourcesJar( + sourcesJarFile = archiveSourcesFile.get().asFile, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, + entryCompression = entryCompression, + isZip64 = isZip64, + metadataCharset = metadataCharset, + preserveFileTimestamps = isPreserveFileTimestamps, + ) } public companion object { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt index 34c5aa036f..22806dcf21 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -145,20 +145,17 @@ class ShadowSourcesJarTest { ) val outputJar = tempDir.resolve("output-sources.jar") - outputJar - .createZipOutputStream( - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - encoding = null, - ) - .use { zos -> - generateShadowedSourcesJar( - zos = zos, - sourceSetsSourceDirs = listOf(srcDir), - includedSourcesJars = emptyList(), - relocators = listOf(SimpleRelocator("com.example", "shadow.example")), - ) - } + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = listOf(SimpleRelocator("com.example", "shadow.example")), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) assertThat(outputJar.exists()).isTrue() val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } From d231e625c13eef2feff9ba496f54b475840470d6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:09:37 +0800 Subject: [PATCH 20/68] Filter included sources jars based on DependencyFilter include and exclude rules --- .../gradle/plugins/shadow/FilteringTest.kt | 31 +++++++++++++++++++ .../internal/DefaultDependencyFilter.kt | 14 +++++++++ 2 files changed, 45 insertions(+) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index e378ef5524..a5f96da1dc 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -2,6 +2,8 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader +import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import kotlin.io.path.appendText @@ -228,6 +230,35 @@ class FilteringTest : BasePluginTest() { } } + @Test + fun excludeDependencyFromSourcesJar() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + | implementation 'my:h:1.0' + |} + |$shadowJarTask { + | dependencies { + | exclude(dependency('my:h:1.0')) + | } + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsAtLeast("g/G.class") + containsNone("h/H.class", "h/UnusedH.class") + } + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("g/G.java") + containsNone("h/H.java", "h/UnusedH.java") + } + } + private fun commonAssertions() { assertThat(outputShadowedJar).useAll { containsOnly("c.properties", *entriesInAB, *manifestEntries) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 475788ff2d..2b85cca501 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -34,11 +34,25 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) } private fun resolveSourcesJars(configuration: Configuration): FileCollection { + val includes = mutableSetOf() + val excludes = mutableSetOf() + resolve( + dependencies = configuration.resolvedConfiguration.firstLevelModuleDependencies, + includedDependencies = includes, + excludedDependencies = excludes, + ) val componentIds = configuration.incoming.resolutionResult.allDependencies .filterIsInstance() .map { it.selected.id } .filterIsInstance() + .filter { id -> + includes.any { + it.moduleGroup == id.group && + it.moduleName == id.module && + it.moduleVersion == id.version + } + } .toSet() val files = project.dependencies From 4d0ca8ac22ad5c8fbfe1f84c97219bc855b7e457 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:17:29 +0800 Subject: [PATCH 21/68] Document Shadowed Sources JAR features, publication and configuration --- docs/configuration/minimizing/README.md | 10 ++ docs/getting-started/README.md | 4 +- docs/kotlin-plugins/README.md | 4 + docs/publishing/README.md | 151 ++++++++++++++++++ .../gradle/plugins/shadow/PublishingTest.kt | 93 ++++++++++- 5 files changed, 257 insertions(+), 5 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 0941360749..5a1f3acadd 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -107,6 +107,16 @@ rules published in dependency JARs, for example under `META-INF/proguard`. > Alternatively, if you use [R8 Repackaging][r8-repackaging] (e.g. `-repackageclasses`), R8 applies embedded rules > natively without needing rule rewriting. +> [!NOTE] +> **Shadowed Sources JAR and R8** +> +> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 (`minimize { r8 { ... } }`), +> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR will +> contain all relocated source files without responding to R8 shrinking results. +> +> If you need unused source files to be filtered out of the shadowed sources JAR, use the default dependency analyzer +> minimization (`minimize()`) instead. + === ":material-language-kotlin: build.gradle.kts" ```kotlin diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index f75096494a..0734fdba4b 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -137,8 +137,10 @@ in their build logic), Shadow will automatically configure the following behavio - `META-INF/*.RSA` - `META-INF/versions/**/module-info.class` - `module-info.class` +- Configures the [`ShadowJar`][ShadowJar] task to generate a companion **Shadowed Sources JAR** containing both + project sources and shadowed dependency sources with relocated packages. - Creates and registers the `shadow` component in the project (used for integrating with - [`maven-publish`][maven-publish]). + [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. ## ShadowJar Command Line options diff --git a/docs/kotlin-plugins/README.md b/docs/kotlin-plugins/README.md index e97c39ceb3..c1bac80a35 100644 --- a/docs/kotlin-plugins/README.md +++ b/docs/kotlin-plugins/README.md @@ -139,6 +139,9 @@ automatically configure additional tasks for bundling the shadowed JAR for its ` } ``` +For details on publishing shadowed artifacts and sources JAR in KMP projects, see +[Publishing with Kotlin Multiplatform (KMP)][publishing-with-kmp]. + ## Kotlin Module Metadata Remapping Kotlin module metadata (`.kotlin_module`) files contain information about package parts and facades. When relocating @@ -169,4 +172,5 @@ To explicitly apply this remapping (recommended for future compatibility), add [KotlinModuleMetadataTransformer]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-kotlin-module-metadata-transformer/index.html [dependency-on-the-standard-library]: https://kotlinlang.org/docs/gradle-configure-project.html#dependency-on-the-standard-library [publishing-libraries]: ../publishing/README.md +[publishing-with-kmp]: ../publishing/README.md#publishing-with-kotlin-multiplatform-kmp [running-applications]: ../application-plugin/README.md diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 1a5dafbbef..bcef984282 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -515,6 +515,155 @@ customizable properties listed in [Configuring Output Name][configuring-output-n We modified `archiveClassifier`, `archiveExtension` and `archiveBaseName` in this example, the published artifact will be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. +## Shadowed Sources JAR + +When publishing a shadowed library, consumers and IDEs need a corresponding sources JAR to navigate source code and +inspect implementations. A standard sources JAR only contains your project's original un-relocated sources, which +causes broken navigation when consumers reference relocated packages. + +Shadow automatically generates a **Shadowed Sources JAR** containing: + +- Source files from your project's source sets (`Java`, `Kotlin`, `Groovy`, `Scala`). +- Source files resolved and merged from all bundled dependencies' `-sources.jar` archives. +- Relocated package declarations, imports, and symbol references that match your [`relocate`][ShadowJar.relocate] rules. +- Normalized package directory layout matching the declared `package` in each source file. +- Automatic filtering: dependencies excluded in `dependencies { exclude(...) }` or unused classes removed via + `minimize()` are automatically excluded from the shadowed sources JAR as well. + +### Publishing with `withSourcesJar()` + +When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin automatically registers the +`shadowSourcesElements` variant and publishes the shadowed sources JAR alongside the shadowed binary JAR: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + java + `maven-publish` + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + + publishing { + publications { + create("shadow") { + from(components["shadow"]) + } + } + repositories { + maven("https://repo.myorg.com") + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' + } + + java { + withSourcesJar() + } + + publishing { + publications { + shadow(MavenPublication) { + from components.shadow + } + } + repositories { + maven { url = 'https://repo.myorg.com' } + } + } + ``` + +The published Maven publication will include both `--all.jar` and +`--all-sources.jar`. + +### Customizing the Sources Archive File + +The shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], +which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.shadowJar { + archiveSourcesFile = layout.buildDirectory.file("custom-libs/my-sources.jar") + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveSourcesFile = layout.buildDirectory.file('custom-libs/my-sources.jar') + } + ``` + +### Publishing with Kotlin Multiplatform (KMP) + +In Kotlin Multiplatform (KMP) projects, publications are managed by the Kotlin Gradle Plugin (KGP) per target (such as +the `jvm` publication). You can attach the shadowed sources JAR artifact to the `jvm` Maven publication: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.gradleup.shadow") + `maven-publish` + } + + kotlin { + jvm() + } + + publishing { + publications { + named("jvm") { + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + } + } + repositories { + maven("https://repo.myorg.com") + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'com.gradleup.shadow' + id 'maven-publish' + } + + kotlin { + jvm() + } + + publishing { + publications { + named('jvm', MavenPublication) { + artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + } + } + repositories { + maven { url = 'https://repo.myorg.com' } + } + } + ``` + ## Generating Javadoc or Dokka from Shadowed Sources When creating fat / shadowed libraries, you may want to generate a complete Javadoc or Dokka JAR covering both your @@ -592,6 +741,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source [Jar]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html +[ShadowJar.archiveSourcesFile]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/archive-sources-file.html +[ShadowJar.relocate]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/relocate.html [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 6018cbb55a..f929fb4e62 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -18,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.util.GradleModuleMetadata +import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import com.github.jengelman.gradle.plugins.shadow.util.coordinate import com.github.jengelman.gradle.plugins.shadow.util.prependText import com.squareup.moshi.JsonAdapter @@ -30,6 +31,7 @@ import kotlin.io.path.inputStream import kotlin.io.path.listDirectoryEntries import kotlin.io.path.name import kotlin.io.path.readText +import kotlin.io.path.writeText import org.apache.maven.model.Dependency import org.apache.maven.model.Model import org.apache.maven.model.io.xpp3.MavenXpp3Reader @@ -684,6 +686,87 @@ class PublishingTest : BasePluginTest() { } } + @Test + fun publishKmpWithShadowedSources() { + path("gradle.properties").writeText("kotlin.stdlib.default.dependency=false") + projectScript.writeText( + """ + |plugins { + | id 'org.jetbrains.kotlin.multiplatform' + | id 'com.gradleup.shadow' + | id 'maven-publish' + |} + |group = 'my' + |version = '1.0' + |kotlin { + | jvm() + | sourceSets { + | commonMain { + | dependencies { + | implementation 'my:g:1.0' + | compileOnly 'org.jetbrains.kotlin:kotlin-stdlib' + | } + | } + | jvmMain { + | dependencies { + | implementation 'my:h:1.0' + | } + | } + | } + |} + |$shadowJarTask { + | archiveClassifier = '' + |} + |publishing { + | repositories { + | maven { url = '${remoteRepoPath.toUri()}' } + | } + | publications { + | shadow(MavenPublication) { + | artifactId = 'my-all' + | artifact($shadowJarTask) + | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) + | } + | } + |} + """ + .trimMargin() + ) + writeClass(sourceSet = "commonMain", jvmLang = JvmLang.Kotlin, className = "CommonMain") + writeClass(sourceSet = "jvmMain", jvmLang = JvmLang.Kotlin, className = "JvmMain") + + publish() + + val artifactRoot = "my/my-all/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "my-all-1.0.jar", + "my-all-1.0-sources.jar", + ) + + assertThat(repoJarPath("$artifactRoot/my-all-1.0.jar")).useAll { + containsAtLeast( + "my/CommonMain.class", + "my/JvmMain.class", + "g/G.class", + "h/H.class", + *manifestEntries, + ) + } + + assertThat(repoJarPath("$artifactRoot/my-all-1.0-sources.jar")).useAll { + containsAtLeast( + "my/CommonMain.kt", + "my/JvmMain.kt", + "g/G.java", + "h/H.java", + ) + } + + assertPomCommon(repoPath("$artifactRoot/my-all-1.0.pom"), emptyArray()) + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".module") }).isEmpty() + } + private fun repoPath(relative: String): Path { return remoteRepoPath.resolve(relative).also { check(it.exists()) { "Path not found: $it" } } } @@ -744,10 +827,12 @@ class PublishingTest : BasePluginTest() { private fun assertPomCommon(pomPath: Path, coordinates: Array = arrayOf("my:b:1.0")) { assertThat(pomReader.read(pomPath)).all { transform { it.dependencies.map(Dependency::coordinate) }.containsOnly(*coordinates) - // All scopes should be runtime. - transform { it.dependencies.map(Dependency::getScope).distinct() } - .single() - .isEqualTo("runtime") + if (coordinates.isNotEmpty()) { + // All scopes should be runtime. + transform { it.dependencies.map(Dependency::getScope).distinct() } + .single() + .isEqualTo("runtime") + } } } From 51d5feb5e5003c463ba557047d477902033bad59 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:51:59 +0800 Subject: [PATCH 22/68] Support bundling sources JAR from local subproject dependencies --- .../gradle/plugins/shadow/BasePluginTest.kt | 6 +++ .../gradle/plugins/shadow/FilteringTest.kt | 11 +++++ .../gradle/plugins/shadow/JavaPluginsTest.kt | 8 ++++ .../internal/DefaultDependencyFilter.kt | 41 +++++++++++++++++-- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index ab3a9c1131..71185f1655 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -82,6 +82,9 @@ abstract class BasePluginTest { val outputServerShadowedJar: JarPath get() = jarPath("server/build/libs/server-1.0-all.jar") + val outputServerShadowedSourcesJar: JarPath + get() = jarPath("server/build/libs/server-1.0-all-sources.jar") + @BeforeAll fun beforeAll() { localRepo = createDefaultLocalMavenRepository(junitJar).apply { publish() } @@ -260,6 +263,9 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} + |java { + | withSourcesJar() + |} |dependencies { | implementation 'junit:junit:3.8.2' |} diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index a5f96da1dc..1a0cd3c048 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -184,6 +184,14 @@ class FilteringTest : BasePluginTest() { loadClass("server.Server") } } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly( + "client/", + "server/", + "client/Client.java", + "server/Server.java", + ) + } } @Test @@ -273,5 +281,8 @@ class FilteringTest : BasePluginTest() { loadClass("junit.framework.Test") } } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly("server/", "server/Server.java") + } } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 067cf37c46..2fd03ebc36 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -144,6 +144,14 @@ class JavaPluginsTest : BasePluginTest() { *manifestEntries, ) } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly( + "client/", + "server/", + "client/Client.java", + "server/Server.java", + ) + } } @Test diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 2b85cca501..12edefe997 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -5,8 +5,11 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedArtifactResult import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType import org.gradle.api.file.FileCollection import org.gradle.jvm.JvmLibrary import org.gradle.language.base.artifact.SourcesArtifact @@ -45,6 +48,10 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) configuration.incoming.resolutionResult.allDependencies .filterIsInstance() .map { it.selected.id } + .toSet() + + val externalComponentIds = + componentIds .filterIsInstance() .filter { id -> includes.any { @@ -54,16 +61,44 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) } } .toSet() - val files = + + val externalSourcesFiles = project.dependencies .createArtifactResolutionQuery() - .forComponents(componentIds) + .forComponents(externalComponentIds) .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java) .execute() .resolvedComponents .flatMap { it.getArtifacts(SourcesArtifact::class.java) } .filterIsInstance() .map { it.file } - return project.files(files) + + val includedProjectNames = includes.map { it.moduleName }.toSet() + val projectSourcesFiles = + try { + configuration.incoming + .artifactView { view -> + view.withVariantReselection() + view.attributes { attrs -> + attrs.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attrs.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + project.objects.named(DocsType::class.java, DocsType.SOURCES), + ) + } + view.componentFilter { id -> + id is ProjectComponentIdentifier && id.projectName in includedProjectNames + } + view.lenient(true) + } + .files + } catch (_: Exception) { + project.files() + } + + return project.files(externalSourcesFiles) + projectSourcesFiles } } From 0100ab212984ec1b14fc8eec3aefea68b2e2c546 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:56:16 +0800 Subject: [PATCH 23/68] Use Worker API --- api/shadow.api | 3 +- .../shadow/internal/ShadowSourcesJar.kt | 36 ++++++++++ .../plugins/shadow/relocation/Relocator.kt | 3 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 72 +++++++++++-------- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index b7118a4f6b..9465d5f408 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -128,7 +128,7 @@ public final class com/github/jengelman/gradle/plugins/shadow/relocation/Relocat public static final fun relocatePath (Ljava/lang/Iterable;Ljava/lang/String;)Ljava/lang/String; } -public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator { +public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator : java/io/Serializable { public abstract fun applyToSourceContent (Ljava/lang/String;)Ljava/lang/String; public abstract fun canRelocateClass (Ljava/lang/String;)Z public abstract fun canRelocatePath (Ljava/lang/String;)Z @@ -284,6 +284,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getToMinimize ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getTransformers ()Lorg/gradle/api/provider/SetProperty; + protected abstract fun getWorkerExecutor ()Lorg/gradle/workers/WorkerExecutor; public fun mergeGroovyExtensionModules ()V public final fun mergeServiceFiles ()V public fun mergeServiceFiles (Ljava/lang/String;)V diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ae5503c278..ed8e12aed2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,7 +4,43 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters + +internal abstract class GenerateShadowedSourcesJarWorkAction : + WorkAction { + interface Params : WorkParameters { + val sourcesJarFile: RegularFileProperty + val sourceSetsSourceDirs: ConfigurableFileCollection + val includedSourcesJars: ConfigurableFileCollection + val relocators: SetProperty + val unusedClasses: SetProperty + val entryCompression: Property + val zip64: Property + val metadataCharset: Property + val preserveFileTimestamps: Property + } + + override fun execute() { + val params = parameters + generateShadowedSourcesJar( + sourcesJarFile = params.sourcesJarFile.get().asFile, + sourceSetsSourceDirs = params.sourceSetsSourceDirs.files, + includedSourcesJars = params.includedSourcesJars.files, + relocators = params.relocators.get(), + unusedClasses = params.unusedClasses.get(), + entryCompression = params.entryCompression.get(), + isZip64 = params.zip64.get(), + metadataCharset = params.metadataCharset.orNull, + preserveFileTimestamps = params.preserveFileTimestamps.get(), + ) + } +} internal fun generateShadowedSourcesJar( sourcesJarFile: File, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt index 91d5953b24..416fb03d7b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer +import java.io.Serializable import org.gradle.api.tasks.Input /** @@ -12,7 +13,7 @@ import org.gradle.api.tasks.Input * @author John Engelman */ @ShadowDsl -public interface Relocator { +public interface Relocator : Serializable { public fun canRelocatePath(path: String): Boolean public fun relocatePath(context: RelocatePathContext): String diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 0299cb4da5..bf5318baac 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,11 +7,11 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec +import com.github.jengelman.gradle.plugins.shadow.internal.GenerateShadowedSourcesJarWorkAction import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses -import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService @@ -73,6 +73,7 @@ import org.gradle.api.tasks.options.Option import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.ExecOperations +import org.gradle.workers.WorkerExecutor @ShadowDsl @CacheableTask @@ -85,6 +86,29 @@ public abstract class ShadowJar : Jar() { project.configurations.findByName(ShadowBasePlugin.CONFIGURATION_NAME) ?: project.files() } + @Transient private var _unusedClasses: Set? = null + + private val unusedClasses: Set + get() = + _unusedClasses + ?: (if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + }) + .also { _unusedClasses = it } + + @Transient private var _actualRelocators: Set? = null + + private val actualRelocators: Set + get() = + _actualRelocators ?: (relocators.get() + packageRelocators).also { _actualRelocators = it } + init { group = LifecycleBasePlugin.BUILD_GROUP description = "Create a combined JAR of project and runtime dependencies" @@ -368,6 +392,8 @@ public abstract class ShadowJar : Jar() { @get:Inject protected abstract val archiveOperations: ArchiveOperations + @get:Inject protected abstract val workerExecutor: WorkerExecutor + /** Enable minimization and execute the [action] with the [MinimizeSpec] for minimize. */ @JvmOverloads public open fun minimize(action: Action = Action {}) { @@ -561,25 +587,14 @@ public abstract class ShadowJar : Jar() { override fun copy() { addIncludedDependencies() injectManifestAttributes() + generateShadowedSourcesJar() super.copy() + workerExecutor.await() runR8Minimization() - generateShadowedSourcesJar() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { - val unusedClasses = - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } - .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -613,7 +628,7 @@ public abstract class ShadowJar : Jar() { zipFile = zipFile, zipOutStream = zipOutStream, transformers = actualTransformers, - relocators = relocators.get() + packageRelocators, + relocators = actualRelocators, unusedClasses = unusedClasses, isPreserveFileTimestamps = isPreserveFileTimestamps, failOnDuplicateEntries = failOnDuplicateEntries.get(), @@ -769,25 +784,24 @@ public abstract class ShadowJar : Jar() { javaLauncher = javaLauncher, sourceSetsClassesDirs = sourceSetsClassesDirs, keptDependencyFiles = includedDependencies - toMinimize, - relocators = relocators.get() + packageRelocators, + relocators = actualRelocators, ) } - private var unusedClasses: Set = emptySet() - private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - generateShadowedSourcesJar( - sourcesJarFile = archiveSourcesFile.get().asFile, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, - includedSourcesJars = includedSourcesJars.files, - relocators = relocators.get() + packageRelocators, - unusedClasses = unusedClasses, - entryCompression = entryCompression, - isZip64 = isZip64, - metadataCharset = metadataCharset, - preserveFileTimestamps = isPreserveFileTimestamps, - ) + workerExecutor.noIsolation().submit(GenerateShadowedSourcesJarWorkAction::class.java) { params + -> + params.sourcesJarFile.set(archiveSourcesFile) + params.sourceSetsSourceDirs.from(sourceSetsSourceDirs) + params.includedSourcesJars.from(includedSourcesJars) + params.relocators.set(actualRelocators) + params.unusedClasses.set(unusedClasses) + params.entryCompression.set(entryCompression) + params.zip64.set(isZip64) + params.metadataCharset.set(metadataCharset) + params.preserveFileTimestamps.set(isPreserveFileTimestamps) + } } public companion object { From 09c2cea013cec3a5137ad522276a27488417cdda Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 20:52:33 +0800 Subject: [PATCH 24/68] Revert "Use Worker API" This reverts commit 0100ab212984ec1b14fc8eec3aefea68b2e2c546. ### 1. Test Versions | Commit Hash | Local Version Tag | Description | | :--- | :--- | :--- | | [`51d5feb5`](https://github.com/GradleUp/shadow/commit/51d5feb5e5003c463ba557047d477902033bad59) | `9.0.3-51d5feb5` | **Before Worker API** (single-threaded serial execution) | | [`0100ab21`](https://github.com/GradleUp/shadow/commit/0100ab212984ec1b14fc8eec3aefea68b2e2c546) | `9.0.3-0100ab21` | **After Worker API** (asynchronous parallel execution via Gradle Worker API) | --- ### 2. Detailed 10-Iteration Benchmark Results (Unit: ms) | Iteration | Before Worker API (`51d5feb5`) | After Worker API (`0100ab21`) | | :---: | :---: | :---: | | **Warm-up 1** | 19,131.87 | 7,158.15 | | **Warm-up 2** | 937.74 | 927.13 | | **Warm-up 3** | 867.42 | 892.92 | | **Build 1** | 949.85 | 887.27 | | **Build 2** | 855.78 | 895.07 | | **Build 3** | 870.54 | 913.32 | | **Build 4** | 842.07 | 962.29 | | **Build 5** | 834.61 | 902.32 | | **Build 6** | 895.61 | 863.34 | | **Build 7** | 842.98 | 882.96 | | **Build 8** | 836.82 | 862.21 | | **Build 9** | 843.10 | 888.14 | | **Build 10** | 874.22 | 867.52 | --- ### 3. Summary Statistics | Metric | Before Worker API (`51d5feb5`) | After Worker API (`0100ab21`) | Difference | | :--- | :---: | :---: | :--- | | **Mean** | **864.56 ms** | **892.44 ms** | +27.88 ms (+3.2%) | | **Median** | **849.44 ms** | **891.17 ms** | +41.73 ms (+4.9%) | | **Min** | **834.61 ms** | **862.21 ms** | +27.60 ms | | **Max** | **949.85 ms** | **962.29 ms** | +12.44 ms | --- api/shadow.api | 3 +- .../shadow/internal/ShadowSourcesJar.kt | 36 ---------- .../plugins/shadow/relocation/Relocator.kt | 3 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 72 ++++++++----------- 4 files changed, 31 insertions(+), 83 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 9465d5f408..b7118a4f6b 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -128,7 +128,7 @@ public final class com/github/jengelman/gradle/plugins/shadow/relocation/Relocat public static final fun relocatePath (Ljava/lang/Iterable;Ljava/lang/String;)Ljava/lang/String; } -public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator : java/io/Serializable { +public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator { public abstract fun applyToSourceContent (Ljava/lang/String;)Ljava/lang/String; public abstract fun canRelocateClass (Ljava/lang/String;)Z public abstract fun canRelocatePath (Ljava/lang/String;)Z @@ -284,7 +284,6 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getToMinimize ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getTransformers ()Lorg/gradle/api/provider/SetProperty; - protected abstract fun getWorkerExecutor ()Lorg/gradle/workers/WorkerExecutor; public fun mergeGroovyExtensionModules ()V public final fun mergeServiceFiles ()V public fun mergeServiceFiles (Ljava/lang/String;)V diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ed8e12aed2..ae5503c278 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,43 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.bundling.ZipEntryCompression -import org.gradle.workers.WorkAction -import org.gradle.workers.WorkParameters - -internal abstract class GenerateShadowedSourcesJarWorkAction : - WorkAction { - interface Params : WorkParameters { - val sourcesJarFile: RegularFileProperty - val sourceSetsSourceDirs: ConfigurableFileCollection - val includedSourcesJars: ConfigurableFileCollection - val relocators: SetProperty - val unusedClasses: SetProperty - val entryCompression: Property - val zip64: Property - val metadataCharset: Property - val preserveFileTimestamps: Property - } - - override fun execute() { - val params = parameters - generateShadowedSourcesJar( - sourcesJarFile = params.sourcesJarFile.get().asFile, - sourceSetsSourceDirs = params.sourceSetsSourceDirs.files, - includedSourcesJars = params.includedSourcesJars.files, - relocators = params.relocators.get(), - unusedClasses = params.unusedClasses.get(), - entryCompression = params.entryCompression.get(), - isZip64 = params.zip64.get(), - metadataCharset = params.metadataCharset.orNull, - preserveFileTimestamps = params.preserveFileTimestamps.get(), - ) - } -} internal fun generateShadowedSourcesJar( sourcesJarFile: File, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt index 416fb03d7b..91d5953b24 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt @@ -2,7 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer -import java.io.Serializable import org.gradle.api.tasks.Input /** @@ -13,7 +12,7 @@ import org.gradle.api.tasks.Input * @author John Engelman */ @ShadowDsl -public interface Relocator : Serializable { +public interface Relocator { public fun canRelocatePath(path: String): Boolean public fun relocatePath(context: RelocatePathContext): String diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index bf5318baac..0299cb4da5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,11 +7,11 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec -import com.github.jengelman.gradle.plugins.shadow.internal.GenerateShadowedSourcesJarWorkAction import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses +import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService @@ -73,7 +73,6 @@ import org.gradle.api.tasks.options.Option import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.ExecOperations -import org.gradle.workers.WorkerExecutor @ShadowDsl @CacheableTask @@ -86,29 +85,6 @@ public abstract class ShadowJar : Jar() { project.configurations.findByName(ShadowBasePlugin.CONFIGURATION_NAME) ?: project.files() } - @Transient private var _unusedClasses: Set? = null - - private val unusedClasses: Set - get() = - _unusedClasses - ?: (if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - }) - .also { _unusedClasses = it } - - @Transient private var _actualRelocators: Set? = null - - private val actualRelocators: Set - get() = - _actualRelocators ?: (relocators.get() + packageRelocators).also { _actualRelocators = it } - init { group = LifecycleBasePlugin.BUILD_GROUP description = "Create a combined JAR of project and runtime dependencies" @@ -392,8 +368,6 @@ public abstract class ShadowJar : Jar() { @get:Inject protected abstract val archiveOperations: ArchiveOperations - @get:Inject protected abstract val workerExecutor: WorkerExecutor - /** Enable minimization and execute the [action] with the [MinimizeSpec] for minimize. */ @JvmOverloads public open fun minimize(action: Action = Action {}) { @@ -587,14 +561,25 @@ public abstract class ShadowJar : Jar() { override fun copy() { addIncludedDependencies() injectManifestAttributes() - generateShadowedSourcesJar() super.copy() - workerExecutor.await() runR8Minimization() + generateShadowedSourcesJar() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { + val unusedClasses = + if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -628,7 +613,7 @@ public abstract class ShadowJar : Jar() { zipFile = zipFile, zipOutStream = zipOutStream, transformers = actualTransformers, - relocators = actualRelocators, + relocators = relocators.get() + packageRelocators, unusedClasses = unusedClasses, isPreserveFileTimestamps = isPreserveFileTimestamps, failOnDuplicateEntries = failOnDuplicateEntries.get(), @@ -784,24 +769,25 @@ public abstract class ShadowJar : Jar() { javaLauncher = javaLauncher, sourceSetsClassesDirs = sourceSetsClassesDirs, keptDependencyFiles = includedDependencies - toMinimize, - relocators = actualRelocators, + relocators = relocators.get() + packageRelocators, ) } + private var unusedClasses: Set = emptySet() + private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - workerExecutor.noIsolation().submit(GenerateShadowedSourcesJarWorkAction::class.java) { params - -> - params.sourcesJarFile.set(archiveSourcesFile) - params.sourceSetsSourceDirs.from(sourceSetsSourceDirs) - params.includedSourcesJars.from(includedSourcesJars) - params.relocators.set(actualRelocators) - params.unusedClasses.set(unusedClasses) - params.entryCompression.set(entryCompression) - params.zip64.set(isZip64) - params.metadataCharset.set(metadataCharset) - params.preserveFileTimestamps.set(isPreserveFileTimestamps) - } + generateShadowedSourcesJar( + sourcesJarFile = archiveSourcesFile.get().asFile, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, + entryCompression = entryCompression, + isZip64 = isZip64, + metadataCharset = metadataCharset, + preserveFileTimestamps = isPreserveFileTimestamps, + ) } public companion object { From 0ca34ccf93f6d506bc088311675f3ae84ff51190 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 21:05:11 +0800 Subject: [PATCH 25/68] Include META-INF/MANIFEST.MF in shadowed sources JAR --- .../jengelman/gradle/plugins/shadow/FilteringTest.kt | 3 ++- .../gradle/plugins/shadow/JavaPluginsTest.kt | 3 ++- .../jengelman/gradle/plugins/shadow/RelocationTest.kt | 6 +++--- .../plugins/shadow/internal/ShadowSourcesJar.kt | 11 +++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index 1a0cd3c048..f05d4d930c 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -190,6 +190,7 @@ class FilteringTest : BasePluginTest() { "server/", "client/Client.java", "server/Server.java", + *manifestEntries, ) } } @@ -282,7 +283,7 @@ class FilteringTest : BasePluginTest() { } } assertThat(outputServerShadowedSourcesJar).useAll { - containsOnly("server/", "server/Server.java") + containsOnly("server/", "server/Server.java", *manifestEntries) } } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 2fd03ebc36..681d41c6d0 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -150,6 +150,7 @@ class JavaPluginsTest : BasePluginTest() { "server/", "client/Client.java", "server/Server.java", + *manifestEntries, ) } } @@ -1360,7 +1361,7 @@ class JavaPluginsTest : BasePluginTest() { |} |tasks.named('javadoc', Javadoc) { | classpath = files($shadowJarTask.flatMap { it.archiveFile }) - | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }) + | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }).matching { include('**/*.java') } |} """ .trimMargin() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 7a4e5f9983..d7fbfdaa6e 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -2,7 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains -import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo @@ -16,7 +15,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain -import com.github.jengelman.gradle.plugins.shadow.testkit.toEntries import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -790,6 +788,7 @@ class RelocationTest : BasePluginTest() { "shadow/", "shadow/g/", "shadow/g/G.java", + *manifestEntries, ) getContent("my/Main.java") .isEqualTo( @@ -831,6 +830,7 @@ class RelocationTest : BasePluginTest() { containsOnly( "my/", "my/Main.java", + *manifestEntries, ) } } @@ -848,7 +848,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(outputShadowedSourcesJar).useAll { toEntries().isEmpty() } + assertThat(outputShadowedSourcesJar).useAll { containsOnly(*manifestEntries) } } private companion object { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ae5503c278..053f33b850 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -31,6 +31,17 @@ internal fun generateShadowedSourcesJar( encoding = metadataCharset, ) .use { zos -> + val manifestEntry = "META-INF/MANIFEST.MF" + visitedFiles.add(manifestEntry) + zos.writeEntry( + name = manifestEntry, + preserveLastModified = preserveFileTimestamps, + lastModified = if (preserveFileTimestamps) System.currentTimeMillis() else -1, + unixMode = UnixMode.file(), + ) { + write("Manifest-Version: 1.0\n\n".toByteArray(charset)) + } + for (srcDir in sourceSetsSourceDirs) { if (!srcDir.exists()) continue srcDir diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 0299cb4da5..435cba0ce9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -562,8 +562,8 @@ public abstract class ShadowJar : Jar() { addIncludedDependencies() injectManifestAttributes() super.copy() - runR8Minimization() generateShadowedSourcesJar() + runR8Minimization() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. From 03c024f8b27d712cff2952a02a35a49385ae70b4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 21:11:58 +0800 Subject: [PATCH 26/68] Fix KMP publications configuration in publishing docs --- docs/publishing/README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index bcef984282..3ccd026343 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -629,8 +629,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publishing { publications { - named("jvm") { - artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + withType().configureEach { + if (name == "jvm") { + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + } } } repositories { @@ -654,8 +656,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publishing { publications { - named('jvm', MavenPublication) { - artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + withType(MavenPublication).configureEach { + if (name == 'jvm') { + artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + } } } repositories { From 95650a719e24ed92fc496ce4638b6c9112bb6208 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:17:32 +0800 Subject: [PATCH 27/68] Explicitly configure sources classifier for publications --- docs/publishing/README.md | 8 ++++++-- .../jengelman/gradle/plugins/shadow/PublishingTest.kt | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 3ccd026343..9022fe33e5 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -631,7 +631,9 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publications { withType().configureEach { if (name == "jvm") { - artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) { + classifier = "sources" + } } } } @@ -658,7 +660,9 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publications { withType(MavenPublication).configureEach { if (name == 'jvm') { - artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + artifact(tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile }) { + classifier = 'sources' + } } } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index f929fb4e62..5df9072aaf 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -725,7 +725,9 @@ class PublishingTest : BasePluginTest() { | shadow(MavenPublication) { | artifactId = 'my-all' | artifact($shadowJarTask) - | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) + | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) { + | classifier = 'sources' + | } | } | } |} From cd245cb7f4ee3978592a165a158e660dfe4cb8a2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:26:35 +0800 Subject: [PATCH 28/68] Cache unusedClasses and packageRelocators by lazy --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 435cba0ce9..a0d21d40b0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -568,18 +568,6 @@ public abstract class ShadowJar : Jar() { @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { - val unusedClasses = - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } - .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -636,29 +624,41 @@ public abstract class ShadowJar : Jar() { private val isR8Enabled: Boolean get() = _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.R8 - private val packageRelocators: List - get() { - if (enableAutoRelocation.get()) { - logger.info( - "Adding auto relocation packages in the dependencies with prefix '{}'.", - relocationPrefix.get(), - ) - } else { - logger.info("Skipping package relocators as auto relocation is disabled.") - return emptyList() - } - val prefix = relocationPrefix.get() - return includedDependencies.flatMap { file -> - file.useZip { - entries() - .toList() - .filter { it.name.endsWith(".class") && it.name != "module-info.class" } - .map { it.name.substringBeforeLast('/').replace('/', '.') } - .toSet() - .map { SimpleRelocator(it, "$prefix.$it") } - } + private val unusedClasses by lazy { + if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + } + + private val packageRelocators by lazy { + if (enableAutoRelocation.get()) { + logger.info( + "Adding auto relocation packages in the dependencies with prefix '{}'.", + relocationPrefix.get(), + ) + } else { + logger.info("Skipping package relocators as auto relocation is disabled.") + return@lazy emptyList() + } + val prefix = relocationPrefix.get() + return@lazy includedDependencies.flatMap { file -> + file.useZip { + entries() + .toList() + .filter { it.name.endsWith(".class") && it.name != "module-info.class" } + .map { it.name.substringBeforeLast('/').replace('/', '.') } + .toSet() + .map { SimpleRelocator(it, "$prefix.$it") } } } + } private fun addIncludedDependencies() { val isAar: File.() -> Boolean = { @@ -773,8 +773,6 @@ public abstract class ShadowJar : Jar() { ) } - private var unusedClasses: Set = emptySet() - private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( From 0350f8f7f24a99066c0c9e5ae9f5fdb698e23a94 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:34:10 +0800 Subject: [PATCH 29/68] Revert "Cache unusedClasses and packageRelocators by lazy" This reverts commit cd245cb7f4ee3978592a165a158e660dfe4cb8a2. --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index a0d21d40b0..435cba0ce9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -568,6 +568,18 @@ public abstract class ShadowJar : Jar() { @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { + val unusedClasses = + if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -624,41 +636,29 @@ public abstract class ShadowJar : Jar() { private val isR8Enabled: Boolean get() = _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.R8 - private val unusedClasses by lazy { - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } - } - - private val packageRelocators by lazy { - if (enableAutoRelocation.get()) { - logger.info( - "Adding auto relocation packages in the dependencies with prefix '{}'.", - relocationPrefix.get(), - ) - } else { - logger.info("Skipping package relocators as auto relocation is disabled.") - return@lazy emptyList() - } - val prefix = relocationPrefix.get() - return@lazy includedDependencies.flatMap { file -> - file.useZip { - entries() - .toList() - .filter { it.name.endsWith(".class") && it.name != "module-info.class" } - .map { it.name.substringBeforeLast('/').replace('/', '.') } - .toSet() - .map { SimpleRelocator(it, "$prefix.$it") } + private val packageRelocators: List + get() { + if (enableAutoRelocation.get()) { + logger.info( + "Adding auto relocation packages in the dependencies with prefix '{}'.", + relocationPrefix.get(), + ) + } else { + logger.info("Skipping package relocators as auto relocation is disabled.") + return emptyList() + } + val prefix = relocationPrefix.get() + return includedDependencies.flatMap { file -> + file.useZip { + entries() + .toList() + .filter { it.name.endsWith(".class") && it.name != "module-info.class" } + .map { it.name.substringBeforeLast('/').replace('/', '.') } + .toSet() + .map { SimpleRelocator(it, "$prefix.$it") } + } } } - } private fun addIncludedDependencies() { val isAar: File.() -> Boolean = { @@ -773,6 +773,8 @@ public abstract class ShadowJar : Jar() { ) } + private var unusedClasses: Set = emptySet() + private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( From 1b26f8baadf20699986db285e8db4a924b0fd815 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:44:56 +0800 Subject: [PATCH 30/68] Remove currentTimeMillis for lastModified --- .../gradle/plugins/shadow/internal/ShadowSourcesJar.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 053f33b850..d79d66e083 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -36,7 +36,6 @@ internal fun generateShadowedSourcesJar( zos.writeEntry( name = manifestEntry, preserveLastModified = preserveFileTimestamps, - lastModified = if (preserveFileTimestamps) System.currentTimeMillis() else -1, unixMode = UnixMode.file(), ) { write("Manifest-Version: 1.0\n\n".toByteArray(charset)) @@ -148,14 +147,12 @@ internal fun generateShadowedSourcesJar( val entries = zos.entries.map { it.name } val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() entries.forEach { name -> name.parentDirectoryEntries().asReversed().forEach { entryName -> if (!added.add(entryName)) return@forEach zos.writeEntry( name = entryName, preserveLastModified = preserveFileTimestamps, - lastModified = currentTimeMillis, unixMode = UnixMode.directory(), ) } From 5232d50b6680d594bbccd57203b10c8515964121 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:46:52 +0800 Subject: [PATCH 31/68] Update regexes --- .../gradle/plugins/shadow/internal/ShadowSourcesJar.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index d79d66e083..c50477b2ee 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -164,12 +164,11 @@ internal fun generateShadowedSourcesJar( } } -private val packageRegex = Regex("""(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""") +private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() private val jvmNameRegex = - Regex( - """@file\s*:\s*(?:\[[^\]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" - ) + """@file\s*:\s*(?:\[[^]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" + .toRegex() internal fun extractPackage(text: String): String { val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() From 6079bbc991fcbc30302039a201d2322db2458407 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:49:10 +0800 Subject: [PATCH 32/68] Safe cast for DefaultDependencyFilter --- .../github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 435cba0ce9..1ddc0f3646 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -193,8 +193,7 @@ public abstract class ShadowJar : Jar() { @get:Classpath internal val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { dependencyFilter.zip(configurations) { df, cs -> - df as DefaultDependencyFilter - df.resolveSourcesJars(cs) + (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() } } From ac67d84d987a831e9a2022fed105209d7f5de8a0 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 10:10:16 +0800 Subject: [PATCH 33/68] Only publish shadowSourcesElements when java sources variant is present --- .../gradle/plugins/shadow/PublishingTest.kt | 93 +++++++++++-------- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 21 +++-- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 5df9072aaf..c1438f6c5a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -9,7 +9,6 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME -import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath @@ -265,6 +264,59 @@ class PublishingTest : BasePluginTest() { publish() + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries) + .containsOnly( + "maven-1.0.jar", + "maven-1.0.module", + "maven-1.0.pom", + "maven-1.0.jar.md5", + "maven-1.0.module.md5", + "maven-1.0.pom.md5", + "maven-1.0.jar.sha1", + "maven-1.0.module.sha1", + "maven-1.0.pom.sha1", + "maven-1.0.jar.sha256", + "maven-1.0.module.sha256", + "maven-1.0.pom.sha256", + "maven-1.0.jar.sha512", + "maven-1.0.module.sha512", + "maven-1.0.pom.sha512", + ) + assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) + assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertShadowVariantCommon(gmm) + } + + @Test + fun publishShadowJarWithSourcesWhenWithSourcesJarEnabled() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = '' + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + publish() + val artifactRoot = "my/maven/1.0" assertThat(repoPath(artifactRoot).entries) .containsOnly( @@ -424,18 +476,11 @@ class PublishingTest : BasePluginTest() { "my-artifact-2.0-my-classifier.my-ext.md5", "my-artifact-2.0.pom.md5", "my-artifact-2.0.pom.sha1", - "my-artifact-2.0-sources.my-ext", - "my-artifact-2.0-sources.my-ext.md5", - "my-artifact-2.0-sources.my-ext.sha1", - "my-artifact-2.0-sources.my-ext.sha256", - "my-artifact-2.0-sources.my-ext.sha512", ) assertShadowJarCommon(repoJarPath("$artifactRoot/my-artifact-2.0-my-classifier.my-ext")) assertPomCommon(repoPath("$artifactRoot/my-artifact-2.0.pom")) - val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module")) - assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) + assertShadowVariantCommon(gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module"))) } @Test @@ -489,12 +534,6 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", - // Entries of maven-1.0-sources.jar - "maven-1.0-sources.jar", - "maven-1.0-sources.jar.md5", - "maven-1.0-sources.jar.sha1", - "maven-1.0-sources.jar.sha256", - "maven-1.0-sources.jar.sha512", ) assertThat(repoPath("my/maven-all/1.0").entries) .containsOnly( @@ -513,12 +552,6 @@ class PublishingTest : BasePluginTest() { "maven-all-1.0-all.jar.sha512", "maven-all-1.0.module.sha512", "maven-all-1.0.pom.sha512", - // Entries of maven-all-1.0-sources.jar - "maven-all-1.0-sources.jar", - "maven-all-1.0-sources.jar.md5", - "maven-all-1.0-sources.jar.sha1", - "maven-all-1.0-sources.jar.sha256", - "maven-all-1.0-sources.jar.sha512", ) assertThat(repoJarPath("my/maven/1.0/maven-1.0.jar")).useAll { containsOnly(*manifestEntries) } @@ -528,13 +561,12 @@ class PublishingTest : BasePluginTest() { assertPomCommon(repoPath("my/maven/1.0/maven-1.0.pom"), arrayOf("my:a:1.0", "my:b:1.0")) gmmAdapter.fromJson(repoPath("my/maven/1.0/maven-1.0.module")).let { gmm -> - // apiElements, runtimeElements, shadowRuntimeElements, shadowSourcesElements + // apiElements, runtimeElements, shadowRuntimeElements assertThat(gmm.variantNames) .containsOnly( API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertThat(gmm.apiElementsVariant).all { transform { it.attributes } @@ -555,18 +587,12 @@ class PublishingTest : BasePluginTest() { transform { it.coordinates }.containsOnly("my:a:1.0", "my:b:1.0") } assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) } assertPomCommon(repoPath("my/maven-all/1.0/maven-all-1.0.pom")) gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")).let { gmm -> - assertThat(gmm.variantNames) - .containsOnly( - SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, - ) + assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) } } @@ -659,11 +685,6 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", - "maven-1.0-sources.jar", - "maven-1.0-sources.jar.md5", - "maven-1.0-sources.jar.sha1", - "maven-1.0-sources.jar.sha256", - "maven-1.0-sources.jar.sha512", *entriesCommon, ) assertThat(gmm.variantNames) @@ -671,11 +692,9 @@ class PublishingTest : BasePluginTest() { API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertVariantsCommon(gmm) assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) assertThat(pomDependencies).containsOnly("my:a:1.0" to "runtime", "my:b:1.0" to "compile") } else { assertThat(artifactEntries).containsOnly(*entriesCommon) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index a59d87fdd7..64855f7c47 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -23,7 +23,7 @@ import org.gradle.api.component.AdhocComponentWithVariants import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME -import org.gradle.api.provider.Provider +import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -125,35 +125,42 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl protected open fun Project.configureComponents() { val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements + // If `withSourcesJar` presents. + val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) shadowComponent.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> variant.mapToMavenScope("runtime") } - shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) {} + shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) { variant -> + if (sourcesElements() == null) { + variant.skip() + } + } components.named("java", AdhocComponentWithVariants::class.java) { component -> val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent component.addVariants( - addIntoJavaComponent = addIntoJavaComponent, outgoingConfiguration = shadowRuntimeElements, logger = logger, + shouldAdd = addIntoJavaComponent::get, ) component.addVariants( - addIntoJavaComponent = addIntoJavaComponent, outgoingConfiguration = shadowSourcesElements, logger = logger, - ) + ) { + addIntoJavaComponent.get() && sourcesElements() != null + } } } private fun AdhocComponentWithVariants.addVariants( - addIntoJavaComponent: Provider, outgoingConfiguration: NamedDomainObjectProvider, logger: Logger, + shouldAdd: () -> Boolean, ) { addVariantsFromConfiguration(outgoingConfiguration) { variant -> variant.mapToOptional() - if (addIntoJavaComponent.get()) { + if (shouldAdd()) { logger.info("Adding {} variant to Java component.", outgoingConfiguration.name) } else { logger.info("Skipping adding {} variant to Java component.", outgoingConfiguration.name) From 41142b13d31b307a18129d5bb2bc6f7ef640e2c8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 11:29:18 +0800 Subject: [PATCH 34/68] Ensure deterministic sources JAR entry ordering and fix jvmNameRegex --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../shadow/internal/ShadowSourcesJar.kt | 109 +++++++++--------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 1 + .../shadow/internal/ShadowSourcesJarTest.kt | 88 ++++++++++++++ 4 files changed, 147 insertions(+), 53 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 4b85d81815..3bd66bdcf1 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -127,7 +127,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements - // If `withSourcesJar` presents. + // If `withSourcesJar` is present. val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index c50477b2ee..190272840b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -17,7 +17,7 @@ internal fun generateShadowedSourcesJar( metadataCharset: String?, preserveFileTimestamps: Boolean, ) { - val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() @@ -41,11 +41,13 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - for (srcDir in sourceSetsSourceDirs) { - if (!srcDir.exists()) continue + val sortedSourceDirs = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } + for (srcDir in sortedSourceDirs) { srcDir .walkTopDown() .filter { it.isFile } + .toList() + .sortedBy { it.relativeTo(srcDir).invariantSeparatorsPath } .forEach { file -> val relPath = file.relativeTo(srcDir).invariantSeparatorsPath val isSource = isSourceFile(relPath) @@ -91,64 +93,67 @@ internal fun generateShadowedSourcesJar( sourcesJars.forEach { jarFile -> jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val isSource = isSourceFile(name) - if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } + entries() + .toList() + .filterNot { it.isDirectory } + .sortedBy { it.name } + .forEach { entry -> + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = getInputStream(entry).readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } } } } - } } } val entries = zos.entries.map { it.name } val added = entries.toMutableSet() entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> + name.parentDirectoryEntries().forEach { entryName -> if (!added.add(entryName)) return@forEach zos.writeEntry( name = entryName, @@ -167,7 +172,7 @@ internal fun generateShadowedSourcesJar( private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() private val jvmNameRegex = - """@file\s*:\s*(?:\[[^]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" + """@file\s*:\s*(?:\[[^]]*?)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" .toRegex() internal fun extractPackage(text: String): String { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 1ddc0f3646..49df528798 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -51,6 +51,7 @@ import org.gradle.api.file.DuplicatesStrategy.EXCLUDE import org.gradle.api.file.DuplicatesStrategy.FAIL import org.gradle.api.file.DuplicatesStrategy.INCLUDE import org.gradle.api.file.DuplicatesStrategy.INHERIT +import org.gradle.api.file.DuplicatesStrategy.WARN import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt index 22806dcf21..f4f99548ad 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -125,6 +125,48 @@ class ShadowSourcesJarTest { ) ) .isFalse() + assertThat( + isUnused( + "BracketedUtils.kt", + "com.example", + """ + @file:[JvmName("CustomFacade")] + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "BracketedMultiUtils.kt", + "com.example", + """ + @file:[Suppress("unused") JvmName("CustomFacade")] + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "BracketedMultiUtilsReversed.kt", + "com.example", + """ + @file:[JvmName("CustomFacade") Suppress("unused")] + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", emptySet())) .isFalse() @@ -161,4 +203,50 @@ class ShadowSourcesJarTest { val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } assertThat(entries).containsAtLeast("shadow/example/nested/Mismatched.kt") } + + @Test + fun generateShadowedSourcesJarDeterministicOrdering(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + srcDir.resolve("z/sub/Z.java").apply { + parentFile.mkdirs() + writeText("package z.sub;\nclass Z {}") + } + srcDir.resolve("a/A.java").apply { + parentFile.mkdirs() + writeText("package a;\nclass A {}") + } + srcDir.resolve("m/M.java").apply { + parentFile.mkdirs() + writeText("package m;\nclass M {}") + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = emptyList(), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } + assertThat(entries) + .isEqualTo( + listOf( + "META-INF/MANIFEST.MF", + "a/A.java", + "m/M.java", + "z/sub/Z.java", + "META-INF/", + "a/", + "m/", + "z/", + "z/sub/", + ) + ) + } } From 7edd7d64e906ec62a68ea3dd85513dc43f1e33dd Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 11:36:11 +0800 Subject: [PATCH 35/68] Clarify shadowed sources JAR generation vs publishing in docs --- docs/publishing/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 9022fe33e5..cae8ead4d9 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -588,6 +588,12 @@ When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin aut The published Maven publication will include both `--all.jar` and `--all-sources.jar`. +> [!NOTE] +> The companion shadowed sources JAR is generated automatically whenever the `shadowJar` task runs (as long as project +> or dependency sources are present). However, **it is only published to Maven repositories when `java.withSourcesJar()` +> is enabled**. If `withSourcesJar()` is omitted, publishing from `components["shadow"]` will only publish the shadowed +> binary JAR, preserving backward compatibility for existing builds. + ### Customizing the Sources Archive File The shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], From 485aa65f8b228f93860c54ee5098a51f187c0dc8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 11:57:23 +0800 Subject: [PATCH 36/68] Expose sourceSetsSourceDirs and includedSourcesJars as public with docs and tests --- api/shadow.api | 4 +- docs/publishing/README.md | 32 +++++- .../gradle/plugins/shadow/RelocationTest.kt | 99 +++++++++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 29 ++++-- 4 files changed, 156 insertions(+), 8 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index b7118a4f6b..83c4fad292 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -261,7 +261,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getAddMultiReleaseAttribute ()Lorg/gradle/api/provider/Property; public fun getApiJars ()Lorg/gradle/api/file/ConfigurableFileCollection; protected abstract fun getArchiveOperations ()Lorg/gradle/api/file/ArchiveOperations; - public final fun getArchiveSourcesFile ()Lorg/gradle/api/file/RegularFileProperty; + public fun getArchiveSourcesFile ()Lorg/gradle/api/file/RegularFileProperty; public fun getConfigurations ()Lorg/gradle/api/provider/SetProperty; public fun getDependencyFilter ()Lorg/gradle/api/provider/Property; public fun getDuplicatesStrategy ()Lorg/gradle/api/file/DuplicatesStrategy; @@ -271,6 +271,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations; public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property; public fun getIncludedDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection; + public fun getIncludedSourcesJars ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludes ()Ljava/util/Set; public fun getJavaLauncher ()Lorg/gradle/api/provider/Property; public fun getMainClass ()Lorg/gradle/api/provider/Property; @@ -282,6 +283,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getRelocationPrefix ()Lorg/gradle/api/provider/Property; public fun getRelocators ()Lorg/gradle/api/provider/SetProperty; public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; + public fun getSourceSetsSourceDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getToMinimize ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getTransformers ()Lorg/gradle/api/provider/SetProperty; public fun mergeGroovyExtensionModules ()V diff --git a/docs/publishing/README.md b/docs/publishing/README.md index cae8ead4d9..7df00a7674 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -596,7 +596,7 @@ The published Maven publication will include both `--all.ja ### Customizing the Sources Archive File -The shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], +The companion shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix: === ":material-language-kotlin: build.gradle.kts" @@ -615,6 +615,34 @@ which defaults to the same destination and base name as `archiveFile` with `-sou } ``` +You can also customize the source inputs included in the companion sources JAR using +[`sourceSetsSourceDirs`][ShadowJar.sourceSetsSourceDirs] and +[`includedSourcesJars`][ShadowJar.includedSourcesJars]: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.shadowJar { + // Add custom source directories + sourceSetsSourceDirs.from("src/extra/java") + + // Add additional dependency sources JARs + includedSourcesJars.from("libs/external-lib-sources.jar") + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + // Add custom source directories + sourceSetsSourceDirs.from('src/extra/java') + + // Add additional dependency sources JARs + includedSourcesJars.from('libs/external-lib-sources.jar') + } + ``` + ### Publishing with Kotlin Multiplatform (KMP) In Kotlin Multiplatform (KMP) projects, publications are managed by the Kotlin Gradle Plugin (KGP) per target (such as @@ -756,7 +784,9 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html [ShadowJar.archiveSourcesFile]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/archive-sources-file.html +[ShadowJar.includedSourcesJars]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/included-sources-jars.html [ShadowJar.relocate]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/relocate.html +[ShadowJar.sourceSetsSourceDirs]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/source-sets-source-dirs.html [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index d7fbfdaa6e..ddb70510e5 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -15,6 +15,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain +import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -851,6 +852,104 @@ class RelocationTest : BasePluginTest() { assertThat(outputShadowedSourcesJar).useAll { containsOnly(*manifestEntries) } } + @Test + fun generateShadowedSourcesJarWithCustomSourceSetsSourceDirs() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main {} + """ + .trimMargin() + ) + path("src/extra/java/extra/Extra.java") + .writeText( + """ + |package extra; + |public class Extra {} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |$shadowJarTask { + | sourceSetsSourceDirs.from('src/extra/java') + | relocate('extra', 'shadow.extra') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/extra/", + "shadow/extra/Extra.java", + *manifestEntries, + ) + getContent("shadow/extra/Extra.java") + .isEqualTo( + """ + |package shadow.extra; + |public class Extra {} + """ + .trimMargin() + ) + } + } + + @Test + fun generateShadowedSourcesJarWithCustomIncludedSourcesJars() { + writeClass() + val customSourcesJar = path("libs/external-sources.jar") + customSourcesJar.parent.toFile().mkdirs() + JarBuilder(customSourcesJar) + .insert( + "ext/Ext.java", + """ + package ext; + public class Ext {} + """ + .trimIndent(), + ) + .write() + + projectScript.appendText( + """ + |$shadowJarTask { + | includedSourcesJars.from('libs/external-sources.jar') + | relocate('ext', 'shadow.ext') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/ext/", + "shadow/ext/Ext.java", + *manifestEntries, + ) + getContent("shadow/ext/Ext.java") + .isEqualTo( + """ + |package shadow.ext; + |public class Ext {} + """ + .trimMargin() + ) + } + } + private companion object { @JvmStatic fun preserveLastModifiedProvider() = diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 49df528798..a9cdc92f4b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -191,16 +191,37 @@ public abstract class ShadowJar : Jar() { dependencyFilter.zip(configurations) { df, cs -> df.resolve(cs) } } + /** + * Source JARs resolved from bundled dependencies to be merged into the companion shadowed sources + * JAR. + */ @get:Classpath - internal val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { + public open val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { dependencyFilter.zip(configurations) { df, cs -> (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() } } + /** + * Source directories from project source sets to be included in the companion shadowed sources + * JAR. + * + * In projects applying the `shadow` plugin for Java or Kotlin Multiplatform, this defaults to the + * relevant source sets' source directories. + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + public open val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() + + /** + * The destination location of the companion shadowed sources JAR. + * + * Defaults to + * `/--sources.`. + */ @get:Optional @get:OutputFile - public val archiveSourcesFile: RegularFileProperty = + public open val archiveSourcesFile: RegularFileProperty = objectFactory .fileProperty() .convention( @@ -216,10 +237,6 @@ public abstract class ShadowJar : Jar() { ) ) - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() - /** * Enables auto relocation of packages in the dependencies. * From 984fee442d1c3968f81a6df665c3a0db825c742c Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 12:03:01 +0800 Subject: [PATCH 37/68] Annotate includedSourcesJars with @InputFiles and @PathSensitive(NONE) --- .../github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index a9cdc92f4b..f09e1c8a21 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -195,7 +195,8 @@ public abstract class ShadowJar : Jar() { * Source JARs resolved from bundled dependencies to be merged into the companion shadowed sources * JAR. */ - @get:Classpath + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) public open val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { dependencyFilter.zip(configurations) { df, cs -> (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() From ea3d52998a05a75a4a45bd29011067e72ad5a0e7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 12:39:49 +0800 Subject: [PATCH 38/68] Introduce generateSourcesJar property gated by withSourcesJar by default --- api/shadow.api | 1 + docs/getting-started/README.md | 6 +++- docs/publishing/README.md | 18 +++++++++--- .../gradle/plugins/shadow/BasePluginTest.kt | 3 ++ .../gradle/plugins/shadow/FilteringTest.kt | 1 + .../gradle/plugins/shadow/JavaPluginsTest.kt | 3 ++ .../plugins/shadow/KotlinPluginsTest.kt | 2 ++ .../gradle/plugins/shadow/MinimizeTest.kt | 1 + .../gradle/plugins/shadow/PublishingTest.kt | 1 + .../gradle/plugins/shadow/RelocationTest.kt | 28 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 3 ++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 17 ++++++++++- .../plugins/shadow/ShadowPropertiesTest.kt | 20 +++++++++++++ 13 files changed, 98 insertions(+), 6 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 83c4fad292..4428364a48 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -270,6 +270,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getExcludes ()Ljava/util/Set; protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations; public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property; + public fun getGenerateSourcesJar ()Lorg/gradle/api/provider/Property; public fun getIncludedDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludedSourcesJars ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludes ()Ljava/util/Set; diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 0734fdba4b..fed55c39aa 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -138,7 +138,8 @@ in their build logic), Shadow will automatically configure the following behavio - `META-INF/versions/**/module-info.class` - `module-info.class` - Configures the [`ShadowJar`][ShadowJar] task to generate a companion **Shadowed Sources JAR** containing both - project sources and shadowed dependency sources with relocated packages. + project sources and shadowed dependency sources with relocated packages when `java.withSourcesJar()` is enabled + (or when [`generateSourcesJar`][ShadowJar.generateSourcesJar] is set to `true`). - Creates and registers the `shadow` component in the project (used for integrating with [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. @@ -157,6 +158,8 @@ Here are the options that can be passed to the `shadowJar`: --no-enable-kotlin-module-remapping Disables option --enable-kotlin-module-remapping. --fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate. --no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries. +--generate-sources-jar Generates a companion shadowed sources JAR containing project and dependency sources. +--no-generate-sources-jar Disables option --generate-sources-jar. --main-class Main class attribute to add to manifest. --minimize-jar Minimizes the jar by removing unused classes. --no-minimize-jar Disables option --minimize-jar. @@ -177,5 +180,6 @@ Refer to [listing command line options][listing-command-line-options]. [JavaPlugin]: https://docs.gradle.org/current/userguide/java_plugin.html [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html +[ShadowJar.generateSourcesJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/generate-sources-jar.html [gradle-plugin-portal]: https://plugins.gradle.org/plugin/com.gradleup.shadow [listing-command-line-options]: https://docs.gradle.org/current/userguide/custom_tasks.html#sec:listing_task_options diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 7df00a7674..147227b6b5 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -589,10 +589,11 @@ The published Maven publication will include both `--all.ja `--all-sources.jar`. > [!NOTE] -> The companion shadowed sources JAR is generated automatically whenever the `shadowJar` task runs (as long as project -> or dependency sources are present). However, **it is only published to Maven repositories when `java.withSourcesJar()` -> is enabled**. If `withSourcesJar()` is omitted, publishing from `components["shadow"]` will only publish the shadowed -> binary JAR, preserving backward compatibility for existing builds. +> Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. +> In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid +> unnecessary build overhead for application builds. If `withSourcesJar()` is omitted, publishing from +> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing builds. +> You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`). ### Customizing the Sources Archive File @@ -675,6 +676,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the maven("https://repo.myorg.com") } } + + tasks.named("shadowJar") { + generateSourcesJar = true + } ``` === ":simple-apachegroovy: build.gradle" @@ -704,6 +709,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the maven { url = 'https://repo.myorg.com' } } } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + generateSourcesJar = true + } ``` ## Generating Javadoc or Dokka from Shadowed Sources @@ -784,6 +793,7 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html [ShadowJar.archiveSourcesFile]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/archive-sources-file.html +[ShadowJar.generateSourcesJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/generate-sources-jar.html [ShadowJar.includedSourcesJars]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/included-sources-jars.html [ShadowJar.relocate]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/relocate.html [ShadowJar.sourceSetsSourceDirs]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/source-sets-source-dirs.html diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 71185f1655..0c3b31f687 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -287,6 +287,9 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} + |java { + | withSourcesJar() + |} |dependencies { | implementation project(':client') |} diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index f05d4d930c..d349a07d8b 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -248,6 +248,7 @@ class FilteringTest : BasePluginTest() { | implementation 'my:h:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | dependencies { | exclude(dependency('my:h:1.0')) | } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 681d41c6d0..1b3b139869 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -121,6 +121,8 @@ class JavaPluginsTest : BasePluginTest() { "--no-enable-kotlin-module-remapping Disables option --enable-kotlin-module-remapping.", "--fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate.", "--no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries", + "--generate-sources-jar Generates a companion shadowed sources JAR containing project and dependency sources.", + "--no-generate-sources-jar Disables option --generate-sources-jar.", "--main-class Main class attribute to add to manifest.", "--minimize-jar Minimizes the jar by removing unused classes.", "--no-minimize-jar Disables option --minimize-jar.", @@ -1357,6 +1359,7 @@ class JavaPluginsTest : BasePluginTest() { | implementation 'my:g:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | relocate 'g', 'shadow.g' |} |tasks.named('javadoc', Javadoc) { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 852e050f0a..76c98bdb38 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -306,6 +306,7 @@ class KotlinPluginsTest : BasePluginTest() { | implementation 'my:g:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | relocate 'g', 'shadow.g' |} |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { @@ -349,6 +350,7 @@ class KotlinPluginsTest : BasePluginTest() { """ |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} |$shadowJarTask { + | generateSourcesJar = true | relocate 'my.custom', 'shadow.custom' |} """ diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 550c22967f..4216754c3f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -148,6 +148,7 @@ class MinimizeTest : BasePluginTest() { | implementation 'my:k:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | minimize() |} """ diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 8de9fcc0ff..4c3fab5e3d 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -735,6 +735,7 @@ class PublishingTest : BasePluginTest() { |} |$shadowJarTask { | archiveClassifier = '' + | generateSourcesJar = true |} |publishing { | repositories { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index ddb70510e5..5f6fd43de8 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo +import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -17,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import kotlin.io.path.appendText +import kotlin.io.path.exists import kotlin.io.path.readBytes import kotlin.io.path.writeText import kotlin.time.Duration.Companion.seconds @@ -755,6 +757,23 @@ class RelocationTest : BasePluginTest() { } } + @Test + fun generateNoShadowedSourcesJarByDefault() { + writeClass() + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + } + @Test fun generateShadowedSourcesJarWithRelocation() { path("src/main/java/my/Main.java") @@ -774,6 +793,7 @@ class RelocationTest : BasePluginTest() { | implementation 'my:g:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | relocate('g', 'shadow.g') |} """ @@ -821,6 +841,9 @@ class RelocationTest : BasePluginTest() { |dependencies { | implementation 'my:b:1.0' |} + |$shadowJarTask { + | generateSourcesJar = true + |} """ .trimMargin() ) @@ -843,6 +866,9 @@ class RelocationTest : BasePluginTest() { |dependencies { | implementation 'my:b:1.0' |} + |$shadowJarTask { + | generateSourcesJar = true + |} """ .trimMargin() ) @@ -873,6 +899,7 @@ class RelocationTest : BasePluginTest() { projectScript.appendText( """ |$shadowJarTask { + | generateSourcesJar = true | sourceSetsSourceDirs.from('src/extra/java') | relocate('extra', 'shadow.extra') |} @@ -921,6 +948,7 @@ class RelocationTest : BasePluginTest() { projectScript.appendText( """ |$shadowJarTask { + | generateSourcesJar = true | includedSourcesJars.from('libs/external-sources.jar') | relocate('ext', 'shadow.ext') |} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 3bd66bdcf1..9ed6780d74 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -45,6 +45,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> task.from(mainSourceSet.map { it.output }) task.sourceSetsSourceDirs.convention(mainSourceSet.map { it.allSource.srcDirs }) + task.generateSourcesJar.convention( + provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } + ) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index f09e1c8a21..b4f846c0de 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -214,6 +214,21 @@ public abstract class ShadowJar : Jar() { @get:PathSensitive(PathSensitivity.RELATIVE) public open val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() + /** + * If `true`, generates a companion shadowed sources JAR containing project and dependency + * sources. + * + * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when + * `java.withSourcesJar()` is enabled, and `false` otherwise. + */ + @get:Input + @get:Option( + option = "generate-sources-jar", + description = + "Generates a companion shadowed sources JAR containing project and dependency sources.", + ) + public open val generateSourcesJar: Property = objectFactory.property(false) + /** * The destination location of the companion shadowed sources JAR. * @@ -794,7 +809,7 @@ public abstract class ShadowJar : Jar() { private var unusedClasses: Set = emptySet() private fun generateShadowedSourcesJar() { - if (!archiveSourcesFile.isPresent) return + if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return generateShadowedSourcesJar( sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs.files, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 91c70e5ab5..04994e2512 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -4,6 +4,7 @@ import assertk.all import assertk.assertThat import assertk.assertions.containsNone import assertk.assertions.containsOnly +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull @@ -162,9 +163,28 @@ class ShadowPropertiesTest { assertThat(relocationPrefix.get()).isEqualTo(ShadowBasePlugin.SHADOW) assertThat(configurations.get()).containsOnly(runtimeConfiguration) + assertThat(generateSourcesJar.get()).isFalse() + assertThat(archiveSourcesFile.get().asFile).all { + isEqualTo(destinationDirectory.file("my-project-1.0.0-all-sources.jar").get().asFile) + isEqualTo(projectDir.resolve("build/libs/my-project-1.0.0-all-sources.jar")) + } + assertThat(sourceSetsSourceDirs.files) + .containsOnly( + *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + ) + assertThat(includedSourcesJars.files).isEmpty() } } + @Test + fun applyJavaPluginWithSourcesJar() = + with(project) { + plugins.apply(JavaPlugin::class.java) + javaPluginExtension.withSourcesJar() + val shadowJarTask = tasks.shadowJar.get() + assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + } + @Test fun applyApplicationPlugin() = with(project) { From 97105cd4e6390e19e691e372cd344524c92733e4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 14:59:19 +0800 Subject: [PATCH 39/68] Polish error message in generateShadowedSourcesJar --- .../{ShadowSourcesJar.kt => SourcesJar.kt} | 2 +- ...dowSourcesJarTest.kt => SourcesJarTest.kt} | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) rename src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/{ShadowSourcesJar.kt => SourcesJar.kt} (99%) rename src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/{ShadowSourcesJarTest.kt => SourcesJarTest.kt} (87%) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt similarity index 99% rename from src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt rename to src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 190272840b..3a6e40c276 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -165,7 +165,7 @@ internal fun generateShadowedSourcesJar( } } catch (e: Exception) { sourcesJarFile.delete() - throw e + gradleError("Could not create shadowed sources JAR '$sourcesJarFile'.", e) } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt similarity index 87% rename from src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt rename to src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index f4f99548ad..f02787e7d1 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -1,18 +1,22 @@ package com.github.jengelman.gradle.plugins.shadow.internal +import assertk.assertFailure import assertk.assertThat import assertk.assertions.containsAtLeast +import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import java.io.File import java.util.zip.ZipFile +import org.gradle.api.GradleException import org.gradle.api.tasks.bundling.ZipEntryCompression import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -class ShadowSourcesJarTest { +class SourcesJarTest { @Test fun extractPackageStatements() { @@ -249,4 +253,30 @@ class ShadowSourcesJarTest { ) ) } + + @Test + fun throwsGradleExceptionOnFailure(@TempDir tempDir: File) { + val invalidFile = tempDir.resolve("not-a-file").apply { mkdirs() } + val srcDir = + tempDir.resolve("src").apply { + mkdirs() + resolve("Main.java").writeText("public class Main {}") + } + + assertFailure { + generateShadowedSourcesJar( + sourcesJarFile = invalidFile, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = emptyList(), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + } + .isInstanceOf() + .hasMessage("Could not create shadowed sources JAR '$invalidFile'.") + } } From 9040faaf116ad1c54ce48f3f52a19a607ffd96c0 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 15:57:54 +0800 Subject: [PATCH 40/68] Gate default source inputs by generateSourcesJar --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 10 ++++- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 8 +++- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 42 +++++++++++-------- .../plugins/shadow/ShadowPropertiesTest.kt | 9 ++-- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 9ed6780d74..c44b5f4e25 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -44,10 +44,18 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val taskProvider = registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> task.from(mainSourceSet.map { it.output }) - task.sourceSetsSourceDirs.convention(mainSourceSet.map { it.allSource.srcDirs }) task.generateSourcesJar.convention( provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } ) + task.sourceSetsSourceDirs.convention( + task.generateSourcesJar.flatMap { generate -> + if (generate) { + mainSourceSet.map { it.allSource.srcDirs } + } else { + provider { emptySet() } + } + } + ) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index 68c1d389f0..7a765e81e0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -37,7 +37,13 @@ public abstract class ShadowKmpPlugin : Plugin { registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) task.sourceSetsSourceDirs.convention( - kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + task.generateSourcesJar.flatMap { generate -> + if (generate) { + kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + } else { + provider { emptySet() } + } + } ) task.configurations.convention( kotlinJvmMain diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index e1ed6c22ed..6aaaa6d604 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -201,6 +201,21 @@ public abstract class ShadowJar : Jar() { dependencyFilter.zip(configurations) { df, cs -> df.resolve(cs) } } + /** + * If `true`, generates a companion shadowed sources JAR containing project and dependency + * sources. + * + * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when + * `java.withSourcesJar()` is enabled, and `false` otherwise. + */ + @get:Input + @get:Option( + option = "generate-sources-jar", + description = + "Generates a companion shadowed sources JAR containing project and dependency sources.", + ) + public open val generateSourcesJar: Property = objectFactory.property(false) + /** * Source JARs resolved from bundled dependencies to be merged into the companion shadowed sources * JAR. @@ -208,8 +223,16 @@ public abstract class ShadowJar : Jar() { @get:InputFiles @get:PathSensitive(PathSensitivity.NONE) public open val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { - dependencyFilter.zip(configurations) { df, cs -> - (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() + // Avoid resolving sources JARs during task input snapshotting when sources JAR generation is + // disabled. + generateSourcesJar.flatMap { + if (it) { + dependencyFilter.zip(configurations) { df, cs -> + (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() + } + } else { + project.provider { emptySet() } + } } } @@ -224,21 +247,6 @@ public abstract class ShadowJar : Jar() { @get:PathSensitive(PathSensitivity.RELATIVE) public open val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() - /** - * If `true`, generates a companion shadowed sources JAR containing project and dependency - * sources. - * - * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when - * `java.withSourcesJar()` is enabled, and `false` otherwise. - */ - @get:Input - @get:Option( - option = "generate-sources-jar", - description = - "Generates a companion shadowed sources JAR containing project and dependency sources.", - ) - public open val generateSourcesJar: Property = objectFactory.property(false) - /** * The destination location of the companion shadowed sources JAR. * diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 04994e2512..d18d8eaa7d 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -168,10 +168,7 @@ class ShadowPropertiesTest { isEqualTo(destinationDirectory.file("my-project-1.0.0-all-sources.jar").get().asFile) isEqualTo(projectDir.resolve("build/libs/my-project-1.0.0-all-sources.jar")) } - assertThat(sourceSetsSourceDirs.files) - .containsOnly( - *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() - ) + assertThat(sourceSetsSourceDirs.files).isEmpty() assertThat(includedSourcesJars.files).isEmpty() } } @@ -183,6 +180,10 @@ class ShadowPropertiesTest { javaPluginExtension.withSourcesJar() val shadowJarTask = tasks.shadowJar.get() assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + assertThat(shadowJarTask.sourceSetsSourceDirs.files) + .containsOnly( + *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + ) } @Test From 74ac176a82429866a07ea779e7d23289edb89846 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:02:58 +0800 Subject: [PATCH 41/68] Skip shadowSourcesElements variant when generateSourcesJar is disabled --- .../gradle/plugins/shadow/PublishingTest.kt | 40 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 9 +++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 4c3fab5e3d..eb763ec5e7 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -348,6 +348,46 @@ class PublishingTest : BasePluginTest() { assertShadowSourcesVariantCommon(gmm) } + @Test + fun dontPublishSourcesWhenGenerateSourcesJarDisabled() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = '' + |generateSourcesJar = false + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + val result = publish(infoArgument) + + assertThat(result.output) + .contains("Skipping adding shadowSourcesElements variant to shadow component.") + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.contains("sources") }).isEmpty() + assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) + assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertShadowVariantCommon(gmm) + assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + } + @Test fun publishCustomShadowJar() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index c44b5f4e25..802b139d83 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -138,8 +138,11 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements - // If `withSourcesJar` is present. + // If `withSourcesJar` is present and `generateSourcesJar` is enabled. val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } + val shouldAddSources = { + sourcesElements() != null && tasks.shadowJar.flatMap { it.generateSourcesJar }.get() + } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) @@ -152,7 +155,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl shadowComponent.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = { sourcesElements() != null }, + shouldAdd = shouldAddSources, ) components.named("java", AdhocComponentWithVariants::class.java) { component -> @@ -166,7 +169,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl component.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = { addIntoJavaComponent.get() && sourcesElements() != null }, + shouldAdd = { addIntoJavaComponent.get() && shouldAddSources() }, ) { mapToOptional() } From 00d47e92418d5efa5917a2eff3699212ac18c203 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:07:19 +0800 Subject: [PATCH 42/68] Document replacing standard artifacts and avoiding sourcesJar task conflict with empty classifier --- docs/publishing/README.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 147227b6b5..b229f3c255 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -295,8 +295,13 @@ You may want to publish the shadowed JAR instead of the original JAR. This can b ``` Because the default `archiveClassifier` of [`Jar`][Jar] is `""` (empty), setting the `archiveClassifier` of -[`ShadowJar`][ShadowJar] to `""` (empty) will make collisions between the outputs of these two tasks in some cases. If -you don't need the standard JAR, you can disable the `jar` task like: +[`ShadowJar`][ShadowJar] to `""` (empty) will make collisions between the outputs of standard tasks and `shadowJar`: + +- The binary shadowed JAR is output to `-.jar`, conflicting with the `jar` task. +- When `generateSourcesJar` is enabled (such as when `java.withSourcesJar()` is used), the companion shadowed sources + JAR is output to `--sources.jar`, conflicting with the standard `sourcesJar` task. + +If you want to replace standard JARs with the shadowed ones, disable the standard `jar` and `sourcesJar` tasks: === ":material-language-kotlin: build.gradle.kts" @@ -304,6 +309,11 @@ you don't need the standard JAR, you can disable the `jar` task like: tasks.jar { enabled = false } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == "sourcesJar" }.configureEach { + enabled = false + } ``` === ":simple-apachegroovy: build.gradle" @@ -312,9 +322,14 @@ you don't need the standard JAR, you can disable the `jar` task like: tasks.named('jar', Jar) { enabled = false } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == 'sourcesJar' }.configureEach { + enabled = false + } ``` -Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like: +Or set different `archiveClassifier` values for the standard tasks: === ":material-language-kotlin: build.gradle.kts" @@ -322,6 +337,11 @@ Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like: tasks.jar { archiveClassifier = "ignored" } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == "sourcesJar" }.configureEach { + (this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources" + } ``` === ":simple-apachegroovy: build.gradle" @@ -330,6 +350,11 @@ Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like: tasks.named('jar', Jar) { archiveClassifier = 'ignored' } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == 'sourcesJar' }.configureEach { + archiveClassifier = 'ignored-sources' + } ``` ## Publishing the Shadowed Gradle Plugins From 52536ee56a04e7435ad416548dc8bfd63bcb80f1 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:17:46 +0800 Subject: [PATCH 43/68] Track compiled class source files with ASM to prevent minimize from deleting used sources --- .../gradle/plugins/shadow/BasePluginTest.kt | 8 +- .../shadow/util/LocalMavenRepository.kt | 7 +- .../plugins/shadow/internal/SourcesJar.kt | 108 ++++++++++++--- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 + .../plugins/shadow/internal/SourcesJarTest.kt | 130 ++++-------------- 5 files changed, 129 insertions(+), 126 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 0c3b31f687..77b64140f8 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -408,10 +408,16 @@ abstract class BasePluginTest { } } - fun createEmptyClassBytes(internalName: String): ByteArray { + fun createEmptyClassBytes( + internalName: String, + sourceFile: String? = "${internalName.substringAfterLast('/')}.java", + ): ByteArray { return ClassWriter(0) .apply { visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null) + if (sourceFile != null) { + visitSource(sourceFile, null) + } visitEnd() } .toByteArray() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt index 26dfb6d0e4..5eead420c0 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt @@ -80,8 +80,11 @@ fun createDefaultLocalMavenRepository(junitJar: Path): AppendableMavenRepository val k = jarModule("my", "k", "1.0") { buildJar { - insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) - insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils", "Utils.kt")) + insert( + "k/CustomUnusedUtils.class", + createEmptyClassBytes("k/CustomUnusedUtils", "UnusedUtils.kt"), + ) } buildSourcesJar { insert( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 3a6e40c276..48c3e08958 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -10,6 +10,8 @@ internal fun generateShadowedSourcesJar( sourcesJarFile: File, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, + classesDirs: Iterable = emptyList(), + dependencies: Iterable = emptyList(), relocators: Iterable, unusedClasses: Set = emptySet(), entryCompression: ZipEntryCompression, @@ -22,6 +24,12 @@ internal fun generateShadowedSourcesJar( val visitedFiles = mutableSetOf() val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 + val sourceToClasses = + if (unusedClasses.isNotEmpty()) { + buildSourceToClassesMap(classesDirs = classesDirs, dependencies = dependencies) + } else { + emptyMap() + } try { sourcesJarFile @@ -55,9 +63,9 @@ internal fun generateShadowedSourcesJar( val text = file.readText(charset) val pkg = extractPackage(text) val simpleName = file.name - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocatePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { var transformedText = text @@ -113,9 +121,9 @@ internal fun generateShadowedSourcesJar( val text = getInputStream(entry).bufferedReader(charset).readText() val pkg = extractPackage(text) val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocatePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { var transformedText = text @@ -171,34 +179,92 @@ internal fun generateShadowedSourcesJar( private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() -private val jvmNameRegex = - """@file\s*:\s*(?:\[[^]]*?)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" - .toRegex() - internal fun extractPackage(text: String): String { val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() return if (matches.isEmpty()) "" else matches.joinToString(".") } +internal fun buildSourceToClassesMap( + classesDirs: Iterable, + dependencies: Iterable, +): Map> { + val sourceToClasses = mutableMapOf>() + + fun processClassBytes(bytes: ByteArray) { + try { + var internalName: String? = null + var sourceFile: String? = null + val reader = org.vafer.jdeb.shaded.objectweb.asm.ClassReader(bytes) + reader.accept( + object : + org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor( + org.vafer.jdeb.shaded.objectweb.asm.Opcodes.ASM9 + ) { + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?, + ) { + internalName = name + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitSource(source: String?, debug: String?) { + sourceFile = source + super.visitSource(source, debug) + } + }, + org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_CODE or + org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_FRAMES, + ) + + val name = internalName ?: return + val source = sourceFile ?: return + val pkg = name.substringBeforeLast('/', "") + val canonicalSourcePath = if (pkg.isEmpty()) source else "$pkg/$source" + val className = name.replace('/', '.') + sourceToClasses.getOrPut(canonicalSourcePath) { mutableSetOf() }.add(className) + } catch (_: Exception) { + // Ignore invalid class files + } + } + + for (dir in classesDirs.filter { it.isDirectory }) { + dir + .walkTopDown() + .filter { it.isFile && it.name.endsWith(".class") } + .forEach { file -> processClassBytes(file.readBytes()) } + } + + for (file in + dependencies.filter { it.isFile && (it.name.endsWith(".jar") || it.name.endsWith(".zip")) }) { + try { + file.useZip { + entries() + .toList() + .filter { !it.isDirectory && it.name.endsWith(".class") } + .forEach { entry -> processClassBytes(getInputStream(entry).readBytes()) } + } + } catch (_: Exception) { + // Ignore invalid archives + } + } + + return sourceToClasses +} + internal fun isUnused( - fileName: String, - pkg: String, - text: String, + canonicalPath: String, unusedClasses: Set, + sourceToClasses: Map>, ): Boolean { if (unusedClasses.isEmpty()) return false - val simpleName = fileName.substringBeforeLast('.') - val className = if (pkg.isEmpty()) simpleName else "$pkg.$simpleName" - if (unusedClasses.contains(className)) return true - - if (fileName.endsWith(".kt")) { - val customJvmName = jvmNameRegex.find(text)?.groupValues?.get(1) - val facadeName = customJvmName ?: "${simpleName}Kt" - val facadeClassName = if (pkg.isEmpty()) facadeName else "$pkg.$facadeName" - if (unusedClasses.contains(facadeClassName)) return true - } - - return false + val classes = sourceToClasses[canonicalPath] ?: return false + if (classes.isEmpty()) return false + return classes.all { it in unusedClasses } } private fun isSourceFile(path: String): Boolean { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 6aaaa6d604..98fab688c1 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -825,6 +825,8 @@ public abstract class ShadowJar : Jar() { sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs.files, includedSourcesJars = includedSourcesJars.files, + classesDirs = sourceSetsClassesDirs.files, + dependencies = includedDependencies.files, relocators = relocators.get() + packageRelocators, unusedClasses = unusedClasses, entryCompression = entryCompression, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index f02787e7d1..aa5a4703d0 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -67,115 +67,41 @@ class SourcesJarTest { val unusedSet = setOf( "com.example.UnusedJava", + "com.example.UnusedJava\$Inner", "com.example.UnusedKtClass", "com.example.DefaultFacadeKt", "com.example.CustomFacade", ) + val sourceToClasses = + mapOf( + "com/example/UnusedJava.java" to + setOf("com.example.UnusedJava", "com.example.UnusedJava\$Inner"), + "com/example/PartiallyUsedJava.java" to + setOf("com.example.UnusedJava", "com.example.UsedHelper"), + "com/example/UsedJava.java" to setOf("com.example.UsedJava"), + "com/example/UnusedKtClass.kt" to setOf("com.example.UnusedKtClass"), + "com/example/DefaultFacade.kt" to setOf("com.example.DefaultFacadeKt"), + "com/example/Utils.kt" to setOf("com.example.CustomFacade"), + "com/example/MixedUtils.kt" to setOf("com.example.CustomFacade", "com.example.UsedClass"), + "Main.java" to setOf("Main"), + ) - assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", unusedSet)) - .isTrue() - assertThat(isUnused("UsedJava.java", "com.example", "class UsedJava {}", unusedSet)).isFalse() + // All classes unused in file -> unused + assertThat(isUnused("com/example/UnusedJava.java", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/UnusedKtClass.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/DefaultFacade.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/Utils.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("Main.java", setOf("Main"), sourceToClasses)).isTrue() - assertThat(isUnused("UnusedKtClass.kt", "com.example", "class UnusedKtClass", unusedSet)) - .isTrue() - assertThat( - isUnused( - "DefaultFacade.kt", - "com.example", - "fun topLevel() {}", - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "Utils.kt", - "com.example", - """ - @file:JvmName("CustomFacade") - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "Utils.kt", - "com.example", - """ - @file:kotlin.jvm.JvmName(name = "CustomFacade") - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "UsedUtils.kt", - "com.example", - """ - @file:JvmName("UsedFacade") - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isFalse() - assertThat( - isUnused( - "BracketedUtils.kt", - "com.example", - """ - @file:[JvmName("CustomFacade")] - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "BracketedMultiUtils.kt", - "com.example", - """ - @file:[Suppress("unused") JvmName("CustomFacade")] - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "BracketedMultiUtilsReversed.kt", - "com.example", - """ - @file:[JvmName("CustomFacade") Suppress("unused")] - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() + // At least one class is used in file -> NOT unused (kept!) + assertThat(isUnused("com/example/PartiallyUsedJava.java", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/MixedUtils.kt", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/UsedJava.java", unusedSet, sourceToClasses)).isFalse() - assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", emptySet())) - .isFalse() - assertThat(isUnused("Main.java", "", "class Main {}", setOf("Main"))).isTrue() - assertThat(isUnused("Main.java", "", "class Main {}", setOf("Other"))).isFalse() + // Unknown source file or empty unused set -> kept + assertThat(isUnused("com/example/Unknown.java", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/UnusedJava.java", emptySet(), sourceToClasses)).isFalse() + assertThat(isUnused("Main.java", setOf("Other"), sourceToClasses)).isFalse() } @Test From b2342dca81b4c2e4e21845ab5609f5c5d7ecbe8c Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:27:55 +0800 Subject: [PATCH 44/68] Fix include and dynamic exclude filtering in source content relocation --- .../shadow/relocation/SimpleRelocator.kt | 109 +++++++++++------- .../shadow/relocation/SimpleRelocatorTest.kt | 74 +++++++++++- 2 files changed, 139 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index c9677d0d69..70f783f484 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -66,32 +66,16 @@ constructor( if (!excludes.isNullOrEmpty()) { 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(), "") - ) - } - } - } } public open fun include(pattern: String) { includes.addAll(normalizePatterns(listOf(pattern))) + includes.add(pattern) } public open fun exclude(pattern: String) { excludes.addAll(normalizePatterns(listOf(pattern))) + excludes.add(pattern) } override fun canRelocatePath(path: String): Boolean { @@ -128,10 +112,26 @@ constructor( } override fun applyToSourceContent(sourceContent: String): String { - if (rawString) return sourceContent + if (rawString || pattern.isEmpty()) return sourceContent + val sourceIncludes = getSourceSubpatterns(includes, pattern) + val sourceExcludes = getSourceSubpatterns(excludes, pattern) val content = - shadeSourceWithExcludes(sourceContent, pattern, shadedPattern, sourcePackageExcludes) - return shadeSourceWithExcludes(content, pathPattern, shadedPathPattern, sourcePathExcludes) + shadeSourceWithFilters( + sourceContent = sourceContent, + patternFrom = pattern, + patternTo = shadedPattern, + includedPatterns = sourceIncludes, + hasIncludes = includes.isNotEmpty(), + excludedPatterns = sourceExcludes, + ) + return shadeSourceWithFilters( + sourceContent = content, + patternFrom = pathPattern, + patternTo = shadedPathPattern, + includedPatterns = sourceIncludes, + hasIncludes = includes.isNotEmpty(), + excludedPatterns = sourceExcludes, + ) } override fun equals(other: Any?): Boolean { @@ -143,8 +143,6 @@ constructor( pathPattern == other.pathPattern && shadedPattern == other.shadedPattern && shadedPathPattern == other.shadedPathPattern && - sourcePackageExcludes == other.sourcePackageExcludes && - sourcePathExcludes == other.sourcePathExcludes && includes == other.includes && excludes == other.excludes } @@ -157,8 +155,6 @@ constructor( pathPattern, shadedPattern, shadedPathPattern, - sourcePackageExcludes, - sourcePathExcludes, includes, excludes, ) @@ -171,8 +167,6 @@ constructor( append("pathPattern='$pathPattern'").append(", ") append("shadedPattern='$shadedPattern'").append(", ") append("shadedPathPattern='$shadedPathPattern'").append(", ") - append("sourcePackageExcludes=$sourcePackageExcludes").append(", ") - append("sourcePathExcludes=$sourcePathExcludes").append(", ") append("includes=$includes").append(", ") append("excludes=$excludes") append(")") @@ -239,31 +233,63 @@ constructor( } } - fun shadeSourceWithExcludes( + fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + if (patternPrefix.isEmpty()) return emptySet() + val result = mutableSetOf() + val dotPrefix = patternPrefix.replace('/', '.') + val slashPrefix = patternPrefix.replace('.', '/') + val trailingWildcardRegex = "[./][*]+$".toRegex() + + for (pat in patterns) { + val dotPat = pat.replace('/', '.') + if (dotPat.startsWith(dotPrefix)) { + val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('.', '/')) + } + } + val slashPat = pat.replace('.', '/') + if (slashPat.startsWith(slashPrefix)) { + val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('/', '.')) + } + } + } + return result + } + + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, + includedPatterns: Set, + hasIncludes: Boolean, excludedPatterns: Set, ): String { - // Usually shading makes package names a bit longer, so make buffer 10% bigger than original - // source. + if (hasIncludes && includedPatterns.isEmpty()) { + return sourceContent + } + val shadedSourceContent = StringBuilder(sourceContent.length * 11 / 10) - // 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) + snippets.forEachIndexed { i, snippet -> val isFirstSnippet = i == 0 val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] - var doExclude = false - for (excludedPattern in excludedPatterns) { - if (snippet.startsWith(excludedPattern)) { - doExclude = true - break - } - } + + val isIncluded = !hasIncludes || includedPatterns.any { snippet.startsWith(it) } + val isExcluded = excludedPatterns.any { snippet.startsWith(it) } + if (isFirstSnippet) { shadedSourceContent.append(snippet) } else { @@ -271,8 +297,9 @@ constructor( val afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() - val shouldExclude = doExclude || afterDotSlashSpace && !afterJavaKeyWord - shadedSourceContent.append(if (shouldExclude) patternFrom else patternTo).append(snippet) + val shouldRelocate = + isIncluded && !isExcluded && (!afterDotSlashSpace || afterJavaKeyWord) + shadedSourceContent.append(if (shouldRelocate) patternTo else patternFrom).append(snippet) } } return shadedSourceContent.toString() diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index 4a88226aad..8f4bf487d8 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -359,13 +359,17 @@ class SimpleRelocatorTest { @Test fun relocateSourceWithExcludes() { - // Main relocator with in-/excludes + // Main relocator with excludes val relocator = SimpleRelocator( "org.apache.maven", "com.acme.maven", - listOf("foo.bar", "zot.baz"), - listOf("irrelevant.exclude", "org.apache.maven.exclude1", "org.apache.maven.sub.exclude2"), + excludes = + listOf( + "irrelevant.exclude", + "org.apache.maven.exclude1", + "org.apache.maven.sub.exclude2", + ), ) // Make sure not to replace variables 'io' and 'ioInput', package 'java.io' val ioRelocator = SimpleRelocator("io", "shaded.io") @@ -383,6 +387,70 @@ class SimpleRelocatorTest { .isEqualTo(relocatedFile) } + @Test + fun relocateSourceWithIncludes() { + val relocator = + SimpleRelocator( + "org.apache.maven", + "com.acme.maven", + includes = listOf("org.apache.maven.hello.*", "org.apache.maven.In"), + ) + val input = + """ + |package org.apache.maven.hello; + |import org.apache.maven.hello.World; + |import org.apache.maven.other.Other; + |import org.apache.maven.In; + |import org.apache.maven.NotIn; + """ + .trimMargin() + val expected = + """ + |package com.acme.maven.hello; + |import com.acme.maven.hello.World; + |import org.apache.maven.other.Other; + |import com.acme.maven.In; + |import org.apache.maven.NotIn; + """ + .trimMargin() + assertThat(relocator.applyToSourceContent(input)).isEqualTo(expected) + } + + @Test + fun relocateSourceWithDslExcludeAndInclude() { + val relocatorExclude = SimpleRelocator("org.apache.maven", "com.acme.maven") + relocatorExclude.exclude("org.apache.maven.exclude1.*") + val inputExclude = + """ + |import org.apache.maven.hello.World; + |import org.apache.maven.exclude1.Ex1; + """ + .trimMargin() + val expectedExclude = + """ + |import com.acme.maven.hello.World; + |import org.apache.maven.exclude1.Ex1; + """ + .trimMargin() + assertThat(relocatorExclude.applyToSourceContent(inputExclude)).isEqualTo(expectedExclude) + + val relocatorInclude = SimpleRelocator("org.apache.maven", "com.acme.maven") + relocatorInclude.include("org.apache.maven.hello.*") + val inputInclude = + """ + |import org.apache.maven.hello.World; + |import org.apache.maven.other.Other; + """ + .trimMargin() + val expectedInclude = + """ + |import com.acme.maven.hello.World; + |import org.apache.maven.other.Other; + """ + .trimMargin() + assertThat(relocatorInclude.applyToSourceContent(inputInclude)).isEqualTo(expectedInclude) + } + private companion object { val sourceFile = """ From 3046fdaf22a0e53c551f4e84ddc3e1e2a1e273ac Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:38:07 +0800 Subject: [PATCH 45/68] Preserve SourceDirectorySet filters for project sources and match project dependencies by identifier --- .../gradle/plugins/shadow/RelocationTest.kt | 44 +++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 2 +- .../internal/DefaultDependencyFilter.kt | 38 ++++---- .../plugins/shadow/internal/SourcesJar.kt | 95 ++++++++++--------- .../plugins/shadow/ShadowPropertiesTest.kt | 2 +- 6 files changed, 118 insertions(+), 65 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 8f509f158c..48b98c9bb9 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -9,6 +9,8 @@ import assertk.assertions.isNotEqualTo import assertk.fail import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONSTANT_TIME_FOR_ZIP_ENTRIES import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader +import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getBytes import com.github.jengelman.gradle.plugins.shadow.testkit.getContent @@ -882,6 +884,48 @@ class RelocationTest : BasePluginTest() { } } + @Test + fun relocateShadowedSourcesJarRespectsSourceDirectorySetFilters() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main {} + """ + .trimMargin() + ) + path("src/main/java/my/Excluded.java") + .writeText( + """ + |package my; + |public class Excluded {} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |sourceSets { + | main { + | java { + | exclude '**/Excluded.java' + | } + | } + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("my/Main.java") + containsNone("my/Excluded.java") + } + } + @Test fun generateShadowedSourcesJarWithCustomIncludedSourcesJars() { writeClass() diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 802b139d83..1a8412cddf 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -50,7 +50,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - mainSourceSet.map { it.allSource.srcDirs } + mainSourceSet.map { it.allSource } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index 7a765e81e0..704f9390e8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -39,7 +39,7 @@ public abstract class ShadowKmpPlugin : Plugin { task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + kotlinJvmMain.map { it.allKotlinSourceSets.map { ss -> ss.kotlin } } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 12edefe997..86f5544ac6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -44,22 +44,23 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) includedDependencies = includes, excludedDependencies = excludes, ) - val componentIds = - configuration.incoming.resolutionResult.allDependencies - .filterIsInstance() - .map { it.selected.id } - .toSet() + val allResolvedDependencies = + configuration.incoming.resolutionResult.allDependencies.filterIsInstance< + ResolvedDependencyResult + >() + + val includedDependenciesResults = allResolvedDependencies.filter { dep -> + includes.any { inc -> + inc.moduleGroup == dep.selected.moduleVersion?.group && + inc.moduleName == dep.selected.moduleVersion?.name && + inc.moduleVersion == dep.selected.moduleVersion?.version + } + } val externalComponentIds = - componentIds + includedDependenciesResults + .map { it.selected.id } .filterIsInstance() - .filter { id -> - includes.any { - it.moduleGroup == id.group && - it.moduleName == id.module && - it.moduleVersion == id.version - } - } .toSet() val externalSourcesFiles = @@ -73,7 +74,12 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) .filterIsInstance() .map { it.file } - val includedProjectNames = includes.map { it.moduleName }.toSet() + val projectComponentIds = + includedDependenciesResults + .map { it.selected.id } + .filterIsInstance() + .toSet() + val projectSourcesFiles = try { configuration.incoming @@ -89,9 +95,7 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) project.objects.named(DocsType::class.java, DocsType.SOURCES), ) } - view.componentFilter { id -> - id is ProjectComponentIdentifier && id.projectName in includedProjectNames - } + view.componentFilter { id -> id in projectComponentIds } view.lenient(true) } .files diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 48c3e08958..6456aa2665 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -20,7 +20,6 @@ internal fun generateShadowedSourcesJar( preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } - if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 @@ -49,54 +48,60 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - val sortedSourceDirs = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } - for (srcDir in sortedSourceDirs) { - srcDir - .walkTopDown() - .filter { it.isFile } - .toList() - .sortedBy { it.relativeTo(srcDir).invariantSeparatorsPath } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + val sourceItems = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } + for (item in sourceItems) { + val filesWithRelPaths: List> = + if (item.isDirectory) { + item + .walkTopDown() + .filter { it.isFile } + .toList() + .sortedBy { it.relativeTo(item).invariantSeparatorsPath } + .map { it to it.relativeTo(item).invariantSeparatorsPath } + } else { + listOf(item to item.name) + } + + for ((file, relPath) in filesWithRelPaths) { + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } } } + } } sourcesJars.forEach { jarFile -> diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index d18d8eaa7d..73c9df4caf 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -182,7 +182,7 @@ class ShadowPropertiesTest { assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly( - *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + *javaPluginExtension.sourceSets.getByName("main").allSource.files.toTypedArray() ) } From 3f7a56e09e01d295ec8a858e59fa0f620fa83411 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 17:00:48 +0800 Subject: [PATCH 46/68] Dynamically derive shadow sources artifact classifier from archiveClassifier --- docs/publishing/README.md | 58 ++++++++++++++++ .../gradle/plugins/shadow/PublishingTest.kt | 69 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 14 ++-- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index b229f3c255..e61c9712b8 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -613,6 +613,64 @@ When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin aut The published Maven publication will include both `--all.jar` and `--all-sources.jar`. +### Local File Names vs. Published Classifiers + +The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in Maven repositories and Gradle Module Metadata: + +| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case | +|:---|:---|:---|:---|:---| +| `archiveClassifier = "all"` *(default)* | `--all-sources.jar` | `all-sources` | `--all-sources.jar` | **Coexistence** (coexists with standard `sources`) | +| `archiveClassifier = "shaded"` | `--shaded-sources.jar` | `shaded-sources` | `--shaded-sources.jar` | **Coexistence** (custom classifier) | +| `archiveClassifier = ""` | `--sources.jar` | `sources` | `--sources.jar` | **Replacement** (replaces standard `sources`) | + +#### Coexistence Scenario + +When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with `shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as `-sources` (such as `all-sources` or `shaded-sources`). + +#### Replacement Scenario + +When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. In this scenario, ensure standard `jar` and `sourcesJar` tasks are disabled so that only the shadowed artifacts are produced and published without destination or coordinate conflicts: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + java { + withSourcesJar() + } + + tasks.jar { + enabled = false + } + + tasks.named("sourcesJar") { + enabled = false + } + + tasks.shadowJar { + archiveClassifier = "" + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + java { + withSourcesJar() + } + + tasks.named('jar') { + enabled = false + } + + tasks.named('sourcesJar') { + enabled = false + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveClassifier = '' + } + ``` + > [!NOTE] > Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. > In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index eb763ec5e7..cfbb381645 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -348,6 +348,75 @@ class PublishingTest : BasePluginTest() { assertShadowSourcesVariantCommon(gmm) } + @Test + fun publishWithSourcesJarAndCustomClassifier() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = 'shaded' + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0-shaded.jar", + "maven-1.0-shaded-sources.jar", + ) + } + + @Test + fun publishJavaComponentWithShadowAndSourcesVariants() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.java + |} + """ + .trimMargin(), + ) + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0.jar", + "maven-1.0-sources.jar", + "maven-1.0-all.jar", + "maven-1.0-all-sources.jar", + ) + } + @Test fun dontPublishSourcesWhenGenerateSourcesJarDisabled() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 1a8412cddf..02d651519c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -11,6 +11,7 @@ import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurablePublishArtifact import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration import org.gradle.api.attributes.Bundling @@ -93,10 +94,15 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.classifier = "sources" - artifact.type = "jar" + val sourcesArtifact = + outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.type = "jar" + } + tasks.shadowJar.configure { shadowJar -> + val shadowClassifier = shadowJar.archiveClassifier.orNull + (sourcesArtifact as? ConfigurablePublishArtifact)?.classifier = + if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" } } From 46f17b7c954f5824f1a6a10d1ff1f76bb39d116a Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 17:23:35 +0800 Subject: [PATCH 47/68] Fix shadow sources artifact classifier assignment and document replacement publishing --- docs/publishing/README.md | 74 ++++++++++++++++++- .../gradle/plugins/shadow/PublishingTest.kt | 1 + .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 14 ++-- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index e61c9712b8..31d35a07f3 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -629,11 +629,21 @@ When publishing alongside standard Java artifacts (e.g. publishing `from(compone #### Replacement Scenario -When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. In this scenario, ensure standard `jar` and `sourcesJar` tasks are disabled so that only the shadowed artifacts are produced and published without destination or coordinate conflicts: +When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. + +To publish shadowed artifacts as the primary publication: + +1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: === ":material-language-kotlin: build.gradle.kts" ```kotlin + plugins { + java + `maven-publish` + id("com.gradleup.shadow") + } + java { withSourcesJar() } @@ -649,11 +659,25 @@ When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = " tasks.shadowJar { archiveClassifier = "" } + + publishing { + publications { + create("shadow") { + from(components["shadow"]) + } + } + } ``` === ":simple-apachegroovy: build.gradle" ```groovy + plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' + } + java { withSourcesJar() } @@ -669,6 +693,54 @@ When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = " tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { archiveClassifier = '' } + + publishing { + publications { + shadow(MavenPublication) { + from components.shadow + } + } + } + ``` + +2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` tasks does not remove standard variants from the `java` software component. You must also explicitly skip the standard publication variants: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + java + `maven-publish` + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + + components.named("java") { + withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() } + withVariantsFromConfiguration(configurations["sourcesElements"]) { skip() } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' + } + + java { + withSourcesJar() + } + + components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { + withVariantsFromConfiguration(configurations.runtimeElements) { skip() } + withVariantsFromConfiguration(configurations.sourcesElements) { skip() } + } ``` > [!NOTE] diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index cfbb381645..d2e5f4f5aa 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -362,6 +362,7 @@ class PublishingTest : BasePluginTest() { shadowBlock = """ |archiveClassifier = 'shaded' + |archiveSourcesFile = layout.buildDirectory.file('custom.jar') """ .trimMargin(), publicationsBlock = diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 02d651519c..39359df983 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -11,7 +11,6 @@ import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.ConfigurablePublishArtifact import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration import org.gradle.api.attributes.Bundling @@ -94,14 +93,11 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - val sourcesArtifact = - outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.type = "jar" - } - tasks.shadowJar.configure { shadowJar -> - val shadowClassifier = shadowJar.archiveClassifier.orNull - (sourcesArtifact as? ConfigurablePublishArtifact)?.classifier = + outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.type = "jar" + val shadowClassifier = tasks.shadowJar.flatMap { it.archiveClassifier }.orNull + artifact.classifier = if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" } } From effabb045139fc7b7312b70074fa72a1d9d8694c Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 17:52:26 +0800 Subject: [PATCH 48/68] Fix resource relative paths and include/exclude matching in shadowed sources JAR --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 24 ++++ .../gradle/plugins/shadow/PublishingTest.kt | 3 + .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 6 +- .../plugins/shadow/internal/SourcesJar.kt | 113 ++++++++++-------- .../shadow/relocation/SimpleRelocator.kt | 12 +- .../plugins/shadow/ShadowPropertiesTest.kt | 5 +- .../shadow/relocation/SimpleRelocatorTest.kt | 35 ++++++ 8 files changed, 146 insertions(+), 54 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 979623bd80..0fb8980a17 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -1316,6 +1316,30 @@ class JavaPluginsTest : BasePluginTest() { ) } + @Test + fun sourcesJarPreservesResourceRelativePath() { + writeClass() + path("src/main/resources/config/sub/app.properties").writeText("key=value") + + projectScript.appendText( + """ + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast( + "my/Main.java", + "config/sub/app.properties", + ) + } + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index d2e5f4f5aa..d05fb58770 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -383,6 +383,9 @@ class PublishingTest : BasePluginTest() { "maven-1.0-shaded.jar", "maven-1.0-shaded-sources.jar", ) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertThat(gmm.shadowSourcesElementsVariant.fileNames.single()) + .isEqualTo("maven-1.0-shaded-sources.jar") } @Test diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 39359df983..28b69c6e18 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -50,7 +50,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - mainSourceSet.map { it.allSource } + mainSourceSet.map { it.allSource.sourceDirectories + it.allSource } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index 704f9390e8..ddeba32c59 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -39,7 +39,11 @@ public abstract class ShadowKmpPlugin : Plugin { task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - kotlinJvmMain.map { it.allKotlinSourceSets.map { ss -> ss.kotlin } } + kotlinJvmMain.map { + it.allKotlinSourceSets.flatMap { ss -> + listOf(ss.kotlin.sourceDirectories, ss.kotlin) + } + } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 6456aa2665..3bd56948c6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -48,57 +48,72 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - val sourceItems = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } - for (item in sourceItems) { - val filesWithRelPaths: List> = - if (item.isDirectory) { - item - .walkTopDown() - .filter { it.isFile } - .toList() - .sortedBy { it.relativeTo(item).invariantSeparatorsPath } - .map { it to it.relativeTo(item).invariantSeparatorsPath } - } else { - listOf(item to item.name) - } + val sourceItems = sourceSetsSourceDirs.filter { it.exists() } + val (dirs, files) = sourceItems.partition { it.isDirectory } + val sortedDirs = dirs.sortedByDescending { it.path.length } - for ((file, relPath) in filesWithRelPaths) { - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + val filesWithRelPaths = mutableListOf>() + val dirsCoveredByFiles = mutableSetOf() + + for (file in files.sortedBy { it.path }) { + val matchingDir = sortedDirs.firstOrNull { file.startsWith(it) } + if (matchingDir != null) { + dirsCoveredByFiles.add(matchingDir) + filesWithRelPaths.add(file to file.relativeTo(matchingDir).invariantSeparatorsPath) + } else { + filesWithRelPaths.add(file to file.name) + } + } + + for (dir in dirs.sortedBy { it.path }) { + if (dir !in dirsCoveredByFiles) { + dir + .walkTopDown() + .filter { it.isFile } + .toList() + .sortedBy { it.relativeTo(dir).invariantSeparatorsPath } + .forEach { file -> + filesWithRelPaths.add(file to file.relativeTo(dir).invariantSeparatorsPath) } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + } + } + + for ((file, relPath) in filesWithRelPaths) { + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 70f783f484..7ca7a75d5a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -265,6 +265,14 @@ constructor( return result } + private fun matchesSubpattern(snippet: String, subpattern: String): Boolean { + if (!snippet.startsWith(subpattern)) return false + if (subpattern.isEmpty() || snippet.length == subpattern.length) return true + if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true + val nextChar = snippet[subpattern.length] + return !nextChar.isLetterOrDigit() && nextChar != '_' + } + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, @@ -287,8 +295,8 @@ constructor( val isFirstSnippet = i == 0 val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] - val isIncluded = !hasIncludes || includedPatterns.any { snippet.startsWith(it) } - val isExcluded = excludedPatterns.any { snippet.startsWith(it) } + val isIncluded = !hasIncludes || includedPatterns.any { matchesSubpattern(snippet, it) } + val isExcluded = excludedPatterns.any { matchesSubpattern(snippet, it) } if (isFirstSnippet) { shadedSourceContent.append(snippet) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 73c9df4caf..aae0aaf974 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -180,9 +180,12 @@ class ShadowPropertiesTest { javaPluginExtension.withSourcesJar() val shadowJarTask = tasks.shadowJar.get() assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly( - *javaPluginExtension.sourceSets.getByName("main").allSource.files.toTypedArray() + *(mainSourceSet.allSource.sourceDirectories + mainSourceSet.allSource) + .files + .toTypedArray() ) } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index 8f4bf487d8..030cfdb26b 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -357,6 +357,41 @@ class SimpleRelocatorTest { assertThat(relocator.applyToSourceContent(sourceFile)).isEqualTo(sourceFile) } + @Test + fun relocateSourceFileWithPrefixCollision() { + val relocator = + SimpleRelocator( + "org.example", + "relocated.org.example", + includes = listOf("org.example.In"), + ) + val source = + """ + |import org.example.In; + |import org.example.Input; + |import org.example.In.Nested; + | + |public class Test { + | org.example.In a; + | org.example.Input b; + |} + """ + .trimMargin() + val expected = + """ + |import relocated.org.example.In; + |import org.example.Input; + |import relocated.org.example.In.Nested; + | + |public class Test { + | relocated.org.example.In a; + | org.example.Input b; + |} + """ + .trimMargin() + assertThat(relocator.applyToSourceContent(source)).isEqualTo(expected) + } + @Test fun relocateSourceWithExcludes() { // Main relocator with excludes From f9874eda473bf9ec9905169a5bbaba5fbffe137c Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 18:13:12 +0800 Subject: [PATCH 49/68] Dynamically resolve sources publish artifact classifier and ensure path boundary safe directory matching --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 33 ++++++++++++++++ .../gradle/plugins/shadow/PublishingTest.kt | 39 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 38 ++++++++++++++---- .../plugins/shadow/internal/SourcesJar.kt | 20 ++++++---- 4 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 0fb8980a17..5658f00df6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -1340,6 +1340,39 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun sourcesJarHandlesOverlappingSourceDirectoryPrefixes() { + writeClass() + path("src/main/res/a.properties").writeText("a=1") + path("src/main/resources/b.properties").writeText("b=2") + + projectScript.appendText( + """ + |sourceSets { + | main { + | resources { + | srcDir 'src/main/res' + | } + | } + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast( + "my/Main.java", + "a.properties", + "b.properties", + ) + } + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index d05fb58770..6b228540d6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -388,6 +388,45 @@ class PublishingTest : BasePluginTest() { .isEqualTo("maven-1.0-shaded-sources.jar") } + @Test + fun publishWithSourcesJarAndCustomClassifierAfterPublishingBlock() { + projectScript.appendText( + """ + |apply plugin: 'maven-publish' + |java { + | withSourcesJar() + |} + |publishing { + | repositories { + | maven { url = '${remoteRepoPath.toUri()}' } + | } + | publications { + | shadow(MavenPublication) { + | from components.shadow + | } + | } + |} + |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + | archiveClassifier = 'shaded' + | archiveSourcesFile = layout.buildDirectory.file('custom.jar') + |} + """ + .trimMargin() + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0-shaded.jar", + "maven-1.0-shaded-sources.jar", + ) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertThat(gmm.shadowSourcesElementsVariant.fileNames.single()) + .isEqualTo("maven-1.0-shaded-sources.jar") + } + @Test fun publishJavaComponentWithShadowAndSourcesVariants() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 28b69c6e18..eb96bf373e 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -5,14 +5,18 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.sha import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.runtimeConfiguration import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.registerShadowJarCommon import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.shadowJar +import java.io.File +import java.util.Date import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration +import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -25,6 +29,8 @@ import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME +import org.gradle.api.tasks.TaskDependency +import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -93,13 +99,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.type = "jar" - val shadowClassifier = tasks.shadowJar.flatMap { it.archiveClassifier }.orNull - artifact.classifier = - if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" - } + outgoing.artifact(ShadowSourcesPublishArtifact(tasks.shadowJar)) } // See more details in #2086. @@ -232,3 +232,27 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } + +internal class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : + PublishArtifact { + override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull ?: "" + + override fun getExtension(): String = + shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" + + override fun getType(): String = "jar" + + override fun getClassifier(): String { + val shadowClassifier = shadowJarTask.flatMap { it.archiveClassifier }.orNull + return if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" + } + + override fun getFile(): File = shadowJarTask.flatMap { it.archiveSourcesFile }.get().asFile + + override fun getDate(): Date? = null + + @Suppress("EagerGradleConfiguration") + override fun getBuildDependencies(): TaskDependency = TaskDependency { + setOf(shadowJarTask.get()) + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 3bd56948c6..9c63abc29e 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -50,30 +50,34 @@ internal fun generateShadowedSourcesJar( val sourceItems = sourceSetsSourceDirs.filter { it.exists() } val (dirs, files) = sourceItems.partition { it.isDirectory } - val sortedDirs = dirs.sortedByDescending { it.path.length } + val normalizedDirs = + dirs.map { it to it.normalize().toPath() }.sortedByDescending { it.second.nameCount } val filesWithRelPaths = mutableListOf>() - val dirsCoveredByFiles = mutableSetOf() + val coveredDirs = mutableSetOf() for (file in files.sortedBy { it.path }) { - val matchingDir = sortedDirs.firstOrNull { file.startsWith(it) } + val filePath = file.normalize().toPath() + val matchingDir = + normalizedDirs.firstOrNull { (_, dirPath) -> filePath.startsWith(dirPath) }?.first if (matchingDir != null) { - dirsCoveredByFiles.add(matchingDir) + coveredDirs.add(matchingDir) filesWithRelPaths.add(file to file.relativeTo(matchingDir).invariantSeparatorsPath) } else { filesWithRelPaths.add(file to file.name) } } - for (dir in dirs.sortedBy { it.path }) { - if (dir !in dirsCoveredByFiles) { + for ((dir, dirPath) in normalizedDirs.sortedBy { it.second.nameCount }) { + if (coveredDirs.none { dirPath.startsWith(it.normalize().toPath()) }) { + coveredDirs.add(dir) dir .walkTopDown() .filter { it.isFile } .toList() .sortedBy { it.relativeTo(dir).invariantSeparatorsPath } - .forEach { file -> - filesWithRelPaths.add(file to file.relativeTo(dir).invariantSeparatorsPath) + .forEach { f -> + filesWithRelPaths.add(f to f.relativeTo(dir).invariantSeparatorsPath) } } } From a5478943e1524f14a94895206d12b64053df8266 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 18:26:44 +0800 Subject: [PATCH 50/68] Reformat docs --- docs/publishing/README.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 31d35a07f3..51db8f198f 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -615,25 +615,31 @@ The published Maven publication will include both `--all.ja ### Local File Names vs. Published Classifiers -The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in Maven repositories and Gradle Module Metadata: +The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in +Maven repositories and Gradle Module Metadata: -| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case | -|:---|:---|:---|:---|:---| -| `archiveClassifier = "all"` *(default)* | `--all-sources.jar` | `all-sources` | `--all-sources.jar` | **Coexistence** (coexists with standard `sources`) | -| `archiveClassifier = "shaded"` | `--shaded-sources.jar` | `shaded-sources` | `--shaded-sources.jar` | **Coexistence** (custom classifier) | -| `archiveClassifier = ""` | `--sources.jar` | `sources` | `--sources.jar` | **Replacement** (replaces standard `sources`) | +| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case | +|:----------------------------------------|:---------------------------------------------------------|:---------------------|:--------------------------------------------|:---------------------------------------------------| +| `archiveClassifier = "all"` *(default)* | `--all-sources.jar` | `all-sources` | `--all-sources.jar` | **Coexistence** (coexists with standard `sources`) | +| `archiveClassifier = "shaded"` | `--shaded-sources.jar` | `shaded-sources` | `--shaded-sources.jar` | **Coexistence** (custom classifier) | +| `archiveClassifier = ""` | `--sources.jar` | `sources` | `--sources.jar` | **Replacement** (replaces standard `sources`) | #### Coexistence Scenario -When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with `shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as `-sources` (such as `all-sources` or `shaded-sources`). +When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with +`shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent +coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as +`-sources` (such as `all-sources` or `shaded-sources`). #### Replacement Scenario -When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. +When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR +automatically uses the standard `sources` classifier. To publish shadowed artifacts as the primary publication: -1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: +1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven + publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: === ":material-language-kotlin: build.gradle.kts" @@ -703,7 +709,9 @@ To publish shadowed artifacts as the primary publication: } ``` -2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` tasks does not remove standard variants from the `java` software component. You must also explicitly skip the standard publication variants: +2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` + tasks does not remove standard variants from the `java` software component. You must also explicitly skip the + standard publication variants: === ":material-language-kotlin: build.gradle.kts" @@ -747,12 +755,14 @@ To publish shadowed artifacts as the primary publication: > Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. > In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid > unnecessary build overhead for application builds. If `withSourcesJar()` is omitted, publishing from -> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing builds. +> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing +builds. > You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`). ### Customizing the Sources Archive File -The companion shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], +The companion shadowed sources JAR output location is configured via +[`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix: === ":material-language-kotlin: build.gradle.kts" From 3d9235eff277c354e341bd22dcd4aec56ed93f32 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 18:36:18 +0800 Subject: [PATCH 51/68] Update samples in docs --- docs/publishing/README.md | 54 +++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 51db8f198f..ba5d9d48d8 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -306,12 +306,20 @@ If you want to replace standard JARs with the shadowed ones, disable the standar === ":material-language-kotlin: build.gradle.kts" ```kotlin + plugins { + java + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + tasks.jar { enabled = false } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == "sourcesJar" }.configureEach { + tasks.named("sourcesJar") { enabled = false } ``` @@ -319,12 +327,20 @@ If you want to replace standard JARs with the shadowed ones, disable the standar === ":simple-apachegroovy: build.gradle" ```groovy + plugins { + id('java') + id('com.gradleup.shadow') + } + + java { + withSourcesJar() + } + tasks.named('jar', Jar) { enabled = false } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == 'sourcesJar' }.configureEach { + tasks.named('sourcesJar', Jar) { enabled = false } ``` @@ -334,12 +350,20 @@ Or set different `archiveClassifier` values for the standard tasks: === ":material-language-kotlin: build.gradle.kts" ```kotlin + plugins { + java + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + tasks.jar { archiveClassifier = "ignored" } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == "sourcesJar" }.configureEach { + tasks.named("sourcesJar") { (this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources" } ``` @@ -347,12 +371,20 @@ Or set different `archiveClassifier` values for the standard tasks: === ":simple-apachegroovy: build.gradle" ```groovy + plugins { + id('java') + id('com.gradleup.shadow') + } + + java { + withSourcesJar() + } + tasks.named('jar', Jar) { archiveClassifier = 'ignored' } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == 'sourcesJar' }.configureEach { + tasks.named('sourcesJar', Jar) { archiveClassifier = 'ignored-sources' } ``` @@ -658,7 +690,7 @@ To publish shadowed artifacts as the primary publication: enabled = false } - tasks.named("sourcesJar") { + tasks.named("sourcesJar") { enabled = false } @@ -688,11 +720,11 @@ To publish shadowed artifacts as the primary publication: withSourcesJar() } - tasks.named('jar') { + tasks.named('jar', Jar) { enabled = false } - tasks.named('sourcesJar') { + tasks.named('sourcesJar', Jar) { enabled = false } From 5308d0a1bba5d33875f50d9b14b77e18c3917005 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 19:09:45 +0800 Subject: [PATCH 52/68] Prefer containsOnly for assertions --- .../gradle/plugins/shadow/FilteringTest.kt | 8 ++----- .../gradle/plugins/shadow/JavaPluginsTest.kt | 20 +++++++++++++---- .../plugins/shadow/KotlinPluginsTest.kt | 14 +++++++++--- .../gradle/plugins/shadow/MinimizeTest.kt | 22 +++++++++++++++---- .../gradle/plugins/shadow/PublishingTest.kt | 14 ++++++++++-- .../gradle/plugins/shadow/RelocationTest.kt | 9 ++++---- .../shadow/transformers/TransformersTest.kt | 4 ++-- .../plugins/shadow/internal/SourcesJarTest.kt | 12 ++++++++-- 8 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index d349a07d8b..74211d1c56 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -2,8 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader -import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast -import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import kotlin.io.path.appendText @@ -260,12 +258,10 @@ class FilteringTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsAtLeast("g/G.class") - containsNone("h/H.class", "h/UnusedH.class") + containsOnly(*entriesInAB, "g/", "g/G.class", *manifestEntries) } assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("g/G.java") - containsNone("h/H.java", "h/UnusedH.java") + containsOnly("g/", "g/G.java", *manifestEntries) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 5658f00df6..fe75032fe6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -295,7 +295,7 @@ class JavaPluginsTest : BasePluginTest() { // The fact that server compiled successfully against `client.junit.framework.Test` // means it consumed the shadowed artifact during compilation. assertThat(jarPath("server/build/libs/server-1.0.jar")).useAll { - containsAtLeast("server/Server.class") + containsOnly("server/", "server/Server.class", *manifestEntries) } } @@ -1176,7 +1176,13 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess(":app:$SHADOW_JAR_TASK_NAME") assertThat(jarPath("app/build/libs/app-all.jar")).useAll { - containsAtLeast("com/company/Main.class", "com/company/Utils.class", manifestEntry) + containsOnly( + "com/", + "com/company/", + "com/company/Main.class", + "com/company/Utils.class", + *manifestEntries, + ) } } @@ -1333,9 +1339,13 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast( + containsOnly( + "my/", + "config/", + "config/sub/", "my/Main.java", "config/sub/app.properties", + *manifestEntries, ) } } @@ -1365,10 +1375,12 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast( + containsOnly( + "my/", "my/Main.java", "a.properties", "b.properties", + *manifestEntries, ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 76c98bdb38..f0b8d13c93 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -8,7 +8,6 @@ import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast -import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass @@ -337,6 +336,7 @@ class KotlinPluginsTest : BasePluginTest() { @Test fun generateShadowedSourcesJarNormalizesPackageDirectory() { + val stdlib = compileOnlyStdlib(true) path("src/main/kotlin/FlatFile.kt") .writeText( """ @@ -349,6 +349,9 @@ class KotlinPluginsTest : BasePluginTest() { projectScript.writeText( """ |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} + |dependencies { + | $stdlib + |} |$shadowJarTask { | generateSourcesJar = true | relocate 'my.custom', 'shadow.custom' @@ -360,8 +363,13 @@ class KotlinPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("shadow/custom/nested/FlatFile.kt") - containsNone("FlatFile.kt") + containsOnly( + "shadow/", + "shadow/custom/", + "shadow/custom/nested/", + "shadow/custom/nested/FlatFile.kt", + *manifestEntries, + ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 7239ad0e44..7868cf7700 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -156,12 +156,26 @@ class MinimizeTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsAtLeast("my/Main.class", "h/H.class", "k/CustomUtils.class") - containsNone("h/UnusedH.class", "k/CustomUnusedUtils.class") + containsOnly( + "my/", + "h/", + "k/", + "my/Main.class", + "h/H.class", + "k/CustomUtils.class", + *manifestEntries, + ) } assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("my/Main.java", "h/H.java", "k/Utils.kt") - containsNone("h/UnusedH.java", "k/UnusedUtils.kt") + containsOnly( + "my/", + "h/", + "k/", + "my/Main.java", + "h/H.java", + "k/Utils.kt", + *manifestEntries, + ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 6b228540d6..783a683144 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -919,21 +919,31 @@ class PublishingTest : BasePluginTest() { ) assertThat(repoJarPath("$artifactRoot/my-all-1.0.jar")).useAll { - containsAtLeast( + containsOnly( + "my/", + "g/", + "h/", "my/CommonMain.class", "my/JvmMain.class", "g/G.class", "h/H.class", + "h/UnusedH.class", + "META-INF/my_maven.kotlin_module", *manifestEntries, ) } assertThat(repoJarPath("$artifactRoot/my-all-1.0-sources.jar")).useAll { - containsAtLeast( + containsOnly( + "my/", + "g/", + "h/", "my/CommonMain.kt", "my/JvmMain.kt", "g/G.java", "h/H.java", + "h/UnusedH.java", + *manifestEntries, ) } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 48b98c9bb9..7879115f75 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -9,8 +9,6 @@ import assertk.assertions.isNotEqualTo import assertk.fail import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONSTANT_TIME_FOR_ZIP_ENTRIES import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader -import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast -import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getBytes import com.github.jengelman.gradle.plugins.shadow.testkit.getContent @@ -921,8 +919,11 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("my/Main.java") - containsNone("my/Excluded.java") + containsOnly( + "my/", + "my/Main.java", + *manifestEntries, + ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index a707a2fc61..8f901d4bc1 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -219,7 +219,7 @@ class TransformersTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsOnly("META-INF/", "META-INF/LICENSE", *manifestEntries) + containsOnly("META-INF/LICENSE", *manifestEntries) getContent("META-INF/LICENSE") .isEqualTo( """ @@ -379,7 +379,7 @@ class TransformersTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsOnly("META-INF/", "META-INF/kotlin-stdlib.shadow.kotlin_module", *manifestEntries) + containsOnly("META-INF/kotlin-stdlib.shadow.kotlin_module", *manifestEntries) getBytes("META-INF/kotlin-stdlib.shadow.kotlin_module").isNotEqualTo(moduleBytes) } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index aa5a4703d0..8bfc411ab4 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -2,7 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertFailure import assertk.assertThat -import assertk.assertions.containsAtLeast +import assertk.assertions.containsOnly import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isFalse @@ -131,7 +131,15 @@ class SourcesJarTest { assertThat(outputJar.exists()).isTrue() val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } - assertThat(entries).containsAtLeast("shadow/example/nested/Mismatched.kt") + assertThat(entries) + .containsOnly( + "META-INF/", + "META-INF/MANIFEST.MF", + "shadow/", + "shadow/example/", + "shadow/example/nested/", + "shadow/example/nested/Mismatched.kt", + ) } @Test From 1934fdf2f93a4c30ddb509a432d086e506ab06c8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 19:13:16 +0800 Subject: [PATCH 53/68] Cleanups --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 51 ++++++----------- .../internal/DefaultDependencyFilter.kt | 21 ++++--- .../plugins/shadow/internal/SourcesJar.kt | 56 +++++++++---------- .../shadow/relocation/SimpleRelocator.kt | 4 +- 4 files changed, 57 insertions(+), 75 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index eb96bf373e..0a842bb834 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -5,18 +5,14 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.sha import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.runtimeConfiguration import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.registerShadowJarCommon import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.shadowJar -import java.io.File -import java.util.Date import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration -import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -29,8 +25,6 @@ import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME -import org.gradle.api.tasks.TaskDependency -import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -99,7 +93,22 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - outgoing.artifact(ShadowSourcesPublishArtifact(tasks.shadowJar)) + val shadowJarTask = tasks.shadowJar + outgoing.artifact(shadowJarTask.flatMap { it.archiveSourcesFile }) { artifact -> + with(artifact) { + builtBy(shadowJarTask) + name = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() + extension = shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" + type = "jar" + classifier = + shadowJarTask + .flatMap { it.archiveClassifier } + .orNull + .let { shadowClassifier -> + if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" + } + } + } } // See more details in #2086. @@ -141,9 +150,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements // If `withSourcesJar` is present and `generateSourcesJar` is enabled. - val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } val shouldAddSources = { - sourcesElements() != null && tasks.shadowJar.flatMap { it.generateSourcesJar }.get() + configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null && + tasks.shadowJar.flatMap { it.generateSourcesJar }.get() } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) @@ -232,27 +241,3 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } - -internal class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : - PublishArtifact { - override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull ?: "" - - override fun getExtension(): String = - shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" - - override fun getType(): String = "jar" - - override fun getClassifier(): String { - val shadowClassifier = shadowJarTask.flatMap { it.archiveClassifier }.orNull - return if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" - } - - override fun getFile(): File = shadowJarTask.flatMap { it.archiveSourcesFile }.get().asFile - - override fun getDate(): Date? = null - - @Suppress("EagerGradleConfiguration") - override fun getBuildDependencies(): TaskDependency = TaskDependency { - setOf(shadowJarTask.get()) - } -} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 86f5544ac6..03ac029090 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -44,18 +44,17 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) includedDependencies = includes, excludedDependencies = excludes, ) - val allResolvedDependencies = - configuration.incoming.resolutionResult.allDependencies.filterIsInstance< - ResolvedDependencyResult - >() - val includedDependenciesResults = allResolvedDependencies.filter { dep -> - includes.any { inc -> - inc.moduleGroup == dep.selected.moduleVersion?.group && - inc.moduleName == dep.selected.moduleVersion?.name && - inc.moduleVersion == dep.selected.moduleVersion?.version - } - } + val includedDependenciesResults = + configuration.incoming.resolutionResult.allDependencies + .filterIsInstance() + .filter { dep -> + includes.any { inc -> + inc.moduleGroup == dep.selected.moduleVersion?.group && + inc.moduleName == dep.selected.moduleVersion?.name && + inc.moduleVersion == dep.selected.moduleVersion?.version + } + } val externalComponentIds = includedDependenciesResults diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 9c63abc29e..b94dc63e5b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -5,6 +5,9 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.vafer.jdeb.shaded.objectweb.asm.ClassReader +import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor +import org.vafer.jdeb.shaded.objectweb.asm.Opcodes internal fun generateShadowedSourcesJar( sourcesJarFile: File, @@ -218,32 +221,28 @@ internal fun buildSourceToClassesMap( try { var internalName: String? = null var sourceFile: String? = null - val reader = org.vafer.jdeb.shaded.objectweb.asm.ClassReader(bytes) - reader.accept( - object : - org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor( - org.vafer.jdeb.shaded.objectweb.asm.Opcodes.ASM9 - ) { - override fun visit( - version: Int, - access: Int, - name: String, - signature: String?, - superName: String?, - interfaces: Array?, - ) { - internalName = name - super.visit(version, access, name, signature, superName, interfaces) - } + ClassReader(bytes) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?, + ) { + internalName = name + super.visit(version, access, name, signature, superName, interfaces) + } - override fun visitSource(source: String?, debug: String?) { - sourceFile = source - super.visitSource(source, debug) - } - }, - org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_CODE or - org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_FRAMES, - ) + override fun visitSource(source: String?, debug: String?) { + sourceFile = source + super.visitSource(source, debug) + } + }, + ClassReader.SKIP_CODE or ClassReader.SKIP_FRAMES, + ) val name = internalName ?: return val source = sourceFile ?: return @@ -256,7 +255,7 @@ internal fun buildSourceToClassesMap( } } - for (dir in classesDirs.filter { it.isDirectory }) { + for (dir in classesDirs.filter(File::isDirectory)) { dir .walkTopDown() .filter { it.isFile && it.name.endsWith(".class") } @@ -264,7 +263,7 @@ internal fun buildSourceToClassesMap( } for (file in - dependencies.filter { it.isFile && (it.name.endsWith(".jar") || it.name.endsWith(".zip")) }) { + dependencies.filter { it.isFile && (it.extension == "jar" || it.extension == "zip") }) { try { file.useZip { entries() @@ -287,8 +286,7 @@ internal fun isUnused( ): Boolean { if (unusedClasses.isEmpty()) return false val classes = sourceToClasses[canonicalPath] ?: return false - if (classes.isEmpty()) return false - return classes.all { it in unusedClasses } + return classes.isNotEmpty() && classes.all { it in unusedClasses } } private fun isSourceFile(path: String): Boolean { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 7ca7a75d5a..1e33e9d26d 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -233,7 +233,7 @@ constructor( } } - fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + private fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') @@ -273,7 +273,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - fun shadeSourceWithFilters( + private fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, From fb731deb6b519a0ab367a38a8c61d29ba368b7fd Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 19:53:24 +0800 Subject: [PATCH 54/68] Revert private modifiers --- .../gradle/plugins/shadow/relocation/SimpleRelocator.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 1e33e9d26d..924cda8101 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -233,7 +233,7 @@ constructor( } } - private fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') @@ -265,7 +265,7 @@ constructor( return result } - private fun matchesSubpattern(snippet: String, subpattern: String): Boolean { + fun matchesSubpattern(snippet: String, subpattern: String): Boolean { if (!snippet.startsWith(subpattern)) return false if (subpattern.isEmpty() || snippet.length == subpattern.length) return true if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true @@ -273,7 +273,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - private fun shadeSourceWithFilters( + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, From e725a99baea336b2828fbb5dfbb21b6281e035ba Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:19:51 +0800 Subject: [PATCH 55/68] Reformat docs --- docs/configuration/minimizing/README.md | 6 ++++-- docs/getting-started/README.md | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index e2ccba13a2..904f0e7772 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -134,8 +134,10 @@ rules published in dependency JARs, for example under `META-INF/proguard`. > [!NOTE] > **Shadowed Sources JAR and R8** > -> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 (`minimize { r8 { ... } }`), -> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR will +> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 +(`minimize { r8 { ... } }`), +> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR +will > contain all relocated source files without responding to R8 shrinking results. > > If you need unused source files to be filtered out of the shadowed sources JAR, use the default dependency analyzer diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index fed55c39aa..ddd1cbc44f 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -138,10 +138,11 @@ in their build logic), Shadow will automatically configure the following behavio - `META-INF/versions/**/module-info.class` - `module-info.class` - Configures the [`ShadowJar`][ShadowJar] task to generate a companion **Shadowed Sources JAR** containing both - project sources and shadowed dependency sources with relocated packages when `java.withSourcesJar()` is enabled - (or when [`generateSourcesJar`][ShadowJar.generateSourcesJar] is set to `true`). + project sources and shadowed dependency sources with relocated packages when `java.withSourcesJar()` is enabled (or + when [`generateSourcesJar`][ShadowJar.generateSourcesJar] is set to `true`). - Creates and registers the `shadow` component in the project (used for integrating with - [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. + [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is + enabled. ## ShadowJar Command Line options From 9ee4fc3314e977a5a66420ac6cbe42275f92963e Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Sat, 5 Sep 2026 23:30:21 +0800 Subject: [PATCH 56/68] Fix archiveClassifier assignment in README.md Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/publishing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index ba5d9d48d8..8919d5b2dd 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -364,7 +364,7 @@ Or set different `archiveClassifier` values for the standard tasks: } tasks.named("sourcesJar") { - (this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources" + archiveClassifier = "ignored-sources" } ``` From db160ebab2d455ffdcee2e0952b5e4d158a7e928 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:40:42 +0800 Subject: [PATCH 57/68] Align source relocation with bytecode remapping semantics --- .../plugins/shadow/internal/SourceRemapper.kt | 141 ++++++++++++++++++ .../plugins/shadow/internal/SourcesJar.kt | 21 +-- .../shadow/relocation/SimpleRelocator.kt | 2 +- .../shadow/internal/SourceRemapperTest.kt | 107 +++++++++++++ 4 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt new file mode 100644 index 0000000000..59b19e68d8 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -0,0 +1,141 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +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.relocation.relocatePath +import java.util.regex.Pattern + +private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") + +private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = + Pattern.compile( + "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + + "|" + + "\\{@link( \\*)* $" + + "|" + + "([{}(=;,]|\\*/) $" + ) + +/** + * Remaps source content by applying relocators in a single pass with first-match-wins precedence, + * avoiding cascade replacements where earlier relocations get re-relocated by subsequent rules. + */ +internal fun Iterable.remapSource(sourceContent: String): String { + val relocatorList = this.toList() + if (relocatorList.isEmpty() || sourceContent.isEmpty()) return sourceContent + + val simpleRelocators = + relocatorList.filterIsInstance().filter { + !it.rawString && it.pattern.isNotEmpty() + } + + if (simpleRelocators.isEmpty()) { + var content = sourceContent + for (relocator in relocatorList) { + content = relocator.applyToSourceContent(content) + } + return content + } + + val patterns = + simpleRelocators + .flatMap { listOf(it.pattern, it.pathPattern) } + .filter { it.isNotEmpty() } + .distinct() + .sortedByDescending { it.length } + + if (patterns.isEmpty()) return sourceContent + + val patternRegex = Regex("\\b(" + patterns.joinToString("|") { Regex.escape(it) } + ")\\b") + + val result = StringBuilder((sourceContent.length * 1.1).toInt()) + var lastIndex = 0 + + for (match in patternRegex.findAll(sourceContent)) { + val matchStart = match.range.first + val matchEnd = match.range.last + 1 + val matchedText = match.value + + result.append(sourceContent, lastIndex, matchStart) + lastIndex = matchEnd + + val previousSnippet = sourceContent.substring(0, matchStart) + val previousSnippetOneLine = previousSnippet.replace("\\s+".toRegex(), " ") + val afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() + val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() + val contextValid = !afterDotSlashSpace || afterJavaKeyWord + + var replaced = false + if (contextValid) { + val suffixSnippet = sourceContent.substring(matchEnd) + for (relocator in relocatorList) { + if ( + relocator is SimpleRelocator && !relocator.rawString && relocator.pattern.isNotEmpty() + ) { + val isDotMatch = matchedText == relocator.pattern + val isPathMatch = matchedText == relocator.pathPattern + if (isDotMatch || isPathMatch) { + val sourceIncludes = + SimpleRelocator.getSourceSubpatterns(relocator.includes, relocator.pattern) + val sourceExcludes = + SimpleRelocator.getSourceSubpatterns(relocator.excludes, relocator.pattern) + val hasIncludes = relocator.includes.isNotEmpty() + if (hasIncludes && sourceIncludes.isEmpty()) { + continue + } + val isIncluded = + !hasIncludes || + sourceIncludes.any { SimpleRelocator.matchesSubpattern(suffixSnippet, it) } + val isExcluded = sourceExcludes.any { + SimpleRelocator.matchesSubpattern(suffixSnippet, it) + } + if (isIncluded && !isExcluded) { + result.append( + if (isDotMatch) relocator.shadedPattern else relocator.shadedPathPattern + ) + replaced = true + break + } + } + } + } + } + + if (!replaced) { + result.append(matchedText) + } + } + + result.append(sourceContent, lastIndex, sourceContent.length) + return result.toString() +} + +/** + * Relocates a source file path by stripping its extension before matching against class/path + * relocators, ensuring class-level include/exclude patterns work symmetrically with binary classes. + */ +internal fun Iterable.relocateSourcePath(path: String): String { + if (isSourceFile(path)) { + val extension = path.substringAfterLast('.', "") + val pathWithoutExt = path.removeSuffix(".$extension") + val className = pathWithoutExt.replace('/', '.') + + for (relocator in this) { + if (relocator.canRelocateClass(className) || relocator.canRelocatePath(pathWithoutExt)) { + val relocatedWithoutExt = relocator.relocatePath(RelocatePathContext(pathWithoutExt)) + return "$relocatedWithoutExt.$extension" + } + } + return path + } + + return relocatePath(path) +} + +internal fun isSourceFile(path: String): Boolean { + return path.endsWith(".java") || + path.endsWith(".kt") || + path.endsWith(".groovy") || + path.endsWith(".scala") +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index b94dc63e5b..9ff2af5589 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -94,12 +94,9 @@ internal fun generateShadowedSourcesJar( val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocatePath(canonicalPath) + val relocatedPath = relocators.relocateSourcePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } + val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( name = relocatedPath, @@ -151,12 +148,9 @@ internal fun generateShadowedSourcesJar( val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocatePath(canonicalPath) + val relocatedPath = relocators.relocateSourcePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } + val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( name = relocatedPath, @@ -288,10 +282,3 @@ internal fun isUnused( val classes = sourceToClasses[canonicalPath] ?: return false return classes.isNotEmpty() && classes.all { it in unusedClasses } } - -private fun isSourceFile(path: String): Boolean { - return path.endsWith(".java") || - path.endsWith(".kt") || - path.endsWith(".groovy") || - path.endsWith(".scala") -} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 924cda8101..51ac891c31 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -180,7 +180,7 @@ constructor( return excludes.any { SelectorUtils.matchPath(it, path, "/", true) } } - private companion object { + internal companion object { /** Match dot, slash or space at end of string */ val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt new file mode 100644 index 0000000000..5c1fa8728c --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -0,0 +1,107 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import org.junit.jupiter.api.Test + +class SourceRemapperTest { + + @Test + fun chainedRelocatorsDoNotCascade() { + val r1 = SimpleRelocator("a.foo", "b.foo") + val r2 = SimpleRelocator("b.foo", "c.foo") + val relocators = listOf(r1, r2) + + val input = + """ + |package a.foo; + |import b.foo.Bar; + |public class Main { + | a.foo.Baz baz; + | b.foo.Bar bar; + |} + """ + .trimMargin() + + val expected = + """ + |package b.foo; + |import c.foo.Bar; + |public class Main { + | b.foo.Baz baz; + | c.foo.Bar bar; + |} + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } + + @Test + fun relocateSourcePathWithClassInclude() { + val relocator = + SimpleRelocator( + "pkg", + "hidden.pkg", + includes = listOf("pkg.A", "pkg.sub.*"), + ) + val relocators = listOf(relocator) + + // Included class files + assertThat(relocators.relocateSourcePath("pkg/A.java")).isEqualTo("hidden/pkg/A.java") + assertThat(relocators.relocateSourcePath("pkg/A.kt")).isEqualTo("hidden/pkg/A.kt") + assertThat(relocators.relocateSourcePath("pkg/sub/Nested.java")) + .isEqualTo("hidden/pkg/sub/Nested.java") + + // Excluded / un-included class file + assertThat(relocators.relocateSourcePath("pkg/B.java")).isEqualTo("pkg/B.java") + assertThat(relocators.relocateSourcePath("other/Other.java")).isEqualTo("other/Other.java") + } + + @Test + fun relocateSourcePathWithClassExclude() { + val relocator = + SimpleRelocator( + "pkg", + "hidden.pkg", + excludes = listOf("pkg.B"), + ) + val relocators = listOf(relocator) + + assertThat(relocators.relocateSourcePath("pkg/A.java")).isEqualTo("hidden/pkg/A.java") + assertThat(relocators.relocateSourcePath("pkg/B.java")).isEqualTo("pkg/B.java") + } + + @Test + fun remapSourceWithIncludesAndExcludes() { + val relocator = + SimpleRelocator( + "com.example", + "shaded.example", + includes = listOf("com.example.used.*"), + excludes = listOf("com.example.used.Excluded"), + ) + val relocators = listOf(relocator) + + val input = + """ + |package com.example.used; + |import com.example.used.Foo; + |import com.example.used.Excluded; + |import com.example.unused.Bar; + """ + .trimMargin() + + val expected = + """ + |package shaded.example.used; + |import shaded.example.used.Foo; + |import com.example.used.Excluded; + |import com.example.unused.Bar; + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } +} From 5965733b1b31660b72742f66446780bdc15cc7c7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:48:25 +0800 Subject: [PATCH 58/68] Use SourceDirectorySet and FileCollection.asFileTree for sources jar packaging --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 4 +- .../plugins/shadow/internal/SourcesJar.kt | 38 +++---------------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 +- .../plugins/shadow/ShadowPropertiesTest.kt | 6 +-- .../plugins/shadow/internal/SourcesJarTest.kt | 34 +++++++++++++++-- 6 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 0a842bb834..b5bd393d63 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -50,7 +50,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - mainSourceSet.map { it.allSource.sourceDirectories + it.allSource } + mainSourceSet.map { it.allSource } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index ddeba32c59..cb41fd6a72 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -40,9 +40,7 @@ public abstract class ShadowKmpPlugin : Plugin { task.generateSourcesJar.flatMap { generate -> if (generate) { kotlinJvmMain.map { - it.allKotlinSourceSets.flatMap { ss -> - listOf(ss.kotlin.sourceDirectories, ss.kotlin) - } + it.allKotlinSourceSets.map { ss -> ss.kotlin } } } else { provider { emptySet() } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 9ff2af5589..1a75dbca0c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -4,6 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset +import org.gradle.api.file.FileCollection import org.gradle.api.tasks.bundling.ZipEntryCompression import org.vafer.jdeb.shaded.objectweb.asm.ClassReader import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor @@ -11,7 +12,7 @@ import org.vafer.jdeb.shaded.objectweb.asm.Opcodes internal fun generateShadowedSourcesJar( sourcesJarFile: File, - sourceSetsSourceDirs: Iterable, + sourceSetsSourceDirs: FileCollection, includedSourcesJars: Iterable, classesDirs: Iterable = emptyList(), dependencies: Iterable = emptyList(), @@ -51,41 +52,14 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - val sourceItems = sourceSetsSourceDirs.filter { it.exists() } - val (dirs, files) = sourceItems.partition { it.isDirectory } - val normalizedDirs = - dirs.map { it to it.normalize().toPath() }.sortedByDescending { it.second.nameCount } - val filesWithRelPaths = mutableListOf>() - val coveredDirs = mutableSetOf() - - for (file in files.sortedBy { it.path }) { - val filePath = file.normalize().toPath() - val matchingDir = - normalizedDirs.firstOrNull { (_, dirPath) -> filePath.startsWith(dirPath) }?.first - if (matchingDir != null) { - coveredDirs.add(matchingDir) - filesWithRelPaths.add(file to file.relativeTo(matchingDir).invariantSeparatorsPath) - } else { - filesWithRelPaths.add(file to file.name) - } - } - - for ((dir, dirPath) in normalizedDirs.sortedBy { it.second.nameCount }) { - if (coveredDirs.none { dirPath.startsWith(it.normalize().toPath()) }) { - coveredDirs.add(dir) - dir - .walkTopDown() - .filter { it.isFile } - .toList() - .sortedBy { it.relativeTo(dir).invariantSeparatorsPath } - .forEach { f -> - filesWithRelPaths.add(f to f.relativeTo(dir).invariantSeparatorsPath) - } + sourceSetsSourceDirs.asFileTree.visit { details -> + if (!details.isDirectory) { + filesWithRelPaths.add(details.file to details.relativePath.pathString) } } - for ((file, relPath) in filesWithRelPaths) { + for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { val isSource = isSourceFile(relPath) if (isSource) { val text = file.readText(charset) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 98fab688c1..85f12d9351 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -823,7 +823,7 @@ public abstract class ShadowJar : Jar() { if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return generateShadowedSourcesJar( sourcesJarFile = archiveSourcesFile.get().asFile, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, + sourceSetsSourceDirs = sourceSetsSourceDirs, includedSourcesJars = includedSourcesJars.files, classesDirs = sourceSetsClassesDirs.files, dependencies = includedDependencies.files, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index aae0aaf974..2250058314 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -182,11 +182,7 @@ class ShadowPropertiesTest { assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) - .containsOnly( - *(mainSourceSet.allSource.sourceDirectories + mainSourceSet.allSource) - .files - .toTypedArray() - ) + .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) } @Test diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 8bfc411ab4..40305461ee 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -9,6 +9,7 @@ import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File import java.util.zip.ZipFile import org.gradle.api.GradleException @@ -119,7 +120,7 @@ class SourcesJarTest { val outputJar = tempDir.resolve("output-sources.jar") generateShadowedSourcesJar( sourcesJarFile = outputJar, - sourceSetsSourceDirs = listOf(srcDir), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), relocators = listOf(SimpleRelocator("com.example", "shadow.example")), unusedClasses = emptySet(), @@ -161,7 +162,7 @@ class SourcesJarTest { val outputJar = tempDir.resolve("output-sources.jar") generateShadowedSourcesJar( sourcesJarFile = outputJar, - sourceSetsSourceDirs = listOf(srcDir), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), relocators = emptyList(), unusedClasses = emptySet(), @@ -188,6 +189,33 @@ class SourcesJarTest { ) } + @Test + fun generateShadowedSourcesJarRespectsExcludedDirectory(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + srcDir.resolve("Excluded.java").writeText("public class Excluded {}") + + val fileTree = + testObjectFactory.fileCollection().from(srcDir).asFileTree.matching { + it.exclude("**/Excluded.java") + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(fileTree), + includedSourcesJars = emptyList(), + relocators = emptyList(), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } + assertThat(entries).containsOnly("META-INF/", "META-INF/MANIFEST.MF") + } + @Test fun throwsGradleExceptionOnFailure(@TempDir tempDir: File) { val invalidFile = tempDir.resolve("not-a-file").apply { mkdirs() } @@ -200,7 +228,7 @@ class SourcesJarTest { assertFailure { generateShadowedSourcesJar( sourcesJarFile = invalidFile, - sourceSetsSourceDirs = listOf(srcDir), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), relocators = emptyList(), unusedClasses = emptySet(), From f8a3678f52c834a8814345dd88075bf94f7b89ad Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:59:23 +0800 Subject: [PATCH 59/68] Use direct physical relative paths for sources jar entries --- .../plugins/shadow/KotlinPluginsTest.kt | 40 --------- .../plugins/shadow/internal/SourcesJar.kt | 27 ++---- .../plugins/shadow/internal/SourcesJarTest.kt | 83 ------------------- 3 files changed, 6 insertions(+), 144 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 22ff8d2e1f..e72cf05cbb 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -337,46 +337,6 @@ class KotlinPluginsTest : BasePluginTest() { ) } - @Test - fun generateShadowedSourcesJarNormalizesPackageDirectory() { - val stdlib = compileOnlyStdlib(true) - path("src/main/kotlin/FlatFile.kt") - .writeText( - """ - |package my.custom.nested - | - |class FlatClass - """ - .trimMargin() - ) - projectScript.writeText( - """ - |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} - |dependencies { - | $stdlib - |} - |$shadowJarTask { - | generateSourcesJar = true - | relocate 'my.custom', 'shadow.custom' - |} - """ - .trimMargin() - ) - - runWithSuccess(shadowJarPath) - - assertThat(outputShadowedSourcesJar).useAll { - containsOnly( - "shadow/", - "shadow/custom/", - "shadow/custom/nested/", - "shadow/custom/nested/FlatFile.kt", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - } - } - private fun compileOnlyStdlib(exclude: Boolean): String { return if (exclude) { // Disable the stdlib dependency added via `implementation`. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 1a75dbca0c..b810921fa8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -62,14 +62,10 @@ internal fun generateShadowedSourcesJar( for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { val isSource = isSourceFile(relPath) if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocateSourcePath(canonicalPath) + if (isUnused(relPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocateSourcePath(relPath) if (visitedFiles.add(relocatedPath)) { + val text = file.readText(charset) val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( @@ -116,14 +112,10 @@ internal fun generateShadowedSourcesJar( } val isSource = isSourceFile(name) if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocateSourcePath(canonicalPath) + if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach + val relocatedPath = relocators.relocateSourcePath(name) if (visitedFiles.add(relocatedPath)) { + val text = getInputStream(entry).bufferedReader(charset).readText() val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( @@ -172,13 +164,6 @@ internal fun generateShadowedSourcesJar( } } -private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() - -internal fun extractPackage(text: String): String { - val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() - return if (matches.isEmpty()) "" else matches.joinToString(".") -} - internal fun buildSourceToClassesMap( classesDirs: Iterable, dependencies: Iterable, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 40305461ee..4607524b50 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -8,7 +8,6 @@ import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue -import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File import java.util.zip.ZipFile @@ -19,50 +18,6 @@ import org.junit.jupiter.api.io.TempDir class SourcesJarTest { - @Test - fun extractPackageStatements() { - assertThat(extractPackage("package com.example.foo;")).isEqualTo("com.example.foo") - assertThat(extractPackage("package com.example.foo")).isEqualTo("com.example.foo") - assertThat(extractPackage(" package com.example.foo.bar ; ")) - .isEqualTo("com.example.foo.bar") - assertThat( - extractPackage( - """ - /* - * Multi-line header comment. - */ - package com.example.license; - public class License {} - """ - .trimIndent() - ) - ) - .isEqualTo("com.example.license") - assertThat( - extractPackage( - """ - @file:JvmName("MyUtils") - package com.example.annotated - fun test() {} - """ - .trimIndent() - ) - ) - .isEqualTo("com.example.annotated") - assertThat( - extractPackage( - """ - package a - package b.c - class Chained - """ - .trimIndent() - ) - ) - .isEqualTo("a.b.c") - assertThat(extractPackage("public class NoPackage {}")).isEqualTo("") - } - @Test fun isUnusedMatching() { val unusedSet = @@ -105,44 +60,6 @@ class SourcesJarTest { assertThat(isUnused("Main.java", setOf("Other"), sourceToClasses)).isFalse() } - @Test - fun generateShadowedSourcesJarNormalizesPackageDirectory(@TempDir tempDir: File) { - val srcDir = tempDir.resolve("src").apply { mkdirs() } - val flatMismatchedFile = srcDir.resolve("Mismatched.kt") - flatMismatchedFile.writeText( - """ - package com.example.nested - class Mismatched - """ - .trimIndent() - ) - - val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), - includedSourcesJars = emptyList(), - relocators = listOf(SimpleRelocator("com.example", "shadow.example")), - unusedClasses = emptySet(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = null, - preserveFileTimestamps = true, - ) - - assertThat(outputJar.exists()).isTrue() - val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } - assertThat(entries) - .containsOnly( - "META-INF/", - "META-INF/MANIFEST.MF", - "shadow/", - "shadow/example/", - "shadow/example/nested/", - "shadow/example/nested/Mismatched.kt", - ) - } - @Test fun generateShadowedSourcesJarDeterministicOrdering(@TempDir tempDir: File) { val srcDir = tempDir.resolve("src").apply { mkdirs() } From 8b99a5d0cbd3fc8502e6c5faf36e4a68313e5fb3 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:02:16 +0800 Subject: [PATCH 60/68] Use containsExactly for entry ordering assertions in SourcesJarTest --- .../plugins/shadow/internal/SourcesJarTest.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 4607524b50..1d710fc086 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -2,9 +2,9 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertFailure import assertk.assertThat +import assertk.assertions.containsExactly import assertk.assertions.containsOnly import assertk.assertions.hasMessage -import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue @@ -91,18 +91,16 @@ class SourcesJarTest { val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } assertThat(entries) - .isEqualTo( - listOf( - "META-INF/MANIFEST.MF", - "a/A.java", - "m/M.java", - "z/sub/Z.java", - "META-INF/", - "a/", - "m/", - "z/", - "z/sub/", - ) + .containsExactly( + "META-INF/MANIFEST.MF", + "a/A.java", + "m/M.java", + "z/sub/Z.java", + "META-INF/", + "a/", + "m/", + "z/", + "z/sub/", ) } From d6000f9145d81c72de75ad084243c7e30b1efe74 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:03:07 +0800 Subject: [PATCH 61/68] Document containsExactly preference for ordered collection assertions --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 319faa8ea1..43b9d01059 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,8 +105,9 @@ When adding new features or public APIs: - Prefer exact and complete matching using `isEqualTo` for string assertions whenever possible instead of partial matching (e.g., `contains`). -- Prefer complete assertions like `containsOnly` for collections whenever possible instead of partial assertions like - `containsAtLeast` or `containsNone`. +- Prefer complete assertions like `containsOnly` (or `containsExactly` when order matters) for collections whenever + possible instead of partial assertions like `containsAtLeast` or `containsNone`. Prefer `containsExactly(...)` over + `isEqualTo(listOf(...))` when verifying exact collection elements in order. - Raw multiline strings in tests should be constructed using `.trimMargin()`. - Prefer `=` property assignment over `.set(...)` in Gradle build scripts (both in documentation snippets and functional tests) unless `.set(...)` is explicitly required. From ba2adff9803c8a593c185ca7444d3e58d84c4643 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:07:19 +0800 Subject: [PATCH 62/68] Add comments on gating source inputs by generateSourcesJar --- .../github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt | 1 + .../github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index b5bd393d63..651704d89b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -48,6 +48,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } ) task.sourceSetsSourceDirs.convention( + // Avoid snapshotting source inputs when sources JAR generation is disabled. task.generateSourcesJar.flatMap { generate -> if (generate) { mainSourceSet.map { it.allSource } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index cb41fd6a72..df0903bef7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -37,6 +37,7 @@ public abstract class ShadowKmpPlugin : Plugin { registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) task.sourceSetsSourceDirs.convention( + // Avoid snapshotting source inputs when sources JAR generation is disabled. task.generateSourcesJar.flatMap { generate -> if (generate) { kotlinJvmMain.map { From c074522dab80b4de50a7cb80c17d23e5d4674be5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:10:17 +0800 Subject: [PATCH 63/68] Rename generateShadowedSourcesJar --- .../gradle/plugins/shadow/internal/SourcesJar.kt | 2 +- .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 4 ++-- .../gradle/plugins/shadow/internal/SourcesJarTest.kt | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index b810921fa8..1dbebc7a11 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -10,7 +10,7 @@ import org.vafer.jdeb.shaded.objectweb.asm.ClassReader import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor import org.vafer.jdeb.shaded.objectweb.asm.Opcodes -internal fun generateShadowedSourcesJar( +internal fun generateSourcesJar( sourcesJarFile: File, sourceSetsSourceDirs: FileCollection, includedSourcesJars: Iterable, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 85f12d9351..643b33b18b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -11,7 +11,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses -import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar +import com.github.jengelman.gradle.plugins.shadow.internal.generateSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.gradleError import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension @@ -821,7 +821,7 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs, includedSourcesJars = includedSourcesJars.files, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 1d710fc086..f67a6a7fc7 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -61,7 +61,7 @@ class SourcesJarTest { } @Test - fun generateShadowedSourcesJarDeterministicOrdering(@TempDir tempDir: File) { + fun deterministicOrdering(@TempDir tempDir: File) { val srcDir = tempDir.resolve("src").apply { mkdirs() } srcDir.resolve("z/sub/Z.java").apply { parentFile.mkdirs() @@ -77,7 +77,7 @@ class SourcesJarTest { } val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = outputJar, sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), @@ -105,7 +105,7 @@ class SourcesJarTest { } @Test - fun generateShadowedSourcesJarRespectsExcludedDirectory(@TempDir tempDir: File) { + fun respectsExcludedDirectory(@TempDir tempDir: File) { val srcDir = tempDir.resolve("src").apply { mkdirs() } srcDir.resolve("Excluded.java").writeText("public class Excluded {}") @@ -115,7 +115,7 @@ class SourcesJarTest { } val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = outputJar, sourceSetsSourceDirs = testObjectFactory.fileCollection().from(fileTree), includedSourcesJars = emptyList(), @@ -141,7 +141,7 @@ class SourcesJarTest { } assertFailure { - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = invalidFile, sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), From a80544a1f047634bd8049875c36071d21f1959e2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:13:37 +0800 Subject: [PATCH 64/68] Make internal helper functions and constants private --- .../gradle/plugins/shadow/internal/SourcesJar.kt | 2 +- .../gradle/plugins/shadow/relocation/SimpleRelocator.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 1dbebc7a11..ef06e45256 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -164,7 +164,7 @@ internal fun generateSourcesJar( } } -internal fun buildSourceToClassesMap( +private fun buildSourceToClassesMap( classesDirs: Iterable, dependencies: Iterable, ): Map> { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 51ac891c31..0a844d5c1c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -182,7 +182,7 @@ constructor( internal companion object { /** Match dot, slash or space at end of string */ - val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") + private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") /** * Match @@ -193,7 +193,7 @@ constructor( * * at end of string */ - val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = + private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = Pattern.compile( "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + "|" + @@ -202,7 +202,7 @@ constructor( "([{}(=;,]|\\*/) $" ) - fun normalizePatterns(patterns: Collection?) = buildSet { + private fun normalizePatterns(patterns: Collection?) = buildSet { patterns ?: return@buildSet for (pattern in patterns) { // Regex patterns don't need to be normalized and stay as is. @@ -273,7 +273,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - fun shadeSourceWithFilters( + private fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, From bb3f221be0781a2c0ca039afbd99b0f1cfc3b5c9 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 7 Sep 2026 15:57:06 +0800 Subject: [PATCH 65/68] Simplify shouldAddSources --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 651704d89b..685c520005 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -45,6 +45,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> task.from(mainSourceSet.map { it.output }) task.generateSourcesJar.convention( + // If `withSourcesJar` is present in `java` block. provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } ) task.sourceSetsSourceDirs.convention( @@ -150,11 +151,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements - // If `withSourcesJar` is present and `generateSourcesJar` is enabled. - val shouldAddSources = { - configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null && - tasks.shadowJar.flatMap { it.generateSourcesJar }.get() - } + val generateSourcesJar = { tasks.shadowJar.flatMap { it.generateSourcesJar }.get() } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) @@ -167,7 +164,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl shadowComponent.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = shouldAddSources, + shouldAdd = generateSourcesJar, ) components.named("java", AdhocComponentWithVariants::class.java) { component -> @@ -181,7 +178,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl component.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = { addIntoJavaComponent.get() && shouldAddSources() }, + shouldAdd = { addIntoJavaComponent.get() && generateSourcesJar() }, ) { mapToOptional() } From 0c7a80223818182167219996eb1bf6991e1eddb2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 7 Sep 2026 16:06:37 +0800 Subject: [PATCH 66/68] Clean up shadowComponent --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 685c520005..c268a82cc3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -153,19 +153,21 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val shadowSourcesElements = configurations.shadowSourcesElements val generateSourcesJar = { tasks.shadowJar.flatMap { it.generateSourcesJar }.get() } - val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) - components.add(shadowComponent) - shadowComponent.addVariants( - outgoingConfiguration = shadowRuntimeElements, - logger = logger, - ) { - mapToMavenScope("runtime") + softwareComponentFactory.adhoc(COMPONENT_NAME).let { component -> + components.add(component) + component.addVariants( + outgoingConfiguration = shadowRuntimeElements, + logger = logger, + shouldAdd = { true }, + ) { + mapToMavenScope("runtime") + } + component.addVariants( + outgoingConfiguration = shadowSourcesElements, + logger = logger, + shouldAdd = generateSourcesJar, + ) } - shadowComponent.addVariants( - outgoingConfiguration = shadowSourcesElements, - logger = logger, - shouldAdd = generateSourcesJar, - ) components.named("java", AdhocComponentWithVariants::class.java) { component -> component.addVariants( @@ -188,7 +190,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl private fun AdhocComponentWithVariants.addVariants( outgoingConfiguration: NamedDomainObjectProvider, logger: Logger, - shouldAdd: () -> Boolean = { true }, + shouldAdd: () -> Boolean, action: ConfigurationVariantDetails.() -> Unit = {}, ) { addVariantsFromConfiguration(outgoingConfiguration) { variant -> From 26c9f4abad92f858341b5342374bb5dc9df6a408 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 10 Sep 2026 14:42:19 +0800 Subject: [PATCH 67/68] Improve error handling for Dokka issue 4600 --- .../plugins/shadow/SnippetExecutable.kt | 25 ++++++++++--------- .../plugins/shadow/KotlinPluginsTest.kt | 15 ++++++----- .../plugins/shadow/testkit/GradleRunner.kt | 7 ++++++ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt index 12d13cff02..377314c0f6 100644 --- a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt +++ b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt @@ -4,6 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.assertNoDeprecationWar import com.github.jengelman.gradle.plugins.shadow.testkit.commonGradleArgs import com.github.jengelman.gradle.plugins.shadow.testkit.enableNoImplicitLookupInParentProjects import com.github.jengelman.gradle.plugins.shadow.testkit.gradleRunner +import com.github.jengelman.gradle.plugins.shadow.testkit.isDokkaIssue4600 import java.nio.file.Path import java.util.jar.JarOutputStream import kotlin.io.path.createDirectory @@ -51,6 +52,7 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + // TODO: https://github.com/Kotlin/dokka/issues/4488 projectRoot .resolve("gradle.properties") @@ -68,6 +70,7 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + val pluginsBlock = """ |plugins { @@ -104,28 +107,26 @@ sealed interface SnippetExecutable { } projectRoot.resolve("main/LICENSE").writeText("Sample License") - val containsDokka = withoutImports.contains("dokka") // Script-defined classes (e.g., inline custom ResourceTransformer) are not supported by // CC/IP because transient script classloaders cannot be serialized. val runnerArgs = - commonGradleArgs - .filterNot { - (withoutImports.contains("class ") && - (it == "--configuration-cache" || it.contains("isolated-projects"))) || - (containsDokka && it.startsWith("--warning-mode=")) + if (withoutImports.contains("class ")) { + commonGradleArgs.filterNot { + it == "--configuration-cache" || it.contains("isolated-projects") } - .let { if (containsDokka) it + "--warning-mode=all" else it } + } else { + commonGradleArgs.toList() + } gradleRunner(projectDir = projectRoot, arguments = runnerArgs + "build") .build() .also { gradleBuildOutput = it.output } - .apply { - if (!containsDokka) { - assertNoDeprecationWarnings() - } - } + .assertNoDeprecationWarnings() } catch (t: Throwable) { val buildOutput = (t as? UnexpectedBuildFailure)?.buildResult?.output ?: gradleBuildOutput + + if (buildOutput?.isDokkaIssue4600 == true) return + throw AssertionError( buildString { append("The error line in the doc is near $sourceLocation") diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 6848d6a340..60a70f3434 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -7,10 +7,10 @@ import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader -import com.github.jengelman.gradle.plugins.shadow.testkit.commonGradleArgs import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr +import com.github.jengelman.gradle.plugins.shadow.testkit.isDokkaIssue4600 import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import kotlin.io.path.appendText @@ -331,11 +331,14 @@ class KotlinPluginsTest : BasePluginTest() { .trimMargin() ) - runWithSuccess("dokkaGenerateHtml", failOnDeprecations = false) { - withArguments( - commonGradleArgs.filterNot { it.startsWith("--warning-mode=") } + - listOf("dokkaGenerateHtml", "--warning-mode=all") - ) + try { + runWithSuccess("dokkaGenerateHtml") + } catch (t: Throwable) { + if (t.stackTraceToString().isDokkaIssue4600) { + // Do nothing. + } else { + throw t + } } val dokkaDir = projectRoot.resolve("build/dokka/html") diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt index d07a6c32eb..97c0f2336d 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt @@ -49,6 +49,13 @@ val commonGradleArgs = isolatedProjectsFlag, ) +// TODO: https://github.com/Kotlin/dokka/issues/4600 +val String.isDokkaIssue4600: Boolean + get() = let { output -> + output.contains("The Configuration.setVisible(boolean) method has been deprecated") && + output.contains("org.jetbrains.dokka.gradle") + } + fun gradleRunner( projectDir: Path, arguments: Iterable, From af467c1f4f8f20318c03a3cf953d8c2cf8dadc8f Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 10 Sep 2026 15:37:52 +0800 Subject: [PATCH 68/68] Use idiomatic Groovy syntax for method calls and plugins block in docs --- docs/publishing/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 8919d5b2dd..e064137d6d 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -328,8 +328,8 @@ If you want to replace standard JARs with the shadowed ones, disable the standar ```groovy plugins { - id('java') - id('com.gradleup.shadow') + id 'java' + id 'com.gradleup.shadow' } java { @@ -372,8 +372,8 @@ Or set different `archiveClassifier` values for the standard tasks: ```groovy plugins { - id('java') - id('com.gradleup.shadow') + id 'java' + id 'com.gradleup.shadow' } java { @@ -834,10 +834,10 @@ You can also customize the source inputs included in the companion sources JAR u ```groovy tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { // Add custom source directories - sourceSetsSourceDirs.from('src/extra/java') + sourceSetsSourceDirs.from 'src/extra/java' // Add additional dependency sources JARs - includedSourcesJars.from('libs/external-lib-sources.jar') + includedSourcesJars.from 'libs/external-lib-sources.jar' } ``` @@ -980,8 +980,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source dokka { dokkaSourceSets.configureEach { - classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) - sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + classpath.from tasks.named('shadowJar').flatMap { it.archiveFile } + sourceRoots.from extractShadowedSources.map { it.destinationDir } } } ```