From 0ea96b4d57e34dd7ac7b5683149719f9d1810013 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 16:29:23 +0800 Subject: [PATCH 01/39] 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 c743b14b8..6350def1d 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 e39d04eaf..e776ddb5a 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/39] 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 02c3b5feb..df71e569f 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 bc589d716..c014de588 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 fa7eda57b..0643d431a 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 6350def1d..475788ff2 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 e776ddb5a..4fde242c4 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/39] 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 000000000..79ca41799 --- /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 4fde242c4..961b9b3ac 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/39] 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 df71e569f..9e40ddce2 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 c014de588..9728befb3 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/39] 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 b97b26783..a1f8d8d8c 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 c8bb3a257..68c1d389f 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 961b9b3ac..fcf5e71fd 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/39] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc196d0d..1c5717d28 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/39] 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 9728befb3..544bb674a 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 79ca41799..60d5343f0 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/39] 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 5ce64b4d7..b7118a4f6 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 ea6d376d7..6018cbb55 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 544bb674a..98eac49ea 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 fef15ab37..4e888c1c2 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 3221fe188..3d1a28537 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 a1f8d8d8c..144882372 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 60d5343f0..c4422a08a 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 fcf5e71fd..b937fcdae 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/39] 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 85384f069..c9677d0d6 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/39] 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 bb05feec5..b5de68aba 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 2ec088dfd..bc6c4eb69 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 62a63a108..323d4df3f 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 b55e52a46..6dbd62a5d 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 5105912fa..a8f1a72a9 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/39] 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 144882372..a59d87fdd 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/39] 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 244bc6851..64bf20ce0 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/39] 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 bc6c4eb69..1a5dafbbe 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/39] 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 6dbd62a5d..067cf37c4 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 a8f1a72a9..63f9ccf18 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 98eac49ea..7a4e5f998 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 b937fcdae..6ac317600 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 6ba359b9b..cb7d8b8e8 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/39] 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 9e40ddce2..98dc1ca93 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 ebd8e798f..550c22967 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 c4422a08a..377ab43a9 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 6ac317600..0299cb4da 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/39] 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 63f9ccf18..852e050f0 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 377ab43a9..ae5503c27 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 000000000..22806dcf2 --- /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/39] 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 98dc1ca93..ab3a9c113 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 000000000..26dfb6d0e --- /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/39] 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 ae5503c27..924708df5 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 0299cb4da..7732a5969 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 22806dcf2..34c5aa036 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/39] 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 924708df5..ae5503c27 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 7732a5969..0299cb4da 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 34c5aa036..22806dcf2 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/39] 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 e378ef552..a5f96da1d 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 475788ff2..2b85cca50 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/39] 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 094136074..5a1f3acad 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 f75096494..0734fdba4 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 e97c39ceb..c1bac80a3 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 1a5dafbbe..bcef98428 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 6018cbb55..f929fb4e6 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/39] 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 ab3a9c113..71185f165 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 a5f96da1d..1a0cd3c04 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 067cf37c4..2fd03ebc3 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 2b85cca50..12edefe99 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/39] 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 b7118a4f6..9465d5f40 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 ae5503c27..ed8e12aed 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 91d5953b2..416fb03d7 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 0299cb4da..bf5318baa 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/39] 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 9465d5f40..b7118a4f6 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 ed8e12aed..ae5503c27 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 416fb03d7..91d5953b2 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 bf5318baa..0299cb4da 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/39] 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 1a0cd3c04..f05d4d930 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 2fd03ebc3..681d41c6d 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 7a4e5f998..d7fbfdaa6 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 ae5503c27..053f33b85 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 0299cb4da..435cba0ce 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/39] 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 bcef98428..3ccd02634 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/39] 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 3ccd02634..9022fe33e 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 f929fb4e6..5df9072aa 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/39] 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 435cba0ce..a0d21d40b 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/39] 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 a0d21d40b..435cba0ce 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/39] 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 053f33b85..d79d66e08 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/39] 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 d79d66e08..c50477b2e 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/39] 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 435cba0ce..1ddc0f364 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/39] 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 5df9072aa..c1438f6c5 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 a59d87fdd..64855f7c4 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/39] 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 4b85d8181..3bd66bdcf 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 c50477b2e..190272840 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 1ddc0f364..49df52879 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 22806dcf2..f4f99548a 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/39] 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 9022fe33e..cae8ead4d 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/39] 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 b7118a4f6..83c4fad29 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 cae8ead4d..7df00a767 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 d7fbfdaa6..ddb70510e 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 49df52879..a9cdc92f4 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/39] 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 a9cdc92f4..f09e1c8a2 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/39] 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 83c4fad29..4428364a4 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 0734fdba4..fed55c39a 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 7df00a767..147227b6b 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 71185f165..0c3b31f68 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 f05d4d930..d349a07d8 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 681d41c6d..1b3b13986 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 852e050f0..76c98bdb3 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 550c22967..4216754c3 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 8de9fcc0f..4c3fab5e3 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 ddb70510e..5f6fd43de 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 3bd66bdcf..9ed6780d7 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 f09e1c8a2..b4f846c0d 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 91c70e5ab..04994e251 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/39] 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 190272840..3a6e40c27 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 f4f99548a..f02787e7d 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'.") + } }