From 0ea96b4d57e34dd7ac7b5683149719f9d1810013 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 16:29:23 +0800 Subject: [PATCH 001/117] Add includedSourcesJars --- .../internal/DefaultDependencyFilter.kt | 35 ++++++++++++++++++- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 8 +++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index c743b14b84..6350def1da 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -2,9 +2,16 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.tasks.DependencyFilter import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ResolvedDependency +import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.result.ResolvedArtifactResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.file.FileCollection +import org.gradle.jvm.JvmLibrary +import org.gradle.language.base.artifact.SourcesArtifact -internal class DefaultDependencyFilter(project: Project) : +internal class DefaultDependencyFilter(private val project: Project) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, @@ -19,4 +26,30 @@ internal class DefaultDependencyFilter(project: Project) : } } } + + fun resolveSourcesJars(configurations: Collection): FileCollection { + return configurations + .map { resolveSourcesJars(it) } + .reduceOrNull { acc, fileCollection -> acc + fileCollection } ?: project.files() + } + + private fun resolveSourcesJars(configuration: Configuration): FileCollection { + val componentIds = + configuration.incoming.resolutionResult.allDependencies + .filterIsInstance() + .map { it.selected.id } + .filterIsInstance() + .toSet() + val files = + project.dependencies + .createArtifactResolutionQuery() + .forComponents(componentIds) + .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java) + .execute() + .resolvedComponents + .flatMap { it.getArtifacts(SourcesArtifact::class.java) } + .filterIsInstance() + .map { it.file } + return project.files(files) + } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index e39d04eafe..e776ddb5a3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -188,6 +188,14 @@ public abstract class ShadowJar : Jar() { dependencyFilter.zip(configurations) { df, cs -> df.resolve(cs) } } + @get:Classpath + internal val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { + dependencyFilter.zip(configurations) { df, cs -> + df as DefaultDependencyFilter + df.resolveSourcesJars(cs) + } + } + /** * Enables auto relocation of packages in the dependencies. * From a312982948d179c290312ce91a02727fe692c603 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:08:28 +0800 Subject: [PATCH 002/117] Generate shadowed sources jar when includedSourcesJars is present --- .../gradle/plugins/shadow/BasePluginTest.kt | 7 + .../gradle/plugins/shadow/RelocationTest.kt | 76 ++++++++++ .../shadow/util/AppendableMavenRepository.kt | 23 +++ .../internal/DefaultDependencyFilter.kt | 2 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 137 ++++++++++++++++++ 5 files changed, 244 insertions(+), 1 deletion(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 02c3b5feb9..df71e569f4 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -73,6 +73,9 @@ abstract class BasePluginTest { open val outputShadowedJar: JarPath get() = jarPath("build/libs/my-1.0-all.jar") + val outputShadowedSourcesJar: JarPath + get() = jarPath("build/libs/my-1.0-all-sources.jar") + val outputServerShadowedJar: JarPath get() = jarPath("server/build/libs/server-1.0-all.jar") @@ -90,6 +93,10 @@ abstract class BasePluginTest { insert("a.properties", "a") insert("a2.properties", "a2") } + buildSourcesJar { + insert("a/A.java", "package a;\npublic class A {}") + insert("a.properties", "a") + } } val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index bc589d716b..c014de5885 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo +import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -10,11 +11,13 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONS import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getBytes +import com.github.jengelman.gradle.plugins.shadow.testkit.getContent import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import kotlin.io.path.appendText +import kotlin.io.path.exists import kotlin.io.path.readBytes import kotlin.io.path.writeText import kotlin.time.Duration.Companion.seconds @@ -753,6 +756,79 @@ class RelocationTest : BasePluginTest() { } } + @Test + fun generateShadowedSourcesJarWithRelocation() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main { + | String a = "a.A"; + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:a:1.0' + |} + |$shadowJarTask { + | relocate('a', 'shadow.a') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/a/", + "shadow/a/A.java", + "shadow/a.properties", + ) + getContent("my/Main.java") + .isEqualTo( + """ + |package my; + |public class Main { + | String a = "shadow.a.A"; + |} + """ + .trimMargin() + ) + getContent("shadow/a/A.java") + .isEqualTo( + """ + |package shadow.a; + |public class A {} + """ + .trimMargin() + ) + } + } + + @Test + fun skipShadowedSourcesJarWhenNoIncludedSourcesJars() { + writeClass() + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + """ + .trimMargin() + ) + + runWithSuccess("clean", shadowJarPath) + + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + } + private companion object { @JvmStatic fun preserveLastModifiedProvider() = diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt index fa7eda57bc..0643d431aa 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt @@ -97,9 +97,16 @@ class AppendableMavenRepository(val root: Path) { """ .trimMargin() } + val sourcesArtifactLine = + if (module.sourcesArtifactPath != null) { + "artifact('${module.sourcesArtifactPath}') { classifier = 'sources' }" + } else { + "" + } module.createMavenPublication( """ |artifact '${module.artifactPath}' + |$sourcesArtifactLine |pom.withXml { xml -> | def dependenciesNode = xml.asNode().get('dependencies') ?: xml.asNode().appendNode('dependencies') | $nodes @@ -199,6 +206,7 @@ class AppendableMavenRepository(val root: Path) { inner class JarModule(groupId: String, artifactId: String, version: String) : Module(groupId, artifactId, version) { private var existingJar: Path? = null + private var existingSourcesJar: Path? = null val artifactPath: String get() = @@ -210,6 +218,16 @@ class AppendableMavenRepository(val root: Path) { } ?.invariantSeparatorsPathString ?: error("No jar file provided for $coordinate") + val sourcesArtifactPath: String? + get() = + existingSourcesJar + ?.also { + check(it.exists() && it.isRegularFile()) { + "Sources jar file does not exist or is not a regular file: $it" + } + } + ?.invariantSeparatorsPathString + fun useJar(existingJar: Path) { this.existingJar = existingJar } @@ -218,6 +236,11 @@ class AppendableMavenRepository(val root: Path) { val jarPath = jarsDir.resolve("${coordinate.replace(':', '-')}.jar") existingJar = JarBuilder(jarPath).apply(builder).write() } + + fun buildSourcesJar(builder: JarBuilder.() -> Unit) { + val jarPath = jarsDir.resolve("${coordinate.replace(':', '-')}-sources.jar") + existingSourcesJar = JarBuilder(jarPath).apply(builder).write() + } } class BomModule(groupId: String, artifactId: String, version: String) : diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 6350def1da..475788ff2d 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -11,7 +11,7 @@ import org.gradle.api.file.FileCollection import org.gradle.jvm.JvmLibrary import org.gradle.language.base.artifact.SourcesArtifact -internal class DefaultDependencyFilter(private val project: Project) : +internal class DefaultDependencyFilter(@Transient private val project: Project) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index e776ddb5a3..4fde242c4b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,8 +7,10 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec +import com.github.jengelman.gradle.plugins.shadow.internal.UnixMode import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream +import com.github.jengelman.gradle.plugins.shadow.internal.entries import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars @@ -17,13 +19,16 @@ import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.minimizeWithR8 import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey +import com.github.jengelman.gradle.plugins.shadow.internal.parentDirectoryEntries import com.github.jengelman.gradle.plugins.shadow.internal.property import com.github.jengelman.gradle.plugins.shadow.internal.setProperty import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets import com.github.jengelman.gradle.plugins.shadow.internal.useZip +import com.github.jengelman.gradle.plugins.shadow.internal.writeEntry import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer @@ -33,6 +38,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException +import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -62,6 +68,7 @@ import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider @@ -196,6 +203,17 @@ public abstract class ShadowJar : Jar() { } } + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection { + val sourceSets = project.extensions.findByType(SourceSetContainer::class.java) + if (sourceSets != null) { + sourceSets.named("main").map { it.allSource.srcDirs } + } else { + emptySet() + } + } + /** * Enables auto relocation of packages in the dependencies. * @@ -539,6 +557,7 @@ public abstract class ShadowJar : Jar() { injectManifestAttributes() super.copy() runR8Minimization() + generateShadowedSourcesJar() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. @@ -747,6 +766,124 @@ public abstract class ShadowJar : Jar() { ) } + private fun generateShadowedSourcesJar() { + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } + if (sourcesJars.isEmpty) return + + val archive = archiveFile.get().asFile + val sourcesJarFile = + archive.parentFile.resolve("${archive.nameWithoutExtension}-sources.${archive.extension}") + + val actualRelocators = relocators.get() + packageRelocators + val visitedFiles = mutableSetOf() + val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 + + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + for (srcDir in sourceSetsSourceDirs.files) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + if (visitedFiles.add(relPath)) { + val relocatedPath = actualRelocators.relocatePath(relPath) + val bytes = + if (isSourceFile(relPath)) { + var text = file.readText(charset) + for (relocator in actualRelocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + file.readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = isPreserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val relocatedPath = actualRelocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = + if (isSourceFile(name)) { + var text = getInputStream(entry).bufferedReader(charset).readText() + for (relocator in actualRelocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + getInputStream(entry).readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = isPreserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = isPreserveFileTimestamps, + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) + } + } + } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e + } + } + + private fun isSourceFile(path: String): Boolean { + return path.endsWith(".java") || + path.endsWith(".kt") || + path.endsWith(".groovy") || + path.endsWith(".scala") + } + public companion object { public const val SHADOW_JAR_TASK_NAME: String = "shadowJar" From 24fba42e2ee7a6e1d0cc14a43f54d01c5b8625ad Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:10:38 +0800 Subject: [PATCH 003/117] Extract generateShadowedSourcesJar logic to internal package --- .../shadow/internal/ShadowSourcesJar.kt | 134 ++++++++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 131 ++--------------- 2 files changed, 145 insertions(+), 120 deletions(-) create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt new file mode 100644 index 0000000000..79ca417994 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -0,0 +1,134 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath +import java.io.File +import java.nio.charset.Charset +import org.gradle.api.tasks.bundling.ZipEntryCompression + +internal fun generateShadowedSourcesJar( + archiveFile: File, + sourceSetsSourceDirs: Iterable, + includedSourcesJars: Iterable, + relocators: Iterable, + entryCompression: ZipEntryCompression, + isZip64: Boolean, + metadataCharset: String?, + preserveFileTimestamps: Boolean, +) { + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } + if (sourcesJars.isEmpty()) return + + val sourcesJarFile = + archiveFile.parentFile.resolve( + "${archiveFile.nameWithoutExtension}-sources.${archiveFile.extension}" + ) + + val visitedFiles = mutableSetOf() + val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 + + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + for (srcDir in sourceSetsSourceDirs) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + if (visitedFiles.add(relPath)) { + val relocatedPath = relocators.relocatePath(relPath) + val bytes = + if (isSourceFile(relPath)) { + var text = file.readText(charset) + for (relocator in relocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + file.readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = + if (isSourceFile(name)) { + var text = getInputStream(entry).bufferedReader(charset).readText() + for (relocator in relocators) { + text = relocator.applyToSourceContent(text) + } + text.toByteArray(charset) + } else { + getInputStream(entry).readBytes() + } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = preserveFileTimestamps, + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) + } + } + } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e + } +} + +private fun isSourceFile(path: String): Boolean { + return path.endsWith(".java") || + path.endsWith(".kt") || + path.endsWith(".groovy") || + path.endsWith(".scala") +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 4fde242c4b..961b9b3ac8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,28 +7,25 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec -import com.github.jengelman.gradle.plugins.shadow.internal.UnixMode import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.entries import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses +import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.minimizeWithR8 import com.github.jengelman.gradle.plugins.shadow.internal.multiReleaseAttributeKey -import com.github.jengelman.gradle.plugins.shadow.internal.parentDirectoryEntries import com.github.jengelman.gradle.plugins.shadow.internal.property import com.github.jengelman.gradle.plugins.shadow.internal.setProperty import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets import com.github.jengelman.gradle.plugins.shadow.internal.useZip -import com.github.jengelman.gradle.plugins.shadow.internal.writeEntry import com.github.jengelman.gradle.plugins.shadow.relocation.CacheableRelocator import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator -import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer import com.github.jengelman.gradle.plugins.shadow.transformers.GroovyExtensionModuleTransformer @@ -38,7 +35,6 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException -import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -767,121 +763,16 @@ public abstract class ShadowJar : Jar() { } private fun generateShadowedSourcesJar() { - val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } - if (sourcesJars.isEmpty) return - - val archive = archiveFile.get().asFile - val sourcesJarFile = - archive.parentFile.resolve("${archive.nameWithoutExtension}-sources.${archive.extension}") - - val actualRelocators = relocators.get() + packageRelocators - val visitedFiles = mutableSetOf() - val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 - - try { - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - for (srcDir in sourceSetsSourceDirs.files) { - if (!srcDir.exists()) continue - srcDir - .walkTopDown() - .filter { it.isFile } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - if (visitedFiles.add(relPath)) { - val relocatedPath = actualRelocators.relocatePath(relPath) - val bytes = - if (isSourceFile(relPath)) { - var text = file.readText(charset) - for (relocator in actualRelocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - file.readBytes() - } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = isPreserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } - } - - sourcesJars.forEach { jarFile -> - jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val relocatedPath = actualRelocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = - if (isSourceFile(name)) { - var text = getInputStream(entry).bufferedReader(charset).readText() - for (relocator in actualRelocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - getInputStream(entry).readBytes() - } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = isPreserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } - } - } - - val entries = zos.entries.map { it.name } - val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() - entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> - if (!added.add(entryName)) return@forEach - zos.writeEntry( - name = entryName, - preserveLastModified = isPreserveFileTimestamps, - lastModified = currentTimeMillis, - unixMode = UnixMode.directory(), - ) - } - } - } - } catch (e: Exception) { - sourcesJarFile.delete() - throw e - } - } - - private fun isSourceFile(path: String): Boolean { - return path.endsWith(".java") || - path.endsWith(".kt") || - path.endsWith(".groovy") || - path.endsWith(".scala") + generateShadowedSourcesJar( + archiveFile = archiveFile.get().asFile, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + entryCompression = entryCompression, + isZip64 = isZip64, + metadataCharset = metadataCharset, + preserveFileTimestamps = isPreserveFileTimestamps, + ) } public companion object { From 5d4f2c3cc14b28238217cf2da8d49953808b7a6e Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:17:50 +0800 Subject: [PATCH 004/117] Use module g for shadowed sources jar functional test --- .../gradle/plugins/shadow/BasePluginTest.kt | 23 +++++++++++++++---- .../gradle/plugins/shadow/RelocationTest.kt | 21 +++++++++-------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index df71e569f4..9e40ddce29 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -61,6 +61,9 @@ abstract class BasePluginTest { lateinit var artifactBJar: Path private set + lateinit var artifactGJar: Path + private set + val projectScript: Path get() = path("build.gradle") @@ -93,10 +96,6 @@ abstract class BasePluginTest { insert("a.properties", "a") insert("a2.properties", "a2") } - buildSourcesJar { - insert("a/A.java", "package a;\npublic class A {}") - insert("a.properties", "a") - } } val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } @@ -118,6 +117,20 @@ abstract class BasePluginTest { // Circular dependency with e. addDependency(e) } + val g = + jarModule("my", "g", "1.0") { + buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } + buildSourcesJar { + insert( + "g/G.java", + """ + |package g; + |public class G {} + """ + .trimMargin(), + ) + } + } bomModule("my", "bom", "1.0") { addDependency(a) addDependency(b) @@ -125,12 +138,14 @@ abstract class BasePluginTest { addDependency(d) addDependency(e) addDependency(f) + addDependency(g) } } localRepo.publish() artifactAJar = path("my/a/1.0/a-1.0.jar", parent = localRepo.root) artifactBJar = path("my/b/1.0/b-1.0.jar", parent = localRepo.root) + artifactGJar = path("my/g/1.0/g-1.0.jar", parent = localRepo.root) } @BeforeEach diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index c014de5885..9728befb38 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -762,8 +762,9 @@ class RelocationTest : BasePluginTest() { .writeText( """ |package my; + |import g.G; |public class Main { - | String a = "a.A"; + | G g; |} """ .trimMargin() @@ -771,10 +772,10 @@ class RelocationTest : BasePluginTest() { projectScript.appendText( """ |dependencies { - | implementation 'my:a:1.0' + | implementation 'my:g:1.0' |} |$shadowJarTask { - | relocate('a', 'shadow.a') + | relocate('g', 'shadow.g') |} """ .trimMargin() @@ -787,25 +788,25 @@ class RelocationTest : BasePluginTest() { "my/", "my/Main.java", "shadow/", - "shadow/a/", - "shadow/a/A.java", - "shadow/a.properties", + "shadow/g/", + "shadow/g/G.java", ) getContent("my/Main.java") .isEqualTo( """ |package my; + |import shadow.g.G; |public class Main { - | String a = "shadow.a.A"; + | G g; |} """ .trimMargin() ) - getContent("shadow/a/A.java") + getContent("shadow/g/G.java") .isEqualTo( """ - |package shadow.a; - |public class A {} + |package shadow.g; + |public class G {} """ .trimMargin() ) From 1d383df53c62744e2d3d92e6521b27a7ca2297b7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:25:48 +0800 Subject: [PATCH 005/117] Configure sourceSetsSourceDirs in ShadowJavaPlugin and ShadowKmpPlugin --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 4 +++- .../jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt | 3 +++ .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 10 +--------- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index b97b267838..a1f8d8d8c2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -36,9 +36,11 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } protected open fun Project.configureShadowJar() { + val mainSourceSet = sourceSets.named("main") val taskProvider = registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> - task.from(sourceSets.named("main").map { it.output }) + task.from(mainSourceSet.map { it.output }) + task.sourceSetsSourceDirs.convention(mainSourceSet.map { it.allSource.srcDirs }) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index c8bb3a257e..68c1d389f0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -36,6 +36,9 @@ public abstract class ShadowKmpPlugin : Plugin { val kotlinJvmMain = target.compilations.named("main") registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) + task.sourceSetsSourceDirs.convention( + kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + ) task.configurations.convention( kotlinJvmMain .flatMap { configurations.named(it.runtimeDependencyConfigurationName) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 961b9b3ac8..fcf5e71fd3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -64,7 +64,6 @@ import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity -import org.gradle.api.tasks.SourceSetContainer import org.gradle.api.tasks.TaskAction import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider @@ -201,14 +200,7 @@ public abstract class ShadowJar : Jar() { @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) - internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection { - val sourceSets = project.extensions.findByType(SourceSetContainer::class.java) - if (sourceSets != null) { - sourceSets.named("main").map { it.allSource.srcDirs } - } else { - emptySet() - } - } + internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() /** * Enables auto relocation of packages in the dependencies. From 59bcafa54abf95ec177eaf9cf5d82757de14c1a8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:32:15 +0800 Subject: [PATCH 006/117] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc196d0de..1c5717d28f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Support manifest header relocation via configurable `attributesToRelocate` property. - Allow disabling default ProGuard rules in R8 minimization with `R8Spec.useDefaultRules`. ([#2252](https://github.com/GradleUp/shadow/pull/2252)) - Allow passing classpath files to R8 minimization with `R8Spec.classpath`. ([#2255](https://github.com/GradleUp/shadow/pull/2255)) +- Support shadowed sources JAR. ([#2265](https://github.com/GradleUp/shadow/pull/2265)) ### Changed From 04118a4ae03257f372e06c7eb0cafc08067db79a Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 17:40:57 +0800 Subject: [PATCH 007/117] Allow generating shadowed sources jar when project sources are present even without dependency sources --- .../gradle/plugins/shadow/RelocationTest.kt | 25 +++++++++++++++++-- .../shadow/internal/ShadowSourcesJar.kt | 5 +++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 9728befb38..544bb674a4 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -814,7 +814,7 @@ class RelocationTest : BasePluginTest() { } @Test - fun skipShadowedSourcesJarWhenNoIncludedSourcesJars() { + fun generateShadowedSourcesJarWhenNoIncludedSourcesJars() { writeClass() projectScript.appendText( """ @@ -825,7 +825,28 @@ class RelocationTest : BasePluginTest() { .trimMargin() ) - runWithSuccess("clean", shadowJarPath) + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + ) + } + } + + @Test + fun skipShadowedSourcesJarWhenNoSources() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 79ca417994..60d5343f04 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -17,7 +17,10 @@ internal fun generateShadowedSourcesJar( preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } - if (sourcesJars.isEmpty()) return + val hasProjectSources = sourceSetsSourceDirs.any { + it.exists() && it.walkTopDown().any(File::isFile) + } + if (!hasProjectSources && sourcesJars.isEmpty()) return val sourcesJarFile = archiveFile.parentFile.resolve( From 8d99b4144985a717e632ec84c1edbb6fd811b4a7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 18:04:55 +0800 Subject: [PATCH 008/117] Support publishing shadow sources jar --- api/shadow.api | 3 + .../gradle/plugins/shadow/PublishingTest.kt | 69 +++++++++++++++++-- .../gradle/plugins/shadow/RelocationTest.kt | 6 +- .../shadow/util/GradleModuleMetadata.kt | 4 ++ .../plugins/shadow/ShadowApplicationPlugin.kt | 6 +- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 47 +++++++++++++ .../shadow/internal/ShadowSourcesJar.kt | 12 +--- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 24 ++++++- 8 files changed, 148 insertions(+), 23 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 5ce64b4d71..b7118a4f6b 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -58,6 +58,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugi public static final field COMPONENT_NAME Ljava/lang/String; public static final field Companion Lcom/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin$Companion; public static final field SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME Ljava/lang/String; + public static final field SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME Ljava/lang/String; public fun (Lorg/gradle/api/component/SoftwareComponentFactory;)V public synthetic fun apply (Ljava/lang/Object;)V public fun apply (Lorg/gradle/api/Project;)V @@ -69,6 +70,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugi public final class com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin$Companion { public final synthetic fun getShadowRuntimeElements (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider; + public final synthetic fun getShadowSourcesElements (Lorg/gradle/api/artifacts/ConfigurationContainer;)Lorg/gradle/api/NamedDomainObjectProvider; } public abstract class com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin : org/gradle/api/Plugin { @@ -259,6 +261,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getAddMultiReleaseAttribute ()Lorg/gradle/api/provider/Property; public fun getApiJars ()Lorg/gradle/api/file/ConfigurableFileCollection; protected abstract fun getArchiveOperations ()Lorg/gradle/api/file/ArchiveOperations; + public final fun getArchiveSourcesFile ()Lorg/gradle/api/file/RegularFileProperty; public fun getConfigurations ()Lorg/gradle/api/provider/SetProperty; public fun getDependencyFilter ()Lorg/gradle/api/provider/Property; public fun getDuplicatesStrategy ()Lorg/gradle/api/file/DuplicatesStrategy; diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index ea6d376d7d..6018cbb55a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -9,6 +9,7 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath @@ -35,6 +36,7 @@ import org.apache.maven.model.io.xpp3.MavenXpp3Reader import org.gradle.api.JavaVersion import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType import org.gradle.api.attributes.LibraryElements import org.gradle.api.attributes.Usage import org.gradle.api.attributes.java.TargetJvmVersion @@ -279,10 +281,17 @@ class PublishingTest : BasePluginTest() { "maven-1.0.jar.sha512", "maven-1.0.module.sha512", "maven-1.0.pom.sha512", + "maven-1.0-sources.jar", + "maven-1.0-sources.jar.md5", + "maven-1.0-sources.jar.sha1", + "maven-1.0-sources.jar.sha256", + "maven-1.0-sources.jar.sha512", ) assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) - assertShadowVariantCommon(gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module"))) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } @Test @@ -413,11 +422,18 @@ class PublishingTest : BasePluginTest() { "my-artifact-2.0-my-classifier.my-ext.md5", "my-artifact-2.0.pom.md5", "my-artifact-2.0.pom.sha1", + "my-artifact-2.0-sources.my-ext", + "my-artifact-2.0-sources.my-ext.md5", + "my-artifact-2.0-sources.my-ext.sha1", + "my-artifact-2.0-sources.my-ext.sha256", + "my-artifact-2.0-sources.my-ext.sha512", ) assertShadowJarCommon(repoJarPath("$artifactRoot/my-artifact-2.0-my-classifier.my-ext")) assertPomCommon(repoPath("$artifactRoot/my-artifact-2.0.pom")) - assertShadowVariantCommon(gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module"))) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module")) + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } @Test @@ -471,6 +487,12 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", + // Entries of maven-1.0-sources.jar + "maven-1.0-sources.jar", + "maven-1.0-sources.jar.md5", + "maven-1.0-sources.jar.sha1", + "maven-1.0-sources.jar.sha256", + "maven-1.0-sources.jar.sha512", ) assertThat(repoPath("my/maven-all/1.0").entries) .containsOnly( @@ -489,6 +511,12 @@ class PublishingTest : BasePluginTest() { "maven-all-1.0-all.jar.sha512", "maven-all-1.0.module.sha512", "maven-all-1.0.pom.sha512", + // Entries of maven-all-1.0-sources.jar + "maven-all-1.0-sources.jar", + "maven-all-1.0-sources.jar.md5", + "maven-all-1.0-sources.jar.sha1", + "maven-all-1.0-sources.jar.sha256", + "maven-all-1.0-sources.jar.sha512", ) assertThat(repoJarPath("my/maven/1.0/maven-1.0.jar")).useAll { containsOnly(*manifestEntries) } @@ -498,12 +526,13 @@ class PublishingTest : BasePluginTest() { assertPomCommon(repoPath("my/maven/1.0/maven-1.0.pom"), arrayOf("my:a:1.0", "my:b:1.0")) gmmAdapter.fromJson(repoPath("my/maven/1.0/maven-1.0.module")).let { gmm -> - // apiElements, runtimeElements, shadowRuntimeElements + // apiElements, runtimeElements, shadowRuntimeElements, shadowSourcesElements assertThat(gmm.variantNames) .containsOnly( API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertThat(gmm.apiElementsVariant).all { transform { it.attributes } @@ -524,12 +553,18 @@ class PublishingTest : BasePluginTest() { transform { it.coordinates }.containsOnly("my:a:1.0", "my:b:1.0") } assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } assertPomCommon(repoPath("my/maven-all/1.0/maven-all-1.0.pom")) gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")).let { gmm -> - assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + assertThat(gmm.variantNames) + .containsOnly( + SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, + ) assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) } } @@ -622,6 +657,11 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", + "maven-1.0-sources.jar", + "maven-1.0-sources.jar.md5", + "maven-1.0-sources.jar.sha1", + "maven-1.0-sources.jar.sha256", + "maven-1.0-sources.jar.sha512", *entriesCommon, ) assertThat(gmm.variantNames) @@ -629,9 +669,11 @@ class PublishingTest : BasePluginTest() { API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertVariantsCommon(gmm) assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) assertThat(pomDependencies).containsOnly("my:a:1.0" to "runtime", "my:b:1.0" to "compile") } else { assertThat(artifactEntries).containsOnly(*entriesCommon) @@ -722,6 +764,17 @@ class PublishingTest : BasePluginTest() { } } + private fun assertShadowSourcesVariantCommon( + gmm: GradleModuleMetadata, + variantAttrs: Array> = shadowSourcesVariantAttrs, + body: Assert.() -> Unit = {}, + ) { + assertThat(gmm.shadowSourcesElementsVariant).all { + transform { it.attributes }.containsOnly(*variantAttrs) + body() + } + } + private fun assertShadowJarCommon(jarPath: JarPath) { assertThat(jarPath).useAll { containsAtLeast(*entriesInA) @@ -752,6 +805,14 @@ class PublishingTest : BasePluginTest() { Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, ) + val shadowSourcesVariantAttrs = + arrayOf( + Category.CATEGORY_ATTRIBUTE.name to Category.DOCUMENTATION, + Bundling.BUNDLING_ATTRIBUTE.name to Bundling.SHADOWED, + DocsType.DOCS_TYPE_ATTRIBUTE.name to DocsType.SOURCES, + Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, + ) + fun MavenXpp3Reader.read(path: Path): Model = path.inputStream().use { read(it) } fun JsonAdapter.fromJson(path: Path): T = checkNotNull(fromJson(path.readText())) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 544bb674a4..98eac49ead 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -3,7 +3,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo -import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -17,7 +16,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import kotlin.io.path.appendText -import kotlin.io.path.exists import kotlin.io.path.readBytes import kotlin.io.path.writeText import kotlin.time.Duration.Companion.seconds @@ -836,7 +834,7 @@ class RelocationTest : BasePluginTest() { } @Test - fun skipShadowedSourcesJarWhenNoSources() { + fun generateEmptyShadowedSourcesJarWhenNoSources() { projectScript.appendText( """ |dependencies { @@ -848,7 +846,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + assertThat(outputShadowedSourcesJar).useAll { containsOnly() } } private companion object { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt index fef15ab379..4e888c1c2c 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt @@ -1,6 +1,7 @@ package com.github.jengelman.gradle.plugins.shadow.util import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME @@ -18,6 +19,9 @@ data class GradleModuleMetadata(private val variants: List) { val shadowRuntimeElementsVariant: Variant get() = variants.single { it.name == SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME } + val shadowSourcesElementsVariant: Variant + get() = variants.single { it.name == SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME } + val variantNames: List get() = variants.map { it.name } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt index 3221fe1887..3d1a28537c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowApplicationPlugin.kt @@ -46,7 +46,7 @@ public abstract class ShadowApplicationPlugin : Plugin { task.description = "Runs this project as a JVM application using the shadow jar" task.group = ApplicationPlugin.APPLICATION_GROUP - task.classpath = files(tasks.shadowJar) + task.classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) with(applicationExtension) { task.mainModule.convention(mainModule) @@ -63,7 +63,7 @@ public abstract class ShadowApplicationPlugin : Plugin { task.description = "Creates OS specific scripts to run the project as a JVM application using the shadow jar" - task.classpath = files(tasks.shadowJar) + task.classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) @Suppress("InternalGradleApiUsage") // TODO: replace usages of conventionMapping. with(applicationExtension) { @@ -118,7 +118,7 @@ public abstract class ShadowApplicationPlugin : Plugin { dist.contents { distSpec -> distSpec.from(file("src/dist")) distSpec.into("lib") { lib -> - lib.from(tasks.shadowJar) + lib.from(tasks.shadowJar.flatMap { it.archiveFile }) // Reflects the value of the `Class-Path` attribute in the JAR manifest. lib.from(configurations.shadow) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index a1f8d8d8c2..144882372b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -15,6 +15,7 @@ import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType import org.gradle.api.attributes.LibraryElements import org.gradle.api.attributes.Usage import org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE @@ -77,6 +78,35 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl shadowRuntimeElements.outgoing.artifact(tasks.shadowJar) } + val shadowSourcesElements = + configurations.consumable(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME) { shadowSourcesElements + -> + shadowSourcesElements.attributes { attrs -> + attrs.attribute( + Usage.USAGE_ATTRIBUTE, + objects.named(Usage::class.java, Usage.JAVA_RUNTIME), + ) + attrs.attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attrs.attribute( + Bundling.BUNDLING_ATTRIBUTE, + objects.named(Bundling::class.java, Bundling.SHADOWED), + ) + attrs.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + objects.named(DocsType::class.java, DocsType.SOURCES), + ) + } + val sourcesJarFile = tasks.shadowJar.flatMap { it.archiveSourcesFile } + shadowSourcesElements.outgoing.artifact(sourcesJarFile) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.classifier = "sources" + artifact.type = "jar" + } + } + // See more details in #2086. afterEvaluate { if (shadow.addTargetJvmVersionAttribute.get()) { @@ -113,11 +143,13 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl protected open fun Project.configureComponents() { val shadowRuntimeElements = configurations.shadowRuntimeElements + val shadowSourcesElements = configurations.shadowSourcesElements val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) shadowComponent.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> variant.mapToMavenScope("runtime") } + shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) {} components.named("java", AdhocComponentWithVariants::class.java) { component -> component.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> variant.mapToOptional() @@ -128,6 +160,15 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl variant.skip() } } + component.addVariantsFromConfiguration(shadowSourcesElements) { variant -> + variant.mapToOptional() + if (shadow.addShadowVariantIntoJavaComponent.get()) { + logger.info("Adding {} variant to Java component.", shadowSourcesElements.name) + } else { + logger.info("Skipping adding {} variant to Java component.", shadowSourcesElements.name) + variant.skip() + } + } } } @@ -137,10 +178,16 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl public companion object { public const val COMPONENT_NAME: String = SHADOW public const val SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME: String = "shadowRuntimeElements" + public const val SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME: String = "shadowSourcesElements" @get:JvmSynthetic public inline val ConfigurationContainer.shadowRuntimeElements: NamedDomainObjectProvider get() = named(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) + + @get:JvmSynthetic + public inline val ConfigurationContainer.shadowSourcesElements: + NamedDomainObjectProvider + get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 60d5343f04..c4422a08aa 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -7,7 +7,7 @@ import java.nio.charset.Charset import org.gradle.api.tasks.bundling.ZipEntryCompression internal fun generateShadowedSourcesJar( - archiveFile: File, + sourcesJarFile: File, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, @@ -17,15 +17,7 @@ internal fun generateShadowedSourcesJar( preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } - val hasProjectSources = sourceSetsSourceDirs.any { - it.exists() && it.walkTopDown().any(File::isFile) - } - if (!hasProjectSources && sourcesJars.isEmpty()) return - - val sourcesJarFile = - archiveFile.parentFile.resolve( - "${archiveFile.nameWithoutExtension}-sources.${archiveFile.extension}" - ) + if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index fcf5e71fd3..b937fcdae7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -52,7 +52,7 @@ import org.gradle.api.file.DuplicatesStrategy.EXCLUDE import org.gradle.api.file.DuplicatesStrategy.FAIL import org.gradle.api.file.DuplicatesStrategy.INCLUDE import org.gradle.api.file.DuplicatesStrategy.INHERIT -import org.gradle.api.file.DuplicatesStrategy.WARN +import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.CacheableTask @@ -62,6 +62,7 @@ import org.gradle.api.tasks.InputFiles import org.gradle.api.tasks.Internal import org.gradle.api.tasks.Nested import org.gradle.api.tasks.Optional +import org.gradle.api.tasks.OutputFile import org.gradle.api.tasks.PathSensitive import org.gradle.api.tasks.PathSensitivity import org.gradle.api.tasks.TaskAction @@ -198,6 +199,24 @@ public abstract class ShadowJar : Jar() { } } + @get:Optional + @get:OutputFile + public val archiveSourcesFile: RegularFileProperty = + objectFactory + .fileProperty() + .convention( + destinationDirectory.file( + archiveFileName.map { name -> + val idx = name.lastIndexOf('.') + if (idx != -1) { + "${name.substring(0, idx)}-sources${name.substring(idx)}" + } else { + "$name-sources" + } + } + ) + ) + @get:InputFiles @get:PathSensitive(PathSensitivity.RELATIVE) internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() @@ -755,8 +774,9 @@ public abstract class ShadowJar : Jar() { } private fun generateShadowedSourcesJar() { + if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( - archiveFile = archiveFile.get().asFile, + sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs.files, includedSourcesJars = includedSourcesJars.files, relocators = relocators.get() + packageRelocators, From 85f1aa5611d6910efeaf915ce5e14ee1df88f003 Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 18:20:51 +0800 Subject: [PATCH 009/117] Remove outdated comment for applyToSourceContent --- .../gradle/plugins/shadow/relocation/SimpleRelocator.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 85384f0699..c9677d0d69 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -127,10 +127,6 @@ constructor( return if (rawString) clazz else clazz.replaceFirst(pattern.toRegex(), shadedPattern) } - /** - * We don't call this function now, so we don't have to expose [sourcePackageExcludes] and - * [sourcePathExcludes] as inputs. - */ override fun applyToSourceContent(sourceContent: String): String { if (rawString) return sourceContent val content = From 93c8d73550eb75a302e3507a4aba3224a7bb789f Mon Sep 17 00:00:00 2001 From: Goooler Date: Tue, 1 Sep 2026 18:32:10 +0800 Subject: [PATCH 010/117] Add documentation and test for generating Javadoc/Dokka from shadowed sources --- build.gradle.kts | 1 + docs/publishing/README.md | 74 +++++++++++++++++++ gradle/libs.versions.toml | 4 +- .../gradle/plugins/shadow/JavaPluginsTest.kt | 42 +++++++++++ .../plugins/shadow/KotlinPluginsTest.kt | 52 +++++++++++++ 5 files changed, 172 insertions(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index bb05feec52..b5de68abaa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -140,6 +140,7 @@ dependencies { testPluginRuntimeOnly(libs.foojayResolver) testPluginRuntimeOnly(libs.pluginPublish) + testPluginRuntimeOnly(libs.dokka) lintChecks(libs.androidx.gradlePluginLints) } diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 2ec088dfd5..bc6c4eb699 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -515,6 +515,79 @@ customizable properties listed in [Configuring Output Name][configuring-output-n We modified `archiveClassifier`, `archiveExtension` and `archiveBaseName` in this example, the published artifact will be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. +## Generating Javadoc or Dokka from Shadowed Sources + +When creating fat / shadowed libraries, you may want to generate a complete Javadoc or Dokka JAR covering both your +project sources and shadowed dependency sources with relocated packages. + +Because `shadowJar` outputs the shadowed sources archive at `archiveSourcesFile` (where relocated packages and +source contents have already been transformed), you can configure the `javadoc` task (or Dokka task) to consume the +shadowed sources and classes directly from `shadowJar`. The generated documentation will reflect the relocated package +names (e.g. `shadow.g.G` instead of `g.G`). + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.javadoc { + source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) + classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('javadoc', Javadoc) { + source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + } + ``` + +If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed sources and configure `sourceRoots`: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + kotlin("jvm") + id("com.gradleup.shadow") + id("org.jetbrains.dokka") + } + + val extractShadowedSources = tasks.register("extractShadowedSources") { + from(zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile })) + into(layout.buildDirectory.dir("extracted-shadowed-sources")) + } + + dokka { + dokkaSourceSets.configureEach { + sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) + classpath.setFrom(tasks.shadowJar.flatMap { it.archiveFile }) + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'org.jetbrains.kotlin.jvm' + id 'com.gradleup.shadow' + id 'org.jetbrains.dokka' + } + + tasks.register('extractShadowedSources', Sync) { + from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + into layout.buildDirectory.dir('extracted-shadowed-sources') + } + + dokka { + dokkaSourceSets.configureEach { + sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + } + } + ``` [Jar]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) @@ -522,3 +595,4 @@ be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name +[dokka]: https://kotlinlang.org/docs/dokka-introduction.html diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62a63a1088..323d4df3f7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ minGradle = "9.4.0" kotlin = "2.4.10" moshi = "1.15.2" pluginPublish = "2.1.1" +dokka = "2.2.0" [libraries] apache-ant = "org.apache.ant:ant:1.10.17" @@ -22,6 +23,7 @@ foojayResolver = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.to develocity = "com.gradle:develocity-gradle-plugin:4.5.0" kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } pluginPublish = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "pluginPublish" } +dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } androidx-gradlePluginLints = "androidx.lint:lint-gradle:1.0.0" # Dummy to get renovate updates, the version is used in rootProject build.gradle with spotless. @@ -34,7 +36,7 @@ assertk = "com.willowtreeapps.assertk:assertk:0.28.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } android-lint = "com.android.lint:9.4.0" -jetbrains-dokka = "org.jetbrains.dokka:2.2.0" +jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } mavenPublish = "com.vanniktech.maven.publish:0.37.0" pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "pluginPublish" } spotless = "com.diffplug.spotless:8.10.1" diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index b55e52a462..6dbd62a5d3 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -9,6 +9,7 @@ import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.assertions.isNull +import assertk.assertions.isTrue import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.Companion.ENABLE_DEVELOCITY_INTEGRATION_PROPERTY import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey @@ -27,6 +28,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.prependText import kotlin.io.path.appendText import kotlin.io.path.deleteExisting +import kotlin.io.path.exists import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.name import kotlin.io.path.outputStream @@ -1326,6 +1328,46 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun generateJavadocFromShadowedSourcesJar() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |/** Main class doc */ + |public class Main { + | /** Main method doc */ + | public static void main(String[] args) {} + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + | shadow 'my:g:1.0' + |} + | + |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + | relocate 'g', 'shadow.g' + |} + | + |tasks.named('javadoc', Javadoc) { + | source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + | classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + |} + """ + .trimMargin() + ) + + runWithSuccess("javadoc") + + val javadocDir = projectRoot.resolve("build/docs/javadoc") + assertThat(javadocDir.resolve("my/Main.html").exists()).isTrue() + assertThat(javadocDir.resolve("shadow/g/G.html").exists()).isTrue() + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 5105912faa..a8f1a72a96 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -12,6 +12,9 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import kotlin.io.path.appendText +import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.relativeTo +import kotlin.io.path.walk import kotlin.io.path.writeText import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -279,6 +282,55 @@ class KotlinPluginsTest : BasePluginTest() { ) } + @Test + fun generateDokkaFromShadowedSourcesJar() { + projectScript.writeText( + """ + |plugins { + | id 'org.jetbrains.kotlin.jvm' + | id 'com.gradleup.shadow' + | id 'org.jetbrains.dokka' + |} + |dependencies { + | implementation 'my:g:1.0' + | shadow 'my:g:1.0' + |} + |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + | relocate 'g', 'shadow.g' + |} + |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { + | from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + | into layout.buildDirectory.dir('extracted-shadowed-sources') + |} + |dokka { + | dokkaSourceSets.configureEach { + | sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + | classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + | } + |} + """ + .trimMargin() + ) + path("src/main/kotlin/my/Main.kt") + .writeText( + """ + |package my + |/** Main class doc */ + |class Main + """ + .trimMargin() + ) + + runWithSuccess("dokkaGenerateHtml") + + val dokkaDir = projectRoot.resolve("build/dokka/html") + val dokkaFiles = + dokkaDir.walk().map { it.relativeTo(dokkaDir).invariantSeparatorsPathString }.toList() + assertThat(dokkaFiles).contains("index.html") + assertThat(dokkaFiles).contains("my/my/-main/index.html") + assertThat(dokkaFiles).contains("my/shadow.g/-g/index.html") + } + private fun compileOnlyStdlib(exclude: Boolean): String { return if (exclude) { // Disable the stdlib dependency added via `implementation`. From ecad8e4837bc50837f3f463f81fed4c1e5e7feab Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 09:48:43 +0800 Subject: [PATCH 011/117] Clean up ShadowJavaPlugin --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 124 ++++++++++-------- 1 file changed, 66 insertions(+), 58 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 144882372b..a59d87fdd7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -21,7 +21,9 @@ import org.gradle.api.attributes.Usage import org.gradle.api.attributes.java.TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE import org.gradle.api.component.AdhocComponentWithVariants import org.gradle.api.component.SoftwareComponentFactory +import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME +import org.gradle.api.provider.Provider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -54,14 +56,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl compileClasspath.extendsFrom(shadowConfig) } val shadowRuntimeElements = - configurations.consumable(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) { shadowRuntimeElements - -> - shadowRuntimeElements.extendsFrom(shadowConfig) - shadowRuntimeElements.attributes { attrs -> - attrs.attribute( - Usage.USAGE_ATTRIBUTE, - objects.named(Usage::class.java, Usage.JAVA_RUNTIME), - ) + registerConsumableConfiguration(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) { + extendsFrom(shadowConfig) + attributes { attrs -> attrs.attribute( Category.CATEGORY_ATTRIBUTE, objects.named(Category::class.java, Category.LIBRARY), @@ -70,42 +67,26 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements::class.java, LibraryElements.JAR), ) - attrs.attributeProvider( - Bundling.BUNDLING_ATTRIBUTE, - shadow.bundlingAttribute.map { attr -> objects.named(Bundling::class.java, attr) }, - ) } - shadowRuntimeElements.outgoing.artifact(tasks.shadowJar) + outgoing.artifact(tasks.shadowJar) } - - val shadowSourcesElements = - configurations.consumable(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME) { shadowSourcesElements - -> - shadowSourcesElements.attributes { attrs -> - attrs.attribute( - Usage.USAGE_ATTRIBUTE, - objects.named(Usage::class.java, Usage.JAVA_RUNTIME), - ) - attrs.attribute( - Category.CATEGORY_ATTRIBUTE, - objects.named(Category::class.java, Category.DOCUMENTATION), - ) - attrs.attribute( - Bundling.BUNDLING_ATTRIBUTE, - objects.named(Bundling::class.java, Bundling.SHADOWED), - ) - attrs.attribute( - DocsType.DOCS_TYPE_ATTRIBUTE, - objects.named(DocsType::class.java, DocsType.SOURCES), - ) - } - val sourcesJarFile = tasks.shadowJar.flatMap { it.archiveSourcesFile } - shadowSourcesElements.outgoing.artifact(sourcesJarFile) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.classifier = "sources" - artifact.type = "jar" - } + registerConsumableConfiguration(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME) { + attributes { attrs -> + attrs.attribute( + Category.CATEGORY_ATTRIBUTE, + objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attrs.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + objects.named(DocsType::class.java, DocsType.SOURCES), + ) } + outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.classifier = "sources" + artifact.type = "jar" + } + } // See more details in #2086. afterEvaluate { @@ -151,27 +132,54 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) {} components.named("java", AdhocComponentWithVariants::class.java) { component -> - component.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> - variant.mapToOptional() - if (shadow.addShadowVariantIntoJavaComponent.get()) { - logger.info("Adding {} variant to Java component.", shadowRuntimeElements.name) - } else { - logger.info("Skipping adding {} variant to Java component.", shadowRuntimeElements.name) - variant.skip() - } - } - component.addVariantsFromConfiguration(shadowSourcesElements) { variant -> - variant.mapToOptional() - if (shadow.addShadowVariantIntoJavaComponent.get()) { - logger.info("Adding {} variant to Java component.", shadowSourcesElements.name) - } else { - logger.info("Skipping adding {} variant to Java component.", shadowSourcesElements.name) - variant.skip() - } + val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent + component.addVariants( + addIntoJavaComponent = addIntoJavaComponent, + outgoingConfiguration = shadowRuntimeElements, + logger = logger, + ) + component.addVariants( + addIntoJavaComponent = addIntoJavaComponent, + outgoingConfiguration = shadowSourcesElements, + logger = logger, + ) + } + } + + private fun AdhocComponentWithVariants.addVariants( + addIntoJavaComponent: Provider, + outgoingConfiguration: NamedDomainObjectProvider, + logger: Logger, + ) { + addVariantsFromConfiguration(outgoingConfiguration) { variant -> + variant.mapToOptional() + if (addIntoJavaComponent.get()) { + logger.info("Adding {} variant to Java component.", outgoingConfiguration.name) + } else { + logger.info("Skipping adding {} variant to Java component.", outgoingConfiguration.name) + variant.skip() } } } + private fun Project.registerConsumableConfiguration( + name: String, + action: ConsumableConfiguration.() -> Unit, + ) = + configurations.consumable(name) { configuration -> + configuration.attributes { attrs -> + attrs.attribute( + Usage.USAGE_ATTRIBUTE, + objects.named(Usage::class.java, Usage.JAVA_RUNTIME), + ) + attrs.attributeProvider( + Bundling.BUNDLING_ATTRIBUTE, + shadow.bundlingAttribute.map { attr -> objects.named(Bundling::class.java, attr) }, + ) + } + configuration.action() + } + @Deprecated("This method will be removed in Shadow 10.") protected open fun Project.configureJavaGradlePlugin() {} From 68c2e1b8cf704da29eef898dc80c03f30a95838b Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 11:16:17 +0800 Subject: [PATCH 012/117] Set Dokka properties via gradle.properties as workaround for IP --- .../gradle/plugins/shadow/SnippetExecutable.kt | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt index 244bc68510..64bf20ce07 100644 --- a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt +++ b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt @@ -51,6 +51,23 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + // TODO: https://github.com/Kotlin/dokka/issues/4488 + projectRoot + .resolve("gradle.properties") + .writeText( + """ + |# Dokka 2.2.0 DGPv2 is the default, but the plugin still looks up these properties dynamically. + |# Setting them here avoids cross-project property lookups that break isolated projects. + |org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled + |org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true + |org.jetbrains.dokka.experimental.gradle.pluginMode.nowarn=true + |org.jetbrains.dokka.experimental.tryK2=true + |org.jetbrains.dokka.experimental.tryK2.noWarn=true + |org.jetbrains.dokka.experimental.tryK2.nowarn=true + |org.jetbrains.dokka.internal.enableWorkaroundKT80551=true + """ + .trimMargin() + ) val pluginsBlock = """ |plugins { From 0f814e5f5bd9d1fc5a484048836274b83d1b6b46 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 17:51:48 +0800 Subject: [PATCH 013/117] Clean docs/publishing/README.md --- docs/publishing/README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index bc6c4eb699..1a5dafbbef 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -520,17 +520,17 @@ be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. When creating fat / shadowed libraries, you may want to generate a complete Javadoc or Dokka JAR covering both your project sources and shadowed dependency sources with relocated packages. -Because `shadowJar` outputs the shadowed sources archive at `archiveSourcesFile` (where relocated packages and -source contents have already been transformed), you can configure the `javadoc` task (or Dokka task) to consume the -shadowed sources and classes directly from `shadowJar`. The generated documentation will reflect the relocated package -names (e.g. `shadow.g.G` instead of `g.G`). +Because `shadowJar` outputs the shadowed sources archive at `archiveSourcesFile` (where relocated packages and source +contents have already been transformed), you can configure the `javadoc` task (or Dokka task) to consume the shadowed +sources and classes directly from `shadowJar`. The generated documentation will reflect the relocated package names +(e.g. `shadow.com.Example` instead of `com.Example`). === ":material-language-kotlin: build.gradle.kts" ```kotlin tasks.javadoc { - source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) + source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) } ``` @@ -538,8 +538,8 @@ names (e.g. `shadow.g.G` instead of `g.G`). ```groovy tasks.named('javadoc', Javadoc) { - source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) } ``` @@ -550,8 +550,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source ```kotlin plugins { kotlin("jvm") - id("com.gradleup.shadow") id("org.jetbrains.dokka") + id("com.gradleup.shadow") } val extractShadowedSources = tasks.register("extractShadowedSources") { @@ -561,8 +561,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source dokka { dokkaSourceSets.configureEach { - sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) classpath.setFrom(tasks.shadowJar.flatMap { it.archiveFile }) + sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) } } ``` @@ -572,8 +572,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source ```groovy plugins { id 'org.jetbrains.kotlin.jvm' - id 'com.gradleup.shadow' id 'org.jetbrains.dokka' + id 'com.gradleup.shadow' } tasks.register('extractShadowedSources', Sync) { @@ -583,8 +583,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source dokka { dokkaSourceSets.configureEach { - sourceRoots.from(extractShadowedSources.map { it.destinationDir }) classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + sourceRoots.from(extractShadowedSources.map { it.destinationDir }) } } ``` From f4eab8f3a675125b4ce9919368c6016bbb7c7a2d Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 17:57:49 +0800 Subject: [PATCH 014/117] Clean up tests --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 25 ++++++------ .../plugins/shadow/KotlinPluginsTest.kt | 38 ++++++++++--------- .../gradle/plugins/shadow/RelocationTest.kt | 4 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 1 - .../gradle/plugins/shadow/testkit/JarPath.kt | 2 +- 5 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 6dbd62a5d3..067cf37c46 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -3,13 +3,13 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.all import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.containsAtLeast import assertk.assertions.containsMatch import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.assertions.isNull -import assertk.assertions.isTrue import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowPlugin.Companion.ENABLE_DEVELOCITY_INTEGRATION_PROPERTY import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey @@ -28,10 +28,11 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.prependText import kotlin.io.path.appendText import kotlin.io.path.deleteExisting -import kotlin.io.path.exists import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.name import kotlin.io.path.outputStream +import kotlin.io.path.relativeTo +import kotlin.io.path.walk import kotlin.io.path.writeText import kotlin.reflect.full.declaredFunctions import kotlin.reflect.jvm.javaMethod @@ -1336,7 +1337,6 @@ class JavaPluginsTest : BasePluginTest() { |package my; |/** Main class doc */ |public class Main { - | /** Main method doc */ | public static void main(String[] args) {} |} """ @@ -1346,16 +1346,13 @@ class JavaPluginsTest : BasePluginTest() { """ |dependencies { | implementation 'my:g:1.0' - | shadow 'my:g:1.0' |} - | - |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + |$shadowJarTask { | relocate 'g', 'shadow.g' |} - | |tasks.named('javadoc', Javadoc) { - | source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) - | classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + | classpath = files($shadowJarTask.flatMap { it.archiveFile }) + | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }) |} """ .trimMargin() @@ -1364,8 +1361,14 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess("javadoc") val javadocDir = projectRoot.resolve("build/docs/javadoc") - assertThat(javadocDir.resolve("my/Main.html").exists()).isTrue() - assertThat(javadocDir.resolve("shadow/g/G.html").exists()).isTrue() + val javadocFiles = + javadocDir.walk().map { it.relativeTo(javadocDir).invariantSeparatorsPathString } + assertThat(javadocFiles) + .containsAtLeast( + "index.html", + "my/Main.html", + "shadow/g/G.html", + ) } private fun dependencies(configuration: String, vararg flags: String): String { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index a8f1a72a96..63f9ccf183 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.containsAtLeast import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME @@ -284,6 +285,15 @@ class KotlinPluginsTest : BasePluginTest() { @Test fun generateDokkaFromShadowedSourcesJar() { + path("src/main/kotlin/my/Main.kt") + .writeText( + """ + |package my + |/** Main class doc */ + |class Main + """ + .trimMargin() + ) projectScript.writeText( """ |plugins { @@ -293,42 +303,34 @@ class KotlinPluginsTest : BasePluginTest() { |} |dependencies { | implementation 'my:g:1.0' - | shadow 'my:g:1.0' |} - |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + |$shadowJarTask { | relocate 'g', 'shadow.g' |} |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { - | from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + | from zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }) | into layout.buildDirectory.dir('extracted-shadowed-sources') |} |dokka { | dokkaSourceSets.configureEach { + | classpath.from($shadowJarTask.flatMap { it.archiveFile }) | sourceRoots.from(extractShadowedSources.map { it.destinationDir }) - | classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) | } |} """ .trimMargin() ) - path("src/main/kotlin/my/Main.kt") - .writeText( - """ - |package my - |/** Main class doc */ - |class Main - """ - .trimMargin() - ) runWithSuccess("dokkaGenerateHtml") val dokkaDir = projectRoot.resolve("build/dokka/html") - val dokkaFiles = - dokkaDir.walk().map { it.relativeTo(dokkaDir).invariantSeparatorsPathString }.toList() - assertThat(dokkaFiles).contains("index.html") - assertThat(dokkaFiles).contains("my/my/-main/index.html") - assertThat(dokkaFiles).contains("my/shadow.g/-g/index.html") + val dokkaFiles = dokkaDir.walk().map { it.relativeTo(dokkaDir).invariantSeparatorsPathString } + assertThat(dokkaFiles) + .containsAtLeast( + "index.html", + "my/my/-main/index.html", + "my/shadow.g/-g/index.html", + ) } private fun compileOnlyStdlib(exclude: Boolean): String { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 98eac49ead..7a4e5f9983 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo @@ -15,6 +16,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain +import com.github.jengelman.gradle.plugins.shadow.testkit.toEntries import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -846,7 +848,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(outputShadowedSourcesJar).useAll { containsOnly() } + assertThat(outputShadowedSourcesJar).useAll { toEntries().isEmpty() } } private companion object { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index b937fcdae7..6ac3176003 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -9,7 +9,6 @@ import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifes import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream -import com.github.jengelman.gradle.plugins.shadow.internal.entries import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index 6ba359b9bd..cb7d8b8e8e 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -122,6 +122,6 @@ fun Assert.runMain( os.toString().invariantEolString } -private fun Assert.toEntries() = transform { actual -> +fun Assert.toEntries() = transform { actual -> actual.entries().toList().map { it.name } } From 935c73b82de782b4b624d297816009f12818c89d Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 18:32:25 +0800 Subject: [PATCH 015/117] Filter unused classes when generating shadowed sources jar --- .../gradle/plugins/shadow/BasePluginTest.kt | 54 +++++++++++++++++++ .../gradle/plugins/shadow/MinimizeTest.kt | 40 ++++++++++++++ .../shadow/internal/ShadowSourcesJar.kt | 38 +++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 22 ++++---- 4 files changed, 145 insertions(+), 9 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 9e40ddce29..98dc1ca93f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -131,6 +131,58 @@ abstract class BasePluginTest { ) } } + val h = + jarModule("my", "h", "1.0") { + buildJar { + insert("h/H.class", createEmptyClassBytes("h/H")) + insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) + } + buildSourcesJar { + insert( + "h/H.java", + """ + |package h; + |public class H {} + """ + .trimMargin(), + ) + insert( + "h/UnusedH.java", + """ + |package h; + |public class UnusedH {} + """ + .trimMargin(), + ) + } + } + val k = + jarModule("my", "k", "1.0") { + buildJar { + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) + insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + } + buildSourcesJar { + insert( + "k/Utils.kt", + """ + |@file:JvmName("CustomUtils") + |package k + |fun util() {} + """ + .trimMargin(), + ) + insert( + "k/UnusedUtils.kt", + """ + |@file:JvmName("CustomUnusedUtils") + |package k + |fun unusedUtil() {} + """ + .trimMargin(), + ) + } + } bomModule("my", "bom", "1.0") { addDependency(a) addDependency(b) @@ -139,6 +191,8 @@ abstract class BasePluginTest { addDependency(e) addDependency(f) addDependency(g) + addDependency(h) + addDependency(k) } } localRepo.publish() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index ebd8e798f5..550c22967f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -126,6 +126,46 @@ class MinimizeTest : BasePluginTest() { } } + @Test + fun minimizeSourcesJar() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |import h.H; + |import k.CustomUtils; + |public class Main { + | H h; + | CustomUtils u; + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:h:1.0' + | implementation 'my:k:1.0' + |} + |$shadowJarTask { + | minimize() + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsAtLeast("my/Main.class", "h/H.class", "k/CustomUtils.class") + containsNone("h/UnusedH.class", "k/CustomUnusedUtils.class") + } + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("my/Main.java", "h/H.java", "k/Utils.kt") + containsNone("h/UnusedH.java", "k/UnusedUtils.kt") + } + } + /** * 'Client', 'Server' and 'junit' are independent. 'junit' is excluded from the minimize step. The * minimize step shall remove 'Client' but not 'junit'. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index c4422a08aa..377ab43a98 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -11,6 +11,7 @@ internal fun generateShadowedSourcesJar( sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, + unusedClasses: Set = emptySet(), entryCompression: ZipEntryCompression, isZip64: Boolean, metadataCharset: String?, @@ -37,6 +38,7 @@ internal fun generateShadowedSourcesJar( .filter { it.isFile } .forEach { file -> val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + if (isUnused(relPath, { file.readText(charset) }, unusedClasses)) return@forEach if (visitedFiles.add(relPath)) { val relocatedPath = relocators.relocatePath(relPath) val bytes = @@ -75,6 +77,15 @@ internal fun generateShadowedSourcesJar( ) { return@forEach } + if ( + isUnused( + name, + { getInputStream(entry).bufferedReader(charset).readText() }, + unusedClasses, + ) + ) { + return@forEach + } val relocatedPath = relocators.relocatePath(name) if (visitedFiles.add(relocatedPath)) { val bytes = @@ -121,6 +132,33 @@ internal fun generateShadowedSourcesJar( } } +private val jvmNameRegex = + Regex( + """@file\s*:\s*(?:\[[^\]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" + ) + +private fun isUnused( + path: String, + sourceContentProvider: () -> String, + unusedClasses: Set, +): Boolean { + if (unusedClasses.isEmpty() || !isSourceFile(path)) return false + val simpleName = path.substringAfterLast('/').substringBeforeLast('.') + val pkg = path.substringBeforeLast('/', "").replace('/', '.') + val className = if (pkg.isEmpty()) simpleName else "$pkg.$simpleName" + if (unusedClasses.contains(className)) return true + + if (path.endsWith(".kt")) { + val text = sourceContentProvider() + val customJvmName = jvmNameRegex.find(text)?.groupValues?.get(1) + val facadeName = customJvmName ?: "${simpleName}Kt" + val facadeClassName = if (pkg.isEmpty()) facadeName else "$pkg.$facadeName" + if (unusedClasses.contains(facadeClassName)) return true + } + + return false +} + private fun isSourceFile(path: String): Boolean { return path.endsWith(".java") || path.endsWith(".kt") || diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 6ac3176003..0299cb4da5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -570,15 +570,16 @@ public abstract class ShadowJar : Jar() { override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { val unusedClasses = if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -772,6 +773,8 @@ public abstract class ShadowJar : Jar() { ) } + private var unusedClasses: Set = emptySet() + private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( @@ -779,6 +782,7 @@ public abstract class ShadowJar : Jar() { sourceSetsSourceDirs = sourceSetsSourceDirs.files, includedSourcesJars = includedSourcesJars.files, relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, entryCompression = entryCompression, isZip64 = isZip64, metadataCharset = metadataCharset, From 476b36d3ba10922d9201c2cbfe4ee22e0ab18aa4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 18:43:18 +0800 Subject: [PATCH 016/117] Extract declared package from source files to determine canonical path and match unused classes --- .../plugins/shadow/KotlinPluginsTest.kt | 30 ++++ .../shadow/internal/ShadowSourcesJar.kt | 136 +++++++++------ .../shadow/internal/ShadowSourcesJarTest.kt | 164 ++++++++++++++++++ 3 files changed, 277 insertions(+), 53 deletions(-) create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 63f9ccf183..852e050f0a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -8,6 +8,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass @@ -333,6 +334,35 @@ class KotlinPluginsTest : BasePluginTest() { ) } + @Test + fun generateShadowedSourcesJarNormalizesPackageDirectory() { + path("src/main/kotlin/FlatFile.kt") + .writeText( + """ + |package my.custom.nested + | + |class FlatClass + """ + .trimMargin() + ) + projectScript.writeText( + """ + |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} + |$shadowJarTask { + | relocate 'my.custom', 'shadow.custom' + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("shadow/custom/nested/FlatFile.kt") + containsNone("FlatFile.kt") + } + } + private fun compileOnlyStdlib(exclude: Boolean): String { return if (exclude) { // Disable the stdlib dependency added via `implementation`. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 377ab43a98..ae5503c278 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -38,26 +38,42 @@ internal fun generateShadowedSourcesJar( .filter { it.isFile } .forEach { file -> val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - if (isUnused(relPath, { file.readText(charset) }, unusedClasses)) return@forEach - if (visitedFiles.add(relPath)) { + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { val relocatedPath = relocators.relocatePath(relPath) - val bytes = - if (isSourceFile(relPath)) { - var text = file.readText(charset) - for (relocator in relocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - file.readBytes() + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) } } } @@ -77,34 +93,42 @@ internal fun generateShadowedSourcesJar( ) { return@forEach } - if ( - isUnused( - name, - { getInputStream(entry).bufferedReader(charset).readText() }, - unusedClasses, - ) - ) { - return@forEach - } - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = - if (isSourceFile(name)) { - var text = getInputStream(entry).bufferedReader(charset).readText() - for (relocator in relocators) { - text = relocator.applyToSourceContent(text) - } - text.toByteArray(charset) - } else { - getInputStream(entry).readBytes() + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) } - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) } } } @@ -132,24 +156,30 @@ internal fun generateShadowedSourcesJar( } } +private val packageRegex = Regex("""(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""") + private val jvmNameRegex = Regex( """@file\s*:\s*(?:\[[^\]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" ) -private fun isUnused( - path: String, - sourceContentProvider: () -> String, +internal fun extractPackage(text: String): String { + val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() + return if (matches.isEmpty()) "" else matches.joinToString(".") +} + +internal fun isUnused( + fileName: String, + pkg: String, + text: String, unusedClasses: Set, ): Boolean { - if (unusedClasses.isEmpty() || !isSourceFile(path)) return false - val simpleName = path.substringAfterLast('/').substringBeforeLast('.') - val pkg = path.substringBeforeLast('/', "").replace('/', '.') + if (unusedClasses.isEmpty()) return false + val simpleName = fileName.substringBeforeLast('.') val className = if (pkg.isEmpty()) simpleName else "$pkg.$simpleName" if (unusedClasses.contains(className)) return true - if (path.endsWith(".kt")) { - val text = sourceContentProvider() + if (fileName.endsWith(".kt")) { val customJvmName = jvmNameRegex.find(text)?.groupValues?.get(1) val facadeName = customJvmName ?: "${simpleName}Kt" val facadeClassName = if (pkg.isEmpty()) facadeName else "$pkg.$facadeName" diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt new file mode 100644 index 0000000000..22806dcf21 --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -0,0 +1,164 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.containsAtLeast +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import java.io.File +import java.util.zip.ZipFile +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +class ShadowSourcesJarTest { + + @Test + fun extractPackageStatements() { + assertThat(extractPackage("package com.example.foo;")).isEqualTo("com.example.foo") + assertThat(extractPackage("package com.example.foo")).isEqualTo("com.example.foo") + assertThat(extractPackage(" package com.example.foo.bar ; ")) + .isEqualTo("com.example.foo.bar") + assertThat( + extractPackage( + """ + /* + * Multi-line header comment. + */ + package com.example.license; + public class License {} + """ + .trimIndent() + ) + ) + .isEqualTo("com.example.license") + assertThat( + extractPackage( + """ + @file:JvmName("MyUtils") + package com.example.annotated + fun test() {} + """ + .trimIndent() + ) + ) + .isEqualTo("com.example.annotated") + assertThat( + extractPackage( + """ + package a + package b.c + class Chained + """ + .trimIndent() + ) + ) + .isEqualTo("a.b.c") + assertThat(extractPackage("public class NoPackage {}")).isEqualTo("") + } + + @Test + fun isUnusedMatching() { + val unusedSet = + setOf( + "com.example.UnusedJava", + "com.example.UnusedKtClass", + "com.example.DefaultFacadeKt", + "com.example.CustomFacade", + ) + + assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", unusedSet)) + .isTrue() + assertThat(isUnused("UsedJava.java", "com.example", "class UsedJava {}", unusedSet)).isFalse() + + assertThat(isUnused("UnusedKtClass.kt", "com.example", "class UnusedKtClass", unusedSet)) + .isTrue() + assertThat( + isUnused( + "DefaultFacade.kt", + "com.example", + "fun topLevel() {}", + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "Utils.kt", + "com.example", + """ + @file:JvmName("CustomFacade") + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "Utils.kt", + "com.example", + """ + @file:kotlin.jvm.JvmName(name = "CustomFacade") + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "UsedUtils.kt", + "com.example", + """ + @file:JvmName("UsedFacade") + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isFalse() + + assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", emptySet())) + .isFalse() + assertThat(isUnused("Main.java", "", "class Main {}", setOf("Main"))).isTrue() + assertThat(isUnused("Main.java", "", "class Main {}", setOf("Other"))).isFalse() + } + + @Test + fun generateShadowedSourcesJarNormalizesPackageDirectory(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + val flatMismatchedFile = srcDir.resolve("Mismatched.kt") + flatMismatchedFile.writeText( + """ + package com.example.nested + class Mismatched + """ + .trimIndent() + ) + + val outputJar = tempDir.resolve("output-sources.jar") + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = listOf(SimpleRelocator("com.example", "shadow.example")), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + assertThat(outputJar.exists()).isTrue() + val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } + assertThat(entries).containsAtLeast("shadow/example/nested/Mismatched.kt") + } +} From e07d3be2479e5417f8a5ee6af05385429eaccc7f Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 18:56:02 +0800 Subject: [PATCH 017/117] Extract createDefaultLocalMavenRepository to LocalMavenRepository.kt --- .../gradle/plugins/shadow/BasePluginTest.kt | 115 +---------------- .../shadow/util/LocalMavenRepository.kt | 119 ++++++++++++++++++ 2 files changed, 121 insertions(+), 113 deletions(-) create mode 100644 src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 98dc1ca93f..ab3a9c1131 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -18,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.util.AppendableMavenRepository import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import com.github.jengelman.gradle.plugins.shadow.util.JvmLang +import com.github.jengelman.gradle.plugins.shadow.util.createDefaultLocalMavenRepository import java.io.Closeable import java.nio.file.Path import java.util.Properties @@ -27,7 +28,6 @@ import kotlin.io.path.appendText import kotlin.io.path.createDirectories import kotlin.io.path.createDirectory import kotlin.io.path.createFile -import kotlin.io.path.createTempDirectory import kotlin.io.path.deleteExisting import kotlin.io.path.deleteRecursively import kotlin.io.path.exists @@ -84,118 +84,7 @@ abstract class BasePluginTest { @BeforeAll fun beforeAll() { - localRepo = - AppendableMavenRepository( - root = createTempDirectory().resolve("local-maven-repo").createDirectories() - ) - .apply { - jarModule("junit", "junit", "3.8.2") { useJar(junitJar) } - val a = - jarModule("my", "a", "1.0") { - buildJar { - insert("a.properties", "a") - insert("a2.properties", "a2") - } - } - val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } - val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } - val d = - jarModule("my", "d", "1.0") { - buildJar { insert("d.properties", "d") } - // Depends on c but c does not depend on d. - addDependency(c) - } - val e = - jarModule("my", "e", "1.0") { - buildJar { insert("e.properties", "e") } - // Circular dependency with f. - addDependency("my:f:1.0") - } - val f = - jarModule("my", "f", "1.0") { - buildJar { insert("f.properties", "f") } - // Circular dependency with e. - addDependency(e) - } - val g = - jarModule("my", "g", "1.0") { - buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } - buildSourcesJar { - insert( - "g/G.java", - """ - |package g; - |public class G {} - """ - .trimMargin(), - ) - } - } - val h = - jarModule("my", "h", "1.0") { - buildJar { - insert("h/H.class", createEmptyClassBytes("h/H")) - insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) - } - buildSourcesJar { - insert( - "h/H.java", - """ - |package h; - |public class H {} - """ - .trimMargin(), - ) - insert( - "h/UnusedH.java", - """ - |package h; - |public class UnusedH {} - """ - .trimMargin(), - ) - } - } - val k = - jarModule("my", "k", "1.0") { - buildJar { - insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) - insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) - } - buildSourcesJar { - insert( - "k/Utils.kt", - """ - |@file:JvmName("CustomUtils") - |package k - |fun util() {} - """ - .trimMargin(), - ) - insert( - "k/UnusedUtils.kt", - """ - |@file:JvmName("CustomUnusedUtils") - |package k - |fun unusedUtil() {} - """ - .trimMargin(), - ) - } - } - bomModule("my", "bom", "1.0") { - addDependency(a) - addDependency(b) - addDependency(c) - addDependency(d) - addDependency(e) - addDependency(f) - addDependency(g) - addDependency(h) - addDependency(k) - } - } - localRepo.publish() + localRepo = createDefaultLocalMavenRepository(junitJar).apply { publish() } artifactAJar = path("my/a/1.0/a-1.0.jar", parent = localRepo.root) artifactBJar = path("my/b/1.0/b-1.0.jar", parent = localRepo.root) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt new file mode 100644 index 0000000000..26dfb6d0e4 --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt @@ -0,0 +1,119 @@ +package com.github.jengelman.gradle.plugins.shadow.util + +import com.github.jengelman.gradle.plugins.shadow.BasePluginTest.Companion.createEmptyClassBytes +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.createTempDirectory + +fun createDefaultLocalMavenRepository(junitJar: Path): AppendableMavenRepository { + return AppendableMavenRepository( + root = createTempDirectory().resolve("local-maven-repo").createDirectories() + ) + .apply { + jarModule("junit", "junit", "3.8.2") { useJar(junitJar) } + val a = + jarModule("my", "a", "1.0") { + buildJar { + insert("a.properties", "a") + insert("a2.properties", "a2") + } + } + val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } + val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } + val d = + jarModule("my", "d", "1.0") { + buildJar { insert("d.properties", "d") } + // Depends on c but c does not depend on d. + addDependency(c) + } + val e = + jarModule("my", "e", "1.0") { + buildJar { insert("e.properties", "e") } + // Circular dependency with f. + addDependency("my:f:1.0") + } + val f = + jarModule("my", "f", "1.0") { + buildJar { insert("f.properties", "f") } + // Circular dependency with e. + addDependency(e) + } + val g = + jarModule("my", "g", "1.0") { + buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } + buildSourcesJar { + insert( + "g/G.java", + """ + |package g; + |public class G {} + """ + .trimMargin(), + ) + } + } + val h = + jarModule("my", "h", "1.0") { + buildJar { + insert("h/H.class", createEmptyClassBytes("h/H")) + insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) + } + buildSourcesJar { + insert( + "h/H.java", + """ + |package h; + |public class H {} + """ + .trimMargin(), + ) + insert( + "h/UnusedH.java", + """ + |package h; + |public class UnusedH {} + """ + .trimMargin(), + ) + } + } + val k = + jarModule("my", "k", "1.0") { + buildJar { + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) + insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + } + buildSourcesJar { + insert( + "k/Utils.kt", + """ + |@file:JvmName("CustomUtils") + |package k + |fun util() {} + """ + .trimMargin(), + ) + insert( + "k/UnusedUtils.kt", + """ + |@file:JvmName("CustomUnusedUtils") + |package k + |fun unusedUtil() {} + """ + .trimMargin(), + ) + } + } + bomModule("my", "bom", "1.0") { + addDependency(a) + addDependency(b) + addDependency(c) + addDependency(d) + addDependency(e) + addDependency(f) + addDependency(g) + addDependency(h) + addDependency(k) + } + } +} From 23adce55337387e88813d9c8930b29467a828990 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:01:49 +0800 Subject: [PATCH 018/117] Pass zos for generateShadowedSourcesJar --- .../shadow/internal/ShadowSourcesJar.kt | 237 ++++++++---------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 35 ++- .../shadow/internal/ShadowSourcesJarTest.kt | 25 +- 3 files changed, 148 insertions(+), 149 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ae5503c278..924708df52 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,155 +4,138 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset -import org.gradle.api.tasks.bundling.ZipEntryCompression internal fun generateShadowedSourcesJar( - sourcesJarFile: File, + zos: TrackingZipOutputStream, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, unusedClasses: Set = emptySet(), - entryCompression: ZipEntryCompression, - isZip64: Boolean, - metadataCharset: String?, - preserveFileTimestamps: Boolean, + charset: Charset = Charsets.UTF_8, + preserveFileTimestamps: Boolean = true, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() - val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 - try { - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - for (srcDir in sourceSetsSourceDirs) { - if (!srcDir.exists()) continue - srcDir - .walkTopDown() - .filter { it.isFile } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } + for (srcDir in sourceSetsSourceDirs) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) } - } - - sourcesJars.forEach { jarFile -> - jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val isSource = isSourceFile(name) - if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = getInputStream(entry).readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } } } + } + } - val entries = zos.entries.map { it.name } - val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() - entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> - if (!added.add(entryName)) return@forEach + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() zos.writeEntry( - name = entryName, + name = relocatedPath, preserveLastModified = preserveFileTimestamps, - lastModified = currentTimeMillis, - unixMode = UnixMode.directory(), - ) + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } } } } - } catch (e: Exception) { - sourcesJarFile.delete() - throw e + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = preserveFileTimestamps, + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) + } } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 0299cb4da5..7732a59692 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -34,6 +34,7 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException +import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -777,17 +778,29 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - generateShadowedSourcesJar( - sourcesJarFile = archiveSourcesFile.get().asFile, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, - includedSourcesJars = includedSourcesJars.files, - relocators = relocators.get() + packageRelocators, - unusedClasses = unusedClasses, - entryCompression = entryCompression, - isZip64 = isZip64, - metadataCharset = metadataCharset, - preserveFileTimestamps = isPreserveFileTimestamps, - ) + val sourcesJarFile = archiveSourcesFile.get().asFile + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + generateShadowedSourcesJar( + zos = zos, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, + charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8, + preserveFileTimestamps = isPreserveFileTimestamps, + ) + } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e + } } public companion object { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt index 22806dcf21..34c5aa036f 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -145,17 +145,20 @@ class ShadowSourcesJarTest { ) val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = listOf(srcDir), - includedSourcesJars = emptyList(), - relocators = listOf(SimpleRelocator("com.example", "shadow.example")), - unusedClasses = emptySet(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = null, - preserveFileTimestamps = true, - ) + outputJar + .createZipOutputStream( + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + encoding = null, + ) + .use { zos -> + generateShadowedSourcesJar( + zos = zos, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = listOf(SimpleRelocator("com.example", "shadow.example")), + ) + } assertThat(outputJar.exists()).isTrue() val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } From 30b2c88714b390c72c3d4b6b27f0b8f84326a128 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:01:52 +0800 Subject: [PATCH 019/117] Revert "Pass zos for generateShadowedSourcesJar" This reverts commit 23adce55337387e88813d9c8930b29467a828990. --- .../shadow/internal/ShadowSourcesJar.kt | 237 ++++++++++-------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 35 +-- .../shadow/internal/ShadowSourcesJarTest.kt | 25 +- 3 files changed, 149 insertions(+), 148 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 924708df52..ae5503c278 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,138 +4,155 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset +import org.gradle.api.tasks.bundling.ZipEntryCompression internal fun generateShadowedSourcesJar( - zos: TrackingZipOutputStream, + sourcesJarFile: File, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, relocators: Iterable, unusedClasses: Set = emptySet(), - charset: Charset = Charsets.UTF_8, - preserveFileTimestamps: Boolean = true, + entryCompression: ZipEntryCompression, + isZip64: Boolean, + metadataCharset: String?, + preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() + val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 - for (srcDir in sourceSetsSourceDirs) { - if (!srcDir.exists()) continue - srcDir - .walkTopDown() - .filter { it.isFile } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) + try { + sourcesJarFile + .createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ) + .use { zos -> + for (srcDir in sourceSetsSourceDirs) { + if (!srcDir.exists()) continue + srcDir + .walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relPath = file.relativeTo(srcDir).invariantSeparatorsPath + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } } - } } - } - } - sourcesJars.forEach { jarFile -> - jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val isSource = isSourceFile(name) - if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) + sourcesJars.forEach { jarFile -> + jarFile.useZip { + entries().toList().forEach { entry -> + if (entry.isDirectory) return@forEach + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach + } + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } } } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = getInputStream(entry).readBytes() + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + val currentTimeMillis = System.currentTimeMillis() + entries.forEach { name -> + name.parentDirectoryEntries().asReversed().forEach { entryName -> + if (!added.add(entryName)) return@forEach zos.writeEntry( - name = relocatedPath, + name = entryName, preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } + lastModified = currentTimeMillis, + unixMode = UnixMode.directory(), + ) } } } - } - } - - val entries = zos.entries.map { it.name } - val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() - entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> - if (!added.add(entryName)) return@forEach - zos.writeEntry( - name = entryName, - preserveLastModified = preserveFileTimestamps, - lastModified = currentTimeMillis, - unixMode = UnixMode.directory(), - ) - } + } catch (e: Exception) { + sourcesJarFile.delete() + throw e } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 7732a59692..0299cb4da5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -34,7 +34,6 @@ import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransform import com.github.jengelman.gradle.plugins.shadow.transformers.ServiceFileTransformer import java.io.File import java.io.IOException -import java.nio.charset.Charset import java.util.GregorianCalendar import java.util.jar.JarFile import java.util.zip.ZipException @@ -778,29 +777,17 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - val sourcesJarFile = archiveSourcesFile.get().asFile - try { - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - generateShadowedSourcesJar( - zos = zos, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, - includedSourcesJars = includedSourcesJars.files, - relocators = relocators.get() + packageRelocators, - unusedClasses = unusedClasses, - charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8, - preserveFileTimestamps = isPreserveFileTimestamps, - ) - } - } catch (e: Exception) { - sourcesJarFile.delete() - throw e - } + generateShadowedSourcesJar( + sourcesJarFile = archiveSourcesFile.get().asFile, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, + entryCompression = entryCompression, + isZip64 = isZip64, + metadataCharset = metadataCharset, + preserveFileTimestamps = isPreserveFileTimestamps, + ) } public companion object { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt index 34c5aa036f..22806dcf21 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -145,20 +145,17 @@ class ShadowSourcesJarTest { ) val outputJar = tempDir.resolve("output-sources.jar") - outputJar - .createZipOutputStream( - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - encoding = null, - ) - .use { zos -> - generateShadowedSourcesJar( - zos = zos, - sourceSetsSourceDirs = listOf(srcDir), - includedSourcesJars = emptyList(), - relocators = listOf(SimpleRelocator("com.example", "shadow.example")), - ) - } + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = listOf(SimpleRelocator("com.example", "shadow.example")), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) assertThat(outputJar.exists()).isTrue() val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } From d231e625c13eef2feff9ba496f54b475840470d6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:09:37 +0800 Subject: [PATCH 020/117] Filter included sources jars based on DependencyFilter include and exclude rules --- .../gradle/plugins/shadow/FilteringTest.kt | 31 +++++++++++++++++++ .../internal/DefaultDependencyFilter.kt | 14 +++++++++ 2 files changed, 45 insertions(+) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index e378ef5524..a5f96da1dc 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -2,6 +2,8 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader +import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import kotlin.io.path.appendText @@ -228,6 +230,35 @@ class FilteringTest : BasePluginTest() { } } + @Test + fun excludeDependencyFromSourcesJar() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + | implementation 'my:h:1.0' + |} + |$shadowJarTask { + | dependencies { + | exclude(dependency('my:h:1.0')) + | } + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedJar).useAll { + containsAtLeast("g/G.class") + containsNone("h/H.class", "h/UnusedH.class") + } + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("g/G.java") + containsNone("h/H.java", "h/UnusedH.java") + } + } + private fun commonAssertions() { assertThat(outputShadowedJar).useAll { containsOnly("c.properties", *entriesInAB, *manifestEntries) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 475788ff2d..2b85cca501 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -34,11 +34,25 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) } private fun resolveSourcesJars(configuration: Configuration): FileCollection { + val includes = mutableSetOf() + val excludes = mutableSetOf() + resolve( + dependencies = configuration.resolvedConfiguration.firstLevelModuleDependencies, + includedDependencies = includes, + excludedDependencies = excludes, + ) val componentIds = configuration.incoming.resolutionResult.allDependencies .filterIsInstance() .map { it.selected.id } .filterIsInstance() + .filter { id -> + includes.any { + it.moduleGroup == id.group && + it.moduleName == id.module && + it.moduleVersion == id.version + } + } .toSet() val files = project.dependencies From 4d0ca8ac22ad5c8fbfe1f84c97219bc855b7e457 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:17:29 +0800 Subject: [PATCH 021/117] Document Shadowed Sources JAR features, publication and configuration --- docs/configuration/minimizing/README.md | 10 ++ docs/getting-started/README.md | 4 +- docs/kotlin-plugins/README.md | 4 + docs/publishing/README.md | 151 ++++++++++++++++++ .../gradle/plugins/shadow/PublishingTest.kt | 93 ++++++++++- 5 files changed, 257 insertions(+), 5 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 0941360749..5a1f3acadd 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -107,6 +107,16 @@ rules published in dependency JARs, for example under `META-INF/proguard`. > Alternatively, if you use [R8 Repackaging][r8-repackaging] (e.g. `-repackageclasses`), R8 applies embedded rules > natively without needing rule rewriting. +> [!NOTE] +> **Shadowed Sources JAR and R8** +> +> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 (`minimize { r8 { ... } }`), +> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR will +> contain all relocated source files without responding to R8 shrinking results. +> +> If you need unused source files to be filtered out of the shadowed sources JAR, use the default dependency analyzer +> minimization (`minimize()`) instead. + === ":material-language-kotlin: build.gradle.kts" ```kotlin diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index f75096494a..0734fdba4b 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -137,8 +137,10 @@ in their build logic), Shadow will automatically configure the following behavio - `META-INF/*.RSA` - `META-INF/versions/**/module-info.class` - `module-info.class` +- Configures the [`ShadowJar`][ShadowJar] task to generate a companion **Shadowed Sources JAR** containing both + project sources and shadowed dependency sources with relocated packages. - Creates and registers the `shadow` component in the project (used for integrating with - [`maven-publish`][maven-publish]). + [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. ## ShadowJar Command Line options diff --git a/docs/kotlin-plugins/README.md b/docs/kotlin-plugins/README.md index e97c39ceb3..c1bac80a35 100644 --- a/docs/kotlin-plugins/README.md +++ b/docs/kotlin-plugins/README.md @@ -139,6 +139,9 @@ automatically configure additional tasks for bundling the shadowed JAR for its ` } ``` +For details on publishing shadowed artifacts and sources JAR in KMP projects, see +[Publishing with Kotlin Multiplatform (KMP)][publishing-with-kmp]. + ## Kotlin Module Metadata Remapping Kotlin module metadata (`.kotlin_module`) files contain information about package parts and facades. When relocating @@ -169,4 +172,5 @@ To explicitly apply this remapping (recommended for future compatibility), add [KotlinModuleMetadataTransformer]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-kotlin-module-metadata-transformer/index.html [dependency-on-the-standard-library]: https://kotlinlang.org/docs/gradle-configure-project.html#dependency-on-the-standard-library [publishing-libraries]: ../publishing/README.md +[publishing-with-kmp]: ../publishing/README.md#publishing-with-kotlin-multiplatform-kmp [running-applications]: ../application-plugin/README.md diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 1a5dafbbef..bcef984282 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -515,6 +515,155 @@ customizable properties listed in [Configuring Output Name][configuring-output-n We modified `archiveClassifier`, `archiveExtension` and `archiveBaseName` in this example, the published artifact will be named `my-artifact-2.0-my-classifier.my-ext` instead of `1.0-all.jar`. +## Shadowed Sources JAR + +When publishing a shadowed library, consumers and IDEs need a corresponding sources JAR to navigate source code and +inspect implementations. A standard sources JAR only contains your project's original un-relocated sources, which +causes broken navigation when consumers reference relocated packages. + +Shadow automatically generates a **Shadowed Sources JAR** containing: + +- Source files from your project's source sets (`Java`, `Kotlin`, `Groovy`, `Scala`). +- Source files resolved and merged from all bundled dependencies' `-sources.jar` archives. +- Relocated package declarations, imports, and symbol references that match your [`relocate`][ShadowJar.relocate] rules. +- Normalized package directory layout matching the declared `package` in each source file. +- Automatic filtering: dependencies excluded in `dependencies { exclude(...) }` or unused classes removed via + `minimize()` are automatically excluded from the shadowed sources JAR as well. + +### Publishing with `withSourcesJar()` + +When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin automatically registers the +`shadowSourcesElements` variant and publishes the shadowed sources JAR alongside the shadowed binary JAR: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + java + `maven-publish` + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + + publishing { + publications { + create("shadow") { + from(components["shadow"]) + } + } + repositories { + maven("https://repo.myorg.com") + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' + } + + java { + withSourcesJar() + } + + publishing { + publications { + shadow(MavenPublication) { + from components.shadow + } + } + repositories { + maven { url = 'https://repo.myorg.com' } + } + } + ``` + +The published Maven publication will include both `--all.jar` and +`--all-sources.jar`. + +### Customizing the Sources Archive File + +The shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], +which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.shadowJar { + archiveSourcesFile = layout.buildDirectory.file("custom-libs/my-sources.jar") + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveSourcesFile = layout.buildDirectory.file('custom-libs/my-sources.jar') + } + ``` + +### Publishing with Kotlin Multiplatform (KMP) + +In Kotlin Multiplatform (KMP) projects, publications are managed by the Kotlin Gradle Plugin (KGP) per target (such as +the `jvm` publication). You can attach the shadowed sources JAR artifact to the `jvm` Maven publication: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + id("org.jetbrains.kotlin.multiplatform") + id("com.gradleup.shadow") + `maven-publish` + } + + kotlin { + jvm() + } + + publishing { + publications { + named("jvm") { + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + } + } + repositories { + maven("https://repo.myorg.com") + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'com.gradleup.shadow' + id 'maven-publish' + } + + kotlin { + jvm() + } + + publishing { + publications { + named('jvm', MavenPublication) { + artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + } + } + repositories { + maven { url = 'https://repo.myorg.com' } + } + } + ``` + ## Generating Javadoc or Dokka from Shadowed Sources When creating fat / shadowed libraries, you may want to generate a complete Javadoc or Dokka JAR covering both your @@ -592,6 +741,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source [Jar]: https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html +[ShadowJar.archiveSourcesFile]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/archive-sources-file.html +[ShadowJar.relocate]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/relocate.html [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 6018cbb55a..f929fb4e62 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -18,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.util.GradleModuleMetadata +import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import com.github.jengelman.gradle.plugins.shadow.util.coordinate import com.github.jengelman.gradle.plugins.shadow.util.prependText import com.squareup.moshi.JsonAdapter @@ -30,6 +31,7 @@ import kotlin.io.path.inputStream import kotlin.io.path.listDirectoryEntries import kotlin.io.path.name import kotlin.io.path.readText +import kotlin.io.path.writeText import org.apache.maven.model.Dependency import org.apache.maven.model.Model import org.apache.maven.model.io.xpp3.MavenXpp3Reader @@ -684,6 +686,87 @@ class PublishingTest : BasePluginTest() { } } + @Test + fun publishKmpWithShadowedSources() { + path("gradle.properties").writeText("kotlin.stdlib.default.dependency=false") + projectScript.writeText( + """ + |plugins { + | id 'org.jetbrains.kotlin.multiplatform' + | id 'com.gradleup.shadow' + | id 'maven-publish' + |} + |group = 'my' + |version = '1.0' + |kotlin { + | jvm() + | sourceSets { + | commonMain { + | dependencies { + | implementation 'my:g:1.0' + | compileOnly 'org.jetbrains.kotlin:kotlin-stdlib' + | } + | } + | jvmMain { + | dependencies { + | implementation 'my:h:1.0' + | } + | } + | } + |} + |$shadowJarTask { + | archiveClassifier = '' + |} + |publishing { + | repositories { + | maven { url = '${remoteRepoPath.toUri()}' } + | } + | publications { + | shadow(MavenPublication) { + | artifactId = 'my-all' + | artifact($shadowJarTask) + | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) + | } + | } + |} + """ + .trimMargin() + ) + writeClass(sourceSet = "commonMain", jvmLang = JvmLang.Kotlin, className = "CommonMain") + writeClass(sourceSet = "jvmMain", jvmLang = JvmLang.Kotlin, className = "JvmMain") + + publish() + + val artifactRoot = "my/my-all/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "my-all-1.0.jar", + "my-all-1.0-sources.jar", + ) + + assertThat(repoJarPath("$artifactRoot/my-all-1.0.jar")).useAll { + containsAtLeast( + "my/CommonMain.class", + "my/JvmMain.class", + "g/G.class", + "h/H.class", + *manifestEntries, + ) + } + + assertThat(repoJarPath("$artifactRoot/my-all-1.0-sources.jar")).useAll { + containsAtLeast( + "my/CommonMain.kt", + "my/JvmMain.kt", + "g/G.java", + "h/H.java", + ) + } + + assertPomCommon(repoPath("$artifactRoot/my-all-1.0.pom"), emptyArray()) + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".module") }).isEmpty() + } + private fun repoPath(relative: String): Path { return remoteRepoPath.resolve(relative).also { check(it.exists()) { "Path not found: $it" } } } @@ -744,10 +827,12 @@ class PublishingTest : BasePluginTest() { private fun assertPomCommon(pomPath: Path, coordinates: Array = arrayOf("my:b:1.0")) { assertThat(pomReader.read(pomPath)).all { transform { it.dependencies.map(Dependency::coordinate) }.containsOnly(*coordinates) - // All scopes should be runtime. - transform { it.dependencies.map(Dependency::getScope).distinct() } - .single() - .isEqualTo("runtime") + if (coordinates.isNotEmpty()) { + // All scopes should be runtime. + transform { it.dependencies.map(Dependency::getScope).distinct() } + .single() + .isEqualTo("runtime") + } } } From 51d5feb5e5003c463ba557047d477902033bad59 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:51:59 +0800 Subject: [PATCH 022/117] Support bundling sources JAR from local subproject dependencies --- .../gradle/plugins/shadow/BasePluginTest.kt | 6 +++ .../gradle/plugins/shadow/FilteringTest.kt | 11 +++++ .../gradle/plugins/shadow/JavaPluginsTest.kt | 8 ++++ .../internal/DefaultDependencyFilter.kt | 41 +++++++++++++++++-- 4 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index ab3a9c1131..71185f1655 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -82,6 +82,9 @@ abstract class BasePluginTest { val outputServerShadowedJar: JarPath get() = jarPath("server/build/libs/server-1.0-all.jar") + val outputServerShadowedSourcesJar: JarPath + get() = jarPath("server/build/libs/server-1.0-all-sources.jar") + @BeforeAll fun beforeAll() { localRepo = createDefaultLocalMavenRepository(junitJar).apply { publish() } @@ -260,6 +263,9 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} + |java { + | withSourcesJar() + |} |dependencies { | implementation 'junit:junit:3.8.2' |} diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index a5f96da1dc..1a0cd3c048 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -184,6 +184,14 @@ class FilteringTest : BasePluginTest() { loadClass("server.Server") } } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly( + "client/", + "server/", + "client/Client.java", + "server/Server.java", + ) + } } @Test @@ -273,5 +281,8 @@ class FilteringTest : BasePluginTest() { loadClass("junit.framework.Test") } } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly("server/", "server/Server.java") + } } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 067cf37c46..2fd03ebc36 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -144,6 +144,14 @@ class JavaPluginsTest : BasePluginTest() { *manifestEntries, ) } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly( + "client/", + "server/", + "client/Client.java", + "server/Server.java", + ) + } } @Test diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 2b85cca501..12edefe997 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -5,8 +5,11 @@ import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ResolvedDependency import org.gradle.api.artifacts.component.ModuleComponentIdentifier +import org.gradle.api.artifacts.component.ProjectComponentIdentifier import org.gradle.api.artifacts.result.ResolvedArtifactResult import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.attributes.Category +import org.gradle.api.attributes.DocsType import org.gradle.api.file.FileCollection import org.gradle.jvm.JvmLibrary import org.gradle.language.base.artifact.SourcesArtifact @@ -45,6 +48,10 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) configuration.incoming.resolutionResult.allDependencies .filterIsInstance() .map { it.selected.id } + .toSet() + + val externalComponentIds = + componentIds .filterIsInstance() .filter { id -> includes.any { @@ -54,16 +61,44 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) } } .toSet() - val files = + + val externalSourcesFiles = project.dependencies .createArtifactResolutionQuery() - .forComponents(componentIds) + .forComponents(externalComponentIds) .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java) .execute() .resolvedComponents .flatMap { it.getArtifacts(SourcesArtifact::class.java) } .filterIsInstance() .map { it.file } - return project.files(files) + + val includedProjectNames = includes.map { it.moduleName }.toSet() + val projectSourcesFiles = + try { + configuration.incoming + .artifactView { view -> + view.withVariantReselection() + view.attributes { attrs -> + attrs.attribute( + Category.CATEGORY_ATTRIBUTE, + project.objects.named(Category::class.java, Category.DOCUMENTATION), + ) + attrs.attribute( + DocsType.DOCS_TYPE_ATTRIBUTE, + project.objects.named(DocsType::class.java, DocsType.SOURCES), + ) + } + view.componentFilter { id -> + id is ProjectComponentIdentifier && id.projectName in includedProjectNames + } + view.lenient(true) + } + .files + } catch (_: Exception) { + project.files() + } + + return project.files(externalSourcesFiles) + projectSourcesFiles } } From 0100ab212984ec1b14fc8eec3aefea68b2e2c546 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 19:56:16 +0800 Subject: [PATCH 023/117] Use Worker API --- api/shadow.api | 3 +- .../shadow/internal/ShadowSourcesJar.kt | 36 ++++++++++ .../plugins/shadow/relocation/Relocator.kt | 3 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 72 +++++++++++-------- 4 files changed, 83 insertions(+), 31 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index b7118a4f6b..9465d5f408 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -128,7 +128,7 @@ public final class com/github/jengelman/gradle/plugins/shadow/relocation/Relocat public static final fun relocatePath (Ljava/lang/Iterable;Ljava/lang/String;)Ljava/lang/String; } -public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator { +public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator : java/io/Serializable { public abstract fun applyToSourceContent (Ljava/lang/String;)Ljava/lang/String; public abstract fun canRelocateClass (Ljava/lang/String;)Z public abstract fun canRelocatePath (Ljava/lang/String;)Z @@ -284,6 +284,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getToMinimize ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getTransformers ()Lorg/gradle/api/provider/SetProperty; + protected abstract fun getWorkerExecutor ()Lorg/gradle/workers/WorkerExecutor; public fun mergeGroovyExtensionModules ()V public final fun mergeServiceFiles ()V public fun mergeServiceFiles (Ljava/lang/String;)V diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ae5503c278..ed8e12aed2 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,7 +4,43 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset +import org.gradle.api.file.ConfigurableFileCollection +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.provider.Property +import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.gradle.workers.WorkAction +import org.gradle.workers.WorkParameters + +internal abstract class GenerateShadowedSourcesJarWorkAction : + WorkAction { + interface Params : WorkParameters { + val sourcesJarFile: RegularFileProperty + val sourceSetsSourceDirs: ConfigurableFileCollection + val includedSourcesJars: ConfigurableFileCollection + val relocators: SetProperty + val unusedClasses: SetProperty + val entryCompression: Property + val zip64: Property + val metadataCharset: Property + val preserveFileTimestamps: Property + } + + override fun execute() { + val params = parameters + generateShadowedSourcesJar( + sourcesJarFile = params.sourcesJarFile.get().asFile, + sourceSetsSourceDirs = params.sourceSetsSourceDirs.files, + includedSourcesJars = params.includedSourcesJars.files, + relocators = params.relocators.get(), + unusedClasses = params.unusedClasses.get(), + entryCompression = params.entryCompression.get(), + isZip64 = params.zip64.get(), + metadataCharset = params.metadataCharset.orNull, + preserveFileTimestamps = params.preserveFileTimestamps.get(), + ) + } +} internal fun generateShadowedSourcesJar( sourcesJarFile: File, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt index 91d5953b24..416fb03d7b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt @@ -2,6 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer +import java.io.Serializable import org.gradle.api.tasks.Input /** @@ -12,7 +13,7 @@ import org.gradle.api.tasks.Input * @author John Engelman */ @ShadowDsl -public interface Relocator { +public interface Relocator : Serializable { public fun canRelocatePath(path: String): Boolean public fun relocatePath(context: RelocatePathContext): String diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 0299cb4da5..bf5318baac 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,11 +7,11 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec +import com.github.jengelman.gradle.plugins.shadow.internal.GenerateShadowedSourcesJarWorkAction import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses -import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService @@ -73,6 +73,7 @@ import org.gradle.api.tasks.options.Option import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.ExecOperations +import org.gradle.workers.WorkerExecutor @ShadowDsl @CacheableTask @@ -85,6 +86,29 @@ public abstract class ShadowJar : Jar() { project.configurations.findByName(ShadowBasePlugin.CONFIGURATION_NAME) ?: project.files() } + @Transient private var _unusedClasses: Set? = null + + private val unusedClasses: Set + get() = + _unusedClasses + ?: (if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + }) + .also { _unusedClasses = it } + + @Transient private var _actualRelocators: Set? = null + + private val actualRelocators: Set + get() = + _actualRelocators ?: (relocators.get() + packageRelocators).also { _actualRelocators = it } + init { group = LifecycleBasePlugin.BUILD_GROUP description = "Create a combined JAR of project and runtime dependencies" @@ -368,6 +392,8 @@ public abstract class ShadowJar : Jar() { @get:Inject protected abstract val archiveOperations: ArchiveOperations + @get:Inject protected abstract val workerExecutor: WorkerExecutor + /** Enable minimization and execute the [action] with the [MinimizeSpec] for minimize. */ @JvmOverloads public open fun minimize(action: Action = Action {}) { @@ -561,25 +587,14 @@ public abstract class ShadowJar : Jar() { override fun copy() { addIncludedDependencies() injectManifestAttributes() + generateShadowedSourcesJar() super.copy() + workerExecutor.await() runR8Minimization() - generateShadowedSourcesJar() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { - val unusedClasses = - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } - .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -613,7 +628,7 @@ public abstract class ShadowJar : Jar() { zipFile = zipFile, zipOutStream = zipOutStream, transformers = actualTransformers, - relocators = relocators.get() + packageRelocators, + relocators = actualRelocators, unusedClasses = unusedClasses, isPreserveFileTimestamps = isPreserveFileTimestamps, failOnDuplicateEntries = failOnDuplicateEntries.get(), @@ -769,25 +784,24 @@ public abstract class ShadowJar : Jar() { javaLauncher = javaLauncher, sourceSetsClassesDirs = sourceSetsClassesDirs, keptDependencyFiles = includedDependencies - toMinimize, - relocators = relocators.get() + packageRelocators, + relocators = actualRelocators, ) } - private var unusedClasses: Set = emptySet() - private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - generateShadowedSourcesJar( - sourcesJarFile = archiveSourcesFile.get().asFile, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, - includedSourcesJars = includedSourcesJars.files, - relocators = relocators.get() + packageRelocators, - unusedClasses = unusedClasses, - entryCompression = entryCompression, - isZip64 = isZip64, - metadataCharset = metadataCharset, - preserveFileTimestamps = isPreserveFileTimestamps, - ) + workerExecutor.noIsolation().submit(GenerateShadowedSourcesJarWorkAction::class.java) { params + -> + params.sourcesJarFile.set(archiveSourcesFile) + params.sourceSetsSourceDirs.from(sourceSetsSourceDirs) + params.includedSourcesJars.from(includedSourcesJars) + params.relocators.set(actualRelocators) + params.unusedClasses.set(unusedClasses) + params.entryCompression.set(entryCompression) + params.zip64.set(isZip64) + params.metadataCharset.set(metadataCharset) + params.preserveFileTimestamps.set(isPreserveFileTimestamps) + } } public companion object { From 09c2cea013cec3a5137ad522276a27488417cdda Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 20:52:33 +0800 Subject: [PATCH 024/117] Revert "Use Worker API" This reverts commit 0100ab212984ec1b14fc8eec3aefea68b2e2c546. ### 1. Test Versions | Commit Hash | Local Version Tag | Description | | :--- | :--- | :--- | | [`51d5feb5`](https://github.com/GradleUp/shadow/commit/51d5feb5e5003c463ba557047d477902033bad59) | `9.0.3-51d5feb5` | **Before Worker API** (single-threaded serial execution) | | [`0100ab21`](https://github.com/GradleUp/shadow/commit/0100ab212984ec1b14fc8eec3aefea68b2e2c546) | `9.0.3-0100ab21` | **After Worker API** (asynchronous parallel execution via Gradle Worker API) | --- ### 2. Detailed 10-Iteration Benchmark Results (Unit: ms) | Iteration | Before Worker API (`51d5feb5`) | After Worker API (`0100ab21`) | | :---: | :---: | :---: | | **Warm-up 1** | 19,131.87 | 7,158.15 | | **Warm-up 2** | 937.74 | 927.13 | | **Warm-up 3** | 867.42 | 892.92 | | **Build 1** | 949.85 | 887.27 | | **Build 2** | 855.78 | 895.07 | | **Build 3** | 870.54 | 913.32 | | **Build 4** | 842.07 | 962.29 | | **Build 5** | 834.61 | 902.32 | | **Build 6** | 895.61 | 863.34 | | **Build 7** | 842.98 | 882.96 | | **Build 8** | 836.82 | 862.21 | | **Build 9** | 843.10 | 888.14 | | **Build 10** | 874.22 | 867.52 | --- ### 3. Summary Statistics | Metric | Before Worker API (`51d5feb5`) | After Worker API (`0100ab21`) | Difference | | :--- | :---: | :---: | :--- | | **Mean** | **864.56 ms** | **892.44 ms** | +27.88 ms (+3.2%) | | **Median** | **849.44 ms** | **891.17 ms** | +41.73 ms (+4.9%) | | **Min** | **834.61 ms** | **862.21 ms** | +27.60 ms | | **Max** | **949.85 ms** | **962.29 ms** | +12.44 ms | --- api/shadow.api | 3 +- .../shadow/internal/ShadowSourcesJar.kt | 36 ---------- .../plugins/shadow/relocation/Relocator.kt | 3 +- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 72 ++++++++----------- 4 files changed, 31 insertions(+), 83 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 9465d5f408..b7118a4f6b 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -128,7 +128,7 @@ public final class com/github/jengelman/gradle/plugins/shadow/relocation/Relocat public static final fun relocatePath (Ljava/lang/Iterable;Ljava/lang/String;)Ljava/lang/String; } -public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator : java/io/Serializable { +public abstract interface class com/github/jengelman/gradle/plugins/shadow/relocation/Relocator { public abstract fun applyToSourceContent (Ljava/lang/String;)Ljava/lang/String; public abstract fun canRelocateClass (Ljava/lang/String;)Z public abstract fun canRelocatePath (Ljava/lang/String;)Z @@ -284,7 +284,6 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getToMinimize ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getTransformers ()Lorg/gradle/api/provider/SetProperty; - protected abstract fun getWorkerExecutor ()Lorg/gradle/workers/WorkerExecutor; public fun mergeGroovyExtensionModules ()V public final fun mergeServiceFiles ()V public fun mergeServiceFiles (Ljava/lang/String;)V diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ed8e12aed2..ae5503c278 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -4,43 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset -import org.gradle.api.file.ConfigurableFileCollection -import org.gradle.api.file.RegularFileProperty -import org.gradle.api.provider.Property -import org.gradle.api.provider.SetProperty import org.gradle.api.tasks.bundling.ZipEntryCompression -import org.gradle.workers.WorkAction -import org.gradle.workers.WorkParameters - -internal abstract class GenerateShadowedSourcesJarWorkAction : - WorkAction { - interface Params : WorkParameters { - val sourcesJarFile: RegularFileProperty - val sourceSetsSourceDirs: ConfigurableFileCollection - val includedSourcesJars: ConfigurableFileCollection - val relocators: SetProperty - val unusedClasses: SetProperty - val entryCompression: Property - val zip64: Property - val metadataCharset: Property - val preserveFileTimestamps: Property - } - - override fun execute() { - val params = parameters - generateShadowedSourcesJar( - sourcesJarFile = params.sourcesJarFile.get().asFile, - sourceSetsSourceDirs = params.sourceSetsSourceDirs.files, - includedSourcesJars = params.includedSourcesJars.files, - relocators = params.relocators.get(), - unusedClasses = params.unusedClasses.get(), - entryCompression = params.entryCompression.get(), - isZip64 = params.zip64.get(), - metadataCharset = params.metadataCharset.orNull, - preserveFileTimestamps = params.preserveFileTimestamps.get(), - ) - } -} internal fun generateShadowedSourcesJar( sourcesJarFile: File, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt index 416fb03d7b..91d5953b24 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/Relocator.kt @@ -2,7 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.transformers.CacheableTransformer -import java.io.Serializable import org.gradle.api.tasks.Input /** @@ -13,7 +12,7 @@ import org.gradle.api.tasks.Input * @author John Engelman */ @ShadowDsl -public interface Relocator : Serializable { +public interface Relocator { public fun canRelocatePath(path: String): Boolean public fun relocatePath(context: RelocatePathContext): String diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index bf5318baac..0299cb4da5 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -7,11 +7,11 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowDsl import com.github.jengelman.gradle.plugins.shadow.internal.DefaultDependencyFilter import com.github.jengelman.gradle.plugins.shadow.internal.DefaultInheritManifest import com.github.jengelman.gradle.plugins.shadow.internal.DefaultMinimizeSpec -import com.github.jengelman.gradle.plugins.shadow.internal.GenerateShadowedSourcesJarWorkAction import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses +import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService @@ -73,7 +73,6 @@ import org.gradle.api.tasks.options.Option import org.gradle.jvm.toolchain.JavaLauncher import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.process.ExecOperations -import org.gradle.workers.WorkerExecutor @ShadowDsl @CacheableTask @@ -86,29 +85,6 @@ public abstract class ShadowJar : Jar() { project.configurations.findByName(ShadowBasePlugin.CONFIGURATION_NAME) ?: project.files() } - @Transient private var _unusedClasses: Set? = null - - private val unusedClasses: Set - get() = - _unusedClasses - ?: (if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - }) - .also { _unusedClasses = it } - - @Transient private var _actualRelocators: Set? = null - - private val actualRelocators: Set - get() = - _actualRelocators ?: (relocators.get() + packageRelocators).also { _actualRelocators = it } - init { group = LifecycleBasePlugin.BUILD_GROUP description = "Create a combined JAR of project and runtime dependencies" @@ -392,8 +368,6 @@ public abstract class ShadowJar : Jar() { @get:Inject protected abstract val archiveOperations: ArchiveOperations - @get:Inject protected abstract val workerExecutor: WorkerExecutor - /** Enable minimization and execute the [action] with the [MinimizeSpec] for minimize. */ @JvmOverloads public open fun minimize(action: Action = Action {}) { @@ -587,14 +561,25 @@ public abstract class ShadowJar : Jar() { override fun copy() { addIncludedDependencies() injectManifestAttributes() - generateShadowedSourcesJar() super.copy() - workerExecutor.await() runR8Minimization() + generateShadowedSourcesJar() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { + val unusedClasses = + if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -628,7 +613,7 @@ public abstract class ShadowJar : Jar() { zipFile = zipFile, zipOutStream = zipOutStream, transformers = actualTransformers, - relocators = actualRelocators, + relocators = relocators.get() + packageRelocators, unusedClasses = unusedClasses, isPreserveFileTimestamps = isPreserveFileTimestamps, failOnDuplicateEntries = failOnDuplicateEntries.get(), @@ -784,24 +769,25 @@ public abstract class ShadowJar : Jar() { javaLauncher = javaLauncher, sourceSetsClassesDirs = sourceSetsClassesDirs, keptDependencyFiles = includedDependencies - toMinimize, - relocators = actualRelocators, + relocators = relocators.get() + packageRelocators, ) } + private var unusedClasses: Set = emptySet() + private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return - workerExecutor.noIsolation().submit(GenerateShadowedSourcesJarWorkAction::class.java) { params - -> - params.sourcesJarFile.set(archiveSourcesFile) - params.sourceSetsSourceDirs.from(sourceSetsSourceDirs) - params.includedSourcesJars.from(includedSourcesJars) - params.relocators.set(actualRelocators) - params.unusedClasses.set(unusedClasses) - params.entryCompression.set(entryCompression) - params.zip64.set(isZip64) - params.metadataCharset.set(metadataCharset) - params.preserveFileTimestamps.set(isPreserveFileTimestamps) - } + generateShadowedSourcesJar( + sourcesJarFile = archiveSourcesFile.get().asFile, + sourceSetsSourceDirs = sourceSetsSourceDirs.files, + includedSourcesJars = includedSourcesJars.files, + relocators = relocators.get() + packageRelocators, + unusedClasses = unusedClasses, + entryCompression = entryCompression, + isZip64 = isZip64, + metadataCharset = metadataCharset, + preserveFileTimestamps = isPreserveFileTimestamps, + ) } public companion object { From 0ca34ccf93f6d506bc088311675f3ae84ff51190 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 21:05:11 +0800 Subject: [PATCH 025/117] Include META-INF/MANIFEST.MF in shadowed sources JAR --- .../jengelman/gradle/plugins/shadow/FilteringTest.kt | 3 ++- .../gradle/plugins/shadow/JavaPluginsTest.kt | 3 ++- .../jengelman/gradle/plugins/shadow/RelocationTest.kt | 6 +++--- .../plugins/shadow/internal/ShadowSourcesJar.kt | 11 +++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 +- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index 1a0cd3c048..f05d4d930c 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -190,6 +190,7 @@ class FilteringTest : BasePluginTest() { "server/", "client/Client.java", "server/Server.java", + *manifestEntries, ) } } @@ -282,7 +283,7 @@ class FilteringTest : BasePluginTest() { } } assertThat(outputServerShadowedSourcesJar).useAll { - containsOnly("server/", "server/Server.java") + containsOnly("server/", "server/Server.java", *manifestEntries) } } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 2fd03ebc36..681d41c6d0 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -150,6 +150,7 @@ class JavaPluginsTest : BasePluginTest() { "server/", "client/Client.java", "server/Server.java", + *manifestEntries, ) } } @@ -1360,7 +1361,7 @@ class JavaPluginsTest : BasePluginTest() { |} |tasks.named('javadoc', Javadoc) { | classpath = files($shadowJarTask.flatMap { it.archiveFile }) - | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }) + | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }).matching { include('**/*.java') } |} """ .trimMargin() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 7a4e5f9983..d7fbfdaa6e 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -2,7 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains -import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo @@ -16,7 +15,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain -import com.github.jengelman.gradle.plugins.shadow.testkit.toEntries import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -790,6 +788,7 @@ class RelocationTest : BasePluginTest() { "shadow/", "shadow/g/", "shadow/g/G.java", + *manifestEntries, ) getContent("my/Main.java") .isEqualTo( @@ -831,6 +830,7 @@ class RelocationTest : BasePluginTest() { containsOnly( "my/", "my/Main.java", + *manifestEntries, ) } } @@ -848,7 +848,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(outputShadowedSourcesJar).useAll { toEntries().isEmpty() } + assertThat(outputShadowedSourcesJar).useAll { containsOnly(*manifestEntries) } } private companion object { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index ae5503c278..053f33b850 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -31,6 +31,17 @@ internal fun generateShadowedSourcesJar( encoding = metadataCharset, ) .use { zos -> + val manifestEntry = "META-INF/MANIFEST.MF" + visitedFiles.add(manifestEntry) + zos.writeEntry( + name = manifestEntry, + preserveLastModified = preserveFileTimestamps, + lastModified = if (preserveFileTimestamps) System.currentTimeMillis() else -1, + unixMode = UnixMode.file(), + ) { + write("Manifest-Version: 1.0\n\n".toByteArray(charset)) + } + for (srcDir in sourceSetsSourceDirs) { if (!srcDir.exists()) continue srcDir diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 0299cb4da5..435cba0ce9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -562,8 +562,8 @@ public abstract class ShadowJar : Jar() { addIncludedDependencies() injectManifestAttributes() super.copy() - runR8Minimization() generateShadowedSourcesJar() + runR8Minimization() } @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. From 03c024f8b27d712cff2952a02a35a49385ae70b4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Wed, 2 Sep 2026 21:11:58 +0800 Subject: [PATCH 026/117] Fix KMP publications configuration in publishing docs --- docs/publishing/README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index bcef984282..3ccd026343 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -629,8 +629,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publishing { publications { - named("jvm") { - artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + withType().configureEach { + if (name == "jvm") { + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + } } } repositories { @@ -654,8 +656,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publishing { publications { - named('jvm', MavenPublication) { - artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + withType(MavenPublication).configureEach { + if (name == 'jvm') { + artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + } } } repositories { From 95650a719e24ed92fc496ce4638b6c9112bb6208 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:17:32 +0800 Subject: [PATCH 027/117] Explicitly configure sources classifier for publications --- docs/publishing/README.md | 8 ++++++-- .../jengelman/gradle/plugins/shadow/PublishingTest.kt | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 3ccd026343..9022fe33e5 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -631,7 +631,9 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publications { withType().configureEach { if (name == "jvm") { - artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) { + classifier = "sources" + } } } } @@ -658,7 +660,9 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the publications { withType(MavenPublication).configureEach { if (name == 'jvm') { - artifact tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile } + artifact(tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile }) { + classifier = 'sources' + } } } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index f929fb4e62..5df9072aaf 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -725,7 +725,9 @@ class PublishingTest : BasePluginTest() { | shadow(MavenPublication) { | artifactId = 'my-all' | artifact($shadowJarTask) - | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) + | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) { + | classifier = 'sources' + | } | } | } |} From cd245cb7f4ee3978592a165a158e660dfe4cb8a2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:26:35 +0800 Subject: [PATCH 028/117] Cache unusedClasses and packageRelocators by lazy --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 68 +++++++++---------- 1 file changed, 33 insertions(+), 35 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 435cba0ce9..a0d21d40b0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -568,18 +568,6 @@ public abstract class ShadowJar : Jar() { @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { - val unusedClasses = - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } - .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -636,29 +624,41 @@ public abstract class ShadowJar : Jar() { private val isR8Enabled: Boolean get() = _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.R8 - private val packageRelocators: List - get() { - if (enableAutoRelocation.get()) { - logger.info( - "Adding auto relocation packages in the dependencies with prefix '{}'.", - relocationPrefix.get(), - ) - } else { - logger.info("Skipping package relocators as auto relocation is disabled.") - return emptyList() - } - val prefix = relocationPrefix.get() - return includedDependencies.flatMap { file -> - file.useZip { - entries() - .toList() - .filter { it.name.endsWith(".class") && it.name != "module-info.class" } - .map { it.name.substringBeforeLast('/').replace('/', '.') } - .toSet() - .map { SimpleRelocator(it, "$prefix.$it") } - } + private val unusedClasses by lazy { + if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + } + + private val packageRelocators by lazy { + if (enableAutoRelocation.get()) { + logger.info( + "Adding auto relocation packages in the dependencies with prefix '{}'.", + relocationPrefix.get(), + ) + } else { + logger.info("Skipping package relocators as auto relocation is disabled.") + return@lazy emptyList() + } + val prefix = relocationPrefix.get() + return@lazy includedDependencies.flatMap { file -> + file.useZip { + entries() + .toList() + .filter { it.name.endsWith(".class") && it.name != "module-info.class" } + .map { it.name.substringBeforeLast('/').replace('/', '.') } + .toSet() + .map { SimpleRelocator(it, "$prefix.$it") } } } + } private fun addIncludedDependencies() { val isAar: File.() -> Boolean = { @@ -773,8 +773,6 @@ public abstract class ShadowJar : Jar() { ) } - private var unusedClasses: Set = emptySet() - private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( From 0350f8f7f24a99066c0c9e5ae9f5fdb698e23a94 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:34:10 +0800 Subject: [PATCH 029/117] Revert "Cache unusedClasses and packageRelocators by lazy" This reverts commit cd245cb7f4ee3978592a165a158e660dfe4cb8a2. --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 68 ++++++++++--------- 1 file changed, 35 insertions(+), 33 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index a0d21d40b0..435cba0ce9 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -568,6 +568,18 @@ public abstract class ShadowJar : Jar() { @Suppress("InternalGradleApiUsage") // For creating ShadowCopyAction. override fun createCopyAction(): org.gradle.api.internal.file.copy.CopyAction { + val unusedClasses = + if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + .also { this.unusedClasses = it } val actualTransformers = transformers.get().let { set -> if ( @@ -624,41 +636,29 @@ public abstract class ShadowJar : Jar() { private val isR8Enabled: Boolean get() = _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.R8 - private val unusedClasses by lazy { - if (_minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER) { - findUnusedClasses( - sourceSetsClassesDirs = sourceSetsClassesDirs, - classJars = apiJars, - toMinimize = toMinimize, - dependencies = includedDependencies, - ) - } else { - emptySet() - } - } - - private val packageRelocators by lazy { - if (enableAutoRelocation.get()) { - logger.info( - "Adding auto relocation packages in the dependencies with prefix '{}'.", - relocationPrefix.get(), - ) - } else { - logger.info("Skipping package relocators as auto relocation is disabled.") - return@lazy emptyList() - } - val prefix = relocationPrefix.get() - return@lazy includedDependencies.flatMap { file -> - file.useZip { - entries() - .toList() - .filter { it.name.endsWith(".class") && it.name != "module-info.class" } - .map { it.name.substringBeforeLast('/').replace('/', '.') } - .toSet() - .map { SimpleRelocator(it, "$prefix.$it") } + private val packageRelocators: List + get() { + if (enableAutoRelocation.get()) { + logger.info( + "Adding auto relocation packages in the dependencies with prefix '{}'.", + relocationPrefix.get(), + ) + } else { + logger.info("Skipping package relocators as auto relocation is disabled.") + return emptyList() + } + val prefix = relocationPrefix.get() + return includedDependencies.flatMap { file -> + file.useZip { + entries() + .toList() + .filter { it.name.endsWith(".class") && it.name != "module-info.class" } + .map { it.name.substringBeforeLast('/').replace('/', '.') } + .toSet() + .map { SimpleRelocator(it, "$prefix.$it") } + } } } - } private fun addIncludedDependencies() { val isAar: File.() -> Boolean = { @@ -773,6 +773,8 @@ public abstract class ShadowJar : Jar() { ) } + private var unusedClasses: Set = emptySet() + private fun generateShadowedSourcesJar() { if (!archiveSourcesFile.isPresent) return generateShadowedSourcesJar( From 1b26f8baadf20699986db285e8db4a924b0fd815 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:44:56 +0800 Subject: [PATCH 030/117] Remove currentTimeMillis for lastModified --- .../gradle/plugins/shadow/internal/ShadowSourcesJar.kt | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index 053f33b850..d79d66e083 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -36,7 +36,6 @@ internal fun generateShadowedSourcesJar( zos.writeEntry( name = manifestEntry, preserveLastModified = preserveFileTimestamps, - lastModified = if (preserveFileTimestamps) System.currentTimeMillis() else -1, unixMode = UnixMode.file(), ) { write("Manifest-Version: 1.0\n\n".toByteArray(charset)) @@ -148,14 +147,12 @@ internal fun generateShadowedSourcesJar( val entries = zos.entries.map { it.name } val added = entries.toMutableSet() - val currentTimeMillis = System.currentTimeMillis() entries.forEach { name -> name.parentDirectoryEntries().asReversed().forEach { entryName -> if (!added.add(entryName)) return@forEach zos.writeEntry( name = entryName, preserveLastModified = preserveFileTimestamps, - lastModified = currentTimeMillis, unixMode = UnixMode.directory(), ) } From 5232d50b6680d594bbccd57203b10c8515964121 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:46:52 +0800 Subject: [PATCH 031/117] Update regexes --- .../gradle/plugins/shadow/internal/ShadowSourcesJar.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index d79d66e083..c50477b2ee 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -164,12 +164,11 @@ internal fun generateShadowedSourcesJar( } } -private val packageRegex = Regex("""(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""") +private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() private val jvmNameRegex = - Regex( - """@file\s*:\s*(?:\[[^\]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" - ) + """@file\s*:\s*(?:\[[^]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" + .toRegex() internal fun extractPackage(text: String): String { val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() From 6079bbc991fcbc30302039a201d2322db2458407 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 09:49:10 +0800 Subject: [PATCH 032/117] Safe cast for DefaultDependencyFilter --- .../github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 435cba0ce9..1ddc0f3646 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -193,8 +193,7 @@ public abstract class ShadowJar : Jar() { @get:Classpath internal val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { dependencyFilter.zip(configurations) { df, cs -> - df as DefaultDependencyFilter - df.resolveSourcesJars(cs) + (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() } } From ac67d84d987a831e9a2022fed105209d7f5de8a0 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 10:10:16 +0800 Subject: [PATCH 033/117] Only publish shadowSourcesElements when java sources variant is present --- .../gradle/plugins/shadow/PublishingTest.kt | 93 +++++++++++-------- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 21 +++-- 2 files changed, 70 insertions(+), 44 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 5df9072aaf..c1438f6c5a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -9,7 +9,6 @@ import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.single import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME -import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.testkit.JarPath @@ -265,6 +264,59 @@ class PublishingTest : BasePluginTest() { publish() + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries) + .containsOnly( + "maven-1.0.jar", + "maven-1.0.module", + "maven-1.0.pom", + "maven-1.0.jar.md5", + "maven-1.0.module.md5", + "maven-1.0.pom.md5", + "maven-1.0.jar.sha1", + "maven-1.0.module.sha1", + "maven-1.0.pom.sha1", + "maven-1.0.jar.sha256", + "maven-1.0.module.sha256", + "maven-1.0.pom.sha256", + "maven-1.0.jar.sha512", + "maven-1.0.module.sha512", + "maven-1.0.pom.sha512", + ) + assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) + assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertShadowVariantCommon(gmm) + } + + @Test + fun publishShadowJarWithSourcesWhenWithSourcesJarEnabled() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = '' + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + publish() + val artifactRoot = "my/maven/1.0" assertThat(repoPath(artifactRoot).entries) .containsOnly( @@ -424,18 +476,11 @@ class PublishingTest : BasePluginTest() { "my-artifact-2.0-my-classifier.my-ext.md5", "my-artifact-2.0.pom.md5", "my-artifact-2.0.pom.sha1", - "my-artifact-2.0-sources.my-ext", - "my-artifact-2.0-sources.my-ext.md5", - "my-artifact-2.0-sources.my-ext.sha1", - "my-artifact-2.0-sources.my-ext.sha256", - "my-artifact-2.0-sources.my-ext.sha512", ) assertShadowJarCommon(repoJarPath("$artifactRoot/my-artifact-2.0-my-classifier.my-ext")) assertPomCommon(repoPath("$artifactRoot/my-artifact-2.0.pom")) - val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module")) - assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) + assertShadowVariantCommon(gmmAdapter.fromJson(repoPath("$artifactRoot/my-artifact-2.0.module"))) } @Test @@ -489,12 +534,6 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", - // Entries of maven-1.0-sources.jar - "maven-1.0-sources.jar", - "maven-1.0-sources.jar.md5", - "maven-1.0-sources.jar.sha1", - "maven-1.0-sources.jar.sha256", - "maven-1.0-sources.jar.sha512", ) assertThat(repoPath("my/maven-all/1.0").entries) .containsOnly( @@ -513,12 +552,6 @@ class PublishingTest : BasePluginTest() { "maven-all-1.0-all.jar.sha512", "maven-all-1.0.module.sha512", "maven-all-1.0.pom.sha512", - // Entries of maven-all-1.0-sources.jar - "maven-all-1.0-sources.jar", - "maven-all-1.0-sources.jar.md5", - "maven-all-1.0-sources.jar.sha1", - "maven-all-1.0-sources.jar.sha256", - "maven-all-1.0-sources.jar.sha512", ) assertThat(repoJarPath("my/maven/1.0/maven-1.0.jar")).useAll { containsOnly(*manifestEntries) } @@ -528,13 +561,12 @@ class PublishingTest : BasePluginTest() { assertPomCommon(repoPath("my/maven/1.0/maven-1.0.pom"), arrayOf("my:a:1.0", "my:b:1.0")) gmmAdapter.fromJson(repoPath("my/maven/1.0/maven-1.0.module")).let { gmm -> - // apiElements, runtimeElements, shadowRuntimeElements, shadowSourcesElements + // apiElements, runtimeElements, shadowRuntimeElements assertThat(gmm.variantNames) .containsOnly( API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertThat(gmm.apiElementsVariant).all { transform { it.attributes } @@ -555,18 +587,12 @@ class PublishingTest : BasePluginTest() { transform { it.coordinates }.containsOnly("my:a:1.0", "my:b:1.0") } assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) } assertPomCommon(repoPath("my/maven-all/1.0/maven-all-1.0.pom")) gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")).let { gmm -> - assertThat(gmm.variantNames) - .containsOnly( - SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, - ) + assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) } } @@ -659,11 +685,6 @@ class PublishingTest : BasePluginTest() { "maven-1.0-all.jar.sha1", "maven-1.0-all.jar.sha256", "maven-1.0-all.jar.sha512", - "maven-1.0-sources.jar", - "maven-1.0-sources.jar.md5", - "maven-1.0-sources.jar.sha1", - "maven-1.0-sources.jar.sha256", - "maven-1.0-sources.jar.sha512", *entriesCommon, ) assertThat(gmm.variantNames) @@ -671,11 +692,9 @@ class PublishingTest : BasePluginTest() { API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ) assertVariantsCommon(gmm) assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) assertThat(pomDependencies).containsOnly("my:a:1.0" to "runtime", "my:b:1.0" to "compile") } else { assertThat(artifactEntries).containsOnly(*entriesCommon) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index a59d87fdd7..64855f7c47 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -23,7 +23,7 @@ import org.gradle.api.component.AdhocComponentWithVariants import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME -import org.gradle.api.provider.Provider +import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -125,35 +125,42 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl protected open fun Project.configureComponents() { val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements + // If `withSourcesJar` presents. + val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) shadowComponent.addVariantsFromConfiguration(shadowRuntimeElements) { variant -> variant.mapToMavenScope("runtime") } - shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) {} + shadowComponent.addVariantsFromConfiguration(shadowSourcesElements) { variant -> + if (sourcesElements() == null) { + variant.skip() + } + } components.named("java", AdhocComponentWithVariants::class.java) { component -> val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent component.addVariants( - addIntoJavaComponent = addIntoJavaComponent, outgoingConfiguration = shadowRuntimeElements, logger = logger, + shouldAdd = addIntoJavaComponent::get, ) component.addVariants( - addIntoJavaComponent = addIntoJavaComponent, outgoingConfiguration = shadowSourcesElements, logger = logger, - ) + ) { + addIntoJavaComponent.get() && sourcesElements() != null + } } } private fun AdhocComponentWithVariants.addVariants( - addIntoJavaComponent: Provider, outgoingConfiguration: NamedDomainObjectProvider, logger: Logger, + shouldAdd: () -> Boolean, ) { addVariantsFromConfiguration(outgoingConfiguration) { variant -> variant.mapToOptional() - if (addIntoJavaComponent.get()) { + if (shouldAdd()) { logger.info("Adding {} variant to Java component.", outgoingConfiguration.name) } else { logger.info("Skipping adding {} variant to Java component.", outgoingConfiguration.name) From 41142b13d31b307a18129d5bb2bc6f7ef640e2c8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 11:29:18 +0800 Subject: [PATCH 034/117] Ensure deterministic sources JAR entry ordering and fix jvmNameRegex --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../shadow/internal/ShadowSourcesJar.kt | 109 +++++++++--------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 1 + .../shadow/internal/ShadowSourcesJarTest.kt | 88 ++++++++++++++ 4 files changed, 147 insertions(+), 53 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 4b85d81815..3bd66bdcf1 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -127,7 +127,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements - // If `withSourcesJar` presents. + // If `withSourcesJar` is present. val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt index c50477b2ee..190272840b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt @@ -17,7 +17,7 @@ internal fun generateShadowedSourcesJar( metadataCharset: String?, preserveFileTimestamps: Boolean, ) { - val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile } + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() @@ -41,11 +41,13 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - for (srcDir in sourceSetsSourceDirs) { - if (!srcDir.exists()) continue + val sortedSourceDirs = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } + for (srcDir in sortedSourceDirs) { srcDir .walkTopDown() .filter { it.isFile } + .toList() + .sortedBy { it.relativeTo(srcDir).invariantSeparatorsPath } .forEach { file -> val relPath = file.relativeTo(srcDir).invariantSeparatorsPath val isSource = isSourceFile(relPath) @@ -91,64 +93,67 @@ internal fun generateShadowedSourcesJar( sourcesJars.forEach { jarFile -> jarFile.useZip { - entries().toList().forEach { entry -> - if (entry.isDirectory) return@forEach - val name = entry.name - if ( - name == "META-INF/MANIFEST.MF" || - name.endsWith(".class") || - name.startsWith("META-INF/INDEX.LIST") || - (name.startsWith("META-INF/") && - (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) - ) { - return@forEach - } - val isSource = isSourceFile(name) - if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } + entries() + .toList() + .filterNot { it.isDirectory } + .sortedBy { it.name } + .forEach { entry -> + val name = entry.name + if ( + name == "META-INF/MANIFEST.MF" || + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + ) { + return@forEach } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - val bytes = getInputStream(entry).readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) + val isSource = isSourceFile(name) + if (isSource) { + val text = getInputStream(entry).bufferedReader(charset).readText() + val pkg = extractPackage(text) + val simpleName = name.substringAfterLast('/') + if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + val bytes = getInputStream(entry).readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } } } } - } } } val entries = zos.entries.map { it.name } val added = entries.toMutableSet() entries.forEach { name -> - name.parentDirectoryEntries().asReversed().forEach { entryName -> + name.parentDirectoryEntries().forEach { entryName -> if (!added.add(entryName)) return@forEach zos.writeEntry( name = entryName, @@ -167,7 +172,7 @@ internal fun generateShadowedSourcesJar( private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() private val jvmNameRegex = - """@file\s*:\s*(?:\[[^]]*\b)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" + """@file\s*:\s*(?:\[[^]]*?)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" .toRegex() internal fun extractPackage(text: String): String { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 1ddc0f3646..49df528798 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -51,6 +51,7 @@ import org.gradle.api.file.DuplicatesStrategy.EXCLUDE import org.gradle.api.file.DuplicatesStrategy.FAIL import org.gradle.api.file.DuplicatesStrategy.INCLUDE import org.gradle.api.file.DuplicatesStrategy.INHERIT +import org.gradle.api.file.DuplicatesStrategy.WARN import org.gradle.api.file.RegularFileProperty import org.gradle.api.provider.Property import org.gradle.api.provider.SetProperty diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt index 22806dcf21..f4f99548ad 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt @@ -125,6 +125,48 @@ class ShadowSourcesJarTest { ) ) .isFalse() + assertThat( + isUnused( + "BracketedUtils.kt", + "com.example", + """ + @file:[JvmName("CustomFacade")] + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "BracketedMultiUtils.kt", + "com.example", + """ + @file:[Suppress("unused") JvmName("CustomFacade")] + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() + assertThat( + isUnused( + "BracketedMultiUtilsReversed.kt", + "com.example", + """ + @file:[JvmName("CustomFacade") Suppress("unused")] + package com.example + fun util() {} + """ + .trimIndent(), + unusedSet, + ) + ) + .isTrue() assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", emptySet())) .isFalse() @@ -161,4 +203,50 @@ class ShadowSourcesJarTest { val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } assertThat(entries).containsAtLeast("shadow/example/nested/Mismatched.kt") } + + @Test + fun generateShadowedSourcesJarDeterministicOrdering(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + srcDir.resolve("z/sub/Z.java").apply { + parentFile.mkdirs() + writeText("package z.sub;\nclass Z {}") + } + srcDir.resolve("a/A.java").apply { + parentFile.mkdirs() + writeText("package a;\nclass A {}") + } + srcDir.resolve("m/M.java").apply { + parentFile.mkdirs() + writeText("package m;\nclass M {}") + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = emptyList(), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } + assertThat(entries) + .isEqualTo( + listOf( + "META-INF/MANIFEST.MF", + "a/A.java", + "m/M.java", + "z/sub/Z.java", + "META-INF/", + "a/", + "m/", + "z/", + "z/sub/", + ) + ) + } } From 7edd7d64e906ec62a68ea3dd85513dc43f1e33dd Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 11:36:11 +0800 Subject: [PATCH 035/117] Clarify shadowed sources JAR generation vs publishing in docs --- docs/publishing/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 9022fe33e5..cae8ead4d9 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -588,6 +588,12 @@ When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin aut The published Maven publication will include both `--all.jar` and `--all-sources.jar`. +> [!NOTE] +> The companion shadowed sources JAR is generated automatically whenever the `shadowJar` task runs (as long as project +> or dependency sources are present). However, **it is only published to Maven repositories when `java.withSourcesJar()` +> is enabled**. If `withSourcesJar()` is omitted, publishing from `components["shadow"]` will only publish the shadowed +> binary JAR, preserving backward compatibility for existing builds. + ### Customizing the Sources Archive File The shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], From 485aa65f8b228f93860c54ee5098a51f187c0dc8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 11:57:23 +0800 Subject: [PATCH 036/117] Expose sourceSetsSourceDirs and includedSourcesJars as public with docs and tests --- api/shadow.api | 4 +- docs/publishing/README.md | 32 +++++- .../gradle/plugins/shadow/RelocationTest.kt | 99 +++++++++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 29 ++++-- 4 files changed, 156 insertions(+), 8 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index b7118a4f6b..83c4fad292 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -261,7 +261,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getAddMultiReleaseAttribute ()Lorg/gradle/api/provider/Property; public fun getApiJars ()Lorg/gradle/api/file/ConfigurableFileCollection; protected abstract fun getArchiveOperations ()Lorg/gradle/api/file/ArchiveOperations; - public final fun getArchiveSourcesFile ()Lorg/gradle/api/file/RegularFileProperty; + public fun getArchiveSourcesFile ()Lorg/gradle/api/file/RegularFileProperty; public fun getConfigurations ()Lorg/gradle/api/provider/SetProperty; public fun getDependencyFilter ()Lorg/gradle/api/provider/Property; public fun getDuplicatesStrategy ()Lorg/gradle/api/file/DuplicatesStrategy; @@ -271,6 +271,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations; public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property; public fun getIncludedDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection; + public fun getIncludedSourcesJars ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludes ()Ljava/util/Set; public fun getJavaLauncher ()Lorg/gradle/api/provider/Property; public fun getMainClass ()Lorg/gradle/api/provider/Property; @@ -282,6 +283,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getRelocationPrefix ()Lorg/gradle/api/provider/Property; public fun getRelocators ()Lorg/gradle/api/provider/SetProperty; public fun getSourceSetsClassesDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; + public fun getSourceSetsSourceDirs ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getToMinimize ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getTransformers ()Lorg/gradle/api/provider/SetProperty; public fun mergeGroovyExtensionModules ()V diff --git a/docs/publishing/README.md b/docs/publishing/README.md index cae8ead4d9..7df00a7674 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -596,7 +596,7 @@ The published Maven publication will include both `--all.ja ### Customizing the Sources Archive File -The shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], +The companion shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix: === ":material-language-kotlin: build.gradle.kts" @@ -615,6 +615,34 @@ which defaults to the same destination and base name as `archiveFile` with `-sou } ``` +You can also customize the source inputs included in the companion sources JAR using +[`sourceSetsSourceDirs`][ShadowJar.sourceSetsSourceDirs] and +[`includedSourcesJars`][ShadowJar.includedSourcesJars]: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.shadowJar { + // Add custom source directories + sourceSetsSourceDirs.from("src/extra/java") + + // Add additional dependency sources JARs + includedSourcesJars.from("libs/external-lib-sources.jar") + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + // Add custom source directories + sourceSetsSourceDirs.from('src/extra/java') + + // Add additional dependency sources JARs + includedSourcesJars.from('libs/external-lib-sources.jar') + } + ``` + ### Publishing with Kotlin Multiplatform (KMP) In Kotlin Multiplatform (KMP) projects, publications are managed by the Kotlin Gradle Plugin (KGP) per target (such as @@ -756,7 +784,9 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html [ShadowJar.archiveSourcesFile]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/archive-sources-file.html +[ShadowJar.includedSourcesJars]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/included-sources-jars.html [ShadowJar.relocate]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/relocate.html +[ShadowJar.sourceSetsSourceDirs]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/source-sets-source-dirs.html [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index d7fbfdaa6e..ddb70510e5 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -15,6 +15,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isAssignableFrom import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain +import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -851,6 +852,104 @@ class RelocationTest : BasePluginTest() { assertThat(outputShadowedSourcesJar).useAll { containsOnly(*manifestEntries) } } + @Test + fun generateShadowedSourcesJarWithCustomSourceSetsSourceDirs() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main {} + """ + .trimMargin() + ) + path("src/extra/java/extra/Extra.java") + .writeText( + """ + |package extra; + |public class Extra {} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |$shadowJarTask { + | sourceSetsSourceDirs.from('src/extra/java') + | relocate('extra', 'shadow.extra') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/extra/", + "shadow/extra/Extra.java", + *manifestEntries, + ) + getContent("shadow/extra/Extra.java") + .isEqualTo( + """ + |package shadow.extra; + |public class Extra {} + """ + .trimMargin() + ) + } + } + + @Test + fun generateShadowedSourcesJarWithCustomIncludedSourcesJars() { + writeClass() + val customSourcesJar = path("libs/external-sources.jar") + customSourcesJar.parent.toFile().mkdirs() + JarBuilder(customSourcesJar) + .insert( + "ext/Ext.java", + """ + package ext; + public class Ext {} + """ + .trimIndent(), + ) + .write() + + projectScript.appendText( + """ + |$shadowJarTask { + | includedSourcesJars.from('libs/external-sources.jar') + | relocate('ext', 'shadow.ext') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/ext/", + "shadow/ext/Ext.java", + *manifestEntries, + ) + getContent("shadow/ext/Ext.java") + .isEqualTo( + """ + |package shadow.ext; + |public class Ext {} + """ + .trimMargin() + ) + } + } + private companion object { @JvmStatic fun preserveLastModifiedProvider() = diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 49df528798..a9cdc92f4b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -191,16 +191,37 @@ public abstract class ShadowJar : Jar() { dependencyFilter.zip(configurations) { df, cs -> df.resolve(cs) } } + /** + * Source JARs resolved from bundled dependencies to be merged into the companion shadowed sources + * JAR. + */ @get:Classpath - internal val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { + public open val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { dependencyFilter.zip(configurations) { df, cs -> (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() } } + /** + * Source directories from project source sets to be included in the companion shadowed sources + * JAR. + * + * In projects applying the `shadow` plugin for Java or Kotlin Multiplatform, this defaults to the + * relevant source sets' source directories. + */ + @get:InputFiles + @get:PathSensitive(PathSensitivity.RELATIVE) + public open val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() + + /** + * The destination location of the companion shadowed sources JAR. + * + * Defaults to + * `/--sources.`. + */ @get:Optional @get:OutputFile - public val archiveSourcesFile: RegularFileProperty = + public open val archiveSourcesFile: RegularFileProperty = objectFactory .fileProperty() .convention( @@ -216,10 +237,6 @@ public abstract class ShadowJar : Jar() { ) ) - @get:InputFiles - @get:PathSensitive(PathSensitivity.RELATIVE) - internal val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() - /** * Enables auto relocation of packages in the dependencies. * From 984fee442d1c3968f81a6df665c3a0db825c742c Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 12:03:01 +0800 Subject: [PATCH 037/117] Annotate includedSourcesJars with @InputFiles and @PathSensitive(NONE) --- .../github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index a9cdc92f4b..f09e1c8a21 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -195,7 +195,8 @@ public abstract class ShadowJar : Jar() { * Source JARs resolved from bundled dependencies to be merged into the companion shadowed sources * JAR. */ - @get:Classpath + @get:InputFiles + @get:PathSensitive(PathSensitivity.NONE) public open val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { dependencyFilter.zip(configurations) { df, cs -> (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() From ea3d52998a05a75a4a45bd29011067e72ad5a0e7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 12:39:49 +0800 Subject: [PATCH 038/117] Introduce generateSourcesJar property gated by withSourcesJar by default --- api/shadow.api | 1 + docs/getting-started/README.md | 6 +++- docs/publishing/README.md | 18 +++++++++--- .../gradle/plugins/shadow/BasePluginTest.kt | 3 ++ .../gradle/plugins/shadow/FilteringTest.kt | 1 + .../gradle/plugins/shadow/JavaPluginsTest.kt | 3 ++ .../plugins/shadow/KotlinPluginsTest.kt | 2 ++ .../gradle/plugins/shadow/MinimizeTest.kt | 1 + .../gradle/plugins/shadow/PublishingTest.kt | 1 + .../gradle/plugins/shadow/RelocationTest.kt | 28 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 3 ++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 17 ++++++++++- .../plugins/shadow/ShadowPropertiesTest.kt | 20 +++++++++++++ 13 files changed, 98 insertions(+), 6 deletions(-) diff --git a/api/shadow.api b/api/shadow.api index 83c4fad292..4428364a48 100644 --- a/api/shadow.api +++ b/api/shadow.api @@ -270,6 +270,7 @@ public abstract class com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar public fun getExcludes ()Ljava/util/Set; protected abstract fun getExecOperations ()Lorg/gradle/process/ExecOperations; public fun getFailOnDuplicateEntries ()Lorg/gradle/api/provider/Property; + public fun getGenerateSourcesJar ()Lorg/gradle/api/provider/Property; public fun getIncludedDependencies ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludedSourcesJars ()Lorg/gradle/api/file/ConfigurableFileCollection; public fun getIncludes ()Ljava/util/Set; diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index 0734fdba4b..fed55c39aa 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -138,7 +138,8 @@ in their build logic), Shadow will automatically configure the following behavio - `META-INF/versions/**/module-info.class` - `module-info.class` - Configures the [`ShadowJar`][ShadowJar] task to generate a companion **Shadowed Sources JAR** containing both - project sources and shadowed dependency sources with relocated packages. + project sources and shadowed dependency sources with relocated packages when `java.withSourcesJar()` is enabled + (or when [`generateSourcesJar`][ShadowJar.generateSourcesJar] is set to `true`). - Creates and registers the `shadow` component in the project (used for integrating with [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. @@ -157,6 +158,8 @@ Here are the options that can be passed to the `shadowJar`: --no-enable-kotlin-module-remapping Disables option --enable-kotlin-module-remapping. --fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate. --no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries. +--generate-sources-jar Generates a companion shadowed sources JAR containing project and dependency sources. +--no-generate-sources-jar Disables option --generate-sources-jar. --main-class Main class attribute to add to manifest. --minimize-jar Minimizes the jar by removing unused classes. --no-minimize-jar Disables option --minimize-jar. @@ -177,5 +180,6 @@ Refer to [listing command line options][listing-command-line-options]. [JavaPlugin]: https://docs.gradle.org/current/userguide/java_plugin.html [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html +[ShadowJar.generateSourcesJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/generate-sources-jar.html [gradle-plugin-portal]: https://plugins.gradle.org/plugin/com.gradleup.shadow [listing-command-line-options]: https://docs.gradle.org/current/userguide/custom_tasks.html#sec:listing_task_options diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 7df00a7674..147227b6b5 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -589,10 +589,11 @@ The published Maven publication will include both `--all.ja `--all-sources.jar`. > [!NOTE] -> The companion shadowed sources JAR is generated automatically whenever the `shadowJar` task runs (as long as project -> or dependency sources are present). However, **it is only published to Maven repositories when `java.withSourcesJar()` -> is enabled**. If `withSourcesJar()` is omitted, publishing from `components["shadow"]` will only publish the shadowed -> binary JAR, preserving backward compatibility for existing builds. +> Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. +> In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid +> unnecessary build overhead for application builds. If `withSourcesJar()` is omitted, publishing from +> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing builds. +> You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`). ### Customizing the Sources Archive File @@ -675,6 +676,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the maven("https://repo.myorg.com") } } + + tasks.named("shadowJar") { + generateSourcesJar = true + } ``` === ":simple-apachegroovy: build.gradle" @@ -704,6 +709,10 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the maven { url = 'https://repo.myorg.com' } } } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + generateSourcesJar = true + } ``` ## Generating Javadoc or Dokka from Shadowed Sources @@ -784,6 +793,7 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source [MavenPublication.artifact]: https://docs.gradle.org/current/dsl/org.gradle.api.publish.maven.MavenPublication.html#org.gradle.api.publish.maven.MavenPublication:artifact(java.lang.Object) [ShadowJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/index.html [ShadowJar.archiveSourcesFile]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/archive-sources-file.html +[ShadowJar.generateSourcesJar]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/generate-sources-jar.html [ShadowJar.includedSourcesJars]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/included-sources-jars.html [ShadowJar.relocate]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/relocate.html [ShadowJar.sourceSetsSourceDirs]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.tasks/-shadow-jar/source-sets-source-dirs.html diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 71185f1655..0c3b31f687 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -287,6 +287,9 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} + |java { + | withSourcesJar() + |} |dependencies { | implementation project(':client') |} diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index f05d4d930c..d349a07d8b 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -248,6 +248,7 @@ class FilteringTest : BasePluginTest() { | implementation 'my:h:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | dependencies { | exclude(dependency('my:h:1.0')) | } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 681d41c6d0..1b3b139869 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -121,6 +121,8 @@ class JavaPluginsTest : BasePluginTest() { "--no-enable-kotlin-module-remapping Disables option --enable-kotlin-module-remapping.", "--fail-on-duplicate-entries Fails build if the ZIP entries in the shadowed JAR are duplicate.", "--no-fail-on-duplicate-entries Disables option --fail-on-duplicate-entries", + "--generate-sources-jar Generates a companion shadowed sources JAR containing project and dependency sources.", + "--no-generate-sources-jar Disables option --generate-sources-jar.", "--main-class Main class attribute to add to manifest.", "--minimize-jar Minimizes the jar by removing unused classes.", "--no-minimize-jar Disables option --minimize-jar.", @@ -1357,6 +1359,7 @@ class JavaPluginsTest : BasePluginTest() { | implementation 'my:g:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | relocate 'g', 'shadow.g' |} |tasks.named('javadoc', Javadoc) { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 852e050f0a..76c98bdb38 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -306,6 +306,7 @@ class KotlinPluginsTest : BasePluginTest() { | implementation 'my:g:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | relocate 'g', 'shadow.g' |} |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { @@ -349,6 +350,7 @@ class KotlinPluginsTest : BasePluginTest() { """ |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} |$shadowJarTask { + | generateSourcesJar = true | relocate 'my.custom', 'shadow.custom' |} """ diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 550c22967f..4216754c3f 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -148,6 +148,7 @@ class MinimizeTest : BasePluginTest() { | implementation 'my:k:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | minimize() |} """ diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 8de9fcc0ff..4c3fab5e3d 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -735,6 +735,7 @@ class PublishingTest : BasePluginTest() { |} |$shadowJarTask { | archiveClassifier = '' + | generateSourcesJar = true |} |publishing { | repositories { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index ddb70510e5..5f6fd43de8 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -3,6 +3,7 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.isEqualTo +import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -17,6 +18,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import kotlin.io.path.appendText +import kotlin.io.path.exists import kotlin.io.path.readBytes import kotlin.io.path.writeText import kotlin.time.Duration.Companion.seconds @@ -755,6 +757,23 @@ class RelocationTest : BasePluginTest() { } } + @Test + fun generateNoShadowedSourcesJarByDefault() { + writeClass() + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + } + @Test fun generateShadowedSourcesJarWithRelocation() { path("src/main/java/my/Main.java") @@ -774,6 +793,7 @@ class RelocationTest : BasePluginTest() { | implementation 'my:g:1.0' |} |$shadowJarTask { + | generateSourcesJar = true | relocate('g', 'shadow.g') |} """ @@ -821,6 +841,9 @@ class RelocationTest : BasePluginTest() { |dependencies { | implementation 'my:b:1.0' |} + |$shadowJarTask { + | generateSourcesJar = true + |} """ .trimMargin() ) @@ -843,6 +866,9 @@ class RelocationTest : BasePluginTest() { |dependencies { | implementation 'my:b:1.0' |} + |$shadowJarTask { + | generateSourcesJar = true + |} """ .trimMargin() ) @@ -873,6 +899,7 @@ class RelocationTest : BasePluginTest() { projectScript.appendText( """ |$shadowJarTask { + | generateSourcesJar = true | sourceSetsSourceDirs.from('src/extra/java') | relocate('extra', 'shadow.extra') |} @@ -921,6 +948,7 @@ class RelocationTest : BasePluginTest() { projectScript.appendText( """ |$shadowJarTask { + | generateSourcesJar = true | includedSourcesJars.from('libs/external-sources.jar') | relocate('ext', 'shadow.ext') |} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 3bd66bdcf1..9ed6780d74 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -45,6 +45,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> task.from(mainSourceSet.map { it.output }) task.sourceSetsSourceDirs.convention(mainSourceSet.map { it.allSource.srcDirs }) + task.generateSourcesJar.convention( + provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } + ) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index f09e1c8a21..b4f846c0de 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -214,6 +214,21 @@ public abstract class ShadowJar : Jar() { @get:PathSensitive(PathSensitivity.RELATIVE) public open val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() + /** + * If `true`, generates a companion shadowed sources JAR containing project and dependency + * sources. + * + * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when + * `java.withSourcesJar()` is enabled, and `false` otherwise. + */ + @get:Input + @get:Option( + option = "generate-sources-jar", + description = + "Generates a companion shadowed sources JAR containing project and dependency sources.", + ) + public open val generateSourcesJar: Property = objectFactory.property(false) + /** * The destination location of the companion shadowed sources JAR. * @@ -794,7 +809,7 @@ public abstract class ShadowJar : Jar() { private var unusedClasses: Set = emptySet() private fun generateShadowedSourcesJar() { - if (!archiveSourcesFile.isPresent) return + if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return generateShadowedSourcesJar( sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs.files, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 91c70e5ab5..04994e2512 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -4,6 +4,7 @@ import assertk.all import assertk.assertThat import assertk.assertions.containsNone import assertk.assertions.containsOnly +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull @@ -162,9 +163,28 @@ class ShadowPropertiesTest { assertThat(relocationPrefix.get()).isEqualTo(ShadowBasePlugin.SHADOW) assertThat(configurations.get()).containsOnly(runtimeConfiguration) + assertThat(generateSourcesJar.get()).isFalse() + assertThat(archiveSourcesFile.get().asFile).all { + isEqualTo(destinationDirectory.file("my-project-1.0.0-all-sources.jar").get().asFile) + isEqualTo(projectDir.resolve("build/libs/my-project-1.0.0-all-sources.jar")) + } + assertThat(sourceSetsSourceDirs.files) + .containsOnly( + *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + ) + assertThat(includedSourcesJars.files).isEmpty() } } + @Test + fun applyJavaPluginWithSourcesJar() = + with(project) { + plugins.apply(JavaPlugin::class.java) + javaPluginExtension.withSourcesJar() + val shadowJarTask = tasks.shadowJar.get() + assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + } + @Test fun applyApplicationPlugin() = with(project) { From 97105cd4e6390e19e691e372cd344524c92733e4 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 3 Sep 2026 14:59:19 +0800 Subject: [PATCH 039/117] Polish error message in generateShadowedSourcesJar --- .../{ShadowSourcesJar.kt => SourcesJar.kt} | 2 +- ...dowSourcesJarTest.kt => SourcesJarTest.kt} | 32 ++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) rename src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/{ShadowSourcesJar.kt => SourcesJar.kt} (99%) rename src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/{ShadowSourcesJarTest.kt => SourcesJarTest.kt} (87%) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt similarity index 99% rename from src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt rename to src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 190272840b..3a6e40c276 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -165,7 +165,7 @@ internal fun generateShadowedSourcesJar( } } catch (e: Exception) { sourcesJarFile.delete() - throw e + gradleError("Could not create shadowed sources JAR '$sourcesJarFile'.", e) } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt similarity index 87% rename from src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt rename to src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index f4f99548ad..f02787e7d1 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -1,18 +1,22 @@ package com.github.jengelman.gradle.plugins.shadow.internal +import assertk.assertFailure import assertk.assertThat import assertk.assertions.containsAtLeast +import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isFalse +import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import java.io.File import java.util.zip.ZipFile +import org.gradle.api.GradleException import org.gradle.api.tasks.bundling.ZipEntryCompression import org.junit.jupiter.api.Test import org.junit.jupiter.api.io.TempDir -class ShadowSourcesJarTest { +class SourcesJarTest { @Test fun extractPackageStatements() { @@ -249,4 +253,30 @@ class ShadowSourcesJarTest { ) ) } + + @Test + fun throwsGradleExceptionOnFailure(@TempDir tempDir: File) { + val invalidFile = tempDir.resolve("not-a-file").apply { mkdirs() } + val srcDir = + tempDir.resolve("src").apply { + mkdirs() + resolve("Main.java").writeText("public class Main {}") + } + + assertFailure { + generateShadowedSourcesJar( + sourcesJarFile = invalidFile, + sourceSetsSourceDirs = listOf(srcDir), + includedSourcesJars = emptyList(), + relocators = emptyList(), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + } + .isInstanceOf() + .hasMessage("Could not create shadowed sources JAR '$invalidFile'.") + } } From 9040faaf116ad1c54ce48f3f52a19a607ffd96c0 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 15:57:54 +0800 Subject: [PATCH 040/117] Gate default source inputs by generateSourcesJar --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 10 ++++- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 8 +++- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 42 +++++++++++-------- .../plugins/shadow/ShadowPropertiesTest.kt | 9 ++-- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 9ed6780d74..c44b5f4e25 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -44,10 +44,18 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val taskProvider = registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> task.from(mainSourceSet.map { it.output }) - task.sourceSetsSourceDirs.convention(mainSourceSet.map { it.allSource.srcDirs }) task.generateSourcesJar.convention( provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } ) + task.sourceSetsSourceDirs.convention( + task.generateSourcesJar.flatMap { generate -> + if (generate) { + mainSourceSet.map { it.allSource.srcDirs } + } else { + provider { emptySet() } + } + } + ) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index 68c1d389f0..7a765e81e0 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -37,7 +37,13 @@ public abstract class ShadowKmpPlugin : Plugin { registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) task.sourceSetsSourceDirs.convention( - kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + task.generateSourcesJar.flatMap { generate -> + if (generate) { + kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + } else { + provider { emptySet() } + } + } ) task.configurations.convention( kotlinJvmMain diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index e1ed6c22ed..6aaaa6d604 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -201,6 +201,21 @@ public abstract class ShadowJar : Jar() { dependencyFilter.zip(configurations) { df, cs -> df.resolve(cs) } } + /** + * If `true`, generates a companion shadowed sources JAR containing project and dependency + * sources. + * + * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when + * `java.withSourcesJar()` is enabled, and `false` otherwise. + */ + @get:Input + @get:Option( + option = "generate-sources-jar", + description = + "Generates a companion shadowed sources JAR containing project and dependency sources.", + ) + public open val generateSourcesJar: Property = objectFactory.property(false) + /** * Source JARs resolved from bundled dependencies to be merged into the companion shadowed sources * JAR. @@ -208,8 +223,16 @@ public abstract class ShadowJar : Jar() { @get:InputFiles @get:PathSensitive(PathSensitivity.NONE) public open val includedSourcesJars: ConfigurableFileCollection = objectFactory.fileCollection { - dependencyFilter.zip(configurations) { df, cs -> - (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() + // Avoid resolving sources JARs during task input snapshotting when sources JAR generation is + // disabled. + generateSourcesJar.flatMap { + if (it) { + dependencyFilter.zip(configurations) { df, cs -> + (df as? DefaultDependencyFilter)?.resolveSourcesJars(cs) ?: project.files() + } + } else { + project.provider { emptySet() } + } } } @@ -224,21 +247,6 @@ public abstract class ShadowJar : Jar() { @get:PathSensitive(PathSensitivity.RELATIVE) public open val sourceSetsSourceDirs: ConfigurableFileCollection = objectFactory.fileCollection() - /** - * If `true`, generates a companion shadowed sources JAR containing project and dependency - * sources. - * - * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when - * `java.withSourcesJar()` is enabled, and `false` otherwise. - */ - @get:Input - @get:Option( - option = "generate-sources-jar", - description = - "Generates a companion shadowed sources JAR containing project and dependency sources.", - ) - public open val generateSourcesJar: Property = objectFactory.property(false) - /** * The destination location of the companion shadowed sources JAR. * diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 04994e2512..d18d8eaa7d 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -168,10 +168,7 @@ class ShadowPropertiesTest { isEqualTo(destinationDirectory.file("my-project-1.0.0-all-sources.jar").get().asFile) isEqualTo(projectDir.resolve("build/libs/my-project-1.0.0-all-sources.jar")) } - assertThat(sourceSetsSourceDirs.files) - .containsOnly( - *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() - ) + assertThat(sourceSetsSourceDirs.files).isEmpty() assertThat(includedSourcesJars.files).isEmpty() } } @@ -183,6 +180,10 @@ class ShadowPropertiesTest { javaPluginExtension.withSourcesJar() val shadowJarTask = tasks.shadowJar.get() assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + assertThat(shadowJarTask.sourceSetsSourceDirs.files) + .containsOnly( + *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + ) } @Test From 74ac176a82429866a07ea779e7d23289edb89846 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:02:58 +0800 Subject: [PATCH 041/117] Skip shadowSourcesElements variant when generateSourcesJar is disabled --- .../gradle/plugins/shadow/PublishingTest.kt | 40 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 9 +++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 4c3fab5e3d..eb763ec5e7 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -348,6 +348,46 @@ class PublishingTest : BasePluginTest() { assertShadowSourcesVariantCommon(gmm) } + @Test + fun dontPublishSourcesWhenGenerateSourcesJarDisabled() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = '' + |generateSourcesJar = false + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + val result = publish(infoArgument) + + assertThat(result.output) + .contains("Skipping adding shadowSourcesElements variant to shadow component.") + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.contains("sources") }).isEmpty() + assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) + assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertShadowVariantCommon(gmm) + assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + } + @Test fun publishCustomShadowJar() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index c44b5f4e25..802b139d83 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -138,8 +138,11 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements - // If `withSourcesJar` is present. + // If `withSourcesJar` is present and `generateSourcesJar` is enabled. val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } + val shouldAddSources = { + sourcesElements() != null && tasks.shadowJar.flatMap { it.generateSourcesJar }.get() + } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) @@ -152,7 +155,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl shadowComponent.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = { sourcesElements() != null }, + shouldAdd = shouldAddSources, ) components.named("java", AdhocComponentWithVariants::class.java) { component -> @@ -166,7 +169,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl component.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = { addIntoJavaComponent.get() && sourcesElements() != null }, + shouldAdd = { addIntoJavaComponent.get() && shouldAddSources() }, ) { mapToOptional() } From 00d47e92418d5efa5917a2eff3699212ac18c203 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:07:19 +0800 Subject: [PATCH 042/117] Document replacing standard artifacts and avoiding sourcesJar task conflict with empty classifier --- docs/publishing/README.md | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 147227b6b5..b229f3c255 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -295,8 +295,13 @@ You may want to publish the shadowed JAR instead of the original JAR. This can b ``` Because the default `archiveClassifier` of [`Jar`][Jar] is `""` (empty), setting the `archiveClassifier` of -[`ShadowJar`][ShadowJar] to `""` (empty) will make collisions between the outputs of these two tasks in some cases. If -you don't need the standard JAR, you can disable the `jar` task like: +[`ShadowJar`][ShadowJar] to `""` (empty) will make collisions between the outputs of standard tasks and `shadowJar`: + +- The binary shadowed JAR is output to `-.jar`, conflicting with the `jar` task. +- When `generateSourcesJar` is enabled (such as when `java.withSourcesJar()` is used), the companion shadowed sources + JAR is output to `--sources.jar`, conflicting with the standard `sourcesJar` task. + +If you want to replace standard JARs with the shadowed ones, disable the standard `jar` and `sourcesJar` tasks: === ":material-language-kotlin: build.gradle.kts" @@ -304,6 +309,11 @@ you don't need the standard JAR, you can disable the `jar` task like: tasks.jar { enabled = false } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == "sourcesJar" }.configureEach { + enabled = false + } ``` === ":simple-apachegroovy: build.gradle" @@ -312,9 +322,14 @@ you don't need the standard JAR, you can disable the `jar` task like: tasks.named('jar', Jar) { enabled = false } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == 'sourcesJar' }.configureEach { + enabled = false + } ``` -Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like: +Or set different `archiveClassifier` values for the standard tasks: === ":material-language-kotlin: build.gradle.kts" @@ -322,6 +337,11 @@ Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like: tasks.jar { archiveClassifier = "ignored" } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == "sourcesJar" }.configureEach { + (this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources" + } ``` === ":simple-apachegroovy: build.gradle" @@ -330,6 +350,11 @@ Or set a different `archiveClassifier` for the standard [`Jar`][Jar] like: tasks.named('jar', Jar) { archiveClassifier = 'ignored' } + + // If `java.withSourcesJar()` is enabled: + tasks.matching { it.name == 'sourcesJar' }.configureEach { + archiveClassifier = 'ignored-sources' + } ``` ## Publishing the Shadowed Gradle Plugins From 52536ee56a04e7435ad416548dc8bfd63bcb80f1 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:17:46 +0800 Subject: [PATCH 043/117] Track compiled class source files with ASM to prevent minimize from deleting used sources --- .../gradle/plugins/shadow/BasePluginTest.kt | 8 +- .../shadow/util/LocalMavenRepository.kt | 7 +- .../plugins/shadow/internal/SourcesJar.kt | 108 ++++++++++++--- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 + .../plugins/shadow/internal/SourcesJarTest.kt | 130 ++++-------------- 5 files changed, 129 insertions(+), 126 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 0c3b31f687..77b64140f8 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -408,10 +408,16 @@ abstract class BasePluginTest { } } - fun createEmptyClassBytes(internalName: String): ByteArray { + fun createEmptyClassBytes( + internalName: String, + sourceFile: String? = "${internalName.substringAfterLast('/')}.java", + ): ByteArray { return ClassWriter(0) .apply { visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC, internalName, null, "java/lang/Object", null) + if (sourceFile != null) { + visitSource(sourceFile, null) + } visitEnd() } .toByteArray() diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt index 26dfb6d0e4..5eead420c0 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt @@ -80,8 +80,11 @@ fun createDefaultLocalMavenRepository(junitJar: Path): AppendableMavenRepository val k = jarModule("my", "k", "1.0") { buildJar { - insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) - insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils", "Utils.kt")) + insert( + "k/CustomUnusedUtils.class", + createEmptyClassBytes("k/CustomUnusedUtils", "UnusedUtils.kt"), + ) } buildSourcesJar { insert( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 3a6e40c276..48c3e08958 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -10,6 +10,8 @@ internal fun generateShadowedSourcesJar( sourcesJarFile: File, sourceSetsSourceDirs: Iterable, includedSourcesJars: Iterable, + classesDirs: Iterable = emptyList(), + dependencies: Iterable = emptyList(), relocators: Iterable, unusedClasses: Set = emptySet(), entryCompression: ZipEntryCompression, @@ -22,6 +24,12 @@ internal fun generateShadowedSourcesJar( val visitedFiles = mutableSetOf() val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 + val sourceToClasses = + if (unusedClasses.isNotEmpty()) { + buildSourceToClassesMap(classesDirs = classesDirs, dependencies = dependencies) + } else { + emptyMap() + } try { sourcesJarFile @@ -55,9 +63,9 @@ internal fun generateShadowedSourcesJar( val text = file.readText(charset) val pkg = extractPackage(text) val simpleName = file.name - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocatePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { var transformedText = text @@ -113,9 +121,9 @@ internal fun generateShadowedSourcesJar( val text = getInputStream(entry).bufferedReader(charset).readText() val pkg = extractPackage(text) val simpleName = name.substringAfterLast('/') - if (isUnused(simpleName, pkg, text, unusedClasses)) return@forEach val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocatePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { var transformedText = text @@ -171,34 +179,92 @@ internal fun generateShadowedSourcesJar( private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() -private val jvmNameRegex = - """@file\s*:\s*(?:\[[^]]*?)?(?:kotlin\s*\.\s*jvm\s*\.\s*)?JvmName\s*\(\s*(?:name\s*=\s*)?"([^"]+)"""" - .toRegex() - internal fun extractPackage(text: String): String { val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() return if (matches.isEmpty()) "" else matches.joinToString(".") } +internal fun buildSourceToClassesMap( + classesDirs: Iterable, + dependencies: Iterable, +): Map> { + val sourceToClasses = mutableMapOf>() + + fun processClassBytes(bytes: ByteArray) { + try { + var internalName: String? = null + var sourceFile: String? = null + val reader = org.vafer.jdeb.shaded.objectweb.asm.ClassReader(bytes) + reader.accept( + object : + org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor( + org.vafer.jdeb.shaded.objectweb.asm.Opcodes.ASM9 + ) { + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?, + ) { + internalName = name + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitSource(source: String?, debug: String?) { + sourceFile = source + super.visitSource(source, debug) + } + }, + org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_CODE or + org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_FRAMES, + ) + + val name = internalName ?: return + val source = sourceFile ?: return + val pkg = name.substringBeforeLast('/', "") + val canonicalSourcePath = if (pkg.isEmpty()) source else "$pkg/$source" + val className = name.replace('/', '.') + sourceToClasses.getOrPut(canonicalSourcePath) { mutableSetOf() }.add(className) + } catch (_: Exception) { + // Ignore invalid class files + } + } + + for (dir in classesDirs.filter { it.isDirectory }) { + dir + .walkTopDown() + .filter { it.isFile && it.name.endsWith(".class") } + .forEach { file -> processClassBytes(file.readBytes()) } + } + + for (file in + dependencies.filter { it.isFile && (it.name.endsWith(".jar") || it.name.endsWith(".zip")) }) { + try { + file.useZip { + entries() + .toList() + .filter { !it.isDirectory && it.name.endsWith(".class") } + .forEach { entry -> processClassBytes(getInputStream(entry).readBytes()) } + } + } catch (_: Exception) { + // Ignore invalid archives + } + } + + return sourceToClasses +} + internal fun isUnused( - fileName: String, - pkg: String, - text: String, + canonicalPath: String, unusedClasses: Set, + sourceToClasses: Map>, ): Boolean { if (unusedClasses.isEmpty()) return false - val simpleName = fileName.substringBeforeLast('.') - val className = if (pkg.isEmpty()) simpleName else "$pkg.$simpleName" - if (unusedClasses.contains(className)) return true - - if (fileName.endsWith(".kt")) { - val customJvmName = jvmNameRegex.find(text)?.groupValues?.get(1) - val facadeName = customJvmName ?: "${simpleName}Kt" - val facadeClassName = if (pkg.isEmpty()) facadeName else "$pkg.$facadeName" - if (unusedClasses.contains(facadeClassName)) return true - } - - return false + val classes = sourceToClasses[canonicalPath] ?: return false + if (classes.isEmpty()) return false + return classes.all { it in unusedClasses } } private fun isSourceFile(path: String): Boolean { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 6aaaa6d604..98fab688c1 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -825,6 +825,8 @@ public abstract class ShadowJar : Jar() { sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs.files, includedSourcesJars = includedSourcesJars.files, + classesDirs = sourceSetsClassesDirs.files, + dependencies = includedDependencies.files, relocators = relocators.get() + packageRelocators, unusedClasses = unusedClasses, entryCompression = entryCompression, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index f02787e7d1..aa5a4703d0 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -67,115 +67,41 @@ class SourcesJarTest { val unusedSet = setOf( "com.example.UnusedJava", + "com.example.UnusedJava\$Inner", "com.example.UnusedKtClass", "com.example.DefaultFacadeKt", "com.example.CustomFacade", ) + val sourceToClasses = + mapOf( + "com/example/UnusedJava.java" to + setOf("com.example.UnusedJava", "com.example.UnusedJava\$Inner"), + "com/example/PartiallyUsedJava.java" to + setOf("com.example.UnusedJava", "com.example.UsedHelper"), + "com/example/UsedJava.java" to setOf("com.example.UsedJava"), + "com/example/UnusedKtClass.kt" to setOf("com.example.UnusedKtClass"), + "com/example/DefaultFacade.kt" to setOf("com.example.DefaultFacadeKt"), + "com/example/Utils.kt" to setOf("com.example.CustomFacade"), + "com/example/MixedUtils.kt" to setOf("com.example.CustomFacade", "com.example.UsedClass"), + "Main.java" to setOf("Main"), + ) - assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", unusedSet)) - .isTrue() - assertThat(isUnused("UsedJava.java", "com.example", "class UsedJava {}", unusedSet)).isFalse() + // All classes unused in file -> unused + assertThat(isUnused("com/example/UnusedJava.java", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/UnusedKtClass.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/DefaultFacade.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/Utils.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("Main.java", setOf("Main"), sourceToClasses)).isTrue() - assertThat(isUnused("UnusedKtClass.kt", "com.example", "class UnusedKtClass", unusedSet)) - .isTrue() - assertThat( - isUnused( - "DefaultFacade.kt", - "com.example", - "fun topLevel() {}", - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "Utils.kt", - "com.example", - """ - @file:JvmName("CustomFacade") - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "Utils.kt", - "com.example", - """ - @file:kotlin.jvm.JvmName(name = "CustomFacade") - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "UsedUtils.kt", - "com.example", - """ - @file:JvmName("UsedFacade") - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isFalse() - assertThat( - isUnused( - "BracketedUtils.kt", - "com.example", - """ - @file:[JvmName("CustomFacade")] - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "BracketedMultiUtils.kt", - "com.example", - """ - @file:[Suppress("unused") JvmName("CustomFacade")] - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() - assertThat( - isUnused( - "BracketedMultiUtilsReversed.kt", - "com.example", - """ - @file:[JvmName("CustomFacade") Suppress("unused")] - package com.example - fun util() {} - """ - .trimIndent(), - unusedSet, - ) - ) - .isTrue() + // At least one class is used in file -> NOT unused (kept!) + assertThat(isUnused("com/example/PartiallyUsedJava.java", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/MixedUtils.kt", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/UsedJava.java", unusedSet, sourceToClasses)).isFalse() - assertThat(isUnused("UnusedJava.java", "com.example", "class UnusedJava {}", emptySet())) - .isFalse() - assertThat(isUnused("Main.java", "", "class Main {}", setOf("Main"))).isTrue() - assertThat(isUnused("Main.java", "", "class Main {}", setOf("Other"))).isFalse() + // Unknown source file or empty unused set -> kept + assertThat(isUnused("com/example/Unknown.java", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/UnusedJava.java", emptySet(), sourceToClasses)).isFalse() + assertThat(isUnused("Main.java", setOf("Other"), sourceToClasses)).isFalse() } @Test From b2342dca81b4c2e4e21845ab5609f5c5d7ecbe8c Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:27:55 +0800 Subject: [PATCH 044/117] Fix include and dynamic exclude filtering in source content relocation --- .../shadow/relocation/SimpleRelocator.kt | 109 +++++++++++------- .../shadow/relocation/SimpleRelocatorTest.kt | 74 +++++++++++- 2 files changed, 139 insertions(+), 44 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index c9677d0d69..70f783f484 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -66,32 +66,16 @@ constructor( if (!excludes.isNullOrEmpty()) { this.excludes.addAll(excludes) } - - if (!rawString) { - // Create exclude pattern sets for sources. - for (exclude in this.excludes) { - // Excludes should be subpackages of the global pattern. - if (exclude.startsWith(this.pattern)) { - sourcePackageExcludes.add( - exclude.substring(this.pattern.length).replaceFirst("[.][*]$".toRegex(), "") - ) - } - // Excludes should be subpackages of the global pattern. - if (exclude.startsWith(pathPattern)) { - sourcePathExcludes.add( - exclude.substring(pathPattern.length).replaceFirst("/[*]$".toRegex(), "") - ) - } - } - } } public open fun include(pattern: String) { includes.addAll(normalizePatterns(listOf(pattern))) + includes.add(pattern) } public open fun exclude(pattern: String) { excludes.addAll(normalizePatterns(listOf(pattern))) + excludes.add(pattern) } override fun canRelocatePath(path: String): Boolean { @@ -128,10 +112,26 @@ constructor( } override fun applyToSourceContent(sourceContent: String): String { - if (rawString) return sourceContent + if (rawString || pattern.isEmpty()) return sourceContent + val sourceIncludes = getSourceSubpatterns(includes, pattern) + val sourceExcludes = getSourceSubpatterns(excludes, pattern) val content = - shadeSourceWithExcludes(sourceContent, pattern, shadedPattern, sourcePackageExcludes) - return shadeSourceWithExcludes(content, pathPattern, shadedPathPattern, sourcePathExcludes) + shadeSourceWithFilters( + sourceContent = sourceContent, + patternFrom = pattern, + patternTo = shadedPattern, + includedPatterns = sourceIncludes, + hasIncludes = includes.isNotEmpty(), + excludedPatterns = sourceExcludes, + ) + return shadeSourceWithFilters( + sourceContent = content, + patternFrom = pathPattern, + patternTo = shadedPathPattern, + includedPatterns = sourceIncludes, + hasIncludes = includes.isNotEmpty(), + excludedPatterns = sourceExcludes, + ) } override fun equals(other: Any?): Boolean { @@ -143,8 +143,6 @@ constructor( pathPattern == other.pathPattern && shadedPattern == other.shadedPattern && shadedPathPattern == other.shadedPathPattern && - sourcePackageExcludes == other.sourcePackageExcludes && - sourcePathExcludes == other.sourcePathExcludes && includes == other.includes && excludes == other.excludes } @@ -157,8 +155,6 @@ constructor( pathPattern, shadedPattern, shadedPathPattern, - sourcePackageExcludes, - sourcePathExcludes, includes, excludes, ) @@ -171,8 +167,6 @@ constructor( append("pathPattern='$pathPattern'").append(", ") append("shadedPattern='$shadedPattern'").append(", ") append("shadedPathPattern='$shadedPathPattern'").append(", ") - append("sourcePackageExcludes=$sourcePackageExcludes").append(", ") - append("sourcePathExcludes=$sourcePathExcludes").append(", ") append("includes=$includes").append(", ") append("excludes=$excludes") append(")") @@ -239,31 +233,63 @@ constructor( } } - fun shadeSourceWithExcludes( + fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + if (patternPrefix.isEmpty()) return emptySet() + val result = mutableSetOf() + val dotPrefix = patternPrefix.replace('/', '.') + val slashPrefix = patternPrefix.replace('.', '/') + val trailingWildcardRegex = "[./][*]+$".toRegex() + + for (pat in patterns) { + val dotPat = pat.replace('/', '.') + if (dotPat.startsWith(dotPrefix)) { + val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('.', '/')) + } + } + val slashPat = pat.replace('.', '/') + if (slashPat.startsWith(slashPrefix)) { + val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('/', '.')) + } + } + } + return result + } + + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, + includedPatterns: Set, + hasIncludes: Boolean, excludedPatterns: Set, ): String { - // Usually shading makes package names a bit longer, so make buffer 10% bigger than original - // source. + if (hasIncludes && includedPatterns.isEmpty()) { + return sourceContent + } + val shadedSourceContent = StringBuilder(sourceContent.length * 11 / 10) - // Make sure that search pattern starts at word boundary and that we look for literal ".", not - // regex jokers. val snippets = sourceContent .split(("\\b" + patternFrom.replace(".", "[.]") + "\\b").toRegex()) .filter(CharSequence::isNotEmpty) + snippets.forEachIndexed { i, snippet -> val isFirstSnippet = i == 0 val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] - var doExclude = false - for (excludedPattern in excludedPatterns) { - if (snippet.startsWith(excludedPattern)) { - doExclude = true - break - } - } + + val isIncluded = !hasIncludes || includedPatterns.any { snippet.startsWith(it) } + val isExcluded = excludedPatterns.any { snippet.startsWith(it) } + if (isFirstSnippet) { shadedSourceContent.append(snippet) } else { @@ -271,8 +297,9 @@ constructor( val afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() - val shouldExclude = doExclude || afterDotSlashSpace && !afterJavaKeyWord - shadedSourceContent.append(if (shouldExclude) patternFrom else patternTo).append(snippet) + val shouldRelocate = + isIncluded && !isExcluded && (!afterDotSlashSpace || afterJavaKeyWord) + shadedSourceContent.append(if (shouldRelocate) patternTo else patternFrom).append(snippet) } } return shadedSourceContent.toString() diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index 4a88226aad..8f4bf487d8 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -359,13 +359,17 @@ class SimpleRelocatorTest { @Test fun relocateSourceWithExcludes() { - // Main relocator with in-/excludes + // Main relocator with excludes val relocator = SimpleRelocator( "org.apache.maven", "com.acme.maven", - listOf("foo.bar", "zot.baz"), - listOf("irrelevant.exclude", "org.apache.maven.exclude1", "org.apache.maven.sub.exclude2"), + excludes = + listOf( + "irrelevant.exclude", + "org.apache.maven.exclude1", + "org.apache.maven.sub.exclude2", + ), ) // Make sure not to replace variables 'io' and 'ioInput', package 'java.io' val ioRelocator = SimpleRelocator("io", "shaded.io") @@ -383,6 +387,70 @@ class SimpleRelocatorTest { .isEqualTo(relocatedFile) } + @Test + fun relocateSourceWithIncludes() { + val relocator = + SimpleRelocator( + "org.apache.maven", + "com.acme.maven", + includes = listOf("org.apache.maven.hello.*", "org.apache.maven.In"), + ) + val input = + """ + |package org.apache.maven.hello; + |import org.apache.maven.hello.World; + |import org.apache.maven.other.Other; + |import org.apache.maven.In; + |import org.apache.maven.NotIn; + """ + .trimMargin() + val expected = + """ + |package com.acme.maven.hello; + |import com.acme.maven.hello.World; + |import org.apache.maven.other.Other; + |import com.acme.maven.In; + |import org.apache.maven.NotIn; + """ + .trimMargin() + assertThat(relocator.applyToSourceContent(input)).isEqualTo(expected) + } + + @Test + fun relocateSourceWithDslExcludeAndInclude() { + val relocatorExclude = SimpleRelocator("org.apache.maven", "com.acme.maven") + relocatorExclude.exclude("org.apache.maven.exclude1.*") + val inputExclude = + """ + |import org.apache.maven.hello.World; + |import org.apache.maven.exclude1.Ex1; + """ + .trimMargin() + val expectedExclude = + """ + |import com.acme.maven.hello.World; + |import org.apache.maven.exclude1.Ex1; + """ + .trimMargin() + assertThat(relocatorExclude.applyToSourceContent(inputExclude)).isEqualTo(expectedExclude) + + val relocatorInclude = SimpleRelocator("org.apache.maven", "com.acme.maven") + relocatorInclude.include("org.apache.maven.hello.*") + val inputInclude = + """ + |import org.apache.maven.hello.World; + |import org.apache.maven.other.Other; + """ + .trimMargin() + val expectedInclude = + """ + |import com.acme.maven.hello.World; + |import org.apache.maven.other.Other; + """ + .trimMargin() + assertThat(relocatorInclude.applyToSourceContent(inputInclude)).isEqualTo(expectedInclude) + } + private companion object { val sourceFile = """ From 3046fdaf22a0e53c551f4e84ddc3e1e2a1e273ac Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 16:38:07 +0800 Subject: [PATCH 045/117] Preserve SourceDirectorySet filters for project sources and match project dependencies by identifier --- .../gradle/plugins/shadow/RelocationTest.kt | 44 +++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 2 +- .../internal/DefaultDependencyFilter.kt | 38 ++++---- .../plugins/shadow/internal/SourcesJar.kt | 95 ++++++++++--------- .../plugins/shadow/ShadowPropertiesTest.kt | 2 +- 6 files changed, 118 insertions(+), 65 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 8f509f158c..48b98c9bb9 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -9,6 +9,8 @@ import assertk.assertions.isNotEqualTo import assertk.fail import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONSTANT_TIME_FOR_ZIP_ENTRIES import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader +import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast +import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getBytes import com.github.jengelman.gradle.plugins.shadow.testkit.getContent @@ -882,6 +884,48 @@ class RelocationTest : BasePluginTest() { } } + @Test + fun relocateShadowedSourcesJarRespectsSourceDirectorySetFilters() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main {} + """ + .trimMargin() + ) + path("src/main/java/my/Excluded.java") + .writeText( + """ + |package my; + |public class Excluded {} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |sourceSets { + | main { + | java { + | exclude '**/Excluded.java' + | } + | } + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast("my/Main.java") + containsNone("my/Excluded.java") + } + } + @Test fun generateShadowedSourcesJarWithCustomIncludedSourcesJars() { writeClass() diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 802b139d83..1a8412cddf 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -50,7 +50,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - mainSourceSet.map { it.allSource.srcDirs } + mainSourceSet.map { it.allSource } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index 7a765e81e0..704f9390e8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -39,7 +39,7 @@ public abstract class ShadowKmpPlugin : Plugin { task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + kotlinJvmMain.map { it.allKotlinSourceSets.map { ss -> ss.kotlin } } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 12edefe997..86f5544ac6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -44,22 +44,23 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) includedDependencies = includes, excludedDependencies = excludes, ) - val componentIds = - configuration.incoming.resolutionResult.allDependencies - .filterIsInstance() - .map { it.selected.id } - .toSet() + val allResolvedDependencies = + configuration.incoming.resolutionResult.allDependencies.filterIsInstance< + ResolvedDependencyResult + >() + + val includedDependenciesResults = allResolvedDependencies.filter { dep -> + includes.any { inc -> + inc.moduleGroup == dep.selected.moduleVersion?.group && + inc.moduleName == dep.selected.moduleVersion?.name && + inc.moduleVersion == dep.selected.moduleVersion?.version + } + } val externalComponentIds = - componentIds + includedDependenciesResults + .map { it.selected.id } .filterIsInstance() - .filter { id -> - includes.any { - it.moduleGroup == id.group && - it.moduleName == id.module && - it.moduleVersion == id.version - } - } .toSet() val externalSourcesFiles = @@ -73,7 +74,12 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) .filterIsInstance() .map { it.file } - val includedProjectNames = includes.map { it.moduleName }.toSet() + val projectComponentIds = + includedDependenciesResults + .map { it.selected.id } + .filterIsInstance() + .toSet() + val projectSourcesFiles = try { configuration.incoming @@ -89,9 +95,7 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) project.objects.named(DocsType::class.java, DocsType.SOURCES), ) } - view.componentFilter { id -> - id is ProjectComponentIdentifier && id.projectName in includedProjectNames - } + view.componentFilter { id -> id in projectComponentIds } view.lenient(true) } .files diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 48c3e08958..6456aa2665 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -20,7 +20,6 @@ internal fun generateShadowedSourcesJar( preserveFileTimestamps: Boolean, ) { val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } - if (sourceSetsSourceDirs.none() && sourcesJars.isEmpty()) return val visitedFiles = mutableSetOf() val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 @@ -49,54 +48,60 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - val sortedSourceDirs = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } - for (srcDir in sortedSourceDirs) { - srcDir - .walkTopDown() - .filter { it.isFile } - .toList() - .sortedBy { it.relativeTo(srcDir).invariantSeparatorsPath } - .forEach { file -> - val relPath = file.relativeTo(srcDir).invariantSeparatorsPath - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + val sourceItems = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } + for (item in sourceItems) { + val filesWithRelPaths: List> = + if (item.isDirectory) { + item + .walkTopDown() + .filter { it.isFile } + .toList() + .sortedBy { it.relativeTo(item).invariantSeparatorsPath } + .map { it to it.relativeTo(item).invariantSeparatorsPath } + } else { + listOf(item to item.name) + } + + for ((file, relPath) in filesWithRelPaths) { + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } } } + } } sourcesJars.forEach { jarFile -> diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index d18d8eaa7d..73c9df4caf 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -182,7 +182,7 @@ class ShadowPropertiesTest { assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly( - *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + *javaPluginExtension.sourceSets.getByName("main").allSource.files.toTypedArray() ) } From 3f7a56e09e01d295ec8a858e59fa0f620fa83411 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 17:00:48 +0800 Subject: [PATCH 046/117] Dynamically derive shadow sources artifact classifier from archiveClassifier --- docs/publishing/README.md | 58 ++++++++++++++++ .../gradle/plugins/shadow/PublishingTest.kt | 69 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 14 ++-- 3 files changed, 137 insertions(+), 4 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index b229f3c255..e61c9712b8 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -613,6 +613,64 @@ When Gradle's standard `java.withSourcesJar()` is enabled, the Shadow plugin aut The published Maven publication will include both `--all.jar` and `--all-sources.jar`. +### Local File Names vs. Published Classifiers + +The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in Maven repositories and Gradle Module Metadata: + +| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case | +|:---|:---|:---|:---|:---| +| `archiveClassifier = "all"` *(default)* | `--all-sources.jar` | `all-sources` | `--all-sources.jar` | **Coexistence** (coexists with standard `sources`) | +| `archiveClassifier = "shaded"` | `--shaded-sources.jar` | `shaded-sources` | `--shaded-sources.jar` | **Coexistence** (custom classifier) | +| `archiveClassifier = ""` | `--sources.jar` | `sources` | `--sources.jar` | **Replacement** (replaces standard `sources`) | + +#### Coexistence Scenario + +When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with `shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as `-sources` (such as `all-sources` or `shaded-sources`). + +#### Replacement Scenario + +When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. In this scenario, ensure standard `jar` and `sourcesJar` tasks are disabled so that only the shadowed artifacts are produced and published without destination or coordinate conflicts: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + java { + withSourcesJar() + } + + tasks.jar { + enabled = false + } + + tasks.named("sourcesJar") { + enabled = false + } + + tasks.shadowJar { + archiveClassifier = "" + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + java { + withSourcesJar() + } + + tasks.named('jar') { + enabled = false + } + + tasks.named('sourcesJar') { + enabled = false + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveClassifier = '' + } + ``` + > [!NOTE] > Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. > In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index eb763ec5e7..cfbb381645 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -348,6 +348,75 @@ class PublishingTest : BasePluginTest() { assertShadowSourcesVariantCommon(gmm) } + @Test + fun publishWithSourcesJarAndCustomClassifier() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = 'shaded' + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0-shaded.jar", + "maven-1.0-shaded-sources.jar", + ) + } + + @Test + fun publishJavaComponentWithShadowAndSourcesVariants() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.java + |} + """ + .trimMargin(), + ) + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0.jar", + "maven-1.0-sources.jar", + "maven-1.0-all.jar", + "maven-1.0-all-sources.jar", + ) + } + @Test fun dontPublishSourcesWhenGenerateSourcesJarDisabled() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 1a8412cddf..02d651519c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -11,6 +11,7 @@ import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.artifacts.ConfigurablePublishArtifact import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration import org.gradle.api.attributes.Bundling @@ -93,10 +94,15 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.classifier = "sources" - artifact.type = "jar" + val sourcesArtifact = + outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.type = "jar" + } + tasks.shadowJar.configure { shadowJar -> + val shadowClassifier = shadowJar.archiveClassifier.orNull + (sourcesArtifact as? ConfigurablePublishArtifact)?.classifier = + if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" } } From 46f17b7c954f5824f1a6a10d1ff1f76bb39d116a Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 17:23:35 +0800 Subject: [PATCH 047/117] Fix shadow sources artifact classifier assignment and document replacement publishing --- docs/publishing/README.md | 74 ++++++++++++++++++- .../gradle/plugins/shadow/PublishingTest.kt | 1 + .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 14 ++-- 3 files changed, 79 insertions(+), 10 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index e61c9712b8..31d35a07f3 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -629,11 +629,21 @@ When publishing alongside standard Java artifacts (e.g. publishing `from(compone #### Replacement Scenario -When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. In this scenario, ensure standard `jar` and `sourcesJar` tasks are disabled so that only the shadowed artifacts are produced and published without destination or coordinate conflicts: +When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. + +To publish shadowed artifacts as the primary publication: + +1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: === ":material-language-kotlin: build.gradle.kts" ```kotlin + plugins { + java + `maven-publish` + id("com.gradleup.shadow") + } + java { withSourcesJar() } @@ -649,11 +659,25 @@ When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = " tasks.shadowJar { archiveClassifier = "" } + + publishing { + publications { + create("shadow") { + from(components["shadow"]) + } + } + } ``` === ":simple-apachegroovy: build.gradle" ```groovy + plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' + } + java { withSourcesJar() } @@ -669,6 +693,54 @@ When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = " tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { archiveClassifier = '' } + + publishing { + publications { + shadow(MavenPublication) { + from components.shadow + } + } + } + ``` + +2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` tasks does not remove standard variants from the `java` software component. You must also explicitly skip the standard publication variants: + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + plugins { + java + `maven-publish` + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + + components.named("java") { + withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() } + withVariantsFromConfiguration(configurations["sourcesElements"]) { skip() } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'java' + id 'maven-publish' + id 'com.gradleup.shadow' + } + + java { + withSourcesJar() + } + + components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { + withVariantsFromConfiguration(configurations.runtimeElements) { skip() } + withVariantsFromConfiguration(configurations.sourcesElements) { skip() } + } ``` > [!NOTE] diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index cfbb381645..d2e5f4f5aa 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -362,6 +362,7 @@ class PublishingTest : BasePluginTest() { shadowBlock = """ |archiveClassifier = 'shaded' + |archiveSourcesFile = layout.buildDirectory.file('custom.jar') """ .trimMargin(), publicationsBlock = diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 02d651519c..39359df983 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -11,7 +11,6 @@ import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project -import org.gradle.api.artifacts.ConfigurablePublishArtifact import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration import org.gradle.api.attributes.Bundling @@ -94,14 +93,11 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - val sourcesArtifact = - outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.type = "jar" - } - tasks.shadowJar.configure { shadowJar -> - val shadowClassifier = shadowJar.archiveClassifier.orNull - (sourcesArtifact as? ConfigurablePublishArtifact)?.classifier = + outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> + artifact.builtBy(tasks.shadowJar) + artifact.type = "jar" + val shadowClassifier = tasks.shadowJar.flatMap { it.archiveClassifier }.orNull + artifact.classifier = if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" } } From effabb045139fc7b7312b70074fa72a1d9d8694c Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 17:52:26 +0800 Subject: [PATCH 048/117] Fix resource relative paths and include/exclude matching in shadowed sources JAR --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 24 ++++ .../gradle/plugins/shadow/PublishingTest.kt | 3 + .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 6 +- .../plugins/shadow/internal/SourcesJar.kt | 113 ++++++++++-------- .../shadow/relocation/SimpleRelocator.kt | 12 +- .../plugins/shadow/ShadowPropertiesTest.kt | 5 +- .../shadow/relocation/SimpleRelocatorTest.kt | 35 ++++++ 8 files changed, 146 insertions(+), 54 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 979623bd80..0fb8980a17 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -1316,6 +1316,30 @@ class JavaPluginsTest : BasePluginTest() { ) } + @Test + fun sourcesJarPreservesResourceRelativePath() { + writeClass() + path("src/main/resources/config/sub/app.properties").writeText("key=value") + + projectScript.appendText( + """ + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast( + "my/Main.java", + "config/sub/app.properties", + ) + } + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index d2e5f4f5aa..d05fb58770 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -383,6 +383,9 @@ class PublishingTest : BasePluginTest() { "maven-1.0-shaded.jar", "maven-1.0-shaded-sources.jar", ) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertThat(gmm.shadowSourcesElementsVariant.fileNames.single()) + .isEqualTo("maven-1.0-shaded-sources.jar") } @Test diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 39359df983..28b69c6e18 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -50,7 +50,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - mainSourceSet.map { it.allSource } + mainSourceSet.map { it.allSource.sourceDirectories + it.allSource } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index 704f9390e8..ddeba32c59 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -39,7 +39,11 @@ public abstract class ShadowKmpPlugin : Plugin { task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - kotlinJvmMain.map { it.allKotlinSourceSets.map { ss -> ss.kotlin } } + kotlinJvmMain.map { + it.allKotlinSourceSets.flatMap { ss -> + listOf(ss.kotlin.sourceDirectories, ss.kotlin) + } + } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 6456aa2665..3bd56948c6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -48,57 +48,72 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - val sourceItems = sourceSetsSourceDirs.filter { it.exists() }.sortedBy { it.path } - for (item in sourceItems) { - val filesWithRelPaths: List> = - if (item.isDirectory) { - item - .walkTopDown() - .filter { it.isFile } - .toList() - .sortedBy { it.relativeTo(item).invariantSeparatorsPath } - .map { it to it.relativeTo(item).invariantSeparatorsPath } - } else { - listOf(item to item.name) - } + val sourceItems = sourceSetsSourceDirs.filter { it.exists() } + val (dirs, files) = sourceItems.partition { it.isDirectory } + val sortedDirs = dirs.sortedByDescending { it.path.length } - for ((file, relPath) in filesWithRelPaths) { - val isSource = isSourceFile(relPath) - if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocatePath(canonicalPath) - if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } - val bytes = transformedText.toByteArray(charset) - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + val filesWithRelPaths = mutableListOf>() + val dirsCoveredByFiles = mutableSetOf() + + for (file in files.sortedBy { it.path }) { + val matchingDir = sortedDirs.firstOrNull { file.startsWith(it) } + if (matchingDir != null) { + dirsCoveredByFiles.add(matchingDir) + filesWithRelPaths.add(file to file.relativeTo(matchingDir).invariantSeparatorsPath) + } else { + filesWithRelPaths.add(file to file.name) + } + } + + for (dir in dirs.sortedBy { it.path }) { + if (dir !in dirsCoveredByFiles) { + dir + .walkTopDown() + .filter { it.isFile } + .toList() + .sortedBy { it.relativeTo(dir).invariantSeparatorsPath } + .forEach { file -> + filesWithRelPaths.add(file to file.relativeTo(dir).invariantSeparatorsPath) } - } else { - val relocatedPath = relocators.relocatePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val bytes = file.readBytes() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = file.lastModified(), - unixMode = UnixMode.file(), - ) { - write(bytes) - } + } + } + + for ((file, relPath) in filesWithRelPaths) { + val isSource = isSourceFile(relPath) + if (isSource) { + val text = file.readText(charset) + val pkg = extractPackage(text) + val simpleName = file.name + val canonicalPath = + if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" + if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocatePath(canonicalPath) + if (visitedFiles.add(relocatedPath)) { + var transformedText = text + for (relocator in relocators) { + transformedText = relocator.applyToSourceContent(transformedText) + } + val bytes = transformedText.toByteArray(charset) + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val bytes = file.readBytes() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = file.lastModified(), + unixMode = UnixMode.file(), + ) { + write(bytes) } } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 70f783f484..7ca7a75d5a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -265,6 +265,14 @@ constructor( return result } + private fun matchesSubpattern(snippet: String, subpattern: String): Boolean { + if (!snippet.startsWith(subpattern)) return false + if (subpattern.isEmpty() || snippet.length == subpattern.length) return true + if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true + val nextChar = snippet[subpattern.length] + return !nextChar.isLetterOrDigit() && nextChar != '_' + } + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, @@ -287,8 +295,8 @@ constructor( val isFirstSnippet = i == 0 val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] - val isIncluded = !hasIncludes || includedPatterns.any { snippet.startsWith(it) } - val isExcluded = excludedPatterns.any { snippet.startsWith(it) } + val isIncluded = !hasIncludes || includedPatterns.any { matchesSubpattern(snippet, it) } + val isExcluded = excludedPatterns.any { matchesSubpattern(snippet, it) } if (isFirstSnippet) { shadedSourceContent.append(snippet) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 73c9df4caf..aae0aaf974 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -180,9 +180,12 @@ class ShadowPropertiesTest { javaPluginExtension.withSourcesJar() val shadowJarTask = tasks.shadowJar.get() assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly( - *javaPluginExtension.sourceSets.getByName("main").allSource.files.toTypedArray() + *(mainSourceSet.allSource.sourceDirectories + mainSourceSet.allSource) + .files + .toTypedArray() ) } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index 8f4bf487d8..030cfdb26b 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -357,6 +357,41 @@ class SimpleRelocatorTest { assertThat(relocator.applyToSourceContent(sourceFile)).isEqualTo(sourceFile) } + @Test + fun relocateSourceFileWithPrefixCollision() { + val relocator = + SimpleRelocator( + "org.example", + "relocated.org.example", + includes = listOf("org.example.In"), + ) + val source = + """ + |import org.example.In; + |import org.example.Input; + |import org.example.In.Nested; + | + |public class Test { + | org.example.In a; + | org.example.Input b; + |} + """ + .trimMargin() + val expected = + """ + |import relocated.org.example.In; + |import org.example.Input; + |import relocated.org.example.In.Nested; + | + |public class Test { + | relocated.org.example.In a; + | org.example.Input b; + |} + """ + .trimMargin() + assertThat(relocator.applyToSourceContent(source)).isEqualTo(expected) + } + @Test fun relocateSourceWithExcludes() { // Main relocator with excludes From f9874eda473bf9ec9905169a5bbaba5fbffe137c Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 18:13:12 +0800 Subject: [PATCH 049/117] Dynamically resolve sources publish artifact classifier and ensure path boundary safe directory matching --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 33 ++++++++++++++++ .../gradle/plugins/shadow/PublishingTest.kt | 39 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 38 ++++++++++++++---- .../plugins/shadow/internal/SourcesJar.kt | 20 ++++++---- 4 files changed, 115 insertions(+), 15 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 0fb8980a17..5658f00df6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -1340,6 +1340,39 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun sourcesJarHandlesOverlappingSourceDirectoryPrefixes() { + writeClass() + path("src/main/res/a.properties").writeText("a=1") + path("src/main/resources/b.properties").writeText("b=2") + + projectScript.appendText( + """ + |sourceSets { + | main { + | resources { + | srcDir 'src/main/res' + | } + | } + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsAtLeast( + "my/Main.java", + "a.properties", + "b.properties", + ) + } + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index d05fb58770..6b228540d6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -388,6 +388,45 @@ class PublishingTest : BasePluginTest() { .isEqualTo("maven-1.0-shaded-sources.jar") } + @Test + fun publishWithSourcesJarAndCustomClassifierAfterPublishingBlock() { + projectScript.appendText( + """ + |apply plugin: 'maven-publish' + |java { + | withSourcesJar() + |} + |publishing { + | repositories { + | maven { url = '${remoteRepoPath.toUri()}' } + | } + | publications { + | shadow(MavenPublication) { + | from components.shadow + | } + | } + |} + |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + | archiveClassifier = 'shaded' + | archiveSourcesFile = layout.buildDirectory.file('custom.jar') + |} + """ + .trimMargin() + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0-shaded.jar", + "maven-1.0-shaded-sources.jar", + ) + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertThat(gmm.shadowSourcesElementsVariant.fileNames.single()) + .isEqualTo("maven-1.0-shaded-sources.jar") + } + @Test fun publishJavaComponentWithShadowAndSourcesVariants() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 28b69c6e18..eb96bf373e 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -5,14 +5,18 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.sha import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.runtimeConfiguration import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.registerShadowJarCommon import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.shadowJar +import java.io.File +import java.util.Date import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration +import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -25,6 +29,8 @@ import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME +import org.gradle.api.tasks.TaskDependency +import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -93,13 +99,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - outgoing.artifact(tasks.shadowJar.flatMap { it.archiveSourcesFile }) { artifact -> - artifact.builtBy(tasks.shadowJar) - artifact.type = "jar" - val shadowClassifier = tasks.shadowJar.flatMap { it.archiveClassifier }.orNull - artifact.classifier = - if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" - } + outgoing.artifact(ShadowSourcesPublishArtifact(tasks.shadowJar)) } // See more details in #2086. @@ -232,3 +232,27 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } + +internal class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : + PublishArtifact { + override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull ?: "" + + override fun getExtension(): String = + shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" + + override fun getType(): String = "jar" + + override fun getClassifier(): String { + val shadowClassifier = shadowJarTask.flatMap { it.archiveClassifier }.orNull + return if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" + } + + override fun getFile(): File = shadowJarTask.flatMap { it.archiveSourcesFile }.get().asFile + + override fun getDate(): Date? = null + + @Suppress("EagerGradleConfiguration") + override fun getBuildDependencies(): TaskDependency = TaskDependency { + setOf(shadowJarTask.get()) + } +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 3bd56948c6..9c63abc29e 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -50,30 +50,34 @@ internal fun generateShadowedSourcesJar( val sourceItems = sourceSetsSourceDirs.filter { it.exists() } val (dirs, files) = sourceItems.partition { it.isDirectory } - val sortedDirs = dirs.sortedByDescending { it.path.length } + val normalizedDirs = + dirs.map { it to it.normalize().toPath() }.sortedByDescending { it.second.nameCount } val filesWithRelPaths = mutableListOf>() - val dirsCoveredByFiles = mutableSetOf() + val coveredDirs = mutableSetOf() for (file in files.sortedBy { it.path }) { - val matchingDir = sortedDirs.firstOrNull { file.startsWith(it) } + val filePath = file.normalize().toPath() + val matchingDir = + normalizedDirs.firstOrNull { (_, dirPath) -> filePath.startsWith(dirPath) }?.first if (matchingDir != null) { - dirsCoveredByFiles.add(matchingDir) + coveredDirs.add(matchingDir) filesWithRelPaths.add(file to file.relativeTo(matchingDir).invariantSeparatorsPath) } else { filesWithRelPaths.add(file to file.name) } } - for (dir in dirs.sortedBy { it.path }) { - if (dir !in dirsCoveredByFiles) { + for ((dir, dirPath) in normalizedDirs.sortedBy { it.second.nameCount }) { + if (coveredDirs.none { dirPath.startsWith(it.normalize().toPath()) }) { + coveredDirs.add(dir) dir .walkTopDown() .filter { it.isFile } .toList() .sortedBy { it.relativeTo(dir).invariantSeparatorsPath } - .forEach { file -> - filesWithRelPaths.add(file to file.relativeTo(dir).invariantSeparatorsPath) + .forEach { f -> + filesWithRelPaths.add(f to f.relativeTo(dir).invariantSeparatorsPath) } } } From a5478943e1524f14a94895206d12b64053df8266 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 18:26:44 +0800 Subject: [PATCH 050/117] Reformat docs --- docs/publishing/README.md | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 31d35a07f3..51db8f198f 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -615,25 +615,31 @@ The published Maven publication will include both `--all.ja ### Local File Names vs. Published Classifiers -The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in Maven repositories and Gradle Module Metadata: +The Shadow plugin distinguishes between the **local output file** on disk and the **published artifact classifier** in +Maven repositories and Gradle Module Metadata: -| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case | -|:---|:---|:---|:---|:---| -| `archiveClassifier = "all"` *(default)* | `--all-sources.jar` | `all-sources` | `--all-sources.jar` | **Coexistence** (coexists with standard `sources`) | -| `archiveClassifier = "shaded"` | `--shaded-sources.jar` | `shaded-sources` | `--shaded-sources.jar` | **Coexistence** (custom classifier) | -| `archiveClassifier = ""` | `--sources.jar` | `sources` | `--sources.jar` | **Replacement** (replaces standard `sources`) | +| Configuration | Local Output File (`archiveSourcesFile` in `build/libs`) | Published Classifier | Published File (Maven Repository) | Use Case | +|:----------------------------------------|:---------------------------------------------------------|:---------------------|:--------------------------------------------|:---------------------------------------------------| +| `archiveClassifier = "all"` *(default)* | `--all-sources.jar` | `all-sources` | `--all-sources.jar` | **Coexistence** (coexists with standard `sources`) | +| `archiveClassifier = "shaded"` | `--shaded-sources.jar` | `shaded-sources` | `--shaded-sources.jar` | **Coexistence** (custom classifier) | +| `archiveClassifier = ""` | `--sources.jar` | `sources` | `--sources.jar` | **Replacement** (replaces standard `sources`) | #### Coexistence Scenario -When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with `shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as `-sources` (such as `all-sources` or `shaded-sources`). +When publishing alongside standard Java artifacts (e.g. publishing `from(components["java"])` with +`shadow.addShadowVariantIntoJavaComponent = true`), the standard sources variant uses classifier `sources`. To prevent +coordinate collisions within the same publication, the shadowed sources variant dynamically derives its classifier as +`-sources` (such as `all-sources` or `shaded-sources`). #### Replacement Scenario -When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. +When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR +automatically uses the standard `sources` classifier. To publish shadowed artifacts as the primary publication: -1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: +1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven + publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: === ":material-language-kotlin: build.gradle.kts" @@ -703,7 +709,9 @@ To publish shadowed artifacts as the primary publication: } ``` -2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` tasks does not remove standard variants from the `java` software component. You must also explicitly skip the standard publication variants: +2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` + tasks does not remove standard variants from the `java` software component. You must also explicitly skip the + standard publication variants: === ":material-language-kotlin: build.gradle.kts" @@ -747,12 +755,14 @@ To publish shadowed artifacts as the primary publication: > Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. > In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid > unnecessary build overhead for application builds. If `withSourcesJar()` is omitted, publishing from -> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing builds. +> `components["shadow"]` will only publish the shadowed binary JAR, preserving backward compatibility for existing +builds. > You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`). ### Customizing the Sources Archive File -The companion shadowed sources JAR output location is configured via [`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], +The companion shadowed sources JAR output location is configured via +[`ShadowJar.archiveSourcesFile`][ShadowJar.archiveSourcesFile], which defaults to the same destination and base name as `archiveFile` with `-sources.jar` suffix: === ":material-language-kotlin: build.gradle.kts" From 3d9235eff277c354e341bd22dcd4aec56ed93f32 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 18:36:18 +0800 Subject: [PATCH 051/117] Update samples in docs --- docs/publishing/README.md | 54 +++++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 51db8f198f..ba5d9d48d8 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -306,12 +306,20 @@ If you want to replace standard JARs with the shadowed ones, disable the standar === ":material-language-kotlin: build.gradle.kts" ```kotlin + plugins { + java + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + tasks.jar { enabled = false } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == "sourcesJar" }.configureEach { + tasks.named("sourcesJar") { enabled = false } ``` @@ -319,12 +327,20 @@ If you want to replace standard JARs with the shadowed ones, disable the standar === ":simple-apachegroovy: build.gradle" ```groovy + plugins { + id('java') + id('com.gradleup.shadow') + } + + java { + withSourcesJar() + } + tasks.named('jar', Jar) { enabled = false } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == 'sourcesJar' }.configureEach { + tasks.named('sourcesJar', Jar) { enabled = false } ``` @@ -334,12 +350,20 @@ Or set different `archiveClassifier` values for the standard tasks: === ":material-language-kotlin: build.gradle.kts" ```kotlin + plugins { + java + id("com.gradleup.shadow") + } + + java { + withSourcesJar() + } + tasks.jar { archiveClassifier = "ignored" } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == "sourcesJar" }.configureEach { + tasks.named("sourcesJar") { (this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources" } ``` @@ -347,12 +371,20 @@ Or set different `archiveClassifier` values for the standard tasks: === ":simple-apachegroovy: build.gradle" ```groovy + plugins { + id('java') + id('com.gradleup.shadow') + } + + java { + withSourcesJar() + } + tasks.named('jar', Jar) { archiveClassifier = 'ignored' } - // If `java.withSourcesJar()` is enabled: - tasks.matching { it.name == 'sourcesJar' }.configureEach { + tasks.named('sourcesJar', Jar) { archiveClassifier = 'ignored-sources' } ``` @@ -658,7 +690,7 @@ To publish shadowed artifacts as the primary publication: enabled = false } - tasks.named("sourcesJar") { + tasks.named("sourcesJar") { enabled = false } @@ -688,11 +720,11 @@ To publish shadowed artifacts as the primary publication: withSourcesJar() } - tasks.named('jar') { + tasks.named('jar', Jar) { enabled = false } - tasks.named('sourcesJar') { + tasks.named('sourcesJar', Jar) { enabled = false } From 5308d0a1bba5d33875f50d9b14b77e18c3917005 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 19:09:45 +0800 Subject: [PATCH 052/117] Prefer containsOnly for assertions --- .../gradle/plugins/shadow/FilteringTest.kt | 8 ++----- .../gradle/plugins/shadow/JavaPluginsTest.kt | 20 +++++++++++++---- .../plugins/shadow/KotlinPluginsTest.kt | 14 +++++++++--- .../gradle/plugins/shadow/MinimizeTest.kt | 22 +++++++++++++++---- .../gradle/plugins/shadow/PublishingTest.kt | 14 ++++++++++-- .../gradle/plugins/shadow/RelocationTest.kt | 9 ++++---- .../shadow/transformers/TransformersTest.kt | 4 ++-- .../plugins/shadow/internal/SourcesJarTest.kt | 12 ++++++++-- 8 files changed, 76 insertions(+), 27 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt index d349a07d8b..74211d1c56 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -2,8 +2,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader -import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast -import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import kotlin.io.path.appendText @@ -260,12 +258,10 @@ class FilteringTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsAtLeast("g/G.class") - containsNone("h/H.class", "h/UnusedH.class") + containsOnly(*entriesInAB, "g/", "g/G.class", *manifestEntries) } assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("g/G.java") - containsNone("h/H.java", "h/UnusedH.java") + containsOnly("g/", "g/G.java", *manifestEntries) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 5658f00df6..fe75032fe6 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt @@ -295,7 +295,7 @@ class JavaPluginsTest : BasePluginTest() { // The fact that server compiled successfully against `client.junit.framework.Test` // means it consumed the shadowed artifact during compilation. assertThat(jarPath("server/build/libs/server-1.0.jar")).useAll { - containsAtLeast("server/Server.class") + containsOnly("server/", "server/Server.class", *manifestEntries) } } @@ -1176,7 +1176,13 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess(":app:$SHADOW_JAR_TASK_NAME") assertThat(jarPath("app/build/libs/app-all.jar")).useAll { - containsAtLeast("com/company/Main.class", "com/company/Utils.class", manifestEntry) + containsOnly( + "com/", + "com/company/", + "com/company/Main.class", + "com/company/Utils.class", + *manifestEntries, + ) } } @@ -1333,9 +1339,13 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast( + containsOnly( + "my/", + "config/", + "config/sub/", "my/Main.java", "config/sub/app.properties", + *manifestEntries, ) } } @@ -1365,10 +1375,12 @@ class JavaPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast( + containsOnly( + "my/", "my/Main.java", "a.properties", "b.properties", + *manifestEntries, ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 76c98bdb38..f0b8d13c93 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -8,7 +8,6 @@ import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast -import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass @@ -337,6 +336,7 @@ class KotlinPluginsTest : BasePluginTest() { @Test fun generateShadowedSourcesJarNormalizesPackageDirectory() { + val stdlib = compileOnlyStdlib(true) path("src/main/kotlin/FlatFile.kt") .writeText( """ @@ -349,6 +349,9 @@ class KotlinPluginsTest : BasePluginTest() { projectScript.writeText( """ |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} + |dependencies { + | $stdlib + |} |$shadowJarTask { | generateSourcesJar = true | relocate 'my.custom', 'shadow.custom' @@ -360,8 +363,13 @@ class KotlinPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("shadow/custom/nested/FlatFile.kt") - containsNone("FlatFile.kt") + containsOnly( + "shadow/", + "shadow/custom/", + "shadow/custom/nested/", + "shadow/custom/nested/FlatFile.kt", + *manifestEntries, + ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 7239ad0e44..7868cf7700 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt @@ -156,12 +156,26 @@ class MinimizeTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsAtLeast("my/Main.class", "h/H.class", "k/CustomUtils.class") - containsNone("h/UnusedH.class", "k/CustomUnusedUtils.class") + containsOnly( + "my/", + "h/", + "k/", + "my/Main.class", + "h/H.class", + "k/CustomUtils.class", + *manifestEntries, + ) } assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("my/Main.java", "h/H.java", "k/Utils.kt") - containsNone("h/UnusedH.java", "k/UnusedUtils.kt") + containsOnly( + "my/", + "h/", + "k/", + "my/Main.java", + "h/H.java", + "k/Utils.kt", + *manifestEntries, + ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 6b228540d6..783a683144 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -919,21 +919,31 @@ class PublishingTest : BasePluginTest() { ) assertThat(repoJarPath("$artifactRoot/my-all-1.0.jar")).useAll { - containsAtLeast( + containsOnly( + "my/", + "g/", + "h/", "my/CommonMain.class", "my/JvmMain.class", "g/G.class", "h/H.class", + "h/UnusedH.class", + "META-INF/my_maven.kotlin_module", *manifestEntries, ) } assertThat(repoJarPath("$artifactRoot/my-all-1.0-sources.jar")).useAll { - containsAtLeast( + containsOnly( + "my/", + "g/", + "h/", "my/CommonMain.kt", "my/JvmMain.kt", "g/G.java", "h/H.java", + "h/UnusedH.java", + *manifestEntries, ) } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 48b98c9bb9..7879115f75 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt @@ -9,8 +9,6 @@ import assertk.assertions.isNotEqualTo import assertk.fail import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.CONSTANT_TIME_FOR_ZIP_ENTRIES import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader -import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast -import com.github.jengelman.gradle.plugins.shadow.testkit.containsNone import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getBytes import com.github.jengelman.gradle.plugins.shadow.testkit.getContent @@ -921,8 +919,11 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedSourcesJar).useAll { - containsAtLeast("my/Main.java") - containsNone("my/Excluded.java") + containsOnly( + "my/", + "my/Main.java", + *manifestEntries, + ) } } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt index a707a2fc61..8f901d4bc1 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/transformers/TransformersTest.kt @@ -219,7 +219,7 @@ class TransformersTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsOnly("META-INF/", "META-INF/LICENSE", *manifestEntries) + containsOnly("META-INF/LICENSE", *manifestEntries) getContent("META-INF/LICENSE") .isEqualTo( """ @@ -379,7 +379,7 @@ class TransformersTest : BaseTransformerTest() { runWithSuccess(shadowJarPath) assertThat(outputShadowedJar).useAll { - containsOnly("META-INF/", "META-INF/kotlin-stdlib.shadow.kotlin_module", *manifestEntries) + containsOnly("META-INF/kotlin-stdlib.shadow.kotlin_module", *manifestEntries) getBytes("META-INF/kotlin-stdlib.shadow.kotlin_module").isNotEqualTo(moduleBytes) } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index aa5a4703d0..8bfc411ab4 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -2,7 +2,7 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertFailure import assertk.assertThat -import assertk.assertions.containsAtLeast +import assertk.assertions.containsOnly import assertk.assertions.hasMessage import assertk.assertions.isEqualTo import assertk.assertions.isFalse @@ -131,7 +131,15 @@ class SourcesJarTest { assertThat(outputJar.exists()).isTrue() val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } - assertThat(entries).containsAtLeast("shadow/example/nested/Mismatched.kt") + assertThat(entries) + .containsOnly( + "META-INF/", + "META-INF/MANIFEST.MF", + "shadow/", + "shadow/example/", + "shadow/example/nested/", + "shadow/example/nested/Mismatched.kt", + ) } @Test From 1934fdf2f93a4c30ddb509a432d086e506ab06c8 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 19:13:16 +0800 Subject: [PATCH 053/117] Cleanups --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 51 ++++++----------- .../internal/DefaultDependencyFilter.kt | 21 ++++--- .../plugins/shadow/internal/SourcesJar.kt | 56 +++++++++---------- .../shadow/relocation/SimpleRelocator.kt | 4 +- 4 files changed, 57 insertions(+), 75 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index eb96bf373e..0a842bb834 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -5,18 +5,14 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.sha import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.runtimeConfiguration import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets -import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.registerShadowJarCommon import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.shadowJar -import java.io.File -import java.util.Date import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration -import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -29,8 +25,6 @@ import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME -import org.gradle.api.tasks.TaskDependency -import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -99,7 +93,22 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl objects.named(DocsType::class.java, DocsType.SOURCES), ) } - outgoing.artifact(ShadowSourcesPublishArtifact(tasks.shadowJar)) + val shadowJarTask = tasks.shadowJar + outgoing.artifact(shadowJarTask.flatMap { it.archiveSourcesFile }) { artifact -> + with(artifact) { + builtBy(shadowJarTask) + name = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() + extension = shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" + type = "jar" + classifier = + shadowJarTask + .flatMap { it.archiveClassifier } + .orNull + .let { shadowClassifier -> + if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" + } + } + } } // See more details in #2086. @@ -141,9 +150,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements // If `withSourcesJar` is present and `generateSourcesJar` is enabled. - val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } val shouldAddSources = { - sourcesElements() != null && tasks.shadowJar.flatMap { it.generateSourcesJar }.get() + configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null && + tasks.shadowJar.flatMap { it.generateSourcesJar }.get() } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) @@ -232,27 +241,3 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } - -internal class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : - PublishArtifact { - override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull ?: "" - - override fun getExtension(): String = - shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" - - override fun getType(): String = "jar" - - override fun getClassifier(): String { - val shadowClassifier = shadowJarTask.flatMap { it.archiveClassifier }.orNull - return if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" - } - - override fun getFile(): File = shadowJarTask.flatMap { it.archiveSourcesFile }.get().asFile - - override fun getDate(): Date? = null - - @Suppress("EagerGradleConfiguration") - override fun getBuildDependencies(): TaskDependency = TaskDependency { - setOf(shadowJarTask.get()) - } -} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index 86f5544ac6..03ac029090 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -44,18 +44,17 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) includedDependencies = includes, excludedDependencies = excludes, ) - val allResolvedDependencies = - configuration.incoming.resolutionResult.allDependencies.filterIsInstance< - ResolvedDependencyResult - >() - val includedDependenciesResults = allResolvedDependencies.filter { dep -> - includes.any { inc -> - inc.moduleGroup == dep.selected.moduleVersion?.group && - inc.moduleName == dep.selected.moduleVersion?.name && - inc.moduleVersion == dep.selected.moduleVersion?.version - } - } + val includedDependenciesResults = + configuration.incoming.resolutionResult.allDependencies + .filterIsInstance() + .filter { dep -> + includes.any { inc -> + inc.moduleGroup == dep.selected.moduleVersion?.group && + inc.moduleName == dep.selected.moduleVersion?.name && + inc.moduleVersion == dep.selected.moduleVersion?.version + } + } val externalComponentIds = includedDependenciesResults diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 9c63abc29e..b94dc63e5b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -5,6 +5,9 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.vafer.jdeb.shaded.objectweb.asm.ClassReader +import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor +import org.vafer.jdeb.shaded.objectweb.asm.Opcodes internal fun generateShadowedSourcesJar( sourcesJarFile: File, @@ -218,32 +221,28 @@ internal fun buildSourceToClassesMap( try { var internalName: String? = null var sourceFile: String? = null - val reader = org.vafer.jdeb.shaded.objectweb.asm.ClassReader(bytes) - reader.accept( - object : - org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor( - org.vafer.jdeb.shaded.objectweb.asm.Opcodes.ASM9 - ) { - override fun visit( - version: Int, - access: Int, - name: String, - signature: String?, - superName: String?, - interfaces: Array?, - ) { - internalName = name - super.visit(version, access, name, signature, superName, interfaces) - } + ClassReader(bytes) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?, + ) { + internalName = name + super.visit(version, access, name, signature, superName, interfaces) + } - override fun visitSource(source: String?, debug: String?) { - sourceFile = source - super.visitSource(source, debug) - } - }, - org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_CODE or - org.vafer.jdeb.shaded.objectweb.asm.ClassReader.SKIP_FRAMES, - ) + override fun visitSource(source: String?, debug: String?) { + sourceFile = source + super.visitSource(source, debug) + } + }, + ClassReader.SKIP_CODE or ClassReader.SKIP_FRAMES, + ) val name = internalName ?: return val source = sourceFile ?: return @@ -256,7 +255,7 @@ internal fun buildSourceToClassesMap( } } - for (dir in classesDirs.filter { it.isDirectory }) { + for (dir in classesDirs.filter(File::isDirectory)) { dir .walkTopDown() .filter { it.isFile && it.name.endsWith(".class") } @@ -264,7 +263,7 @@ internal fun buildSourceToClassesMap( } for (file in - dependencies.filter { it.isFile && (it.name.endsWith(".jar") || it.name.endsWith(".zip")) }) { + dependencies.filter { it.isFile && (it.extension == "jar" || it.extension == "zip") }) { try { file.useZip { entries() @@ -287,8 +286,7 @@ internal fun isUnused( ): Boolean { if (unusedClasses.isEmpty()) return false val classes = sourceToClasses[canonicalPath] ?: return false - if (classes.isEmpty()) return false - return classes.all { it in unusedClasses } + return classes.isNotEmpty() && classes.all { it in unusedClasses } } private fun isSourceFile(path: String): Boolean { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 7ca7a75d5a..1e33e9d26d 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -233,7 +233,7 @@ constructor( } } - fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + private fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') @@ -273,7 +273,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - fun shadeSourceWithFilters( + private fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, From fb731deb6b519a0ab367a38a8c61d29ba368b7fd Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 19:53:24 +0800 Subject: [PATCH 054/117] Revert private modifiers --- .../gradle/plugins/shadow/relocation/SimpleRelocator.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 1e33e9d26d..924cda8101 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -233,7 +233,7 @@ constructor( } } - private fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') @@ -265,7 +265,7 @@ constructor( return result } - private fun matchesSubpattern(snippet: String, subpattern: String): Boolean { + fun matchesSubpattern(snippet: String, subpattern: String): Boolean { if (!snippet.startsWith(subpattern)) return false if (subpattern.isEmpty() || snippet.length == subpattern.length) return true if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true @@ -273,7 +273,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - private fun shadeSourceWithFilters( + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, From e725a99baea336b2828fbb5dfbb21b6281e035ba Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:19:51 +0800 Subject: [PATCH 055/117] Reformat docs --- docs/configuration/minimizing/README.md | 6 ++++-- docs/getting-started/README.md | 7 ++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index e2ccba13a2..904f0e7772 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -134,8 +134,10 @@ rules published in dependency JARs, for example under `META-INF/proguard`. > [!NOTE] > **Shadowed Sources JAR and R8** > -> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 (`minimize { r8 { ... } }`), -> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR will +> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 +(`minimize { r8 { ... } }`), +> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR +will > contain all relocated source files without responding to R8 shrinking results. > > If you need unused source files to be filtered out of the shadowed sources JAR, use the default dependency analyzer diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index fed55c39aa..ddd1cbc44f 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -138,10 +138,11 @@ in their build logic), Shadow will automatically configure the following behavio - `META-INF/versions/**/module-info.class` - `module-info.class` - Configures the [`ShadowJar`][ShadowJar] task to generate a companion **Shadowed Sources JAR** containing both - project sources and shadowed dependency sources with relocated packages when `java.withSourcesJar()` is enabled - (or when [`generateSourcesJar`][ShadowJar.generateSourcesJar] is set to `true`). + project sources and shadowed dependency sources with relocated packages when `java.withSourcesJar()` is enabled (or + when [`generateSourcesJar`][ShadowJar.generateSourcesJar] is set to `true`). - Creates and registers the `shadow` component in the project (used for integrating with - [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. + [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is + enabled. ## ShadowJar Command Line options From 9ee4fc3314e977a5a66420ac6cbe42275f92963e Mon Sep 17 00:00:00 2001 From: Zongle Wang Date: Sat, 5 Sep 2026 23:30:21 +0800 Subject: [PATCH 056/117] Fix archiveClassifier assignment in README.md Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- docs/publishing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index ba5d9d48d8..8919d5b2dd 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -364,7 +364,7 @@ Or set different `archiveClassifier` values for the standard tasks: } tasks.named("sourcesJar") { - (this as org.gradle.jvm.tasks.Jar).archiveClassifier = "ignored-sources" + archiveClassifier = "ignored-sources" } ``` From db160ebab2d455ffdcee2e0952b5e4d158a7e928 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:40:42 +0800 Subject: [PATCH 057/117] Align source relocation with bytecode remapping semantics --- .../plugins/shadow/internal/SourceRemapper.kt | 141 ++++++++++++++++++ .../plugins/shadow/internal/SourcesJar.kt | 21 +-- .../shadow/relocation/SimpleRelocator.kt | 2 +- .../shadow/internal/SourceRemapperTest.kt | 107 +++++++++++++ 4 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt new file mode 100644 index 0000000000..59b19e68d8 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -0,0 +1,141 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath +import java.util.regex.Pattern + +private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") + +private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = + Pattern.compile( + "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + + "|" + + "\\{@link( \\*)* $" + + "|" + + "([{}(=;,]|\\*/) $" + ) + +/** + * Remaps source content by applying relocators in a single pass with first-match-wins precedence, + * avoiding cascade replacements where earlier relocations get re-relocated by subsequent rules. + */ +internal fun Iterable.remapSource(sourceContent: String): String { + val relocatorList = this.toList() + if (relocatorList.isEmpty() || sourceContent.isEmpty()) return sourceContent + + val simpleRelocators = + relocatorList.filterIsInstance().filter { + !it.rawString && it.pattern.isNotEmpty() + } + + if (simpleRelocators.isEmpty()) { + var content = sourceContent + for (relocator in relocatorList) { + content = relocator.applyToSourceContent(content) + } + return content + } + + val patterns = + simpleRelocators + .flatMap { listOf(it.pattern, it.pathPattern) } + .filter { it.isNotEmpty() } + .distinct() + .sortedByDescending { it.length } + + if (patterns.isEmpty()) return sourceContent + + val patternRegex = Regex("\\b(" + patterns.joinToString("|") { Regex.escape(it) } + ")\\b") + + val result = StringBuilder((sourceContent.length * 1.1).toInt()) + var lastIndex = 0 + + for (match in patternRegex.findAll(sourceContent)) { + val matchStart = match.range.first + val matchEnd = match.range.last + 1 + val matchedText = match.value + + result.append(sourceContent, lastIndex, matchStart) + lastIndex = matchEnd + + val previousSnippet = sourceContent.substring(0, matchStart) + val previousSnippetOneLine = previousSnippet.replace("\\s+".toRegex(), " ") + val afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() + val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() + val contextValid = !afterDotSlashSpace || afterJavaKeyWord + + var replaced = false + if (contextValid) { + val suffixSnippet = sourceContent.substring(matchEnd) + for (relocator in relocatorList) { + if ( + relocator is SimpleRelocator && !relocator.rawString && relocator.pattern.isNotEmpty() + ) { + val isDotMatch = matchedText == relocator.pattern + val isPathMatch = matchedText == relocator.pathPattern + if (isDotMatch || isPathMatch) { + val sourceIncludes = + SimpleRelocator.getSourceSubpatterns(relocator.includes, relocator.pattern) + val sourceExcludes = + SimpleRelocator.getSourceSubpatterns(relocator.excludes, relocator.pattern) + val hasIncludes = relocator.includes.isNotEmpty() + if (hasIncludes && sourceIncludes.isEmpty()) { + continue + } + val isIncluded = + !hasIncludes || + sourceIncludes.any { SimpleRelocator.matchesSubpattern(suffixSnippet, it) } + val isExcluded = sourceExcludes.any { + SimpleRelocator.matchesSubpattern(suffixSnippet, it) + } + if (isIncluded && !isExcluded) { + result.append( + if (isDotMatch) relocator.shadedPattern else relocator.shadedPathPattern + ) + replaced = true + break + } + } + } + } + } + + if (!replaced) { + result.append(matchedText) + } + } + + result.append(sourceContent, lastIndex, sourceContent.length) + return result.toString() +} + +/** + * Relocates a source file path by stripping its extension before matching against class/path + * relocators, ensuring class-level include/exclude patterns work symmetrically with binary classes. + */ +internal fun Iterable.relocateSourcePath(path: String): String { + if (isSourceFile(path)) { + val extension = path.substringAfterLast('.', "") + val pathWithoutExt = path.removeSuffix(".$extension") + val className = pathWithoutExt.replace('/', '.') + + for (relocator in this) { + if (relocator.canRelocateClass(className) || relocator.canRelocatePath(pathWithoutExt)) { + val relocatedWithoutExt = relocator.relocatePath(RelocatePathContext(pathWithoutExt)) + return "$relocatedWithoutExt.$extension" + } + } + return path + } + + return relocatePath(path) +} + +internal fun isSourceFile(path: String): Boolean { + return path.endsWith(".java") || + path.endsWith(".kt") || + path.endsWith(".groovy") || + path.endsWith(".scala") +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index b94dc63e5b..9ff2af5589 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -94,12 +94,9 @@ internal fun generateShadowedSourcesJar( val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocatePath(canonicalPath) + val relocatedPath = relocators.relocateSourcePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } + val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( name = relocatedPath, @@ -151,12 +148,9 @@ internal fun generateShadowedSourcesJar( val canonicalPath = if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocatePath(canonicalPath) + val relocatedPath = relocators.relocateSourcePath(canonicalPath) if (visitedFiles.add(relocatedPath)) { - var transformedText = text - for (relocator in relocators) { - transformedText = relocator.applyToSourceContent(transformedText) - } + val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( name = relocatedPath, @@ -288,10 +282,3 @@ internal fun isUnused( val classes = sourceToClasses[canonicalPath] ?: return false return classes.isNotEmpty() && classes.all { it in unusedClasses } } - -private fun isSourceFile(path: String): Boolean { - return path.endsWith(".java") || - path.endsWith(".kt") || - path.endsWith(".groovy") || - path.endsWith(".scala") -} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 924cda8101..51ac891c31 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -180,7 +180,7 @@ constructor( return excludes.any { SelectorUtils.matchPath(it, path, "/", true) } } - private companion object { + internal companion object { /** Match dot, slash or space at end of string */ val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt new file mode 100644 index 0000000000..5c1fa8728c --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -0,0 +1,107 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.isEqualTo +import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import org.junit.jupiter.api.Test + +class SourceRemapperTest { + + @Test + fun chainedRelocatorsDoNotCascade() { + val r1 = SimpleRelocator("a.foo", "b.foo") + val r2 = SimpleRelocator("b.foo", "c.foo") + val relocators = listOf(r1, r2) + + val input = + """ + |package a.foo; + |import b.foo.Bar; + |public class Main { + | a.foo.Baz baz; + | b.foo.Bar bar; + |} + """ + .trimMargin() + + val expected = + """ + |package b.foo; + |import c.foo.Bar; + |public class Main { + | b.foo.Baz baz; + | c.foo.Bar bar; + |} + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } + + @Test + fun relocateSourcePathWithClassInclude() { + val relocator = + SimpleRelocator( + "pkg", + "hidden.pkg", + includes = listOf("pkg.A", "pkg.sub.*"), + ) + val relocators = listOf(relocator) + + // Included class files + assertThat(relocators.relocateSourcePath("pkg/A.java")).isEqualTo("hidden/pkg/A.java") + assertThat(relocators.relocateSourcePath("pkg/A.kt")).isEqualTo("hidden/pkg/A.kt") + assertThat(relocators.relocateSourcePath("pkg/sub/Nested.java")) + .isEqualTo("hidden/pkg/sub/Nested.java") + + // Excluded / un-included class file + assertThat(relocators.relocateSourcePath("pkg/B.java")).isEqualTo("pkg/B.java") + assertThat(relocators.relocateSourcePath("other/Other.java")).isEqualTo("other/Other.java") + } + + @Test + fun relocateSourcePathWithClassExclude() { + val relocator = + SimpleRelocator( + "pkg", + "hidden.pkg", + excludes = listOf("pkg.B"), + ) + val relocators = listOf(relocator) + + assertThat(relocators.relocateSourcePath("pkg/A.java")).isEqualTo("hidden/pkg/A.java") + assertThat(relocators.relocateSourcePath("pkg/B.java")).isEqualTo("pkg/B.java") + } + + @Test + fun remapSourceWithIncludesAndExcludes() { + val relocator = + SimpleRelocator( + "com.example", + "shaded.example", + includes = listOf("com.example.used.*"), + excludes = listOf("com.example.used.Excluded"), + ) + val relocators = listOf(relocator) + + val input = + """ + |package com.example.used; + |import com.example.used.Foo; + |import com.example.used.Excluded; + |import com.example.unused.Bar; + """ + .trimMargin() + + val expected = + """ + |package shaded.example.used; + |import shaded.example.used.Foo; + |import com.example.used.Excluded; + |import com.example.unused.Bar; + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } +} From 5965733b1b31660b72742f66446780bdc15cc7c7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:48:25 +0800 Subject: [PATCH 058/117] Use SourceDirectorySet and FileCollection.asFileTree for sources jar packaging --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 2 +- .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 4 +- .../plugins/shadow/internal/SourcesJar.kt | 38 +++---------------- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 +- .../plugins/shadow/ShadowPropertiesTest.kt | 6 +-- .../plugins/shadow/internal/SourcesJarTest.kt | 34 +++++++++++++++-- 6 files changed, 41 insertions(+), 45 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 0a842bb834..b5bd393d63 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -50,7 +50,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl task.sourceSetsSourceDirs.convention( task.generateSourcesJar.flatMap { generate -> if (generate) { - mainSourceSet.map { it.allSource.sourceDirectories + it.allSource } + mainSourceSet.map { it.allSource } } else { provider { emptySet() } } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index ddeba32c59..cb41fd6a72 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -40,9 +40,7 @@ public abstract class ShadowKmpPlugin : Plugin { task.generateSourcesJar.flatMap { generate -> if (generate) { kotlinJvmMain.map { - it.allKotlinSourceSets.flatMap { ss -> - listOf(ss.kotlin.sourceDirectories, ss.kotlin) - } + it.allKotlinSourceSets.map { ss -> ss.kotlin } } } else { provider { emptySet() } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 9ff2af5589..1a75dbca0c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -4,6 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.nio.charset.Charset +import org.gradle.api.file.FileCollection import org.gradle.api.tasks.bundling.ZipEntryCompression import org.vafer.jdeb.shaded.objectweb.asm.ClassReader import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor @@ -11,7 +12,7 @@ import org.vafer.jdeb.shaded.objectweb.asm.Opcodes internal fun generateShadowedSourcesJar( sourcesJarFile: File, - sourceSetsSourceDirs: Iterable, + sourceSetsSourceDirs: FileCollection, includedSourcesJars: Iterable, classesDirs: Iterable = emptyList(), dependencies: Iterable = emptyList(), @@ -51,41 +52,14 @@ internal fun generateShadowedSourcesJar( write("Manifest-Version: 1.0\n\n".toByteArray(charset)) } - val sourceItems = sourceSetsSourceDirs.filter { it.exists() } - val (dirs, files) = sourceItems.partition { it.isDirectory } - val normalizedDirs = - dirs.map { it to it.normalize().toPath() }.sortedByDescending { it.second.nameCount } - val filesWithRelPaths = mutableListOf>() - val coveredDirs = mutableSetOf() - - for (file in files.sortedBy { it.path }) { - val filePath = file.normalize().toPath() - val matchingDir = - normalizedDirs.firstOrNull { (_, dirPath) -> filePath.startsWith(dirPath) }?.first - if (matchingDir != null) { - coveredDirs.add(matchingDir) - filesWithRelPaths.add(file to file.relativeTo(matchingDir).invariantSeparatorsPath) - } else { - filesWithRelPaths.add(file to file.name) - } - } - - for ((dir, dirPath) in normalizedDirs.sortedBy { it.second.nameCount }) { - if (coveredDirs.none { dirPath.startsWith(it.normalize().toPath()) }) { - coveredDirs.add(dir) - dir - .walkTopDown() - .filter { it.isFile } - .toList() - .sortedBy { it.relativeTo(dir).invariantSeparatorsPath } - .forEach { f -> - filesWithRelPaths.add(f to f.relativeTo(dir).invariantSeparatorsPath) - } + sourceSetsSourceDirs.asFileTree.visit { details -> + if (!details.isDirectory) { + filesWithRelPaths.add(details.file to details.relativePath.pathString) } } - for ((file, relPath) in filesWithRelPaths) { + for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { val isSource = isSourceFile(relPath) if (isSource) { val text = file.readText(charset) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 98fab688c1..85f12d9351 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -823,7 +823,7 @@ public abstract class ShadowJar : Jar() { if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return generateShadowedSourcesJar( sourcesJarFile = archiveSourcesFile.get().asFile, - sourceSetsSourceDirs = sourceSetsSourceDirs.files, + sourceSetsSourceDirs = sourceSetsSourceDirs, includedSourcesJars = includedSourcesJars.files, classesDirs = sourceSetsClassesDirs.files, dependencies = includedDependencies.files, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index aae0aaf974..2250058314 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -182,11 +182,7 @@ class ShadowPropertiesTest { assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) - .containsOnly( - *(mainSourceSet.allSource.sourceDirectories + mainSourceSet.allSource) - .files - .toTypedArray() - ) + .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) } @Test diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 8bfc411ab4..40305461ee 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -9,6 +9,7 @@ import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator +import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File import java.util.zip.ZipFile import org.gradle.api.GradleException @@ -119,7 +120,7 @@ class SourcesJarTest { val outputJar = tempDir.resolve("output-sources.jar") generateShadowedSourcesJar( sourcesJarFile = outputJar, - sourceSetsSourceDirs = listOf(srcDir), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), relocators = listOf(SimpleRelocator("com.example", "shadow.example")), unusedClasses = emptySet(), @@ -161,7 +162,7 @@ class SourcesJarTest { val outputJar = tempDir.resolve("output-sources.jar") generateShadowedSourcesJar( sourcesJarFile = outputJar, - sourceSetsSourceDirs = listOf(srcDir), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), relocators = emptyList(), unusedClasses = emptySet(), @@ -188,6 +189,33 @@ class SourcesJarTest { ) } + @Test + fun generateShadowedSourcesJarRespectsExcludedDirectory(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + srcDir.resolve("Excluded.java").writeText("public class Excluded {}") + + val fileTree = + testObjectFactory.fileCollection().from(srcDir).asFileTree.matching { + it.exclude("**/Excluded.java") + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateShadowedSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(fileTree), + includedSourcesJars = emptyList(), + relocators = emptyList(), + unusedClasses = emptySet(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } + assertThat(entries).containsOnly("META-INF/", "META-INF/MANIFEST.MF") + } + @Test fun throwsGradleExceptionOnFailure(@TempDir tempDir: File) { val invalidFile = tempDir.resolve("not-a-file").apply { mkdirs() } @@ -200,7 +228,7 @@ class SourcesJarTest { assertFailure { generateShadowedSourcesJar( sourcesJarFile = invalidFile, - sourceSetsSourceDirs = listOf(srcDir), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), relocators = emptyList(), unusedClasses = emptySet(), From f8a3678f52c834a8814345dd88075bf94f7b89ad Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 5 Sep 2026 23:59:23 +0800 Subject: [PATCH 059/117] Use direct physical relative paths for sources jar entries --- .../plugins/shadow/KotlinPluginsTest.kt | 40 --------- .../plugins/shadow/internal/SourcesJar.kt | 27 ++---- .../plugins/shadow/internal/SourcesJarTest.kt | 83 ------------------- 3 files changed, 6 insertions(+), 144 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 22ff8d2e1f..e72cf05cbb 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -337,46 +337,6 @@ class KotlinPluginsTest : BasePluginTest() { ) } - @Test - fun generateShadowedSourcesJarNormalizesPackageDirectory() { - val stdlib = compileOnlyStdlib(true) - path("src/main/kotlin/FlatFile.kt") - .writeText( - """ - |package my.custom.nested - | - |class FlatClass - """ - .trimMargin() - ) - projectScript.writeText( - """ - |${getDefaultProjectBuildScript(plugin = "org.jetbrains.kotlin.jvm")} - |dependencies { - | $stdlib - |} - |$shadowJarTask { - | generateSourcesJar = true - | relocate 'my.custom', 'shadow.custom' - |} - """ - .trimMargin() - ) - - runWithSuccess(shadowJarPath) - - assertThat(outputShadowedSourcesJar).useAll { - containsOnly( - "shadow/", - "shadow/custom/", - "shadow/custom/nested/", - "shadow/custom/nested/FlatFile.kt", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - } - } - private fun compileOnlyStdlib(exclude: Boolean): String { return if (exclude) { // Disable the stdlib dependency added via `implementation`. diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 1a75dbca0c..b810921fa8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -62,14 +62,10 @@ internal fun generateShadowedSourcesJar( for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { val isSource = isSourceFile(relPath) if (isSource) { - val text = file.readText(charset) - val pkg = extractPackage(text) - val simpleName = file.name - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocateSourcePath(canonicalPath) + if (isUnused(relPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocateSourcePath(relPath) if (visitedFiles.add(relocatedPath)) { + val text = file.readText(charset) val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( @@ -116,14 +112,10 @@ internal fun generateShadowedSourcesJar( } val isSource = isSourceFile(name) if (isSource) { - val text = getInputStream(entry).bufferedReader(charset).readText() - val pkg = extractPackage(text) - val simpleName = name.substringAfterLast('/') - val canonicalPath = - if (pkg.isEmpty()) simpleName else "${pkg.replace('.', '/')}/$simpleName" - if (isUnused(canonicalPath, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocateSourcePath(canonicalPath) + if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach + val relocatedPath = relocators.relocateSourcePath(name) if (visitedFiles.add(relocatedPath)) { + val text = getInputStream(entry).bufferedReader(charset).readText() val transformedText = relocators.remapSource(text) val bytes = transformedText.toByteArray(charset) zos.writeEntry( @@ -172,13 +164,6 @@ internal fun generateShadowedSourcesJar( } } -private val packageRegex = """(?:^|\n)\s*package\s+([a-zA-Z0-9_.]+)""".toRegex() - -internal fun extractPackage(text: String): String { - val matches = packageRegex.findAll(text).map { it.groupValues[1] }.toList() - return if (matches.isEmpty()) "" else matches.joinToString(".") -} - internal fun buildSourceToClassesMap( classesDirs: Iterable, dependencies: Iterable, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 40305461ee..4607524b50 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -8,7 +8,6 @@ import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue -import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File import java.util.zip.ZipFile @@ -19,50 +18,6 @@ import org.junit.jupiter.api.io.TempDir class SourcesJarTest { - @Test - fun extractPackageStatements() { - assertThat(extractPackage("package com.example.foo;")).isEqualTo("com.example.foo") - assertThat(extractPackage("package com.example.foo")).isEqualTo("com.example.foo") - assertThat(extractPackage(" package com.example.foo.bar ; ")) - .isEqualTo("com.example.foo.bar") - assertThat( - extractPackage( - """ - /* - * Multi-line header comment. - */ - package com.example.license; - public class License {} - """ - .trimIndent() - ) - ) - .isEqualTo("com.example.license") - assertThat( - extractPackage( - """ - @file:JvmName("MyUtils") - package com.example.annotated - fun test() {} - """ - .trimIndent() - ) - ) - .isEqualTo("com.example.annotated") - assertThat( - extractPackage( - """ - package a - package b.c - class Chained - """ - .trimIndent() - ) - ) - .isEqualTo("a.b.c") - assertThat(extractPackage("public class NoPackage {}")).isEqualTo("") - } - @Test fun isUnusedMatching() { val unusedSet = @@ -105,44 +60,6 @@ class SourcesJarTest { assertThat(isUnused("Main.java", setOf("Other"), sourceToClasses)).isFalse() } - @Test - fun generateShadowedSourcesJarNormalizesPackageDirectory(@TempDir tempDir: File) { - val srcDir = tempDir.resolve("src").apply { mkdirs() } - val flatMismatchedFile = srcDir.resolve("Mismatched.kt") - flatMismatchedFile.writeText( - """ - package com.example.nested - class Mismatched - """ - .trimIndent() - ) - - val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), - includedSourcesJars = emptyList(), - relocators = listOf(SimpleRelocator("com.example", "shadow.example")), - unusedClasses = emptySet(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = null, - preserveFileTimestamps = true, - ) - - assertThat(outputJar.exists()).isTrue() - val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } - assertThat(entries) - .containsOnly( - "META-INF/", - "META-INF/MANIFEST.MF", - "shadow/", - "shadow/example/", - "shadow/example/nested/", - "shadow/example/nested/Mismatched.kt", - ) - } - @Test fun generateShadowedSourcesJarDeterministicOrdering(@TempDir tempDir: File) { val srcDir = tempDir.resolve("src").apply { mkdirs() } From 8b99a5d0cbd3fc8502e6c5faf36e4a68313e5fb3 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:02:16 +0800 Subject: [PATCH 060/117] Use containsExactly for entry ordering assertions in SourcesJarTest --- .../plugins/shadow/internal/SourcesJarTest.kt | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 4607524b50..1d710fc086 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -2,9 +2,9 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertFailure import assertk.assertThat +import assertk.assertions.containsExactly import assertk.assertions.containsOnly import assertk.assertions.hasMessage -import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue @@ -91,18 +91,16 @@ class SourcesJarTest { val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } assertThat(entries) - .isEqualTo( - listOf( - "META-INF/MANIFEST.MF", - "a/A.java", - "m/M.java", - "z/sub/Z.java", - "META-INF/", - "a/", - "m/", - "z/", - "z/sub/", - ) + .containsExactly( + "META-INF/MANIFEST.MF", + "a/A.java", + "m/M.java", + "z/sub/Z.java", + "META-INF/", + "a/", + "m/", + "z/", + "z/sub/", ) } From d6000f9145d81c72de75ad084243c7e30b1efe74 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:03:07 +0800 Subject: [PATCH 061/117] Document containsExactly preference for ordered collection assertions --- CONTRIBUTING.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 319faa8ea1..43b9d01059 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -105,8 +105,9 @@ When adding new features or public APIs: - Prefer exact and complete matching using `isEqualTo` for string assertions whenever possible instead of partial matching (e.g., `contains`). -- Prefer complete assertions like `containsOnly` for collections whenever possible instead of partial assertions like - `containsAtLeast` or `containsNone`. +- Prefer complete assertions like `containsOnly` (or `containsExactly` when order matters) for collections whenever + possible instead of partial assertions like `containsAtLeast` or `containsNone`. Prefer `containsExactly(...)` over + `isEqualTo(listOf(...))` when verifying exact collection elements in order. - Raw multiline strings in tests should be constructed using `.trimMargin()`. - Prefer `=` property assignment over `.set(...)` in Gradle build scripts (both in documentation snippets and functional tests) unless `.set(...)` is explicitly required. From ba2adff9803c8a593c185ca7444d3e58d84c4643 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:07:19 +0800 Subject: [PATCH 062/117] Add comments on gating source inputs by generateSourcesJar --- .../github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt | 1 + .../github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt | 1 + 2 files changed, 2 insertions(+) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index b5bd393d63..651704d89b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -48,6 +48,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } ) task.sourceSetsSourceDirs.convention( + // Avoid snapshotting source inputs when sources JAR generation is disabled. task.generateSourcesJar.flatMap { generate -> if (generate) { mainSourceSet.map { it.allSource } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index cb41fd6a72..df0903bef7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -37,6 +37,7 @@ public abstract class ShadowKmpPlugin : Plugin { registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) task.sourceSetsSourceDirs.convention( + // Avoid snapshotting source inputs when sources JAR generation is disabled. task.generateSourcesJar.flatMap { generate -> if (generate) { kotlinJvmMain.map { From c074522dab80b4de50a7cb80c17d23e5d4674be5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:10:17 +0800 Subject: [PATCH 063/117] Rename generateShadowedSourcesJar --- .../gradle/plugins/shadow/internal/SourcesJar.kt | 2 +- .../jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt | 4 ++-- .../gradle/plugins/shadow/internal/SourcesJarTest.kt | 10 +++++----- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index b810921fa8..1dbebc7a11 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -10,7 +10,7 @@ import org.vafer.jdeb.shaded.objectweb.asm.ClassReader import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor import org.vafer.jdeb.shaded.objectweb.asm.Opcodes -internal fun generateShadowedSourcesJar( +internal fun generateSourcesJar( sourcesJarFile: File, sourceSetsSourceDirs: FileCollection, includedSourcesJars: Iterable, diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt index 85f12d9351..643b33b18b 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -11,7 +11,7 @@ import com.github.jengelman.gradle.plugins.shadow.internal.classPathAttributeKey import com.github.jengelman.gradle.plugins.shadow.internal.createZipOutputStream import com.github.jengelman.gradle.plugins.shadow.internal.fileCollection import com.github.jengelman.gradle.plugins.shadow.internal.findUnusedClasses -import com.github.jengelman.gradle.plugins.shadow.internal.generateShadowedSourcesJar +import com.github.jengelman.gradle.plugins.shadow.internal.generateSourcesJar import com.github.jengelman.gradle.plugins.shadow.internal.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.gradleError import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension @@ -821,7 +821,7 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = archiveSourcesFile.get().asFile, sourceSetsSourceDirs = sourceSetsSourceDirs, includedSourcesJars = includedSourcesJars.files, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index 1d710fc086..f67a6a7fc7 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -61,7 +61,7 @@ class SourcesJarTest { } @Test - fun generateShadowedSourcesJarDeterministicOrdering(@TempDir tempDir: File) { + fun deterministicOrdering(@TempDir tempDir: File) { val srcDir = tempDir.resolve("src").apply { mkdirs() } srcDir.resolve("z/sub/Z.java").apply { parentFile.mkdirs() @@ -77,7 +77,7 @@ class SourcesJarTest { } val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = outputJar, sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), @@ -105,7 +105,7 @@ class SourcesJarTest { } @Test - fun generateShadowedSourcesJarRespectsExcludedDirectory(@TempDir tempDir: File) { + fun respectsExcludedDirectory(@TempDir tempDir: File) { val srcDir = tempDir.resolve("src").apply { mkdirs() } srcDir.resolve("Excluded.java").writeText("public class Excluded {}") @@ -115,7 +115,7 @@ class SourcesJarTest { } val outputJar = tempDir.resolve("output-sources.jar") - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = outputJar, sourceSetsSourceDirs = testObjectFactory.fileCollection().from(fileTree), includedSourcesJars = emptyList(), @@ -141,7 +141,7 @@ class SourcesJarTest { } assertFailure { - generateShadowedSourcesJar( + generateSourcesJar( sourcesJarFile = invalidFile, sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), includedSourcesJars = emptyList(), From a80544a1f047634bd8049875c36071d21f1959e2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 6 Sep 2026 00:13:37 +0800 Subject: [PATCH 064/117] Make internal helper functions and constants private --- .../gradle/plugins/shadow/internal/SourcesJar.kt | 2 +- .../gradle/plugins/shadow/relocation/SimpleRelocator.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 1dbebc7a11..ef06e45256 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -164,7 +164,7 @@ internal fun generateSourcesJar( } } -internal fun buildSourceToClassesMap( +private fun buildSourceToClassesMap( classesDirs: Iterable, dependencies: Iterable, ): Map> { diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt index 51ac891c31..0a844d5c1c 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -182,7 +182,7 @@ constructor( internal companion object { /** Match dot, slash or space at end of string */ - val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") + private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") /** * Match @@ -193,7 +193,7 @@ constructor( * * at end of string */ - val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = + private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = Pattern.compile( "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + "|" + @@ -202,7 +202,7 @@ constructor( "([{}(=;,]|\\*/) $" ) - fun normalizePatterns(patterns: Collection?) = buildSet { + private fun normalizePatterns(patterns: Collection?) = buildSet { patterns ?: return@buildSet for (pattern in patterns) { // Regex patterns don't need to be normalized and stay as is. @@ -273,7 +273,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - fun shadeSourceWithFilters( + private fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, From bb3f221be0781a2c0ca039afbd99b0f1cfc3b5c9 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 7 Sep 2026 15:57:06 +0800 Subject: [PATCH 065/117] Simplify shouldAddSources --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 651704d89b..685c520005 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -45,6 +45,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl registerShadowJarCommon(tasks.named("jar", Jar::class.java)) { task -> task.from(mainSourceSet.map { it.output }) task.generateSourcesJar.convention( + // If `withSourcesJar` is present in `java` block. provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } ) task.sourceSetsSourceDirs.convention( @@ -150,11 +151,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements val shadowSourcesElements = configurations.shadowSourcesElements - // If `withSourcesJar` is present and `generateSourcesJar` is enabled. - val shouldAddSources = { - configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null && - tasks.shadowJar.flatMap { it.generateSourcesJar }.get() - } + val generateSourcesJar = { tasks.shadowJar.flatMap { it.generateSourcesJar }.get() } val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) @@ -167,7 +164,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl shadowComponent.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = shouldAddSources, + shouldAdd = generateSourcesJar, ) components.named("java", AdhocComponentWithVariants::class.java) { component -> @@ -181,7 +178,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl component.addVariants( outgoingConfiguration = shadowSourcesElements, logger = logger, - shouldAdd = { addIntoJavaComponent.get() && shouldAddSources() }, + shouldAdd = { addIntoJavaComponent.get() && generateSourcesJar() }, ) { mapToOptional() } From 0c7a80223818182167219996eb1bf6991e1eddb2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 7 Sep 2026 16:06:37 +0800 Subject: [PATCH 066/117] Clean up shadowComponent --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 685c520005..c268a82cc3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -153,19 +153,21 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl val shadowSourcesElements = configurations.shadowSourcesElements val generateSourcesJar = { tasks.shadowJar.flatMap { it.generateSourcesJar }.get() } - val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) - components.add(shadowComponent) - shadowComponent.addVariants( - outgoingConfiguration = shadowRuntimeElements, - logger = logger, - ) { - mapToMavenScope("runtime") + softwareComponentFactory.adhoc(COMPONENT_NAME).let { component -> + components.add(component) + component.addVariants( + outgoingConfiguration = shadowRuntimeElements, + logger = logger, + shouldAdd = { true }, + ) { + mapToMavenScope("runtime") + } + component.addVariants( + outgoingConfiguration = shadowSourcesElements, + logger = logger, + shouldAdd = generateSourcesJar, + ) } - shadowComponent.addVariants( - outgoingConfiguration = shadowSourcesElements, - logger = logger, - shouldAdd = generateSourcesJar, - ) components.named("java", AdhocComponentWithVariants::class.java) { component -> component.addVariants( @@ -188,7 +190,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl private fun AdhocComponentWithVariants.addVariants( outgoingConfiguration: NamedDomainObjectProvider, logger: Logger, - shouldAdd: () -> Boolean = { true }, + shouldAdd: () -> Boolean, action: ConfigurationVariantDetails.() -> Unit = {}, ) { addVariantsFromConfiguration(outgoingConfiguration) { variant -> From 26c9f4abad92f858341b5342374bb5dc9df6a408 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 10 Sep 2026 14:42:19 +0800 Subject: [PATCH 067/117] Improve error handling for Dokka issue 4600 --- .../plugins/shadow/SnippetExecutable.kt | 25 ++++++++++--------- .../plugins/shadow/KotlinPluginsTest.kt | 15 ++++++----- .../plugins/shadow/testkit/GradleRunner.kt | 7 ++++++ 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt index 12d13cff02..377314c0f6 100644 --- a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt +++ b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt @@ -4,6 +4,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.assertNoDeprecationWar import com.github.jengelman.gradle.plugins.shadow.testkit.commonGradleArgs import com.github.jengelman.gradle.plugins.shadow.testkit.enableNoImplicitLookupInParentProjects import com.github.jengelman.gradle.plugins.shadow.testkit.gradleRunner +import com.github.jengelman.gradle.plugins.shadow.testkit.isDokkaIssue4600 import java.nio.file.Path import java.util.jar.JarOutputStream import kotlin.io.path.createDirectory @@ -51,6 +52,7 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + // TODO: https://github.com/Kotlin/dokka/issues/4488 projectRoot .resolve("gradle.properties") @@ -68,6 +70,7 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + val pluginsBlock = """ |plugins { @@ -104,28 +107,26 @@ sealed interface SnippetExecutable { } projectRoot.resolve("main/LICENSE").writeText("Sample License") - val containsDokka = withoutImports.contains("dokka") // Script-defined classes (e.g., inline custom ResourceTransformer) are not supported by // CC/IP because transient script classloaders cannot be serialized. val runnerArgs = - commonGradleArgs - .filterNot { - (withoutImports.contains("class ") && - (it == "--configuration-cache" || it.contains("isolated-projects"))) || - (containsDokka && it.startsWith("--warning-mode=")) + if (withoutImports.contains("class ")) { + commonGradleArgs.filterNot { + it == "--configuration-cache" || it.contains("isolated-projects") } - .let { if (containsDokka) it + "--warning-mode=all" else it } + } else { + commonGradleArgs.toList() + } gradleRunner(projectDir = projectRoot, arguments = runnerArgs + "build") .build() .also { gradleBuildOutput = it.output } - .apply { - if (!containsDokka) { - assertNoDeprecationWarnings() - } - } + .assertNoDeprecationWarnings() } catch (t: Throwable) { val buildOutput = (t as? UnexpectedBuildFailure)?.buildResult?.output ?: gradleBuildOutput + + if (buildOutput?.isDokkaIssue4600 == true) return + throw AssertionError( buildString { append("The error line in the doc is near $sourceLocation") diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 6848d6a340..60a70f3434 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -7,10 +7,10 @@ import assertk.assertions.isEqualTo import com.github.jengelman.gradle.plugins.shadow.internal.mainClassAttributeKey import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.SHADOW_JAR_TASK_NAME import com.github.jengelman.gradle.plugins.shadow.testkit.classLoader -import com.github.jengelman.gradle.plugins.shadow.testkit.commonGradleArgs import com.github.jengelman.gradle.plugins.shadow.testkit.containsAtLeast import com.github.jengelman.gradle.plugins.shadow.testkit.containsOnly import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr +import com.github.jengelman.gradle.plugins.shadow.testkit.isDokkaIssue4600 import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import kotlin.io.path.appendText @@ -331,11 +331,14 @@ class KotlinPluginsTest : BasePluginTest() { .trimMargin() ) - runWithSuccess("dokkaGenerateHtml", failOnDeprecations = false) { - withArguments( - commonGradleArgs.filterNot { it.startsWith("--warning-mode=") } + - listOf("dokkaGenerateHtml", "--warning-mode=all") - ) + try { + runWithSuccess("dokkaGenerateHtml") + } catch (t: Throwable) { + if (t.stackTraceToString().isDokkaIssue4600) { + // Do nothing. + } else { + throw t + } } val dokkaDir = projectRoot.resolve("build/dokka/html") diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt index d07a6c32eb..97c0f2336d 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/GradleRunner.kt @@ -49,6 +49,13 @@ val commonGradleArgs = isolatedProjectsFlag, ) +// TODO: https://github.com/Kotlin/dokka/issues/4600 +val String.isDokkaIssue4600: Boolean + get() = let { output -> + output.contains("The Configuration.setVisible(boolean) method has been deprecated") && + output.contains("org.jetbrains.dokka.gradle") + } + fun gradleRunner( projectDir: Path, arguments: Iterable, From af467c1f4f8f20318c03a3cf953d8c2cf8dadc8f Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 10 Sep 2026 15:37:52 +0800 Subject: [PATCH 068/117] Use idiomatic Groovy syntax for method calls and plugins block in docs --- docs/publishing/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 8919d5b2dd..e064137d6d 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -328,8 +328,8 @@ If you want to replace standard JARs with the shadowed ones, disable the standar ```groovy plugins { - id('java') - id('com.gradleup.shadow') + id 'java' + id 'com.gradleup.shadow' } java { @@ -372,8 +372,8 @@ Or set different `archiveClassifier` values for the standard tasks: ```groovy plugins { - id('java') - id('com.gradleup.shadow') + id 'java' + id 'com.gradleup.shadow' } java { @@ -834,10 +834,10 @@ You can also customize the source inputs included in the companion sources JAR u ```groovy tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { // Add custom source directories - sourceSetsSourceDirs.from('src/extra/java') + sourceSetsSourceDirs.from 'src/extra/java' // Add additional dependency sources JARs - includedSourcesJars.from('libs/external-lib-sources.jar') + includedSourcesJars.from 'libs/external-lib-sources.jar' } ``` @@ -980,8 +980,8 @@ If using [Dokka][dokka] for Kotlin projects, you can extract the shadowed source dokka { dokkaSourceSets.configureEach { - classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) - sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + classpath.from tasks.named('shadowJar').flatMap { it.archiveFile } + sourceRoots.from extractShadowedSources.map { it.destinationDir } } } ``` From f38c56760512e26f59c9fd0e5912c59cefaaea71 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sat, 19 Sep 2026 11:51:23 +0800 Subject: [PATCH 069/117] Revert "Set Dokka properties via gradle.properties as workaround for IP" This reverts commit 68c2e1b8 --- .../plugins/shadow/SnippetExecutable.kt | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt index 377314c0f6..eebcb53de6 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 @@ -52,25 +52,6 @@ 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 f945091e9909bfab491eaa3ae2c7e30d4cc84df6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 10:28:46 +0800 Subject: [PATCH 070/117] Fix shadowed sources jar output handling --- .../plugins/shadow/internal/SourcesJar.kt | 13 +++--- .../plugins/shadow/internal/SourcesJarTest.kt | 45 +++++++++++++++++++ 2 files changed, 51 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index ef06e45256..20c5f0b1ad 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -3,7 +3,6 @@ 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.file.FileCollection import org.gradle.api.tasks.bundling.ZipEntryCompression import org.vafer.jdeb.shaded.objectweb.asm.ClassReader @@ -26,7 +25,6 @@ internal fun generateSourcesJar( val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } val visitedFiles = mutableSetOf() - val charset = metadataCharset?.let(Charset::forName) ?: Charsets.UTF_8 val sourceToClasses = if (unusedClasses.isNotEmpty()) { buildSourceToClassesMap(classesDirs = classesDirs, dependencies = dependencies) @@ -35,6 +33,7 @@ internal fun generateSourcesJar( } try { + sourcesJarFile.parentFile?.mkdirs() sourcesJarFile .createZipOutputStream( entryCompression = entryCompression, @@ -49,7 +48,7 @@ internal fun generateSourcesJar( preserveLastModified = preserveFileTimestamps, unixMode = UnixMode.file(), ) { - write("Manifest-Version: 1.0\n\n".toByteArray(charset)) + write("Manifest-Version: 1.0\n\n".toByteArray()) } val filesWithRelPaths = mutableListOf>() @@ -65,9 +64,9 @@ internal fun generateSourcesJar( if (isUnused(relPath, unusedClasses, sourceToClasses)) continue val relocatedPath = relocators.relocateSourcePath(relPath) if (visitedFiles.add(relocatedPath)) { - val text = file.readText(charset) + val text = file.readText() val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray(charset) + val bytes = transformedText.toByteArray() zos.writeEntry( name = relocatedPath, preserveLastModified = preserveFileTimestamps, @@ -115,9 +114,9 @@ internal fun generateSourcesJar( if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocateSourcePath(name) if (visitedFiles.add(relocatedPath)) { - val text = getInputStream(entry).bufferedReader(charset).readText() + val text = getInputStream(entry).bufferedReader().readText() val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray(charset) + val bytes = transformedText.toByteArray() zos.writeEntry( name = relocatedPath, preserveLastModified = preserveFileTimestamps, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt index f67a6a7fc7..f9c521f4eb 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -5,11 +5,13 @@ import assertk.assertThat import assertk.assertions.containsExactly import assertk.assertions.containsOnly import assertk.assertions.hasMessage +import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isInstanceOf import assertk.assertions.isTrue import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory import java.io.File +import java.nio.charset.StandardCharsets import java.util.zip.ZipFile import org.gradle.api.GradleException import org.gradle.api.tasks.bundling.ZipEntryCompression @@ -18,6 +20,49 @@ import org.junit.jupiter.api.io.TempDir class SourcesJarTest { + @Test + fun metadataCharsetDoesNotChangeSourceEncoding(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + val sourceBytes = "class Main { val message = \"你好\" }\n".toByteArray(StandardCharsets.UTF_8) + srcDir.resolve("Main.kt").writeBytes(sourceBytes) + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + includedSourcesJars = emptyList(), + relocators = emptyList(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = "US-ASCII", + preserveFileTimestamps = true, + ) + + val archivedSource = + ZipFile(outputJar).use { zip -> zip.getInputStream(zip.getEntry("Main.kt")).readBytes() } + assertThat(archivedSource).isEqualTo(sourceBytes) + } + + @Test + fun createsParentDirectoryForOutput(@TempDir tempDir: File) { + val srcDir = tempDir.resolve("src").apply { mkdirs() } + srcDir.resolve("Main.java").writeText("class Main {}") + val outputJar = tempDir.resolve("nested/output-sources.jar") + + generateSourcesJar( + sourcesJarFile = outputJar, + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + includedSourcesJars = emptyList(), + relocators = emptyList(), + entryCompression = ZipEntryCompression.DEFLATED, + isZip64 = false, + metadataCharset = null, + preserveFileTimestamps = true, + ) + + assertThat(outputJar.isFile).isTrue() + } + @Test fun isUnusedMatching() { val unusedSet = From 63b33c9c257f30f681dc39337703805ba8d8ac9b Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 11:05:46 +0800 Subject: [PATCH 071/117] Optimize lookback and subpattern matching in SourceRemapper --- .../plugins/shadow/internal/SourceRemapper.kt | 16 +++++++++++----- .../plugins/shadow/relocation/SimpleRelocator.kt | 15 +++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index 59b19e68d8..201414f1d7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -60,15 +60,19 @@ internal fun Iterable.remapSource(sourceContent: String): String { result.append(sourceContent, lastIndex, matchStart) lastIndex = matchEnd - val previousSnippet = sourceContent.substring(0, matchStart) - val previousSnippetOneLine = previousSnippet.replace("\\s+".toRegex(), " ") + var cursor = matchStart - 1 + while (cursor >= 0 && sourceContent[cursor].isWhitespace()) { + cursor-- + } + val lookbackStart = (cursor - 64).coerceAtLeast(0) + val previousSnippetOneLine = + sourceContent.substring(lookbackStart, matchStart).replace("\\s+".toRegex(), " ") val afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() val contextValid = !afterDotSlashSpace || afterJavaKeyWord var replaced = false if (contextValid) { - val suffixSnippet = sourceContent.substring(matchEnd) for (relocator in relocatorList) { if ( relocator is SimpleRelocator && !relocator.rawString && relocator.pattern.isNotEmpty() @@ -86,9 +90,11 @@ internal fun Iterable.remapSource(sourceContent: String): String { } val isIncluded = !hasIncludes || - sourceIncludes.any { SimpleRelocator.matchesSubpattern(suffixSnippet, it) } + sourceIncludes.any { + SimpleRelocator.matchesSubpattern(sourceContent, matchEnd, it) + } val isExcluded = sourceExcludes.any { - SimpleRelocator.matchesSubpattern(suffixSnippet, it) + SimpleRelocator.matchesSubpattern(sourceContent, matchEnd, it) } if (isIncluded && !isExcluded) { result.append( 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 0a844d5c1c..7810a92605 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -265,14 +265,21 @@ constructor( return result } - fun matchesSubpattern(snippet: String, subpattern: String): Boolean { - if (!snippet.startsWith(subpattern)) return false - if (subpattern.isEmpty() || snippet.length == subpattern.length) return true + fun matchesSubpattern(content: CharSequence, offset: Int, subpattern: String): Boolean { + val subLen = subpattern.length + if (offset + subLen > content.length) return false + for (i in 0 until subLen) { + if (content[offset + i] != subpattern[i]) return false + } + if (subLen == 0 || offset + subLen == content.length) return true if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true - val nextChar = snippet[subpattern.length] + val nextChar = content[offset + subLen] return !nextChar.isLetterOrDigit() && nextChar != '_' } + fun matchesSubpattern(snippet: String, subpattern: String): Boolean = + matchesSubpattern(snippet, 0, subpattern) + private fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, From b3f6861596c9b82baf5e08421daa7883d52934bf Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 11:13:08 +0800 Subject: [PATCH 072/117] Restore ShadowSourcesPublishArtifact and conditionally expose shadowSourcesElements --- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 50 +++++++++++++------ .../plugins/shadow/ShadowPropertiesTest.kt | 41 +++++++++++++++ 2 files changed, 77 insertions(+), 14 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 c268a82cc3..3396cfb452 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -5,14 +5,18 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.sha import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.runtimeConfiguration import com.github.jengelman.gradle.plugins.shadow.internal.sourceSets +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.registerShadowJarCommon import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar.Companion.shadowJar +import java.io.File +import java.util.Date import javax.inject.Inject import org.gradle.api.NamedDomainObjectProvider import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration +import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -25,6 +29,8 @@ import org.gradle.api.component.SoftwareComponentFactory import org.gradle.api.logging.Logger import org.gradle.api.plugins.JavaPlugin.COMPILE_CLASSPATH_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.SOURCES_ELEMENTS_CONFIGURATION_NAME +import org.gradle.api.tasks.TaskDependency +import org.gradle.api.tasks.TaskProvider import org.gradle.api.tasks.bundling.Jar public abstract class ShadowJavaPlugin @@ -96,21 +102,14 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl ) } val shadowJarTask = tasks.shadowJar - outgoing.artifact(shadowJarTask.flatMap { it.archiveSourcesFile }) { artifact -> - with(artifact) { - builtBy(shadowJarTask) - name = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() - extension = shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" - type = "jar" - classifier = - shadowJarTask - .flatMap { it.archiveClassifier } - .orNull - .let { shadowClassifier -> - if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" - } + val artifact = ShadowSourcesPublishArtifact(shadowJarTask) + outgoing.artifacts( + shadowJarTask.flatMap { task -> + task.generateSourcesJar.map { generate -> + if (generate) listOf(artifact) else emptyList() + } } - } + ) } // See more details in #2086. @@ -241,3 +240,26 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl get() = named(SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, ConsumableConfiguration::class.java) } } + +private class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : + PublishArtifact { + override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() + + override fun getExtension(): String = + shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" + + override fun getType(): String = "jar" + + override fun getClassifier(): String { + val shadowClassifier = shadowJarTask.flatMap { it.archiveClassifier }.orNull + return if (shadowClassifier.isNullOrEmpty()) "sources" else "$shadowClassifier-sources" + } + + override fun getFile(): File = shadowJarTask.flatMap { it.archiveSourcesFile }.get().asFile + + override fun getDate(): Date? = null + + override fun getBuildDependencies(): TaskDependency = TaskDependency { + setOf(@Suppress("EagerGradleConfiguration") shadowJarTask.get()) + } +} 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 2250058314..f1f60eeb02 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 @@ -16,6 +16,7 @@ import com.github.jengelman.gradle.plugins.shadow.ShadowApplicationPlugin.Compan import com.github.jengelman.gradle.plugins.shadow.ShadowApplicationPlugin.Companion.shadowDistZip import com.github.jengelman.gradle.plugins.shadow.ShadowApplicationPlugin.Companion.startShadowScripts import com.github.jengelman.gradle.plugins.shadow.ShadowBasePlugin.Companion.shadow +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.shadowSourcesElements import com.github.jengelman.gradle.plugins.shadow.internal.applicationExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension import com.github.jengelman.gradle.plugins.shadow.internal.javaToolchainService @@ -171,6 +172,8 @@ class ShadowPropertiesTest { assertThat(sourceSetsSourceDirs.files).isEmpty() assertThat(includedSourcesJars.files).isEmpty() } + + assertThat(configurations.shadowSourcesElements.get().artifacts).isEmpty() } @Test @@ -183,6 +186,44 @@ class ShadowPropertiesTest { val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) + + val shadowSourcesElements = configurations.shadowSourcesElements.get() + val artifact = shadowSourcesElements.artifacts.single() + assertThat(artifact.classifier).isEqualTo("all-sources") + assertThat(artifact.name).isEqualTo("my-project") + assertThat(artifact.extension).isEqualTo("jar") + assertThat(artifact.type).isEqualTo("jar") + assertThat(artifact.file).isEqualTo(shadowJarTask.archiveSourcesFile.get().asFile) + assertThat(artifact.date).isNull() + assertThat(artifact.buildDependencies.getDependencies(null)).containsOnly(shadowJarTask) + + // Test dynamic updates on ShadowSourcesPublishArtifact + shadowJarTask.archiveClassifier.set("custom") + assertThat(artifact.classifier).isEqualTo("custom-sources") + + shadowJarTask.archiveClassifier.set("") + assertThat(artifact.classifier).isEqualTo("sources") + + shadowJarTask.archiveBaseName.set("renamed") + shadowJarTask.archiveExtension.set("zip") + assertThat(artifact.name).isEqualTo("renamed") + assertThat(artifact.extension).isEqualTo("zip") + } + + @Test + fun shadowSourcesElementsArtifactTogglesWithGenerateSourcesJar() = + with(project) { + plugins.apply(JavaPlugin::class.java) + val shadowJarTask = tasks.shadowJar.get() + val shadowSourcesElements = configurations.shadowSourcesElements.get() + + assertThat(shadowSourcesElements.artifacts).isEmpty() + + shadowJarTask.generateSourcesJar.set(true) + assertThat(shadowSourcesElements.artifacts.size).isEqualTo(1) + + shadowJarTask.generateSourcesJar.set(false) + assertThat(shadowSourcesElements.artifacts).isEmpty() } @Test From 2eba78ce9fac73ae6426aff8cbcc02e6ce319817 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 11:06:15 +0800 Subject: [PATCH 073/117] Populate archiveSourcesFile convention only when generateSourcesJar is enabled --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 24 ++++++++++++------- .../plugins/shadow/ShadowPropertiesTest.kt | 11 +++++---- 2 files changed, 22 insertions(+), 13 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 643b33b18b..7309484240 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 @@ -259,16 +259,22 @@ public abstract class ShadowJar : Jar() { 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" - } + generateSourcesJar.flatMap { generate -> + if (generate) { + destinationDirectory.file( + archiveFileName.map { name -> + val idx = name.lastIndexOf('.') + if (idx != -1) { + "${name.substring(0, idx)}-sources${name.substring(idx)}" + } else { + "$name-sources" + } + } + ) + } else { + project.provider { null } } - ) + } ) /** 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 f1f60eeb02..2230eb8184 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 @@ -165,10 +165,7 @@ 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(archiveSourcesFile.orNull).isNull() assertThat(sourceSetsSourceDirs.files).isEmpty() assertThat(includedSourcesJars.files).isEmpty() } @@ -183,6 +180,12 @@ class ShadowPropertiesTest { javaPluginExtension.withSourcesJar() val shadowJarTask = tasks.shadowJar.get() assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + assertThat(shadowJarTask.archiveSourcesFile.get().asFile).all { + isEqualTo( + shadowJarTask.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")) + } val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) From cfee9aa7a9af780aee7280672dcd8fb3d2f49b8d Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 11:06:20 +0800 Subject: [PATCH 074/117] Unify dependency source resolution using artifactView withVariantReselection --- .../internal/DefaultDependencyFilter.kt | 71 ++++++------------- 1 file changed, 21 insertions(+), 50 deletions(-) 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 03ac029090..f202c92cce 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 @@ -4,15 +4,10 @@ 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.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 internal class DefaultDependencyFilter(@Transient private val project: Project) : DependencyFilter.AbstractDependencyFilter(project) { @@ -56,52 +51,28 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) } } - val externalComponentIds = - includedDependenciesResults - .map { it.selected.id } - .filterIsInstance() - .toSet() + val includedComponentIds = includedDependenciesResults.map { it.selected.id }.toSet() - val externalSourcesFiles = - project.dependencies - .createArtifactResolutionQuery() - .forComponents(externalComponentIds) - .withArtifacts(JvmLibrary::class.java, SourcesArtifact::class.java) - .execute() - .resolvedComponents - .flatMap { it.getArtifacts(SourcesArtifact::class.java) } - .filterIsInstance() - .map { it.file } - - val projectComponentIds = - includedDependenciesResults - .map { it.selected.id } - .filterIsInstance() - .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 in projectComponentIds } - view.lenient(true) + return 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), + ) } - .files - } catch (_: Exception) { - project.files() - } - - return project.files(externalSourcesFiles) + projectSourcesFiles + view.componentFilter { id -> id in includedComponentIds } + view.lenient(true) + } + .files + } catch (_: Exception) { + project.files() + } } } From 1379acabea8bc94d38c4df9bf08339848d3fd557 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 11:19:49 +0800 Subject: [PATCH 075/117] Support custom relocators and expressions with qualified names in SourceRemapper --- .../plugins/shadow/internal/SourceRemapper.kt | 23 ++++-- .../shadow/relocation/SimpleRelocator.kt | 4 +- .../shadow/internal/SourceRemapperTest.kt | 70 +++++++++++++++++++ 3 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index 201414f1d7..df611d01a4 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -10,11 +10,11 @@ private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = Pattern.compile( - "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + + "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|transient|native|strictfp|extends|implements|throws|return|new|throw|instanceof|case|default|yield|val|var|fun|is|as|in) $" + "|" + "\\{@link( \\*)* $" + "|" + - "([{}(=;,]|\\*/) $" + "([{}(=;,:<>?&|@\\[\\]]|\\*/) $" ) /** @@ -29,6 +29,7 @@ internal fun Iterable.remapSource(sourceContent: String): String { relocatorList.filterIsInstance().filter { !it.rawString && it.pattern.isNotEmpty() } + val otherRelocators = relocatorList.filter { it !in simpleRelocators } if (simpleRelocators.isEmpty()) { var content = sourceContent @@ -45,7 +46,13 @@ internal fun Iterable.remapSource(sourceContent: String): String { .distinct() .sortedByDescending { it.length } - if (patterns.isEmpty()) return sourceContent + if (patterns.isEmpty()) { + var content = sourceContent + for (relocator in otherRelocators) { + content = relocator.applyToSourceContent(content) + } + return content + } val patternRegex = Regex("\\b(" + patterns.joinToString("|") { Regex.escape(it) } + ")\\b") @@ -114,7 +121,15 @@ internal fun Iterable.remapSource(sourceContent: String): String { } result.append(sourceContent, lastIndex, sourceContent.length) - return result.toString() + if (otherRelocators.isEmpty()) { + return result.toString() + } + + var content = result.toString() + for (relocator in otherRelocators) { + content = relocator.applyToSourceContent(content) + } + return content } /** 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 7810a92605..ce769328c2 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 @@ -195,11 +195,11 @@ constructor( */ private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = Pattern.compile( - "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|extends|implements|throws) $" + + "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|transient|native|strictfp|extends|implements|throws|return|new|throw|instanceof|case|default|yield|val|var|fun|is|as|in) $" + "|" + "\\{@link( \\*)* $" + "|" + - "([{}(=;,]|\\*/) $" + "([{}(=;,:<>?&|@\\[\\]]|\\*/) $" ) private fun normalizePatterns(patterns: Collection?) = buildSet { diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt index 5c1fa8728c..61d865b327 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -2,11 +2,81 @@ package com.github.jengelman.gradle.plugins.shadow.internal import assertk.assertThat import assertk.assertions.isEqualTo +import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext +import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext +import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import org.junit.jupiter.api.Test class SourceRemapperTest { + @Test + fun appliesCustomRelocatorAlongsideSimpleRelocator() { + val customRelocator = + object : Relocator { + override fun canRelocatePath(path: String) = false + + override fun relocatePath(context: RelocatePathContext) = context.path + + override fun canRelocateClass(className: String) = false + + override fun relocateClass(context: RelocateClassContext) = context.className + + override fun applyToSourceContent(sourceContent: String) = + sourceContent.replace("CUSTOM_NAME", "RELOCATED_NAME") + } + val relocators = listOf(SimpleRelocator("com.example", "shaded.example"), customRelocator) + val input = + """ + |package com.example; + |class Main { + | String value = CUSTOM_NAME; + |} + """ + .trimMargin() + + val expected = + """ + |package shaded.example; + |class Main { + | String value = RELOCATED_NAME; + |} + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } + + @Test + fun relocatesQualifiedNamesInExpressionsAndTypeAnnotations() { + val relocators = listOf(SimpleRelocator("com.example", "shaded.example")) + val input = + """ + |class Main { + | void method() { + | return com.example.Factory.create(); + | } + | com.example.Type value = new com.example.Type(); + | val typed: com.example.Type = com.example.Factory.create() + |} + """ + .trimMargin() + + val expected = + """ + |class Main { + | void method() { + | return shaded.example.Factory.create(); + | } + | shaded.example.Type value = new shaded.example.Type(); + | val typed: shaded.example.Type = shaded.example.Factory.create() + |} + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } + @Test fun chainedRelocatorsDoNotCascade() { val r1 = SimpleRelocator("a.foo", "b.foo") From 7c4032f8fdd83ee1d150e10fbf798fcec7d6eb3b Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 11:39:45 +0800 Subject: [PATCH 076/117] Support class-only relocators in relocateSourcePath --- .../plugins/shadow/internal/SourceRemapper.kt | 14 ++++++++--- .../shadow/internal/SourceRemapperTest.kt | 25 +++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index df611d01a4..08b9557a97 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -1,5 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.internal +import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator @@ -143,10 +144,15 @@ internal fun Iterable.relocateSourcePath(path: String): String { val className = pathWithoutExt.replace('/', '.') for (relocator in this) { - if (relocator.canRelocateClass(className) || relocator.canRelocatePath(pathWithoutExt)) { - val relocatedWithoutExt = relocator.relocatePath(RelocatePathContext(pathWithoutExt)) - return "$relocatedWithoutExt.$extension" - } + val relocatedWithoutExt = + when { + relocator.canRelocatePath(pathWithoutExt) -> + relocator.relocatePath(RelocatePathContext(pathWithoutExt)) + relocator.canRelocateClass(className) -> + relocator.relocateClass(RelocateClassContext(className)).replace('.', '/') + else -> continue + } + return "$relocatedWithoutExt.$extension" } return path } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt index 61d865b327..ac3beb8b7f 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -174,4 +174,29 @@ class SourceRemapperTest { assertThat(relocators.remapSource(input)).isEqualTo(expected) } + + @Test + fun relocateSourcePathWithClassOnlyRelocator() { + val classOnlyRelocator = + object : Relocator { + override fun canRelocatePath(path: String) = false + + override fun relocatePath(context: RelocatePathContext) = context.path + + override fun canRelocateClass(className: String) = className.startsWith("custom.pkg.") + + override fun relocateClass(context: RelocateClassContext) = + context.className.replaceFirst("custom.pkg.", "shaded.pkg.") + + override fun applyToSourceContent(sourceContent: String) = sourceContent + } + val relocators = listOf(classOnlyRelocator) + + assertThat(relocators.relocateSourcePath("custom/pkg/MyClass.java")) + .isEqualTo("shaded/pkg/MyClass.java") + assertThat(relocators.relocateSourcePath("custom/pkg/sub/OtherClass.kt")) + .isEqualTo("shaded/pkg/sub/OtherClass.kt") + assertThat(relocators.relocateSourcePath("unrelated/pkg/Unrelated.java")) + .isEqualTo("unrelated/pkg/Unrelated.java") + } } From 535de8a8ada4bc67ef869989c517adb2e1e5300d Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 14:09:52 +0800 Subject: [PATCH 077/117] Derive generateSourcesJar convention from isSourcesPublishable in KMP --- docs/publishing/README.md | 19 +++--- .../plugins/shadow/KotlinPluginsTest.kt | 61 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowKmpPlugin.kt | 1 + .../gradle/plugins/shadow/tasks/ShadowJar.kt | 4 +- 4 files changed, 75 insertions(+), 10 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index e064137d6d..0cedcc0c05 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -592,6 +592,10 @@ Shadow automatically generates a **Shadowed Sources JAR** containing: 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: +> [!TIP] +> In Kotlin/JVM projects applying `id("org.jetbrains.kotlin.jvm")`, configure `java { withSourcesJar() }` in the same +> way. The Kotlin JVM plugin applies Gradle's Java plugin internally. + === ":material-language-kotlin: build.gradle.kts" ```kotlin @@ -788,7 +792,8 @@ To publish shadowed artifacts as the primary publication: > 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. +> builds. In Kotlin Multiplatform (KMP) projects, it defaults to the JVM target's sources publishing setting +> (`true` by default, or `false` when `withSourcesJar(publish = false)` is configured). > You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`). ### Customizing the Sources Archive File @@ -873,10 +878,6 @@ 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" @@ -906,12 +907,12 @@ 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 - } ``` +In KMP projects, `generateSourcesJar` is enabled by default via the JVM target's sources publishing setting +(`target.isSourcesPublishable`). If you want to disable companion sources JAR generation, configure +`withSourcesJar(publish = false)` on the target or set `generateSourcesJar = false` on the `shadowJar` task. + ## 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 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 60a70f3434..a35d16a2d2 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 @@ -4,6 +4,7 @@ import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsAtLeast import assertk.assertions.isEqualTo +import assertk.assertions.isFalse 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 @@ -14,6 +15,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isDokkaIssue4600 import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import kotlin.io.path.appendText +import kotlin.io.path.exists import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.relativeTo import kotlin.io.path.walk @@ -351,6 +353,65 @@ class KotlinPluginsTest : BasePluginTest() { ) } + @Test + fun generateSourcesJarByDefaultInKmp() { + val stdlib = compileOnlyStdlib(true) + writeClass(sourceSet = "jvmMain", jvmLang = JvmLang.Kotlin, className = "JvmMain") + projectScript.appendText( + """ + |kotlin { + | jvm() + | sourceSets { + | jvmMain { + | dependencies { + | $stdlib + | } + | } + | } + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/JvmMain.kt", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + } + } + + @Test + fun disableSourcesJarInKmpWithSourcesJarFalse() { + val stdlib = compileOnlyStdlib(true) + writeClass(sourceSet = "jvmMain", jvmLang = JvmLang.Kotlin, className = "JvmMain") + projectScript.appendText( + """ + |kotlin { + | jvm { + | withSourcesJar(false) + | } + | sourceSets { + | jvmMain { + | dependencies { + | $stdlib + | } + | } + | } + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + } + 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/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index df0903bef7..45adcc4dc7 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,7 @@ 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.generateSourcesJar.convention(provider { target.isSourcesPublishable }) task.sourceSetsSourceDirs.convention( // Avoid snapshotting source inputs when sources JAR generation is disabled. task.generateSourcesJar.flatMap { generate -> 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 7309484240..c8b9658e4b 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 @@ -206,7 +206,9 @@ public abstract class ShadowJar : Jar() { * sources. * * In projects applying the `shadow` plugin for Java, this convention defaults to `true` when - * `java.withSourcesJar()` is enabled, and `false` otherwise. + * `java.withSourcesJar()` is enabled, and `false` otherwise. In projects applying the `shadow` + * plugin for Kotlin Multiplatform, this convention defaults to `true` when sources publishing is + * enabled on the JVM target, and `false` otherwise. */ @get:Input @get:Option( From afb237a1508d1cfa1204b48fb996faad49786817 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 14:29:33 +0800 Subject: [PATCH 078/117] Clarify sourcesJar handling is only needed when withSourcesJar is enabled --- docs/publishing/README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 0cedcc0c05..bfed56594b 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -301,7 +301,9 @@ Because the default `archiveClassifier` of [`Jar`][Jar] is `""` (empty), setting - When `generateSourcesJar` is enabled (such as when `java.withSourcesJar()` is used), the companion shadowed sources JAR is output to `--sources.jar`, conflicting with the standard `sourcesJar` task. -If you want to replace standard JARs with the shadowed ones, disable the standard `jar` and `sourcesJar` tasks: +If you want to replace standard JARs with the shadowed ones, disable the standard tasks (note that `sourcesJar` is only +created and needs handling when `java.withSourcesJar()` is enabled; if `withSourcesJar()` is not used, you only need to +configure the `jar` task): === ":material-language-kotlin: build.gradle.kts" @@ -319,6 +321,7 @@ If you want to replace standard JARs with the shadowed ones, disable the standar enabled = false } + // Only needed when java.withSourcesJar() is enabled. tasks.named("sourcesJar") { enabled = false } @@ -340,6 +343,7 @@ If you want to replace standard JARs with the shadowed ones, disable the standar enabled = false } + // Only needed when java.withSourcesJar() is enabled. tasks.named('sourcesJar', Jar) { enabled = false } @@ -363,6 +367,7 @@ Or set different `archiveClassifier` values for the standard tasks: archiveClassifier = "ignored" } + // Only needed when java.withSourcesJar() is enabled. tasks.named("sourcesJar") { archiveClassifier = "ignored-sources" } @@ -384,6 +389,7 @@ Or set different `archiveClassifier` values for the standard tasks: archiveClassifier = 'ignored' } + // Only needed when java.withSourcesJar() is enabled. tasks.named('sourcesJar', Jar) { archiveClassifier = 'ignored-sources' } @@ -675,7 +681,8 @@ automatically uses the standard `sources` classifier. To publish shadowed artifacts as the primary publication: 1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven - publication, and disable standard archive tasks to prevent destination file collisions in `build/libs`: + publication, and disable standard archive tasks to prevent destination file collisions in `build/libs` (disabling + `sourcesJar` is only needed when `java.withSourcesJar()` is enabled): === ":material-language-kotlin: build.gradle.kts" @@ -694,6 +701,7 @@ To publish shadowed artifacts as the primary publication: enabled = false } + // Only needed when java.withSourcesJar() is enabled. tasks.named("sourcesJar") { enabled = false } @@ -728,6 +736,7 @@ To publish shadowed artifacts as the primary publication: enabled = false } + // Only needed when java.withSourcesJar() is enabled. tasks.named('sourcesJar', Jar) { enabled = false } @@ -747,7 +756,7 @@ To publish shadowed artifacts as the primary publication: 2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` tasks does not remove standard variants from the `java` software component. You must also explicitly skip the - standard publication variants: + standard publication variants (skipping `sourcesElements` is only needed when `java.withSourcesJar()` is enabled): === ":material-language-kotlin: build.gradle.kts" @@ -764,6 +773,7 @@ To publish shadowed artifacts as the primary publication: components.named("java") { withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() } + // Only needed when java.withSourcesJar() is enabled. withVariantsFromConfiguration(configurations["sourcesElements"]) { skip() } } ``` @@ -783,6 +793,7 @@ To publish shadowed artifacts as the primary publication: components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { withVariantsFromConfiguration(configurations.runtimeElements) { skip() } + // Only needed when java.withSourcesJar() is enabled. withVariantsFromConfiguration(configurations.sourcesElements) { skip() } } ``` From 7919f7f4a7b22f6dad0974762af76ab6db5597f9 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 14:48:07 +0800 Subject: [PATCH 079/117] Add functional test and clarify replacement publishing from java component --- docs/publishing/README.md | 42 ++++++++++++- .../gradle/plugins/shadow/PublishingTest.kt | 63 ++++++++++++++++++- 2 files changed, 101 insertions(+), 4 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index bfed56594b..44c479991f 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -754,9 +754,11 @@ To publish shadowed artifacts as the primary publication: } ``` -2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, disabling the `jar` or `sourcesJar` - tasks does not remove standard variants from the `java` software component. You must also explicitly skip the - standard publication variants (skipping `sourcesElements` is only needed when `java.withSourcesJar()` is enabled): +2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, Gradle ties the component's + primary artifact to the `jar` task, so `tasks.jar` must remain enabled (the `shadowJar` task will overwrite its output + in `build/libs`). To avoid publishing standard variants alongside shadowed ones in Gradle Module Metadata and POM, + you must explicitly skip them using `withVariantsFromConfiguration` (skipping `sourcesElements` is only needed when + `java.withSourcesJar()` is enabled, after which `sourcesJar` can also be disabled): === ":material-language-kotlin: build.gradle.kts" @@ -771,11 +773,28 @@ To publish shadowed artifacts as the primary publication: withSourcesJar() } + // Only needed when java.withSourcesJar() is enabled. + tasks.named("sourcesJar") { + enabled = false + } + + tasks.shadowJar { + archiveClassifier = "" + } + components.named("java") { withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() } // Only needed when java.withSourcesJar() is enabled. withVariantsFromConfiguration(configurations["sourcesElements"]) { skip() } } + + publishing { + publications { + create("shadow") { + from(components["java"]) + } + } + } ``` === ":simple-apachegroovy: build.gradle" @@ -791,11 +810,28 @@ To publish shadowed artifacts as the primary publication: withSourcesJar() } + // Only needed when java.withSourcesJar() is enabled. + tasks.named('sourcesJar', Jar) { + enabled = false + } + + tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveClassifier = '' + } + components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { withVariantsFromConfiguration(configurations.runtimeElements) { skip() } // Only needed when java.withSourcesJar() is enabled. withVariantsFromConfiguration(configurations.sourcesElements) { skip() } } + + publishing { + publications { + shadow(MavenPublication) { + from components.java + } + } + } ``` > [!NOTE] diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 7d2b3c30e9..554e6abf9b 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 @@ -460,6 +461,65 @@ class PublishingTest : BasePluginTest() { ) } + @Test + fun publishShadowJarInsteadOfJarFromJavaComponent() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + |tasks.named('sourcesJar', Jar) { + | enabled = false + |} + |components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { + | withVariantsFromConfiguration(configurations.runtimeElements) { skip() } + | withVariantsFromConfiguration(configurations.sourcesElements) { skip() } + |} + """ + .trimMargin(), + shadowBlock = + """ + |archiveClassifier = '' + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.java + |} + """ + .trimMargin(), + ) + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + .containsOnly( + "maven-1.0.jar", + "maven-1.0-sources.jar", + ) + assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) + assertThat(pomReader.read(repoPath("$artifactRoot/maven-1.0.pom"))).all { + transform { it.dependencies.map(Dependency::coordinate) }.containsOnly("my:b:1.0") + transform { it.dependencies.map { dep -> dep.scope to dep.isOptional } } + .single() + .isEqualTo("compile" to true) + } + val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) + assertThat(gmm.variantNames) + .containsOnly( + "apiElements", + SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, + ) + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) + } + @Test fun dontPublishSourcesWhenGenerateSourcesJarDisabled() { projectScript.appendText( @@ -963,7 +1023,8 @@ class PublishingTest : BasePluginTest() { return JarPath(remoteRepoPath.resolve(relative)) } - private fun publish(vararg arguments: String): BuildResult = runWithSuccess("publish", *arguments) + private fun publish(vararg arguments: String): BuildResult = + runWithSuccess("build", "publish", *arguments) private fun publishConfiguration( projectBlock: String = "", From 1456ffaf8ebb544489177b6df1e2af17b74886d9 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 14:55:50 +0800 Subject: [PATCH 080/117] Disable standard archive tasks in replacement publishing tests --- .../gradle/plugins/shadow/PublishingTest.kt | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) 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 554e6abf9b..0678f9b409 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 @@ -248,6 +248,13 @@ class PublishingTest : BasePluginTest() { fun publishShadowJarInsteadOfJar() { projectScript.appendText( publishConfiguration( + projectBlock = + """ + |tasks.named('jar', Jar) { + | enabled = false + |} + """ + .trimMargin(), shadowBlock = """ |archiveClassifier = '' @@ -299,6 +306,12 @@ class PublishingTest : BasePluginTest() { |java { | withSourcesJar() |} + |tasks.named('jar', Jar) { + | enabled = false + |} + |tasks.named('sourcesJar', Jar) { + | enabled = false + |} """ .trimMargin(), shadowBlock = @@ -529,6 +542,9 @@ class PublishingTest : BasePluginTest() { |java { | withSourcesJar() |} + |tasks.named('jar', Jar) { + | enabled = false + |} """ .trimMargin(), shadowBlock = From 75ccea990d5274e36ba9acaef5146dc8848a7cca Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 15:52:39 +0800 Subject: [PATCH 081/117] Workaround for doesNotExist --- .../gradle/plugins/shadow/KotlinPluginsTest.kt | 5 ++--- .../gradle/plugins/shadow/RelocationTest.kt | 5 ++--- .../gradle/plugins/shadow/testkit/AssertK.kt | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 6 deletions(-) create mode 100644 src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/AssertK.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 a35d16a2d2..276627d69c 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 @@ -3,8 +3,8 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsAtLeast +import assertk.assertions.doesNotExist import assertk.assertions.isEqualTo -import assertk.assertions.isFalse 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 @@ -15,7 +15,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.isDokkaIssue4600 import com.github.jengelman.gradle.plugins.shadow.testkit.loadClass import com.github.jengelman.gradle.plugins.shadow.util.JvmLang import kotlin.io.path.appendText -import kotlin.io.path.exists import kotlin.io.path.invariantSeparatorsPathString import kotlin.io.path.relativeTo import kotlin.io.path.walk @@ -409,7 +408,7 @@ class KotlinPluginsTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar")).doesNotExist() } 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 a53eeaecf2..951dd23278 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,8 +2,8 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.assertThat import assertk.assertions.contains +import assertk.assertions.doesNotExist import assertk.assertions.isEqualTo -import assertk.assertions.isFalse import assertk.assertions.isNotEmpty import assertk.assertions.isNotEqualTo import assertk.fail @@ -18,7 +18,6 @@ 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 @@ -756,7 +755,7 @@ class RelocationTest : BasePluginTest() { runWithSuccess(shadowJarPath) - assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar").exists()).isFalse() + assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar")).doesNotExist() } @Test diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/AssertK.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/AssertK.kt new file mode 100644 index 0000000000..84e62a193a --- /dev/null +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/AssertK.kt @@ -0,0 +1,17 @@ +@file:Suppress("PackageDirectoryMismatch") + +package assertk.assertions + +import assertk.Assert +import assertk.assertions.support.expected +import assertk.assertions.support.show +import java.nio.file.Files +import java.nio.file.LinkOption +import java.nio.file.Path + +// TODO: https://github.com/assertk-org/assertk/pull/542 +fun Assert.doesNotExist(vararg options: LinkOption) = given { actual -> + if (!Files.notExists(actual, *options)) { + expected("${show(actual)} does not exist, but it exists") + } +} From 298ac06350b9af72defd0ac2b844f8c0e9e9f1be Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 16:04:16 +0800 Subject: [PATCH 082/117] Move non-relocation sourcesJar tests to JavaPluginsTest and FilteringTest --- .../gradle/plugins/shadow/FilteringTest.kt | 46 ++++ .../gradle/plugins/shadow/JavaPluginsTest.kt | 151 ++++++++++++ .../gradle/plugins/shadow/RelocationTest.kt | 215 ------------------ 3 files changed, 197 insertions(+), 215 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 8630d15075..003a336af6 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 @@ -298,6 +298,52 @@ class FilteringTest : BasePluginTest() { } } + @Test + fun sourcesJarRespectsSourceDirectorySetFilters() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |public class Main {} + """ + .trimMargin() + ) + path("src/main/java/my/Excluded.java") + .writeText( + """ + |package my; + |public class Excluded {} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |sourceSets { + | main { + | java { + | exclude '**/Excluded.java' + | } + | } + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + } + } + private fun commonAssertions() { assertThat(outputShadowedJar).useAll { containsOnly("c.properties", *entriesInAB, "META-INF/", "META-INF/MANIFEST.MF") 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 09f6b3db71..cb3fbbf833 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 @@ -24,6 +24,7 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.getContent import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.getStream import com.github.jengelman.gradle.plugins.shadow.testkit.runMain +import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import com.github.jengelman.gradle.plugins.shadow.util.prependText import kotlin.io.path.appendText import kotlin.io.path.deleteExisting @@ -1405,6 +1406,156 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun generateShadowedSourcesJarWhenNoIncludedSourcesJars() { + writeClass() + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + } + } + + @Test + fun generateEmptyShadowedSourcesJarWhenNoSources() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly("META-INF/", "META-INF/MANIFEST.MF") + } + } + + @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 { + | generateSourcesJar = true + | 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", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + 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 { + | generateSourcesJar = true + | 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", + "META-INF/", + "META-INF/MANIFEST.MF", + ) + getContent("shadow/ext/Ext.java") + .isEqualTo( + """ + |package shadow.ext; + |public class Ext {} + """ + .trimMargin() + ) + } + } + 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/RelocationTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/RelocationTest.kt index 951dd23278..0520e77662 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.doesNotExist 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.util.JarBuilder import kotlin.io.path.appendText import kotlin.io.path.readBytes import kotlin.io.path.writeText @@ -741,23 +739,6 @@ 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")).doesNotExist() - } - @Test fun generateShadowedSourcesJarWithRelocation() { path("src/main/java/my/Main.java") @@ -818,202 +799,6 @@ class RelocationTest : BasePluginTest() { } } - @Test - fun generateShadowedSourcesJarWhenNoIncludedSourcesJars() { - writeClass() - projectScript.appendText( - """ - |dependencies { - | implementation 'my:b:1.0' - |} - |$shadowJarTask { - | generateSourcesJar = true - |} - """ - .trimMargin() - ) - - runWithSuccess(shadowJarPath) - - assertThat(outputShadowedSourcesJar).useAll { - containsOnly( - "my/", - "my/Main.java", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - } - } - - @Test - fun generateEmptyShadowedSourcesJarWhenNoSources() { - projectScript.appendText( - """ - |dependencies { - | implementation 'my:b:1.0' - |} - |$shadowJarTask { - | generateSourcesJar = true - |} - """ - .trimMargin() - ) - - runWithSuccess(shadowJarPath) - - assertThat(outputShadowedSourcesJar).useAll { - containsOnly("META-INF/", "META-INF/MANIFEST.MF") - } - } - - @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 { - | generateSourcesJar = true - | 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", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - getContent("shadow/extra/Extra.java") - .isEqualTo( - """ - |package shadow.extra; - |public class Extra {} - """ - .trimMargin() - ) - } - } - - @Test - fun relocateShadowedSourcesJarRespectsSourceDirectorySetFilters() { - path("src/main/java/my/Main.java") - .writeText( - """ - |package my; - |public class Main {} - """ - .trimMargin() - ) - path("src/main/java/my/Excluded.java") - .writeText( - """ - |package my; - |public class Excluded {} - """ - .trimMargin() - ) - projectScript.appendText( - """ - |sourceSets { - | main { - | java { - | exclude '**/Excluded.java' - | } - | } - |} - |$shadowJarTask { - | generateSourcesJar = true - |} - """ - .trimMargin() - ) - - runWithSuccess(shadowJarPath) - - assertThat(outputShadowedSourcesJar).useAll { - containsOnly( - "my/", - "my/Main.java", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - } - } - - @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 { - | generateSourcesJar = true - | 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", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - getContent("shadow/ext/Ext.java") - .isEqualTo( - """ - |package shadow.ext; - |public class Ext {} - """ - .trimMargin() - ) - } - } - private companion object { @JvmStatic fun preserveLastModifiedProvider() = From 041394e8fa1cefa71f8ab2c4c4c91036b79dbf4a Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 16:24:59 +0800 Subject: [PATCH 083/117] Remove redundant try-catch around artifactView in DefaultDependencyFilter --- .../internal/DefaultDependencyFilter.kt | 42 +++++++++---------- 1 file changed, 19 insertions(+), 23 deletions(-) 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 f202c92cce..bbb4172764 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 @@ -40,7 +40,7 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) excludedDependencies = excludes, ) - val includedDependenciesResults = + val includedComponentIds = configuration.incoming.resolutionResult.allDependencies .filterIsInstance() .filter { dep -> @@ -50,29 +50,25 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) inc.moduleVersion == dep.selected.moduleVersion?.version } } + .map { it.selected.id } + .toSet() - val includedComponentIds = includedDependenciesResults.map { it.selected.id }.toSet() - - return 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 in includedComponentIds } - view.lenient(true) + return 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), + ) } - .files - } catch (_: Exception) { - project.files() - } + view.componentFilter { id -> id in includedComponentIds } + view.lenient(true) + } + .files } } From e9b25d40448a3cd29320f4d45d0f645fb7d4f509 Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 16:39:35 +0800 Subject: [PATCH 084/117] Inject DefaultDependencyFilter --- .../shadow/internal/DefaultDependencyFilter.kt | 17 ++++++++++++----- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 2 +- .../internal/DefaultDependencyFilterTest.kt | 2 +- 3 files changed, 14 insertions(+), 7 deletions(-) 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 bbb4172764..4e516bfbcb 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 @@ -1,6 +1,7 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.tasks.DependencyFilter +import javax.inject.Inject import org.gradle.api.Project import org.gradle.api.artifacts.Configuration import org.gradle.api.artifacts.ResolvedDependency @@ -8,9 +9,14 @@ 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.api.model.ObjectFactory -internal class DefaultDependencyFilter(@Transient private val project: Project) : - DependencyFilter.AbstractDependencyFilter(project) { +internal abstract class DefaultDependencyFilter +@Inject +constructor( + project: Project, + private val objectFactory: ObjectFactory, +) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, includedDependencies: MutableSet, @@ -28,7 +34,8 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) fun resolveSourcesJars(configurations: Collection): FileCollection { return configurations .map { resolveSourcesJars(it) } - .reduceOrNull { acc, fileCollection -> acc + fileCollection } ?: project.files() + .reduceOrNull { acc, fileCollection -> acc + fileCollection } + ?: objectFactory.fileCollection() } private fun resolveSourcesJars(configuration: Configuration): FileCollection { @@ -59,11 +66,11 @@ internal class DefaultDependencyFilter(@Transient private val project: Project) view.attributes { attrs -> attrs.attribute( Category.CATEGORY_ATTRIBUTE, - project.objects.named(Category::class.java, Category.DOCUMENTATION), + objectFactory.named(Category::class.java, Category.DOCUMENTATION), ) attrs.attribute( DocsType.DOCS_TYPE_ATTRIBUTE, - project.objects.named(DocsType::class.java, DocsType.SOURCES), + objectFactory.named(DocsType::class.java, DocsType.SOURCES), ) } view.componentFilter { id -> id in includedComponentIds } 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 c8b9658e4b..ba39aaa9d4 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,7 +193,7 @@ public abstract class ShadowJar : Jar() { @get:Internal // The resolved result is tracked by includedDependencies. public open val dependencyFilter: Property = - objectFactory.property(DefaultDependencyFilter(project)) + objectFactory.property(objectFactory.newInstance(DefaultDependencyFilter::class.java)) /** Final dependencies to be shadowed. */ @get:Classpath diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilterTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilterTest.kt index a08203436b..bdc68815a9 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilterTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilterTest.kt @@ -15,7 +15,7 @@ import org.junit.jupiter.params.provider.Arguments import org.junit.jupiter.params.provider.MethodSource class DefaultDependencyFilterTest { - private val filter = DefaultDependencyFilter(project) + private val filter = project.objects.newInstance(DefaultDependencyFilter::class.java) @ParameterizedTest @MethodSource("dependencyNotationProvider") From 2057735356a198ee8f390abb2ec947b8f5a1f3ef Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 17:04:13 +0800 Subject: [PATCH 085/117] Internal helpers for SimpleRelocator --- .../plugins/shadow/internal/SourceRemapper.kt | 124 +++++++++++++++--- .../shadow/relocation/SimpleRelocator.kt | 88 +++---------- 2 files changed, 118 insertions(+), 94 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index 08b9557a97..8a113afe8a 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -5,19 +5,60 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath -import java.util.regex.Pattern -private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") - -private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = - Pattern.compile( - "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|transient|native|strictfp|extends|implements|throws|return|new|throw|instanceof|case|default|yield|val|var|fun|is|as|in) $" + - "|" + - "\\{@link( \\*)* $" + - "|" + - "([{}(=;,:<>?&|@\\[\\]]|\\*/) $" +private val KEYWORDS = + setOf( + "import", + "package", + "public", + "protected", + "private", + "static", + "final", + "synchronized", + "abstract", + "volatile", + "transient", + "native", + "strictfp", + "extends", + "implements", + "throws", + "return", + "new", + "throw", + "instanceof", + "case", + "default", + "yield", + "val", + "var", + "fun", + "is", + "as", + "in", ) +/** + * Match + * - certain Java keywords + space + * - beginning of Javadoc link + optional line breaks and continuations with '*' + * - (opening curly brace / opening parenthesis / comma / equals / semicolon) + space + * - (closing curly brace / closing multi-line comment) + space + * + * at end of string + */ +internal val RX_ENDS_WITH_JAVA_KEYWORD = + listOf( + "\\b(${KEYWORDS.joinToString("|")}) $", + "\\{@link( \\*)* $", + "([{}(=;,:<>?&|@\\[\\]]|\\*/) $", + ) + .joinToString("|") + .toPattern() + +internal val RX_ENDS_WITH_DOT_SLASH_SPACE = "[./ ]$".toPattern() + /** * Remaps source content by applying relocators in a single pass with first-match-wins precedence, * avoiding cascade replacements where earlier relocations get re-relocated by subsequent rules. @@ -88,22 +129,15 @@ internal fun Iterable.remapSource(sourceContent: String): String { val isDotMatch = matchedText == relocator.pattern val isPathMatch = matchedText == relocator.pathPattern if (isDotMatch || isPathMatch) { - val sourceIncludes = - SimpleRelocator.getSourceSubpatterns(relocator.includes, relocator.pattern) - val sourceExcludes = - SimpleRelocator.getSourceSubpatterns(relocator.excludes, relocator.pattern) + val sourceIncludes = getSourceSubpatterns(relocator.includes, relocator.pattern) + val sourceExcludes = getSourceSubpatterns(relocator.excludes, relocator.pattern) val hasIncludes = relocator.includes.isNotEmpty() if (hasIncludes && sourceIncludes.isEmpty()) { continue } val isIncluded = - !hasIncludes || - sourceIncludes.any { - SimpleRelocator.matchesSubpattern(sourceContent, matchEnd, it) - } - val isExcluded = sourceExcludes.any { - SimpleRelocator.matchesSubpattern(sourceContent, matchEnd, it) - } + !hasIncludes || sourceIncludes.any { matchesSubpattern(sourceContent, matchEnd, it) } + val isExcluded = sourceExcludes.any { matchesSubpattern(sourceContent, matchEnd, it) } if (isIncluded && !isExcluded) { result.append( if (isDotMatch) relocator.shadedPattern else relocator.shadedPathPattern @@ -166,3 +200,51 @@ internal fun isSourceFile(path: String): Boolean { path.endsWith(".groovy") || path.endsWith(".scala") } + +internal fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + if (patternPrefix.isEmpty()) return emptySet() + val result = mutableSetOf() + val dotPrefix = patternPrefix.replace('/', '.') + val slashPrefix = patternPrefix.replace('.', '/') + val trailingWildcardRegex = "[./][*]+$".toRegex() + + for (pat in patterns) { + val dotPat = pat.replace('/', '.') + if (dotPat.startsWith(dotPrefix)) { + val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('.', '/')) + } + } + val slashPat = pat.replace('.', '/') + if (slashPat.startsWith(slashPrefix)) { + val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('/', '.')) + } + } + } + return result +} + +internal fun matchesSubpattern( + content: CharSequence, + offset: Int = 0, + subpattern: String, +): Boolean { + val subLen = subpattern.length + if (offset + subLen > content.length) return false + for (i in 0 until subLen) { + if (content[offset + i] != subpattern[i]) return false + } + if (subLen == 0 || offset + subLen == content.length) return true + if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true + val nextChar = content[offset + subLen] + return !nextChar.isLetterOrDigit() && nextChar != '_' +} 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 ce769328c2..dc71d84d59 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 @@ -1,5 +1,9 @@ package com.github.jengelman.gradle.plugins.shadow.relocation +import com.github.jengelman.gradle.plugins.shadow.internal.RX_ENDS_WITH_DOT_SLASH_SPACE +import com.github.jengelman.gradle.plugins.shadow.internal.RX_ENDS_WITH_JAVA_KEYWORD +import com.github.jengelman.gradle.plugins.shadow.internal.getSourceSubpatterns +import com.github.jengelman.gradle.plugins.shadow.internal.matchesSubpattern import java.util.Objects import java.util.regex.Pattern import org.codehaus.plexus.util.SelectorUtils @@ -180,29 +184,8 @@ constructor( return excludes.any { SelectorUtils.matchPath(it, path, "/", true) } } - internal companion object { - /** Match dot, slash or space at end of string */ - private val RX_ENDS_WITH_DOT_SLASH_SPACE: Pattern = Pattern.compile("[./ ]$") - - /** - * Match - * - certain Java keywords + space - * - beginning of Javadoc link + optional line breaks and continuations with '*' - * - (opening curly brace / opening parenthesis / comma / equals / semicolon) + space - * - (closing curly brace / closing multi-line comment) + space - * - * at end of string - */ - private val RX_ENDS_WITH_JAVA_KEYWORD: Pattern = - Pattern.compile( - "\\b(import|package|public|protected|private|static|final|synchronized|abstract|volatile|transient|native|strictfp|extends|implements|throws|return|new|throw|instanceof|case|default|yield|val|var|fun|is|as|in) $" + - "|" + - "\\{@link( \\*)* $" + - "|" + - "([{}(=;,:<>?&|@\\[\\]]|\\*/) $" - ) - - private fun normalizePatterns(patterns: Collection?) = buildSet { + private companion object { + fun normalizePatterns(patterns: Collection?) = buildSet { patterns ?: return@buildSet for (pattern in patterns) { // Regex patterns don't need to be normalized and stay as is. @@ -233,54 +216,7 @@ constructor( } } - fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { - if (patternPrefix.isEmpty()) return emptySet() - val result = mutableSetOf() - val dotPrefix = patternPrefix.replace('/', '.') - val slashPrefix = patternPrefix.replace('.', '/') - val trailingWildcardRegex = "[./][*]+$".toRegex() - - for (pat in patterns) { - val dotPat = pat.replace('/', '.') - if (dotPat.startsWith(dotPrefix)) { - val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") - if (sub.isEmpty()) { - result.add("") - } else { - result.add(sub) - result.add(sub.replace('.', '/')) - } - } - val slashPat = pat.replace('.', '/') - if (slashPat.startsWith(slashPrefix)) { - val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") - if (sub.isEmpty()) { - result.add("") - } else { - result.add(sub) - result.add(sub.replace('/', '.')) - } - } - } - return result - } - - fun matchesSubpattern(content: CharSequence, offset: Int, subpattern: String): Boolean { - val subLen = subpattern.length - if (offset + subLen > content.length) return false - for (i in 0 until subLen) { - if (content[offset + i] != subpattern[i]) return false - } - if (subLen == 0 || offset + subLen == content.length) return true - if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true - val nextChar = content[offset + subLen] - return !nextChar.isLetterOrDigit() && nextChar != '_' - } - - fun matchesSubpattern(snippet: String, subpattern: String): Boolean = - matchesSubpattern(snippet, 0, subpattern) - - private fun shadeSourceWithFilters( + fun shadeSourceWithFilters( sourceContent: String, patternFrom: String, patternTo: String, @@ -302,8 +238,14 @@ constructor( val isFirstSnippet = i == 0 val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] - val isIncluded = !hasIncludes || includedPatterns.any { matchesSubpattern(snippet, it) } - val isExcluded = excludedPatterns.any { matchesSubpattern(snippet, it) } + val isIncluded = + !hasIncludes || + includedPatterns.any { + matchesSubpattern(content = snippet, subpattern = it) + } + val isExcluded = excludedPatterns.any { + matchesSubpattern(content = snippet, subpattern = it) + } if (isFirstSnippet) { shadedSourceContent.append(snippet) From 74856cfd7ea74f9edcdf5c9dc9fbb240e53ff02e Mon Sep 17 00:00:00 2001 From: Goooler Date: Sun, 20 Sep 2026 17:28:59 +0800 Subject: [PATCH 086/117] Fast-path keyword lookup in isJavaContextValid and reuse whitespace regex --- .../plugins/shadow/internal/SourceRemapper.kt | 32 +++++++++++++++---- .../shadow/relocation/SimpleRelocator.kt | 11 +++---- 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index 8a113afe8a..0a3aa8a7af 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -48,7 +48,7 @@ private val KEYWORDS = * * at end of string */ -internal val RX_ENDS_WITH_JAVA_KEYWORD = +private val RX_ENDS_WITH_JAVA_KEYWORD = listOf( "\\b(${KEYWORDS.joinToString("|")}) $", "\\{@link( \\*)* $", @@ -57,7 +57,29 @@ internal val RX_ENDS_WITH_JAVA_KEYWORD = .joinToString("|") .toPattern() -internal val RX_ENDS_WITH_DOT_SLASH_SPACE = "[./ ]$".toPattern() +private val RX_ENDS_WITH_DOT_SLASH_SPACE = "[./ ]$".toPattern() + +internal val RX_WHITESPACE = "\\s+".toRegex() + +internal fun String.isJavaContextValid(): Boolean { + if (!RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(this).find()) { + return true + } + if (endsWith(' ')) { + var end = length - 1 + while (end > 0 && this[end - 1].isWhitespace()) { + end-- + } + var start = end + while (start > 0 && this[start - 1].isJavaIdentifierPart()) { + start-- + } + if (start < end && substring(start, end) in KEYWORDS) { + return true + } + } + return RX_ENDS_WITH_JAVA_KEYWORD.matcher(this).find() +} /** * Remaps source content by applying relocators in a single pass with first-match-wins precedence, @@ -115,10 +137,8 @@ internal fun Iterable.remapSource(sourceContent: String): String { } val lookbackStart = (cursor - 64).coerceAtLeast(0) val previousSnippetOneLine = - sourceContent.substring(lookbackStart, matchStart).replace("\\s+".toRegex(), " ") - val afterDotSlashSpace = RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() - val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() - val contextValid = !afterDotSlashSpace || afterJavaKeyWord + sourceContent.substring(lookbackStart, matchStart).replace(RX_WHITESPACE, " ") + val contextValid = previousSnippetOneLine.isJavaContextValid() var replaced = false if (contextValid) { 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 dc71d84d59..d284a79fa7 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 @@ -1,8 +1,8 @@ package com.github.jengelman.gradle.plugins.shadow.relocation -import com.github.jengelman.gradle.plugins.shadow.internal.RX_ENDS_WITH_DOT_SLASH_SPACE -import com.github.jengelman.gradle.plugins.shadow.internal.RX_ENDS_WITH_JAVA_KEYWORD +import com.github.jengelman.gradle.plugins.shadow.internal.RX_WHITESPACE import com.github.jengelman.gradle.plugins.shadow.internal.getSourceSubpatterns +import com.github.jengelman.gradle.plugins.shadow.internal.isJavaContextValid import com.github.jengelman.gradle.plugins.shadow.internal.matchesSubpattern import java.util.Objects import java.util.regex.Pattern @@ -250,12 +250,9 @@ constructor( if (isFirstSnippet) { shadedSourceContent.append(snippet) } else { - val previousSnippetOneLine = previousSnippet.replace("\\s+".toRegex(), " ") - val afterDotSlashSpace = - RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(previousSnippetOneLine).find() - val afterJavaKeyWord = RX_ENDS_WITH_JAVA_KEYWORD.matcher(previousSnippetOneLine).find() + val previousSnippetOneLine = previousSnippet.replace(RX_WHITESPACE, " ") val shouldRelocate = - isIncluded && !isExcluded && (!afterDotSlashSpace || afterJavaKeyWord) + isIncluded && !isExcluded && previousSnippetOneLine.isJavaContextValid() shadedSourceContent.append(if (shouldRelocate) patternTo else patternFrom).append(snippet) } } From 5b04831c707fc59f1922bc600e195e626189baf7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 21 Sep 2026 11:45:42 +0800 Subject: [PATCH 087/117] Merge custom sources tests without relocation in JavaPluginsTest --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 85 ++++++------------- 1 file changed, 24 insertions(+), 61 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 cb3fbbf833..8901476be5 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 @@ -24,7 +24,6 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.getContent import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.getStream import com.github.jengelman.gradle.plugins.shadow.testkit.runMain -import com.github.jengelman.gradle.plugins.shadow.util.JarBuilder import com.github.jengelman.gradle.plugins.shadow.util.prependText import kotlin.io.path.appendText import kotlin.io.path.deleteExisting @@ -1455,15 +1454,8 @@ class JavaPluginsTest : BasePluginTest() { } @Test - fun generateShadowedSourcesJarWithCustomSourceSetsSourceDirs() { - path("src/main/java/my/Main.java") - .writeText( - """ - |package my; - |public class Main {} - """ - .trimMargin() - ) + fun generateShadowedSourcesJarWithCustomSources() { + writeClass() path("src/extra/java/extra/Extra.java") .writeText( """ @@ -1472,12 +1464,24 @@ class JavaPluginsTest : BasePluginTest() { """ .trimMargin() ) + val customSourcesJar = + buildJar("libs/external-sources.jar") { + insert( + "ext/Ext.java", + """ + package ext; + public class Ext {} + """ + .trimIndent(), + ) + } + projectScript.appendText( """ |$shadowJarTask { | generateSourcesJar = true - | sourceSetsSourceDirs.from('src/extra/java') - | relocate('extra', 'shadow.extra') + | sourceSetsSourceDirs.from 'src/extra/java' + | includedSourcesJars.from '${customSourcesJar.invariantSeparatorsPathString}' |} """ .trimMargin() @@ -1489,66 +1493,25 @@ class JavaPluginsTest : BasePluginTest() { containsOnly( "my/", "my/Main.java", - "shadow/", - "shadow/extra/", - "shadow/extra/Extra.java", + "extra/", + "extra/Extra.java", + "ext/", + "ext/Ext.java", "META-INF/", "META-INF/MANIFEST.MF", ) - getContent("shadow/extra/Extra.java") + getContent("extra/Extra.java") .isEqualTo( """ - |package shadow.extra; + |package 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 { - | generateSourcesJar = true - | 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", - "META-INF/", - "META-INF/MANIFEST.MF", - ) - getContent("shadow/ext/Ext.java") + getContent("ext/Ext.java") .isEqualTo( """ - |package shadow.ext; + |package ext; |public class Ext {} """ .trimMargin() From 1baf1f69b40ea46e533d23ebe433eb3dd7c9f6e3 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 21 Sep 2026 13:51:10 +0800 Subject: [PATCH 088/117] Use boundary-based context check for multi-language source relocation --- .../plugins/shadow/internal/SourceRemapper.kt | 160 ++++++++---------- .../shadow/relocation/SimpleRelocator.kt | 68 +------- .../shadow/internal/SourceRemapperTest.kt | 128 ++++++++++++++ 3 files changed, 205 insertions(+), 151 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index 0a3aa8a7af..a10edcb476 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -6,81 +6,6 @@ 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 -private val KEYWORDS = - setOf( - "import", - "package", - "public", - "protected", - "private", - "static", - "final", - "synchronized", - "abstract", - "volatile", - "transient", - "native", - "strictfp", - "extends", - "implements", - "throws", - "return", - "new", - "throw", - "instanceof", - "case", - "default", - "yield", - "val", - "var", - "fun", - "is", - "as", - "in", - ) - -/** - * Match - * - certain Java keywords + space - * - beginning of Javadoc link + optional line breaks and continuations with '*' - * - (opening curly brace / opening parenthesis / comma / equals / semicolon) + space - * - (closing curly brace / closing multi-line comment) + space - * - * at end of string - */ -private val RX_ENDS_WITH_JAVA_KEYWORD = - listOf( - "\\b(${KEYWORDS.joinToString("|")}) $", - "\\{@link( \\*)* $", - "([{}(=;,:<>?&|@\\[\\]]|\\*/) $", - ) - .joinToString("|") - .toPattern() - -private val RX_ENDS_WITH_DOT_SLASH_SPACE = "[./ ]$".toPattern() - -internal val RX_WHITESPACE = "\\s+".toRegex() - -internal fun String.isJavaContextValid(): Boolean { - if (!RX_ENDS_WITH_DOT_SLASH_SPACE.matcher(this).find()) { - return true - } - if (endsWith(' ')) { - var end = length - 1 - while (end > 0 && this[end - 1].isWhitespace()) { - end-- - } - var start = end - while (start > 0 && this[start - 1].isJavaIdentifierPart()) { - start-- - } - if (start < end && substring(start, end) in KEYWORDS) { - return true - } - } - return RX_ENDS_WITH_JAVA_KEYWORD.matcher(this).find() -} - /** * Remaps source content by applying relocators in a single pass with first-match-wins precedence, * avoiding cascade replacements where earlier relocations get re-relocated by subsequent rules. @@ -131,14 +56,7 @@ internal fun Iterable.remapSource(sourceContent: String): String { result.append(sourceContent, lastIndex, matchStart) lastIndex = matchEnd - var cursor = matchStart - 1 - while (cursor >= 0 && sourceContent[cursor].isWhitespace()) { - cursor-- - } - val lookbackStart = (cursor - 64).coerceAtLeast(0) - val previousSnippetOneLine = - sourceContent.substring(lookbackStart, matchStart).replace(RX_WHITESPACE, " ") - val contextValid = previousSnippetOneLine.isJavaContextValid() + val contextValid = isSourceContextValid(matchedText, sourceContent, matchStart, matchEnd) var replaced = false if (contextValid) { @@ -221,7 +139,7 @@ internal fun isSourceFile(path: String): Boolean { path.endsWith(".scala") } -internal fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { +private fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') @@ -253,7 +171,7 @@ internal fun getSourceSubpatterns(patterns: Set, patternPrefix: String): return result } -internal fun matchesSubpattern( +private fun matchesSubpattern( content: CharSequence, offset: Int = 0, subpattern: String, @@ -268,3 +186,75 @@ internal fun matchesSubpattern( val nextChar = content[offset + subLen] return !nextChar.isLetterOrDigit() && nextChar != '_' } + +/** + * Determines whether a match in the source code represents a valid package/class reference rather + * than a subpackage of a longer package, a subpath of a longer path, or an unrelated local + * identifier (e.g., variable or parameter name). + */ +private fun isSourceContextValid( + pattern: String, + sourceContent: CharSequence, + matchStart: Int, + matchEnd: Int, +): Boolean { + var prevIndex = matchStart - 1 + while (prevIndex >= 0 && sourceContent[prevIndex].isWhitespace()) { + prevIndex-- + } + + if (prevIndex >= 0) { + val prevChar = sourceContent[prevIndex] + if (prevChar == '.') { + val beforeDot = if (prevIndex > 0) sourceContent[prevIndex - 1] else null + // A dot is only a package separator if it's not a Kotlin range '..' or varargs/spread '...' + if (beforeDot != '.') { + return false + } + } + if (prevChar == '/') { + val beforeSlash = if (prevIndex > 0) sourceContent[prevIndex - 1] else null + // Only reject if pattern does not contain '.' and the slash is an actual path delimiter + // (not closing a block comment '*/' or a single-line comment '//') + if (!pattern.contains('.') && beforeSlash != '*' && beforeSlash != '/') { + return false + } + } + } + + // In all JVM languages, qualified names containing '.' or '/' cannot be local identifiers. + if (pattern.contains('.') || pattern.contains('/')) { + return true + } + + // For unqualified single-word patterns (e.g. "io", "foo"), check if followed by '.' or '/' + var nextIndex = matchEnd + while (nextIndex < sourceContent.length && sourceContent[nextIndex].isWhitespace()) { + nextIndex++ + } + if (nextIndex < sourceContent.length) { + val nextChar = sourceContent[nextIndex] + if (nextChar == '.' || nextChar == '/') { + return true + } + } + + // Check if preceded by 'package', 'import', or '{@link' + if (prevIndex >= 0) { + var tokenStart = prevIndex + while (tokenStart > 0 && sourceContent[tokenStart - 1].isJavaIdentifierPart()) { + tokenStart-- + } + val prevToken = sourceContent.subSequence(tokenStart, prevIndex + 1).toString() + if (prevToken == "package" || prevToken == "import") { + return true + } + val lookbackStart = (matchStart - 32).coerceAtLeast(0) + val lookbackSnippet = sourceContent.substring(lookbackStart, matchStart) + if (lookbackSnippet.contains("{@link")) { + return true + } + } + + return false +} 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 d284a79fa7..61066cf689 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 @@ -1,9 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.relocation -import com.github.jengelman.gradle.plugins.shadow.internal.RX_WHITESPACE -import com.github.jengelman.gradle.plugins.shadow.internal.getSourceSubpatterns -import com.github.jengelman.gradle.plugins.shadow.internal.isJavaContextValid -import com.github.jengelman.gradle.plugins.shadow.internal.matchesSubpattern +import com.github.jengelman.gradle.plugins.shadow.internal.remapSource import java.util.Objects import java.util.regex.Pattern import org.codehaus.plexus.util.SelectorUtils @@ -117,25 +114,7 @@ constructor( override fun applyToSourceContent(sourceContent: String): String { if (rawString || pattern.isEmpty()) return sourceContent - val sourceIncludes = getSourceSubpatterns(includes, pattern) - val sourceExcludes = getSourceSubpatterns(excludes, pattern) - val content = - shadeSourceWithFilters( - sourceContent = sourceContent, - patternFrom = pattern, - patternTo = shadedPattern, - includedPatterns = sourceIncludes, - hasIncludes = includes.isNotEmpty(), - excludedPatterns = sourceExcludes, - ) - return shadeSourceWithFilters( - sourceContent = content, - patternFrom = pathPattern, - patternTo = shadedPathPattern, - includedPatterns = sourceIncludes, - hasIncludes = includes.isNotEmpty(), - excludedPatterns = sourceExcludes, - ) + return listOf(this).remapSource(sourceContent) } override fun equals(other: Any?): Boolean { @@ -215,48 +194,5 @@ constructor( } } } - - fun shadeSourceWithFilters( - sourceContent: String, - patternFrom: String, - patternTo: String, - includedPatterns: Set, - hasIncludes: Boolean, - excludedPatterns: Set, - ): String { - if (hasIncludes && includedPatterns.isEmpty()) { - return sourceContent - } - - val shadedSourceContent = StringBuilder(sourceContent.length * 11 / 10) - val snippets = - sourceContent - .split(("\\b" + patternFrom.replace(".", "[.]") + "\\b").toRegex()) - .filter(CharSequence::isNotEmpty) - - snippets.forEachIndexed { i, snippet -> - val isFirstSnippet = i == 0 - val previousSnippet = if (isFirstSnippet) "" else snippets[i - 1] - - val isIncluded = - !hasIncludes || - includedPatterns.any { - matchesSubpattern(content = snippet, subpattern = it) - } - val isExcluded = excludedPatterns.any { - matchesSubpattern(content = snippet, subpattern = it) - } - - if (isFirstSnippet) { - shadedSourceContent.append(snippet) - } else { - val previousSnippetOneLine = previousSnippet.replace(RX_WHITESPACE, " ") - val shouldRelocate = - isIncluded && !isExcluded && previousSnippetOneLine.isJavaContextValid() - shadedSourceContent.append(if (shouldRelocate) patternTo else patternFrom).append(snippet) - } - } - return shadedSourceContent.toString() - } } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt index ac3beb8b7f..7b7eeb4133 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -199,4 +199,132 @@ class SourceRemapperTest { assertThat(relocators.relocateSourcePath("unrelated/pkg/Unrelated.java")) .isEqualTo("unrelated/pkg/Unrelated.java") } + + @Test + fun remapSourceWithMultiLanguageConstructs() { + val relocators = listOf(SimpleRelocator("com.example", "shaded.example")) + val input = + """ + |// Kotlin constructs + |val delegate by com.example.Delegate() + |val isType = obj is com.example.Type + |val asType = obj as com.example.Type + |val range = 0..com.example.Constants.MAX + |fun compute(factory: () -> com.example.Type = { com.example.Factory.create() }) + | + |// Groovy constructs + |def dynamicVar = com.example.Factory.create() + |def coerced = obj as com.example.Type + | + |// Scala constructs + |case _: com.example.Type => true + |class MyService with com.example.Trait + | + |// Java expressions and operators + |boolean flag = condition ? com.example.Factory.create() : null; + |int divided = total / com.example.Constants.SCALE; + |int bitwise = flags & com.example.Constants.MASK; + """ + .trimMargin() + + val expected = + """ + |// Kotlin constructs + |val delegate by shaded.example.Delegate() + |val isType = obj is shaded.example.Type + |val asType = obj as shaded.example.Type + |val range = 0..shaded.example.Constants.MAX + |fun compute(factory: () -> shaded.example.Type = { shaded.example.Factory.create() }) + | + |// Groovy constructs + |def dynamicVar = shaded.example.Factory.create() + |def coerced = obj as shaded.example.Type + | + |// Scala constructs + |case _: shaded.example.Type => true + |class MyService with shaded.example.Trait + | + |// Java expressions and operators + |boolean flag = condition ? shaded.example.Factory.create() : null; + |int divided = total / shaded.example.Constants.SCALE; + |int bitwise = flags & shaded.example.Constants.MASK; + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } + + @Test + fun remapSourceProtectsPrefixCollisionsAndVariables() { + val relocators = + listOf( + SimpleRelocator("com.example", "shaded.example"), + SimpleRelocator("io", "shaded.io"), + ) + val input = + """ + |package io; + |import io.netty.channel.Channel; + |import other.com.example.Foo; + |import java.io.IOException; + | + |class Main { + | String io, val; + | String path = "dir/com/example/File.txt"; + | String relocatedPath = "com/example/File.txt"; + | /* comment */ com.example.Type t; + | // line comment com.example.Type t2; + |} + """ + .trimMargin() + + val expected = + """ + |package shaded.io; + |import shaded.io.netty.channel.Channel; + |import other.com.example.Foo; + |import java.io.IOException; + | + |class Main { + | String io, val; + | String path = "dir/com/example/File.txt"; + | String relocatedPath = "shaded/example/File.txt"; + | /* comment */ shaded.example.Type t; + | // line comment shaded.example.Type t2; + |} + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } + + @Test + fun remapSourceAtContentStartAndInJavadocLink() { + val relocators = + listOf( + SimpleRelocator("com.example", "shaded.example"), + SimpleRelocator("io", "shaded.io"), + ) + val input = + """ + |com.example.Factory.create() + |io.netty.channel.Channel.open() + |/** + | * See {@link io} or {@link com.example.Type} + | */ + """ + .trimMargin() + + val expected = + """ + |shaded.example.Factory.create() + |shaded.io.netty.channel.Channel.open() + |/** + | * See {@link shaded.io} or {@link shaded.example.Type} + | */ + """ + .trimMargin() + + assertThat(relocators.remapSource(input)).isEqualTo(expected) + } } From a39a076c4244db104867e0fe67dbdb0d5583fc13 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 21 Sep 2026 14:12:21 +0800 Subject: [PATCH 089/117] Simplify remapSource by delegating to applyToSourceContent --- .../plugins/shadow/internal/SourceRemapper.kt | 225 +----------------- .../plugins/shadow/internal/SourcesJar.kt | 4 +- .../shadow/relocation/SimpleRelocator.kt | 179 +++++++++++++- .../shadow/internal/SourceRemapperTest.kt | 6 +- 4 files changed, 187 insertions(+), 227 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index a10edcb476..e1af945e92 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -3,103 +3,11 @@ package com.github.jengelman.gradle.plugins.shadow.internal import com.github.jengelman.gradle.plugins.shadow.relocation.RelocateClassContext import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator -import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath -/** - * Remaps source content by applying relocators in a single pass with first-match-wins precedence, - * avoiding cascade replacements where earlier relocations get re-relocated by subsequent rules. - */ internal fun Iterable.remapSource(sourceContent: String): String { - val relocatorList = this.toList() - if (relocatorList.isEmpty() || sourceContent.isEmpty()) return sourceContent - - val simpleRelocators = - relocatorList.filterIsInstance().filter { - !it.rawString && it.pattern.isNotEmpty() - } - val otherRelocators = relocatorList.filter { it !in simpleRelocators } - - if (simpleRelocators.isEmpty()) { - var content = sourceContent - for (relocator in relocatorList) { - content = relocator.applyToSourceContent(content) - } - return content - } - - val patterns = - simpleRelocators - .flatMap { listOf(it.pattern, it.pathPattern) } - .filter { it.isNotEmpty() } - .distinct() - .sortedByDescending { it.length } - - if (patterns.isEmpty()) { - var content = sourceContent - for (relocator in otherRelocators) { - content = relocator.applyToSourceContent(content) - } - return content - } - - val patternRegex = Regex("\\b(" + patterns.joinToString("|") { Regex.escape(it) } + ")\\b") - - val result = StringBuilder((sourceContent.length * 1.1).toInt()) - var lastIndex = 0 - - for (match in patternRegex.findAll(sourceContent)) { - val matchStart = match.range.first - val matchEnd = match.range.last + 1 - val matchedText = match.value - - result.append(sourceContent, lastIndex, matchStart) - lastIndex = matchEnd - - val contextValid = isSourceContextValid(matchedText, sourceContent, matchStart, matchEnd) - - var replaced = false - if (contextValid) { - for (relocator in relocatorList) { - if ( - relocator is SimpleRelocator && !relocator.rawString && relocator.pattern.isNotEmpty() - ) { - val isDotMatch = matchedText == relocator.pattern - val isPathMatch = matchedText == relocator.pathPattern - if (isDotMatch || isPathMatch) { - val sourceIncludes = getSourceSubpatterns(relocator.includes, relocator.pattern) - val sourceExcludes = getSourceSubpatterns(relocator.excludes, relocator.pattern) - val hasIncludes = relocator.includes.isNotEmpty() - if (hasIncludes && sourceIncludes.isEmpty()) { - continue - } - val isIncluded = - !hasIncludes || sourceIncludes.any { matchesSubpattern(sourceContent, matchEnd, it) } - val isExcluded = sourceExcludes.any { matchesSubpattern(sourceContent, matchEnd, it) } - if (isIncluded && !isExcluded) { - result.append( - if (isDotMatch) relocator.shadedPattern else relocator.shadedPathPattern - ) - replaced = true - break - } - } - } - } - } - - if (!replaced) { - result.append(matchedText) - } - } - - result.append(sourceContent, lastIndex, sourceContent.length) - if (otherRelocators.isEmpty()) { - return result.toString() - } - - var content = result.toString() - for (relocator in otherRelocators) { + var content = sourceContent + for (relocator in this) { content = relocator.applyToSourceContent(content) } return content @@ -110,7 +18,7 @@ internal fun Iterable.remapSource(sourceContent: String): String { * relocators, ensuring class-level include/exclude patterns work symmetrically with binary classes. */ internal fun Iterable.relocateSourcePath(path: String): String { - if (isSourceFile(path)) { + if (path.isSourceFile()) { val extension = path.substringAfterLast('.', "") val pathWithoutExt = path.removeSuffix(".$extension") val className = pathWithoutExt.replace('/', '.') @@ -132,129 +40,6 @@ internal fun Iterable.relocateSourcePath(path: String): String { return relocatePath(path) } -internal fun isSourceFile(path: String): Boolean { - return path.endsWith(".java") || - path.endsWith(".kt") || - path.endsWith(".groovy") || - path.endsWith(".scala") -} - -private fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { - if (patternPrefix.isEmpty()) return emptySet() - val result = mutableSetOf() - val dotPrefix = patternPrefix.replace('/', '.') - val slashPrefix = patternPrefix.replace('.', '/') - val trailingWildcardRegex = "[./][*]+$".toRegex() - - for (pat in patterns) { - val dotPat = pat.replace('/', '.') - if (dotPat.startsWith(dotPrefix)) { - val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") - if (sub.isEmpty()) { - result.add("") - } else { - result.add(sub) - result.add(sub.replace('.', '/')) - } - } - val slashPat = pat.replace('.', '/') - if (slashPat.startsWith(slashPrefix)) { - val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") - if (sub.isEmpty()) { - result.add("") - } else { - result.add(sub) - result.add(sub.replace('/', '.')) - } - } - } - return result -} - -private fun matchesSubpattern( - content: CharSequence, - offset: Int = 0, - subpattern: String, -): Boolean { - val subLen = subpattern.length - if (offset + subLen > content.length) return false - for (i in 0 until subLen) { - if (content[offset + i] != subpattern[i]) return false - } - if (subLen == 0 || offset + subLen == content.length) return true - if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true - val nextChar = content[offset + subLen] - return !nextChar.isLetterOrDigit() && nextChar != '_' -} - -/** - * Determines whether a match in the source code represents a valid package/class reference rather - * than a subpackage of a longer package, a subpath of a longer path, or an unrelated local - * identifier (e.g., variable or parameter name). - */ -private fun isSourceContextValid( - pattern: String, - sourceContent: CharSequence, - matchStart: Int, - matchEnd: Int, -): Boolean { - var prevIndex = matchStart - 1 - while (prevIndex >= 0 && sourceContent[prevIndex].isWhitespace()) { - prevIndex-- - } - - if (prevIndex >= 0) { - val prevChar = sourceContent[prevIndex] - if (prevChar == '.') { - val beforeDot = if (prevIndex > 0) sourceContent[prevIndex - 1] else null - // A dot is only a package separator if it's not a Kotlin range '..' or varargs/spread '...' - if (beforeDot != '.') { - return false - } - } - if (prevChar == '/') { - val beforeSlash = if (prevIndex > 0) sourceContent[prevIndex - 1] else null - // Only reject if pattern does not contain '.' and the slash is an actual path delimiter - // (not closing a block comment '*/' or a single-line comment '//') - if (!pattern.contains('.') && beforeSlash != '*' && beforeSlash != '/') { - return false - } - } - } - - // In all JVM languages, qualified names containing '.' or '/' cannot be local identifiers. - if (pattern.contains('.') || pattern.contains('/')) { - return true - } - - // For unqualified single-word patterns (e.g. "io", "foo"), check if followed by '.' or '/' - var nextIndex = matchEnd - while (nextIndex < sourceContent.length && sourceContent[nextIndex].isWhitespace()) { - nextIndex++ - } - if (nextIndex < sourceContent.length) { - val nextChar = sourceContent[nextIndex] - if (nextChar == '.' || nextChar == '/') { - return true - } - } - - // Check if preceded by 'package', 'import', or '{@link' - if (prevIndex >= 0) { - var tokenStart = prevIndex - while (tokenStart > 0 && sourceContent[tokenStart - 1].isJavaIdentifierPart()) { - tokenStart-- - } - val prevToken = sourceContent.subSequence(tokenStart, prevIndex + 1).toString() - if (prevToken == "package" || prevToken == "import") { - return true - } - val lookbackStart = (matchStart - 32).coerceAtLeast(0) - val lookbackSnippet = sourceContent.substring(lookbackStart, matchStart) - if (lookbackSnippet.contains("{@link")) { - return true - } - } - - return false +internal fun String.isSourceFile(): Boolean { + return endsWith(".java") || endsWith(".kt") || endsWith(".groovy") || endsWith(".scala") } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt index 20c5f0b1ad..035dbdcd99 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -59,7 +59,7 @@ internal fun generateSourcesJar( } for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { - val isSource = isSourceFile(relPath) + val isSource = relPath.isSourceFile() if (isSource) { if (isUnused(relPath, unusedClasses, sourceToClasses)) continue val relocatedPath = relocators.relocateSourcePath(relPath) @@ -109,7 +109,7 @@ internal fun generateSourcesJar( ) { return@forEach } - val isSource = isSourceFile(name) + val isSource = name.isSourceFile() if (isSource) { if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocateSourcePath(name) 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 61066cf689..9f97bbd531 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 @@ -1,6 +1,5 @@ package com.github.jengelman.gradle.plugins.shadow.relocation -import com.github.jengelman.gradle.plugins.shadow.internal.remapSource import java.util.Objects import java.util.regex.Pattern import org.codehaus.plexus.util.SelectorUtils @@ -114,7 +113,25 @@ constructor( override fun applyToSourceContent(sourceContent: String): String { if (rawString || pattern.isEmpty()) return sourceContent - return listOf(this).remapSource(sourceContent) + val sourceIncludes = getSourceSubpatterns(includes, pattern) + val sourceExcludes = getSourceSubpatterns(excludes, pattern) + val content = + shadeSource( + sourceContent = sourceContent, + patternFrom = pattern, + patternTo = shadedPattern, + includedPatterns = sourceIncludes, + hasIncludes = includes.isNotEmpty(), + excludedPatterns = sourceExcludes, + ) + return shadeSource( + sourceContent = content, + patternFrom = pathPattern, + patternTo = shadedPathPattern, + includedPatterns = sourceIncludes, + hasIncludes = includes.isNotEmpty(), + excludedPatterns = sourceExcludes, + ) } override fun equals(other: Any?): Boolean { @@ -194,5 +211,163 @@ constructor( } } } + + fun shadeSource( + sourceContent: String, + patternFrom: String, + patternTo: String, + includedPatterns: Set, + hasIncludes: Boolean, + excludedPatterns: Set, + ): String { + if (hasIncludes && includedPatterns.isEmpty()) { + return sourceContent + } + + val regex = Regex("\\b" + Regex.escape(patternFrom) + "\\b") + val matches = regex.findAll(sourceContent).toList() + if (matches.isEmpty()) return sourceContent + + val result = StringBuilder((sourceContent.length * 1.1).toInt()) + var lastIndex = 0 + + for (match in matches) { + val matchStart = match.range.first + val matchEnd = match.range.last + 1 + + result.append(sourceContent, lastIndex, matchStart) + lastIndex = matchEnd + + val isIncluded = + !hasIncludes || includedPatterns.any { matchesSubpattern(sourceContent, matchEnd, it) } + val isExcluded = excludedPatterns.any { matchesSubpattern(sourceContent, matchEnd, it) } + val contextValid = isSourceContextValid(patternFrom, sourceContent, matchStart, matchEnd) + + if (isIncluded && !isExcluded && contextValid) { + result.append(patternTo) + } else { + result.append(patternFrom) + } + } + + result.append(sourceContent, lastIndex, sourceContent.length) + return result.toString() + } + + fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + if (patternPrefix.isEmpty()) return emptySet() + val result = mutableSetOf() + val dotPrefix = patternPrefix.replace('/', '.') + val slashPrefix = patternPrefix.replace('.', '/') + val trailingWildcardRegex = "[./][*]+$".toRegex() + + for (pat in patterns) { + val dotPat = pat.replace('/', '.') + if (dotPat.startsWith(dotPrefix)) { + val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('.', '/')) + } + } + val slashPat = pat.replace('.', '/') + if (slashPat.startsWith(slashPrefix)) { + val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") + if (sub.isEmpty()) { + result.add("") + } else { + result.add(sub) + result.add(sub.replace('/', '.')) + } + } + } + return result + } + + fun matchesSubpattern( + content: CharSequence, + offset: Int = 0, + subpattern: String, + ): Boolean { + val subLen = subpattern.length + if (offset + subLen > content.length) return false + for (i in 0 until subLen) { + if (content[offset + i] != subpattern[i]) return false + } + if (subLen == 0 || offset + subLen == content.length) return true + if (subpattern.endsWith('.') || subpattern.endsWith('/')) return true + val nextChar = content[offset + subLen] + return !nextChar.isLetterOrDigit() && nextChar != '_' + } + + fun isSourceContextValid( + pattern: String, + sourceContent: CharSequence, + matchStart: Int, + matchEnd: Int, + ): Boolean { + var prevIndex = matchStart - 1 + while (prevIndex >= 0 && sourceContent[prevIndex].isWhitespace()) { + prevIndex-- + } + + if (prevIndex >= 0) { + val prevChar = sourceContent[prevIndex] + if (prevChar == '.') { + val beforeDot = if (prevIndex > 0) sourceContent[prevIndex - 1] else null + // A dot is only a package separator if it's not a Kotlin range '..' or varargs/spread + // '...' + if (beforeDot != '.') { + return false + } + } + if (prevChar == '/') { + val beforeSlash = if (prevIndex > 0) sourceContent[prevIndex - 1] else null + // Only reject if pattern does not contain '.' and the slash is an actual path delimiter + // (not closing a block comment '*/' or a single-line comment '//') + if (!pattern.contains('.') && beforeSlash != '*' && beforeSlash != '/') { + return false + } + } + } + + // In all JVM languages, qualified names containing '.' or '/' cannot be local identifiers. + if (pattern.contains('.') || pattern.contains('/')) { + return true + } + + // For unqualified single-word patterns (e.g. "io", "foo"), check if followed by '.' or '/' + var nextIndex = matchEnd + while (nextIndex < sourceContent.length && sourceContent[nextIndex].isWhitespace()) { + nextIndex++ + } + if (nextIndex < sourceContent.length) { + val nextChar = sourceContent[nextIndex] + if (nextChar == '.' || nextChar == '/') { + return true + } + } + + // Check if preceded by 'package', 'import', or '{@link' + if (prevIndex >= 0) { + var tokenStart = prevIndex + while (tokenStart > 0 && sourceContent[tokenStart - 1].isJavaIdentifierPart()) { + tokenStart-- + } + val prevToken = sourceContent.subSequence(tokenStart, prevIndex + 1).toString() + if (prevToken == "package" || prevToken == "import") { + return true + } + val lookbackStart = (matchStart - 32).coerceAtLeast(0) + val lookbackSnippet = sourceContent.substring(lookbackStart, matchStart) + if (lookbackSnippet.contains("{@link")) { + return true + } + } + + return false + } } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt index 7b7eeb4133..94f837506c 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -78,7 +78,7 @@ class SourceRemapperTest { } @Test - fun chainedRelocatorsDoNotCascade() { + fun appliesMultipleRelocatorsSequentially() { val r1 = SimpleRelocator("a.foo", "b.foo") val r2 = SimpleRelocator("b.foo", "c.foo") val relocators = listOf(r1, r2) @@ -96,10 +96,10 @@ class SourceRemapperTest { val expected = """ - |package b.foo; + |package c.foo; |import c.foo.Bar; |public class Main { - | b.foo.Baz baz; + | c.foo.Baz baz; | c.foo.Bar bar; |} """ From c40c0b0e4a7c2610e5761c15f37d424aced79771 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 21 Sep 2026 14:57:10 +0800 Subject: [PATCH 090/117] Simplify SimpleRelocator --- .../shadow/relocation/SimpleRelocator.kt | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 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 9f97bbd531..3f43f5c486 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 @@ -1,7 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow.relocation import java.util.Objects -import java.util.regex.Pattern import org.codehaus.plexus.util.SelectorUtils import org.gradle.api.tasks.Input @@ -28,8 +27,6 @@ constructor( @get:Input internal val pathPattern: String @get:Input internal val shadedPattern: String @get:Input internal val shadedPathPattern: String - private val sourcePackageExcludes = mutableSetOf() - private val sourcePathExcludes = mutableSetOf() @get:Input public val includes: MutableSet = mutableSetOf() @get:Input public val excludes: MutableSet = mutableSetOf() @@ -70,16 +67,14 @@ constructor( public open fun include(pattern: String) { includes.addAll(normalizePatterns(listOf(pattern))) - includes.add(pattern) } public open fun exclude(pattern: String) { excludes.addAll(normalizePatterns(listOf(pattern))) - excludes.add(pattern) } override fun canRelocatePath(path: String): Boolean { - if (rawString) return Pattern.compile(pathPattern).matcher(path).find() + if (rawString) return pathPattern.toPattern().matcher(path).find() // If string is too short - no need to perform expensive string operations. if (path.length < pathPattern.length) return false var adjustedPath = path.removeSuffix(".class") @@ -113,23 +108,25 @@ constructor( override fun applyToSourceContent(sourceContent: String): String { if (rawString || pattern.isEmpty()) return sourceContent - val sourceIncludes = getSourceSubpatterns(includes, pattern) - val sourceExcludes = getSourceSubpatterns(excludes, pattern) + val sourceIncludes = extractSourceSubpatterns(includes, pattern) + val sourceExcludes = extractSourceSubpatterns(excludes, pattern) + // Relocate package and class references in dot notation (e.g. "org.foo.Bar"). val content = - shadeSource( + relocateSourcePattern( sourceContent = sourceContent, patternFrom = pattern, patternTo = shadedPattern, - includedPatterns = sourceIncludes, hasIncludes = includes.isNotEmpty(), + includedPatterns = sourceIncludes, excludedPatterns = sourceExcludes, ) - return shadeSource( + // Relocate resource and classpath references in slash notation (e.g. "org/foo/Bar"). + return relocateSourcePattern( sourceContent = content, patternFrom = pathPattern, patternTo = shadedPathPattern, - includedPatterns = sourceIncludes, hasIncludes = includes.isNotEmpty(), + includedPatterns = sourceIncludes, excludedPatterns = sourceExcludes, ) } @@ -212,12 +209,12 @@ constructor( } } - fun shadeSource( + fun relocateSourcePattern( sourceContent: String, patternFrom: String, patternTo: String, - includedPatterns: Set, hasIncludes: Boolean, + includedPatterns: Set, excludedPatterns: Set, ): String { if (hasIncludes && includedPatterns.isEmpty()) { @@ -241,7 +238,7 @@ constructor( val isIncluded = !hasIncludes || includedPatterns.any { matchesSubpattern(sourceContent, matchEnd, it) } val isExcluded = excludedPatterns.any { matchesSubpattern(sourceContent, matchEnd, it) } - val contextValid = isSourceContextValid(patternFrom, sourceContent, matchStart, matchEnd) + val contextValid = isValidSourceContext(patternFrom, sourceContent, matchStart, matchEnd) if (isIncluded && !isExcluded && contextValid) { result.append(patternTo) @@ -254,7 +251,7 @@ constructor( return result.toString() } - fun getSourceSubpatterns(patterns: Set, patternPrefix: String): Set { + fun extractSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') @@ -302,7 +299,7 @@ constructor( return !nextChar.isLetterOrDigit() && nextChar != '_' } - fun isSourceContextValid( + fun isValidSourceContext( pattern: String, sourceContent: CharSequence, matchStart: Int, From 126c0fb6edb455505b21dbdc4933da38ddce9433 Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 21 Sep 2026 15:40:58 +0800 Subject: [PATCH 091/117] Clean up ShadowPropertiesTest --- .../plugins/shadow/ShadowPropertiesTest.kt | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) 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 2230eb8184..c02f5a82f5 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 @@ -37,7 +37,6 @@ import org.gradle.api.plugins.JavaPlugin.COMPILE_ONLY_API_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.COMPILE_ONLY_CONFIGURATION_NAME import org.gradle.api.tasks.TaskContainer import org.gradle.api.tasks.TaskProvider -import org.gradle.api.tasks.bundling.AbstractArchiveTask import org.gradle.api.tasks.bundling.Jar import org.gradle.language.base.plugins.LifecycleBasePlugin import org.gradle.language.base.plugins.LifecycleBasePlugin.ASSEMBLE_TASK_NAME @@ -191,26 +190,27 @@ class ShadowPropertiesTest { .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) val shadowSourcesElements = configurations.shadowSourcesElements.get() - val artifact = shadowSourcesElements.artifacts.single() - assertThat(artifact.classifier).isEqualTo("all-sources") - assertThat(artifact.name).isEqualTo("my-project") - assertThat(artifact.extension).isEqualTo("jar") - assertThat(artifact.type).isEqualTo("jar") - assertThat(artifact.file).isEqualTo(shadowJarTask.archiveSourcesFile.get().asFile) - assertThat(artifact.date).isNull() - assertThat(artifact.buildDependencies.getDependencies(null)).containsOnly(shadowJarTask) - - // Test dynamic updates on ShadowSourcesPublishArtifact - shadowJarTask.archiveClassifier.set("custom") - assertThat(artifact.classifier).isEqualTo("custom-sources") - - shadowJarTask.archiveClassifier.set("") - assertThat(artifact.classifier).isEqualTo("sources") - - shadowJarTask.archiveBaseName.set("renamed") - shadowJarTask.archiveExtension.set("zip") - assertThat(artifact.name).isEqualTo("renamed") - assertThat(artifact.extension).isEqualTo("zip") + with(shadowSourcesElements.artifacts.single()) { + assertThat(classifier).isEqualTo("all-sources") + assertThat(name).isEqualTo("my-project") + assertThat(extension).isEqualTo("jar") + assertThat(type).isEqualTo("jar") + assertThat(file).isEqualTo(shadowJarTask.archiveSourcesFile.get().asFile) + assertThat(date).isNull() + assertThat(buildDependencies.getDependencies(null)).containsOnly(shadowJarTask) + + // Test dynamic updates on ShadowSourcesPublishArtifact + shadowJarTask.archiveClassifier.set("custom") + assertThat(classifier).isEqualTo("custom-sources") + + shadowJarTask.archiveClassifier.set("") + assertThat(classifier).isEqualTo("sources") + + shadowJarTask.archiveBaseName.set("renamed") + shadowJarTask.archiveExtension.set("zip") + assertThat(name).isEqualTo("renamed") + assertThat(extension).isEqualTo("zip") + } } @Test @@ -290,7 +290,7 @@ class ShadowPropertiesTest { } listOf(shadowDistZip, shadowDistTar).forEach { - with(it as AbstractArchiveTask) { + with(it) { assertThat(description).isEqualTo("Bundles the project as a distribution.") assertThat(group).isEqualTo("distribution") assertThat(archiveAppendix.orNull).isNull() From 1f7f500555e71a6576872e8c4a5072e38ae5bacb Mon Sep 17 00:00:00 2001 From: Goooler Date: Mon, 21 Sep 2026 16:18:32 +0800 Subject: [PATCH 092/117] Clean up and parameterize source relocation tests --- .../shadow/internal/SourceRemapperTest.kt | 417 +++++++----------- .../shadow/relocation/SimpleRelocatorTest.kt | 232 +++++++--- 2 files changed, 337 insertions(+), 312 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt index 94f837506c..4f7ee99fb9 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -7,104 +7,20 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.RelocatePathContext import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.SimpleRelocator import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource class SourceRemapperTest { - @Test - fun appliesCustomRelocatorAlongsideSimpleRelocator() { - val customRelocator = - object : Relocator { - override fun canRelocatePath(path: String) = false - - override fun relocatePath(context: RelocatePathContext) = context.path - - override fun canRelocateClass(className: String) = false - - override fun relocateClass(context: RelocateClassContext) = context.className - - override fun applyToSourceContent(sourceContent: String) = - sourceContent.replace("CUSTOM_NAME", "RELOCATED_NAME") - } - val relocators = listOf(SimpleRelocator("com.example", "shaded.example"), customRelocator) - val input = - """ - |package com.example; - |class Main { - | String value = CUSTOM_NAME; - |} - """ - .trimMargin() - - val expected = - """ - |package shaded.example; - |class Main { - | String value = RELOCATED_NAME; - |} - """ - .trimMargin() - - assertThat(relocators.remapSource(input)).isEqualTo(expected) - } - - @Test - fun relocatesQualifiedNamesInExpressionsAndTypeAnnotations() { - val relocators = listOf(SimpleRelocator("com.example", "shaded.example")) - val input = - """ - |class Main { - | void method() { - | return com.example.Factory.create(); - | } - | com.example.Type value = new com.example.Type(); - | val typed: com.example.Type = com.example.Factory.create() - |} - """ - .trimMargin() - - val expected = - """ - |class Main { - | void method() { - | return shaded.example.Factory.create(); - | } - | shaded.example.Type value = new shaded.example.Type(); - | val typed: shaded.example.Type = shaded.example.Factory.create() - |} - """ - .trimMargin() - - assertThat(relocators.remapSource(input)).isEqualTo(expected) - } - - @Test - fun appliesMultipleRelocatorsSequentially() { - val r1 = SimpleRelocator("a.foo", "b.foo") - val r2 = SimpleRelocator("b.foo", "c.foo") - val relocators = listOf(r1, r2) - - val input = - """ - |package a.foo; - |import b.foo.Bar; - |public class Main { - | a.foo.Baz baz; - | b.foo.Bar bar; - |} - """ - .trimMargin() - - val expected = - """ - |package c.foo; - |import c.foo.Bar; - |public class Main { - | c.foo.Baz baz; - | c.foo.Bar bar; - |} - """ - .trimMargin() - + @ParameterizedTest(name = "{0}") + @MethodSource("remapSourceProvider") + fun remapSource( + name: String, + relocators: List, + input: String, + expected: String, + ) { assertThat(relocators.remapSource(input)).isEqualTo(expected) } @@ -143,52 +59,14 @@ class SourceRemapperTest { assertThat(relocators.relocateSourcePath("pkg/B.java")).isEqualTo("pkg/B.java") } - @Test - fun remapSourceWithIncludesAndExcludes() { - val relocator = - SimpleRelocator( - "com.example", - "shaded.example", - includes = listOf("com.example.used.*"), - excludes = listOf("com.example.used.Excluded"), - ) - val relocators = listOf(relocator) - - val input = - """ - |package com.example.used; - |import com.example.used.Foo; - |import com.example.used.Excluded; - |import com.example.unused.Bar; - """ - .trimMargin() - - val expected = - """ - |package shaded.example.used; - |import shaded.example.used.Foo; - |import com.example.used.Excluded; - |import com.example.unused.Bar; - """ - .trimMargin() - - assertThat(relocators.remapSource(input)).isEqualTo(expected) - } - @Test fun relocateSourcePathWithClassOnlyRelocator() { val classOnlyRelocator = - object : Relocator { - override fun canRelocatePath(path: String) = false - - override fun relocatePath(context: RelocatePathContext) = context.path - + object : Relocator by DefaultRelocator { override fun canRelocateClass(className: String) = className.startsWith("custom.pkg.") override fun relocateClass(context: RelocateClassContext) = context.className.replaceFirst("custom.pkg.", "shaded.pkg.") - - override fun applyToSourceContent(sourceContent: String) = sourceContent } val relocators = listOf(classOnlyRelocator) @@ -201,130 +79,173 @@ class SourceRemapperTest { } @Test - fun remapSourceWithMultiLanguageConstructs() { - val relocators = listOf(SimpleRelocator("com.example", "shaded.example")) - val input = - """ - |// Kotlin constructs - |val delegate by com.example.Delegate() - |val isType = obj is com.example.Type - |val asType = obj as com.example.Type - |val range = 0..com.example.Constants.MAX - |fun compute(factory: () -> com.example.Type = { com.example.Factory.create() }) - | - |// Groovy constructs - |def dynamicVar = com.example.Factory.create() - |def coerced = obj as com.example.Type - | - |// Scala constructs - |case _: com.example.Type => true - |class MyService with com.example.Trait - | - |// Java expressions and operators - |boolean flag = condition ? com.example.Factory.create() : null; - |int divided = total / com.example.Constants.SCALE; - |int bitwise = flags & com.example.Constants.MASK; - """ - .trimMargin() + fun relocateSourcePathWithNonSourceFileDelegatesToRelocatePath() { + val relocators = listOf(SimpleRelocator("pkg", "hidden.pkg")) + + // Resource files and class files are handled via relocatePath + assertThat(relocators.relocateSourcePath("pkg/config.properties")) + .isEqualTo("hidden/pkg/config.properties") + assertThat(relocators.relocateSourcePath("pkg/A.class")).isEqualTo("hidden/pkg/A.class") + assertThat(relocators.relocateSourcePath("other/file.txt")).isEqualTo("other/file.txt") + } - val expected = - """ - |// Kotlin constructs - |val delegate by shaded.example.Delegate() - |val isType = obj is shaded.example.Type - |val asType = obj as shaded.example.Type - |val range = 0..shaded.example.Constants.MAX - |fun compute(factory: () -> shaded.example.Type = { shaded.example.Factory.create() }) - | - |// Groovy constructs - |def dynamicVar = shaded.example.Factory.create() - |def coerced = obj as shaded.example.Type - | - |// Scala constructs - |case _: shaded.example.Type => true - |class MyService with shaded.example.Trait - | - |// Java expressions and operators - |boolean flag = condition ? shaded.example.Factory.create() : null; - |int divided = total / shaded.example.Constants.SCALE; - |int bitwise = flags & shaded.example.Constants.MASK; - """ - .trimMargin() + @Test + fun relocateSourcePathWithMultipleRelocators() { + val r1 = SimpleRelocator("pkg.one", "shaded.one") + val r2 = SimpleRelocator("pkg.two", "shaded.two") + val relocators = listOf(r1, r2) - assertThat(relocators.remapSource(input)).isEqualTo(expected) + assertThat(relocators.relocateSourcePath("pkg/one/Foo.groovy")) + .isEqualTo("shaded/one/Foo.groovy") + assertThat(relocators.relocateSourcePath("pkg/two/Bar.scala")).isEqualTo("shaded/two/Bar.scala") + assertThat(relocators.relocateSourcePath("pkg/three/Baz.java")).isEqualTo("pkg/three/Baz.java") } @Test - fun remapSourceProtectsPrefixCollisionsAndVariables() { - val relocators = + fun relocateSourcePathWithEmptyRelocators() { + assertThat(emptyList().relocateSourcePath("com/example/Foo.java")) + .isEqualTo("com/example/Foo.java") + } + + private companion object { + @JvmStatic + fun remapSourceProvider(): List = listOf( - SimpleRelocator("com.example", "shaded.example"), - SimpleRelocator("io", "shaded.io"), + Arguments.of( + "custom relocator alongside SimpleRelocator", + listOf( + SimpleRelocator("com.example", "shaded.example"), + object : Relocator by DefaultRelocator { + override fun applyToSourceContent(sourceContent: String) = + sourceContent.replace("CUSTOM_NAME", "RELOCATED_NAME") + }, + ), + """ + |package com.example; + |class Main { + | String value = CUSTOM_NAME; + |} + """ + .trimMargin(), + """ + |package shaded.example; + |class Main { + | String value = RELOCATED_NAME; + |} + """ + .trimMargin(), + ), + Arguments.of( + "multiple relocators sequentially", + listOf( + SimpleRelocator("a.foo", "b.foo"), + SimpleRelocator("b.foo", "c.foo"), + ), + """ + |package a.foo; + |import b.foo.Bar; + |public class Main { + | a.foo.Baz baz; + | b.foo.Bar bar; + |} + """ + .trimMargin(), + """ + |package c.foo; + |import c.foo.Bar; + |public class Main { + | c.foo.Baz baz; + | c.foo.Bar bar; + |} + """ + .trimMargin(), + ), + Arguments.of( + "prefix collisions and variables protection", + listOf( + SimpleRelocator("com.example", "shaded.example"), + SimpleRelocator("io", "shaded.io"), + ), + """ + |package io; + |import io.netty.channel.Channel; + |import other.com.example.Foo; + |import java.io.IOException; + | + |class Main { + | String io, val; + | String path = "dir/com/example/File.txt"; + | String relocatedPath = "com/example/File.txt"; + | /* comment */ com.example.Type t; + | // line comment com.example.Type t2; + |} + """ + .trimMargin(), + """ + |package shaded.io; + |import shaded.io.netty.channel.Channel; + |import other.com.example.Foo; + |import java.io.IOException; + | + |class Main { + | String io, val; + | String path = "dir/com/example/File.txt"; + | String relocatedPath = "shaded/example/File.txt"; + | /* comment */ shaded.example.Type t; + | // line comment shaded.example.Type t2; + |} + """ + .trimMargin(), + ), + Arguments.of( + "content start offset 0 and single-word Javadoc link", + listOf( + SimpleRelocator("com.example", "shaded.example"), + SimpleRelocator("io", "shaded.io"), + ), + """ + |com.example.Factory.create() + |io.netty.channel.Channel.open() + |/** + | * See {@link io} or {@link com.example.Type} + | */ + """ + .trimMargin(), + """ + |shaded.example.Factory.create() + |shaded.io.netty.channel.Channel.open() + |/** + | * See {@link shaded.io} or {@link shaded.example.Type} + | */ + """ + .trimMargin(), + ), + Arguments.of( + "empty relocators", + emptyList(), + """ + |package com.example; + |class Foo {} + """ + .trimMargin(), + """ + |package com.example; + |class Foo {} + """ + .trimMargin(), + ), ) - val input = - """ - |package io; - |import io.netty.channel.Channel; - |import other.com.example.Foo; - |import java.io.IOException; - | - |class Main { - | String io, val; - | String path = "dir/com/example/File.txt"; - | String relocatedPath = "com/example/File.txt"; - | /* comment */ com.example.Type t; - | // line comment com.example.Type t2; - |} - """ - .trimMargin() + } +} - val expected = - """ - |package shaded.io; - |import shaded.io.netty.channel.Channel; - |import other.com.example.Foo; - |import java.io.IOException; - | - |class Main { - | String io, val; - | String path = "dir/com/example/File.txt"; - | String relocatedPath = "shaded/example/File.txt"; - | /* comment */ shaded.example.Type t; - | // line comment shaded.example.Type t2; - |} - """ - .trimMargin() +private object DefaultRelocator : Relocator { + override fun canRelocatePath(path: String) = false - assertThat(relocators.remapSource(input)).isEqualTo(expected) - } + override fun relocatePath(context: RelocatePathContext) = context.path - @Test - fun remapSourceAtContentStartAndInJavadocLink() { - val relocators = - listOf( - SimpleRelocator("com.example", "shaded.example"), - SimpleRelocator("io", "shaded.io"), - ) - val input = - """ - |com.example.Factory.create() - |io.netty.channel.Channel.open() - |/** - | * See {@link io} or {@link com.example.Type} - | */ - """ - .trimMargin() + override fun canRelocateClass(className: String) = false - val expected = - """ - |shaded.example.Factory.create() - |shaded.io.netty.channel.Channel.open() - |/** - | * See {@link shaded.io} or {@link shaded.example.Type} - | */ - """ - .trimMargin() + override fun relocateClass(context: RelocateClassContext) = context.className - assertThat(relocators.remapSource(input)).isEqualTo(expected) - } + override fun applyToSourceContent(sourceContent: String): String = sourceContent } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index 030cfdb26b..c0bc5e0758 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -5,6 +5,9 @@ import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isTrue import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.MethodSource /** * Modified from @@ -357,41 +360,6 @@ class SimpleRelocatorTest { assertThat(relocator.applyToSourceContent(sourceFile)).isEqualTo(sourceFile) } - @Test - fun relocateSourceFileWithPrefixCollision() { - val relocator = - SimpleRelocator( - "org.example", - "relocated.org.example", - includes = listOf("org.example.In"), - ) - val source = - """ - |import org.example.In; - |import org.example.Input; - |import org.example.In.Nested; - | - |public class Test { - | org.example.In a; - | org.example.Input b; - |} - """ - .trimMargin() - val expected = - """ - |import relocated.org.example.In; - |import org.example.Input; - |import relocated.org.example.In.Nested; - | - |public class Test { - | relocated.org.example.In a; - | org.example.Input b; - |} - """ - .trimMargin() - assertThat(relocator.applyToSourceContent(source)).isEqualTo(expected) - } - @Test fun relocateSourceWithExcludes() { // Main relocator with excludes @@ -422,35 +390,6 @@ class SimpleRelocatorTest { .isEqualTo(relocatedFile) } - @Test - fun relocateSourceWithIncludes() { - val relocator = - SimpleRelocator( - "org.apache.maven", - "com.acme.maven", - includes = listOf("org.apache.maven.hello.*", "org.apache.maven.In"), - ) - val input = - """ - |package org.apache.maven.hello; - |import org.apache.maven.hello.World; - |import org.apache.maven.other.Other; - |import org.apache.maven.In; - |import org.apache.maven.NotIn; - """ - .trimMargin() - val expected = - """ - |package com.acme.maven.hello; - |import com.acme.maven.hello.World; - |import org.apache.maven.other.Other; - |import com.acme.maven.In; - |import org.apache.maven.NotIn; - """ - .trimMargin() - assertThat(relocator.applyToSourceContent(input)).isEqualTo(expected) - } - @Test fun relocateSourceWithDslExcludeAndInclude() { val relocatorExclude = SimpleRelocator("org.apache.maven", "com.acme.maven") @@ -486,7 +425,172 @@ class SimpleRelocatorTest { assertThat(relocatorInclude.applyToSourceContent(inputInclude)).isEqualTo(expectedInclude) } + @ParameterizedTest(name = "{0}") + @MethodSource("sourceRelocationProvider") + fun relocateSourceContent( + name: String, + relocator: SimpleRelocator, + input: String, + expected: String, + ) { + assertThat(relocator.applyToSourceContent(input)).isEqualTo(expected) + } + private companion object { + @JvmStatic + fun sourceRelocationProvider(): List = + listOf( + Arguments.of( + "prefix collision with included class", + SimpleRelocator( + "org.example", + "relocated.org.example", + includes = listOf("org.example.In"), + ), + """ + |import org.example.In; + |import org.example.Input; + |import org.example.In.Nested; + | + |public class Test { + | org.example.In a; + | org.example.Input b; + |} + """ + .trimMargin(), + """ + |import relocated.org.example.In; + |import org.example.Input; + |import relocated.org.example.In.Nested; + | + |public class Test { + | relocated.org.example.In a; + | org.example.Input b; + |} + """ + .trimMargin(), + ), + Arguments.of( + "includes only", + SimpleRelocator( + "org.apache.maven", + "com.acme.maven", + includes = listOf("org.apache.maven.hello.*", "org.apache.maven.In"), + ), + """ + |package org.apache.maven.hello; + |import org.apache.maven.hello.World; + |import org.apache.maven.other.Other; + |import org.apache.maven.In; + |import org.apache.maven.NotIn; + """ + .trimMargin(), + """ + |package com.acme.maven.hello; + |import com.acme.maven.hello.World; + |import org.apache.maven.other.Other; + |import com.acme.maven.In; + |import org.apache.maven.NotIn; + """ + .trimMargin(), + ), + Arguments.of( + "qualified names in expressions and type annotations", + SimpleRelocator("com.example", "shaded.example"), + """ + |class Main { + | void method() { + | return com.example.Factory.create(); + | } + | com.example.Type value = new com.example.Type(); + | val typed: com.example.Type = com.example.Factory.create() + |} + """ + .trimMargin(), + """ + |class Main { + | void method() { + | return shaded.example.Factory.create(); + | } + | shaded.example.Type value = new shaded.example.Type(); + | val typed: shaded.example.Type = shaded.example.Factory.create() + |} + """ + .trimMargin(), + ), + Arguments.of( + "includes and excludes combined", + SimpleRelocator( + "com.example", + "shaded.example", + includes = listOf("com.example.used.*"), + excludes = listOf("com.example.used.Excluded"), + ), + """ + |package com.example.used; + |import com.example.used.Foo; + |import com.example.used.Excluded; + |import com.example.unused.Bar; + """ + .trimMargin(), + """ + |package shaded.example.used; + |import shaded.example.used.Foo; + |import com.example.used.Excluded; + |import com.example.unused.Bar; + """ + .trimMargin(), + ), + Arguments.of( + "multi-language constructs in Kotlin, Groovy, Scala, and Java", + SimpleRelocator("com.example", "shaded.example"), + """ + |// Kotlin constructs + |val delegate by com.example.Delegate() + |val isType = obj is com.example.Type + |val asType = obj as com.example.Type + |val range = 0..com.example.Constants.MAX + |fun compute(factory: () -> com.example.Type = { com.example.Factory.create() }) + | + |// Groovy constructs + |def dynamicVar = com.example.Factory.create() + |def coerced = obj as com.example.Type + | + |// Scala constructs + |case _: com.example.Type => true + |class MyService with com.example.Trait + | + |// Java expressions and operators + |boolean flag = condition ? com.example.Factory.create() : null; + |int divided = total / com.example.Constants.SCALE; + |int bitwise = flags & com.example.Constants.MASK; + """ + .trimMargin(), + """ + |// Kotlin constructs + |val delegate by shaded.example.Delegate() + |val isType = obj is shaded.example.Type + |val asType = obj as shaded.example.Type + |val range = 0..shaded.example.Constants.MAX + |fun compute(factory: () -> shaded.example.Type = { shaded.example.Factory.create() }) + | + |// Groovy constructs + |def dynamicVar = shaded.example.Factory.create() + |def coerced = obj as shaded.example.Type + | + |// Scala constructs + |case _: shaded.example.Type => true + |class MyService with shaded.example.Trait + | + |// Java expressions and operators + |boolean flag = condition ? shaded.example.Factory.create() : null; + |int divided = total / shaded.example.Constants.SCALE; + |int bitwise = flags & shaded.example.Constants.MASK; + """ + .trimMargin(), + ), + ) + val sourceFile = """ |package org.apache.maven.hello; From 1010dd7cef6296c2b6c23c32376ab0e9bf718403 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 10:05:52 +0800 Subject: [PATCH 093/117] Place shadow jar block after publishing block to cover more lazy cases --- .../gradle/plugins/shadow/PublishingTest.kt | 42 +------------------ 1 file changed, 2 insertions(+), 40 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 0678f9b409..a44491a263 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 @@ -402,45 +402,6 @@ class PublishingTest : BasePluginTest() { .isEqualTo("maven-1.0-shaded-sources.jar") } - @Test - fun publishWithSourcesJarAndCustomClassifierAfterPublishingBlock() { - projectScript.appendText( - """ - |apply plugin: 'maven-publish' - |java { - | withSourcesJar() - |} - |publishing { - | repositories { - | maven { url = '${remoteRepoPath.toUri()}' } - | } - | publications { - | shadow(MavenPublication) { - | from components.shadow - | } - | } - |} - |tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - | archiveClassifier = 'shaded' - | archiveSourcesFile = layout.buildDirectory.file('custom.jar') - |} - """ - .trimMargin() - ) - - publish() - - val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) - .containsOnly( - "maven-1.0-shaded.jar", - "maven-1.0-shaded-sources.jar", - ) - val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) - assertThat(gmm.shadowSourcesElementsVariant.fileNames.single()) - .isEqualTo("maven-1.0-shaded-sources.jar") - } - @Test fun publishJavaComponentWithShadowAndSourcesVariants() { projectScript.appendText( @@ -1064,10 +1025,11 @@ class PublishingTest : BasePluginTest() { |dependencies { | $dependenciesBlock |} + |${publishingBlock(projectBlock = projectBlock, publicationsBlock = publicationsBlock)} + |// Place shadow jar block after publishing block to cover more lazy cases. |$shadowJarTask { | $shadowBlock |} - |${publishingBlock(projectBlock = projectBlock, publicationsBlock = publicationsBlock)} | """ .trimMargin() From 096fbb85e0284191a577b8b0a9583976ac46bee2 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 10:12:21 +0800 Subject: [PATCH 094/117] Clean up PublishingTest --- .../gradle/plugins/shadow/BasePluginTest.kt | 11 + .../plugins/shadow/KotlinPluginsTest.kt | 10 - .../gradle/plugins/shadow/PublishingTest.kt | 441 ++++++++---------- 3 files changed, 210 insertions(+), 252 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 59bf7036c0..25803693f8 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 @@ -160,6 +160,16 @@ abstract class BasePluginTest { .trimMargin() } + fun compileOnlyStdlib(exclude: Boolean): String { + return if (exclude) { + // Disable the stdlib dependency added via `implementation`. + path("gradle.properties").writeText("kotlin.stdlib.default.dependency=false") + "compileOnly 'org.jetbrains.kotlin:kotlin-stdlib'" + } else { + "" + } + } + fun jarPath(relative: String, parent: Path = projectRoot): JarPath { return JarPath(parent.resolve(relative)) } @@ -421,6 +431,7 @@ abstract class BasePluginTest { "tasks.named('$SHADOW_JAR_TASK_NAME', ${ShadowJar::class.java.name})" const val runShadowTask = "tasks.named('$SHADOW_RUN_TASK_NAME', JavaExec)" const val jarTask = "tasks.named('jar', Jar)" + const val sourcesJarTask = "tasks.named('sourcesJar', Jar)" const val infoArgument = "--info" 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 276627d69c..0872184c76 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 @@ -410,14 +410,4 @@ class KotlinPluginsTest : BasePluginTest() { assertThat(projectRoot.resolve("build/libs/my-1.0-all-sources.jar")).doesNotExist() } - - private fun compileOnlyStdlib(exclude: Boolean): String { - return if (exclude) { - // Disable the stdlib dependency added via `implementation`. - path("gradle.properties").writeText("kotlin.stdlib.default.dependency=false") - "compileOnly 'org.jetbrains.kotlin:kotlin-stdlib'" - } else { - "" - } - } } 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 a44491a263..bb02854070 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 @@ -75,17 +75,15 @@ class PublishingTest : BasePluginTest() { val assertions = { variantAttrs: Array> -> publish() - assertPomCommon(repoPath("my/maven-all/1.0/maven-all-1.0.pom")) - val gmm = gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")) - assertShadowVariantCommon(gmm, variantAttrs = variantAttrs) + assertPomCommon("my/maven-all/1.0/maven-all-1.0.pom") + assertShadowVariantCommon( + "my/maven-all/1.0/maven-all-1.0.module", + variantAttrs = variantAttrs, + ) } assertions(shadowVariantAttrs) - val attrsWithoutTargetJvm = - shadowVariantAttrs - .filterNot { (name, _) -> name == TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name } - .toTypedArray() val targetJvmAttr17 = TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name to "17" val targetJvmAttr11 = TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name to "11" val targetJvmAttr8 = TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name to "8" @@ -106,7 +104,7 @@ class PublishingTest : BasePluginTest() { |""" .trimMargin() ) - assertions(attrsWithoutTargetJvm + targetJvmAttr17) + assertions(shadowVariantAttrsWithoutTargetJvm + targetJvmAttr17) projectScript.appendText( """ @@ -116,7 +114,7 @@ class PublishingTest : BasePluginTest() { |""" .trimMargin() ) - assertions(attrsWithoutTargetJvm + targetJvmAttr11) + assertions(shadowVariantAttrsWithoutTargetJvm + targetJvmAttr11) projectScript.appendText( """ @@ -127,7 +125,7 @@ class PublishingTest : BasePluginTest() { .trimMargin() ) // sourceCompatibility doesn't affect the target JVM version. - assertions(attrsWithoutTargetJvm + targetJvmAttr11) + assertions(shadowVariantAttrsWithoutTargetJvm + targetJvmAttr11) projectScript.appendText( """ @@ -138,7 +136,7 @@ class PublishingTest : BasePluginTest() { .trimMargin() ) // options.release flag is honored. - assertions(attrsWithoutTargetJvm + targetJvmAttr8) + assertions(shadowVariantAttrsWithoutTargetJvm + targetJvmAttr8) } @Test // #1665 @@ -168,11 +166,8 @@ class PublishingTest : BasePluginTest() { "Cannot set the target JVM version to Int.MAX_VALUE when `java.autoTargetJvmDisabled` is enabled or in other cases." ) assertShadowVariantCommon( - gmm = gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")), - variantAttrs = - shadowVariantAttrs - .filterNot { (name, _) -> name == TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name } - .toTypedArray(), + "my/maven-all/1.0/maven-all-1.0.module", + variantAttrs = shadowVariantAttrsWithoutTargetJvm, ) } @@ -203,11 +198,8 @@ class PublishingTest : BasePluginTest() { "Skipping setting org.gradle.jvm.version attribute for shadowRuntimeElements configuration." ) assertShadowVariantCommon( - gmm = gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")), - variantAttrs = - shadowVariantAttrs - .filterNot { (name, _) -> name == TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name } - .toTypedArray(), + "my/maven-all/1.0/maven-all-1.0.module", + variantAttrs = shadowVariantAttrsWithoutTargetJvm, ) } @@ -234,7 +226,7 @@ class PublishingTest : BasePluginTest() { publish() assertShadowVariantCommon( - gmm = gmmAdapter.fromJson(repoPath("my/maven-all/1.0/maven-all-1.0.module")), + "my/maven-all/1.0/maven-all-1.0.module", variantAttrs = commonVariantAttrs + arrayOf( @@ -250,7 +242,7 @@ class PublishingTest : BasePluginTest() { publishConfiguration( projectBlock = """ - |tasks.named('jar', Jar) { + |$jarTask { | enabled = false |} """ @@ -275,26 +267,15 @@ class PublishingTest : BasePluginTest() { 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", + *withChecksums( + "maven-1.0.jar", + "maven-1.0.module", + "maven-1.0.pom", + ) ) - 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) + assertShadowJarCommon("$artifactRoot/maven-1.0.jar") + assertPomCommon("$artifactRoot/maven-1.0.pom") + assertShadowVariantCommon("$artifactRoot/maven-1.0.module") } @Test @@ -306,10 +287,10 @@ class PublishingTest : BasePluginTest() { |java { | withSourcesJar() |} - |tasks.named('jar', Jar) { + |$jarTask { | enabled = false |} - |tasks.named('sourcesJar', Jar) { + |$sourcesJarTask { | enabled = false |} """ @@ -334,32 +315,19 @@ class PublishingTest : BasePluginTest() { 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", - "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", + *withChecksums( + "maven-1.0.jar", + "maven-1.0.module", + "maven-1.0.pom", + "maven-1.0-sources.jar", + ) ) - 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) - assertShadowSourcesVariantCommon(gmm) + assertShadowJarCommon("$artifactRoot/maven-1.0.jar") + assertPomCommon("$artifactRoot/maven-1.0.pom") + repoGmm("$artifactRoot/maven-1.0.module").let { gmm -> + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) + } } @Test @@ -392,13 +360,18 @@ class PublishingTest : BasePluginTest() { publish() val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + assertThat(repoPath(artifactRoot).entries) .containsOnly( - "maven-1.0-shaded.jar", - "maven-1.0-shaded-sources.jar", + *withChecksums( + "maven-1.0-shaded.jar", + "maven-1.0-shaded-sources.jar", + "maven-1.0.module", + "maven-1.0.pom", + ) + ) + assertThat( + repoGmm("$artifactRoot/maven-1.0.module").shadowSourcesElementsVariant.fileNames.single() ) - val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) - assertThat(gmm.shadowSourcesElementsVariant.fileNames.single()) .isEqualTo("maven-1.0-shaded-sources.jar") } @@ -426,12 +399,16 @@ class PublishingTest : BasePluginTest() { publish() val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + assertThat(repoPath(artifactRoot).entries) .containsOnly( - "maven-1.0.jar", - "maven-1.0-sources.jar", - "maven-1.0-all.jar", - "maven-1.0-all-sources.jar", + *withChecksums( + "maven-1.0.jar", + "maven-1.0-sources.jar", + "maven-1.0-all.jar", + "maven-1.0-all-sources.jar", + "maven-1.0.module", + "maven-1.0.pom", + ) ) } @@ -444,7 +421,7 @@ class PublishingTest : BasePluginTest() { |java { | withSourcesJar() |} - |tasks.named('sourcesJar', Jar) { + |$sourcesJarTask { | enabled = false |} |components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { @@ -471,27 +448,32 @@ class PublishingTest : BasePluginTest() { publish() val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + assertThat(repoPath(artifactRoot).entries) .containsOnly( - "maven-1.0.jar", - "maven-1.0-sources.jar", + *withChecksums( + "maven-1.0.jar", + "maven-1.0-sources.jar", + "maven-1.0.module", + "maven-1.0.pom", + ) ) - assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) - assertThat(pomReader.read(repoPath("$artifactRoot/maven-1.0.pom"))).all { + assertShadowJarCommon("$artifactRoot/maven-1.0.jar") + assertThat(repoPom("$artifactRoot/maven-1.0.pom")).all { transform { it.dependencies.map(Dependency::coordinate) }.containsOnly("my:b:1.0") transform { it.dependencies.map { dep -> dep.scope to dep.isOptional } } .single() .isEqualTo("compile" to true) } - val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) - assertThat(gmm.variantNames) - .containsOnly( - "apiElements", - SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, - ) - assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) + repoGmm("$artifactRoot/maven-1.0.module").let { gmm -> + assertThat(gmm.variantNames) + .containsOnly( + API_ELEMENTS_CONFIGURATION_NAME, + SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, + SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, + ) + assertShadowVariantCommon(gmm) + assertShadowSourcesVariantCommon(gmm) + } } @Test @@ -503,7 +485,7 @@ class PublishingTest : BasePluginTest() { |java { | withSourcesJar() |} - |tasks.named('jar', Jar) { + |$jarTask { | enabled = false |} """ @@ -529,12 +511,20 @@ class PublishingTest : BasePluginTest() { assertThat(result.output) .contains("Skipping adding shadowSourcesElements variant to shadow component.") val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.contains("sources") }).isEmpty() - assertShadowJarCommon(repoJarPath("$artifactRoot/maven-1.0.jar")) - assertPomCommon(repoPath("$artifactRoot/maven-1.0.pom")) - val gmm = gmmAdapter.fromJson(repoPath("$artifactRoot/maven-1.0.module")) - assertShadowVariantCommon(gmm) - assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + assertThat(repoPath(artifactRoot).entries) + .containsOnly( + *withChecksums( + "maven-1.0.jar", + "maven-1.0.module", + "maven-1.0.pom", + ) + ) + assertShadowJarCommon("$artifactRoot/maven-1.0.jar") + assertPomCommon("$artifactRoot/maven-1.0.pom") + repoGmm("$artifactRoot/maven-1.0.module").let { gmm -> + assertShadowVariantCommon(gmm) + assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) + } } @Test @@ -603,18 +593,20 @@ class PublishingTest : BasePluginTest() { publish() val artifactRoot = "my/plugin/my-gradle-plugin/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + assertThat(repoPath(artifactRoot).entries) .containsOnly( - "my-gradle-plugin-1.0.jar", - "my-gradle-plugin-1.0-javadoc.jar", - "my-gradle-plugin-1.0-sources.jar", + *withChecksums( + "my-gradle-plugin-1.0.jar", + "my-gradle-plugin-1.0-javadoc.jar", + "my-gradle-plugin-1.0-sources.jar", + "my-gradle-plugin-1.0.module", + "my-gradle-plugin-1.0.pom", + ) ) - assertShadowJarCommon(repoJarPath("$artifactRoot/my-gradle-plugin-1.0.jar")) - assertPomCommon(repoPath("$artifactRoot/my-gradle-plugin-1.0.pom")) - assertShadowVariantCommon( - gmmAdapter.fromJson(repoPath("$artifactRoot/my-gradle-plugin-1.0.module")) - ) + assertShadowJarCommon("$artifactRoot/my-gradle-plugin-1.0.jar") + assertPomCommon("$artifactRoot/my-gradle-plugin-1.0.pom") + assertShadowVariantCommon("$artifactRoot/my-gradle-plugin-1.0.module") } @Test // #614, #860, #945 @@ -650,26 +642,16 @@ class PublishingTest : BasePluginTest() { val artifactRoot = "my-group/my-artifact/2.0" assertThat(repoPath(artifactRoot).entries) .containsOnly( - "my-artifact-2.0-my-classifier.my-ext.sha512", - "my-artifact-2.0-my-classifier.my-ext", - "my-artifact-2.0.pom.sha256", - "my-artifact-2.0.module", - "my-artifact-2.0.pom", - "my-artifact-2.0.module.sha256", - "my-artifact-2.0.module.sha1", - "my-artifact-2.0.module.md5", - "my-artifact-2.0.pom.sha512", - "my-artifact-2.0-my-classifier.my-ext.sha256", - "my-artifact-2.0.module.sha512", - "my-artifact-2.0-my-classifier.my-ext.sha1", - "my-artifact-2.0-my-classifier.my-ext.md5", - "my-artifact-2.0.pom.md5", - "my-artifact-2.0.pom.sha1", + *withChecksums( + "my-artifact-2.0-my-classifier.my-ext", + "my-artifact-2.0.module", + "my-artifact-2.0.pom", + ) ) - 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"))) + assertShadowJarCommon("$artifactRoot/my-artifact-2.0-my-classifier.my-ext") + assertPomCommon("$artifactRoot/my-artifact-2.0.pom") + assertShadowVariantCommon("$artifactRoot/my-artifact-2.0.module") } @Test @@ -701,46 +683,20 @@ class PublishingTest : BasePluginTest() { assertThat(repoPath("my/maven/1.0").entries) .containsOnly( - // Entries of maven-1.0.jar - "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", - // Entries of maven-1.0-all.jar - "maven-1.0-all.jar", - "maven-1.0-all.jar.md5", - "maven-1.0-all.jar.sha1", - "maven-1.0-all.jar.sha256", - "maven-1.0-all.jar.sha512", + *withChecksums( + "maven-1.0.jar", + "maven-1.0.module", + "maven-1.0.pom", + "maven-1.0-all.jar", + ) ) assertThat(repoPath("my/maven-all/1.0").entries) .containsOnly( - "maven-all-1.0-all.jar", - "maven-all-1.0.module", - "maven-all-1.0.pom", - "maven-all-1.0-all.jar.md5", - "maven-all-1.0.module.md5", - "maven-all-1.0.pom.md5", - "maven-all-1.0-all.jar.sha1", - "maven-all-1.0.module.sha1", - "maven-all-1.0.pom.sha1", - "maven-all-1.0-all.jar.sha256", - "maven-all-1.0.module.sha256", - "maven-all-1.0.pom.sha256", - "maven-all-1.0-all.jar.sha512", - "maven-all-1.0.module.sha512", - "maven-all-1.0.pom.sha512", + *withChecksums( + "maven-all-1.0-all.jar", + "maven-all-1.0.module", + "maven-all-1.0.pom", + ) ) assertThat(repoJarPath("my/maven/1.0/maven-1.0.jar")).useAll { @@ -750,8 +706,8 @@ class PublishingTest : BasePluginTest() { containsOnly(*entriesInAB, "META-INF/", "META-INF/MANIFEST.MF") } - 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 -> + assertPomCommon("my/maven/1.0/maven-1.0.pom", arrayOf("my:a:1.0", "my:b:1.0")) + repoGmm("my/maven/1.0/maven-1.0.module").let { gmm -> // apiElements, runtimeElements, shadowRuntimeElements assertThat(gmm.variantNames) .containsOnly( @@ -759,29 +715,12 @@ class PublishingTest : BasePluginTest() { RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, ) - assertThat(gmm.apiElementsVariant).all { - transform { it.attributes } - .containsOnly( - *commonVariantAttrs, - Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL, - Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_API, - ) - transform { it.coordinates }.isEmpty() - } - assertThat(gmm.runtimeElementsVariant).all { - transform { it.attributes } - .containsOnly( - *commonVariantAttrs, - Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL, - Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, - ) - transform { it.coordinates }.containsOnly("my:a:1.0", "my:b:1.0") - } + assertJavaVariantsCommon(gmm, arrayOf("my:a:1.0", "my:b:1.0")) assertShadowVariantCommon(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 -> + assertPomCommon("my/maven-all/1.0/maven-all-1.0.pom") + repoGmm("my/maven-all/1.0/maven-all-1.0.module").let { gmm -> assertThat(gmm.variantNames).containsOnly(SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME) assertShadowVariantCommon(gmm) } @@ -823,59 +762,23 @@ class PublishingTest : BasePluginTest() { "Skipping adding shadowRuntimeElements variant to java component." } ) - val assertVariantsCommon = { gmm: GradleModuleMetadata -> - assertThat(gmm.apiElementsVariant).all { - transform { it.attributes } - .containsOnly( - *commonVariantAttrs, - Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL, - Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_API, - ) - transform { it.coordinates }.isEmpty() - } - assertThat(gmm.runtimeElementsVariant).all { - transform { it.attributes } - .containsOnly( - *commonVariantAttrs, - Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL, - Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, - ) - transform { it.coordinates }.containsOnly("my:a:1.0") - } - } val entriesCommon = - arrayOf( + withChecksums( "maven-1.0.jar", - "maven-1.0.jar.md5", - "maven-1.0.jar.sha1", - "maven-1.0.jar.sha256", - "maven-1.0.jar.sha512", "maven-1.0.module", - "maven-1.0.module.md5", - "maven-1.0.module.sha1", - "maven-1.0.module.sha256", - "maven-1.0.module.sha512", "maven-1.0.pom", - "maven-1.0.pom.md5", - "maven-1.0.pom.sha1", - "maven-1.0.pom.sha256", - "maven-1.0.pom.sha512", ) val artifactEntries = repoPath("my/maven/1.0/").entries - val gmm = gmmAdapter.fromJson(repoPath("my/maven/1.0/maven-1.0.module")) + val gmm = repoGmm("my/maven/1.0/maven-1.0.module") val pomDependencies = - pomReader.read(repoPath("my/maven/1.0/maven-1.0.pom")).dependencies.map { + repoPom("my/maven/1.0/maven-1.0.pom").dependencies.map { it.coordinate to it.scope } if (addShadowVariant) { assertThat(artifactEntries) .containsOnly( - "maven-1.0-all.jar", - "maven-1.0-all.jar.md5", - "maven-1.0-all.jar.sha1", - "maven-1.0-all.jar.sha256", - "maven-1.0-all.jar.sha512", + *withChecksums("maven-1.0-all.jar"), *entriesCommon, ) assertThat(gmm.variantNames) @@ -884,21 +787,21 @@ class PublishingTest : BasePluginTest() { RUNTIME_ELEMENTS_CONFIGURATION_NAME, SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, ) - assertVariantsCommon(gmm) + assertJavaVariantsCommon(gmm) assertShadowVariantCommon(gmm) assertThat(pomDependencies).containsOnly("my:a:1.0" to "runtime", "my:b:1.0" to "compile") } else { assertThat(artifactEntries).containsOnly(*entriesCommon) assertThat(gmm.variantNames) .containsOnly(API_ELEMENTS_CONFIGURATION_NAME, RUNTIME_ELEMENTS_CONFIGURATION_NAME) - assertVariantsCommon(gmm) + assertJavaVariantsCommon(gmm) assertThat(pomDependencies).containsOnly("my:a:1.0" to "runtime") } } @Test fun publishKmpWithShadowedSources() { - path("gradle.properties").writeText("kotlin.stdlib.default.dependency=false") + val stdlib = compileOnlyStdlib(true) projectScript.writeText( """ |plugins { @@ -914,7 +817,7 @@ class PublishingTest : BasePluginTest() { | commonMain { | dependencies { | implementation 'my:g:1.0' - | compileOnly 'org.jetbrains.kotlin:kotlin-stdlib' + | $stdlib | } | } | jvmMain { @@ -951,10 +854,13 @@ class PublishingTest : BasePluginTest() { publish() val artifactRoot = "my/my-all/1.0" - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".jar") }) + assertThat(repoPath(artifactRoot).entries) .containsOnly( - "my-all-1.0.jar", - "my-all-1.0-sources.jar", + *withChecksums( + "my-all-1.0.jar", + "my-all-1.0-sources.jar", + "my-all-1.0.pom", + ) ) assertThat(repoJarPath("$artifactRoot/my-all-1.0.jar")).useAll { @@ -988,8 +894,7 @@ class PublishingTest : BasePluginTest() { ) } - assertPomCommon(repoPath("$artifactRoot/my-all-1.0.pom"), emptyArray()) - assertThat(repoPath(artifactRoot).entries.filter { it.endsWith(".module") }).isEmpty() + assertPomCommon("$artifactRoot/my-all-1.0.pom", emptyArray()) } private fun repoPath(relative: String): Path { @@ -1000,6 +905,14 @@ class PublishingTest : BasePluginTest() { return JarPath(remoteRepoPath.resolve(relative)) } + private fun repoGmm(relative: String): GradleModuleMetadata { + return gmmAdapter.fromJson(repoPath(relative)) + } + + private fun repoPom(relative: String): Model { + return pomReader.read(repoPath(relative)) + } + private fun publish(vararg arguments: String): BuildResult = runWithSuccess("build", "publish", *arguments) @@ -1051,8 +964,8 @@ class PublishingTest : BasePluginTest() { .trimMargin() } - private fun assertPomCommon(pomPath: Path, coordinates: Array = arrayOf("my:b:1.0")) { - assertThat(pomReader.read(pomPath)).all { + private fun assertPomCommon(relative: String, coordinates: Array = arrayOf("my:b:1.0")) { + assertThat(repoPom(relative)).all { transform { it.dependencies.map(Dependency::coordinate) }.containsOnly(*coordinates) if (coordinates.isNotEmpty()) { // All scopes should be runtime. @@ -1063,13 +976,33 @@ class PublishingTest : BasePluginTest() { } } - private fun assertShadowVariantCommon( + private fun assertJavaVariantsCommon( gmm: GradleModuleMetadata, + runtimeCoordinates: Array = arrayOf("my:a:1.0"), + ) { + assertThat(gmm.apiElementsVariant).all { + transform { it.attributes }.containsOnly(*apiVariantAttrs) + transform { it.coordinates }.isEmpty() + } + assertThat(gmm.runtimeElementsVariant).all { + transform { it.attributes }.containsOnly(*runtimeVariantAttrs) + transform { it.coordinates }.containsOnly(*runtimeCoordinates) + } + } + + private fun assertShadowVariantCommon( + gmm: Any, variantAttrs: Array> = shadowVariantAttrs, coordinates: Array = arrayOf("my:b:1.0"), body: Assert.() -> Unit = {}, ) { - assertThat(gmm.shadowRuntimeElementsVariant).all { + val realGmm = + when (gmm) { + is String -> repoGmm(gmm) + is GradleModuleMetadata -> gmm + else -> error("Unsupported type $gmm") + } + assertThat(realGmm.shadowRuntimeElementsVariant).all { transform { it.attributes }.containsOnly(*variantAttrs) transform { it.coordinates }.containsOnly(*coordinates) body() @@ -1087,8 +1020,8 @@ class PublishingTest : BasePluginTest() { } } - private fun assertShadowJarCommon(jarPath: JarPath) { - assertThat(jarPath).useAll { + private fun assertShadowJarCommon(relative: String) { + assertThat(repoJarPath(relative)).useAll { containsAtLeast(*entriesInA) containsNone(*entriesInB) getMainAttr(classPathAttributeKey).isEqualTo("b-1.0.jar") @@ -1110,6 +1043,20 @@ class PublishingTest : BasePluginTest() { TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name to JavaVersion.current().majorVersion, ) + val apiVariantAttrs = + commonVariantAttrs + + arrayOf( + Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL, + Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_API, + ) + + val runtimeVariantAttrs = + commonVariantAttrs + + arrayOf( + Bundling.BUNDLING_ATTRIBUTE.name to Bundling.EXTERNAL, + Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, + ) + val shadowVariantAttrs = commonVariantAttrs + arrayOf( @@ -1117,6 +1064,11 @@ class PublishingTest : BasePluginTest() { Usage.USAGE_ATTRIBUTE.name to Usage.JAVA_RUNTIME, ) + val shadowVariantAttrsWithoutTargetJvm = + shadowVariantAttrs + .filterNot { (name, _) -> name == TargetJvmVersion.TARGET_JVM_VERSION_ATTRIBUTE.name } + .toTypedArray() + val shadowSourcesVariantAttrs = arrayOf( Category.CATEGORY_ATTRIBUTE.name to Category.DOCUMENTATION, @@ -1131,5 +1083,10 @@ class PublishingTest : BasePluginTest() { val Path.entries: List get() = listDirectoryEntries().map { it.name } + + fun withChecksums(vararg baseNames: String): Array = + baseNames + .flatMap { listOf(it, "$it.md5", "$it.sha1", "$it.sha256", "$it.sha512") } + .toTypedArray() } } From 7bf983b2575b9a0260b6ff1e129d89b7fff33ec7 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 11:11:35 +0800 Subject: [PATCH 095/117] Clean up SimpleRelocator --- .../shadow/relocation/SimpleRelocator.kt | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 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 3f43f5c486..fa11c6e353 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 @@ -217,9 +217,7 @@ constructor( includedPatterns: Set, excludedPatterns: Set, ): String { - if (hasIncludes && includedPatterns.isEmpty()) { - return sourceContent - } + if (hasIncludes && includedPatterns.isEmpty()) return sourceContent val regex = Regex("\\b" + Regex.escape(patternFrom) + "\\b") val matches = regex.findAll(sourceContent).toList() @@ -316,24 +314,18 @@ constructor( val beforeDot = if (prevIndex > 0) sourceContent[prevIndex - 1] else null // A dot is only a package separator if it's not a Kotlin range '..' or varargs/spread // '...' - if (beforeDot != '.') { - return false - } + if (beforeDot != '.') return false } if (prevChar == '/') { val beforeSlash = if (prevIndex > 0) sourceContent[prevIndex - 1] else null // Only reject if pattern does not contain '.' and the slash is an actual path delimiter // (not closing a block comment '*/' or a single-line comment '//') - if (!pattern.contains('.') && beforeSlash != '*' && beforeSlash != '/') { - return false - } + if (!pattern.contains('.') && beforeSlash != '*' && beforeSlash != '/') return false } } // In all JVM languages, qualified names containing '.' or '/' cannot be local identifiers. - if (pattern.contains('.') || pattern.contains('/')) { - return true - } + if (pattern.contains('.') || pattern.contains('/')) return true // For unqualified single-word patterns (e.g. "io", "foo"), check if followed by '.' or '/' var nextIndex = matchEnd @@ -342,9 +334,7 @@ constructor( } if (nextIndex < sourceContent.length) { val nextChar = sourceContent[nextIndex] - if (nextChar == '.' || nextChar == '/') { - return true - } + if (nextChar == '.' || nextChar == '/') return true } // Check if preceded by 'package', 'import', or '{@link' @@ -354,14 +344,10 @@ constructor( tokenStart-- } val prevToken = sourceContent.subSequence(tokenStart, prevIndex + 1).toString() - if (prevToken == "package" || prevToken == "import") { - return true - } + if (prevToken == "package" || prevToken == "import") return true val lookbackStart = (matchStart - 32).coerceAtLeast(0) val lookbackSnippet = sourceContent.substring(lookbackStart, matchStart) - if (lookbackSnippet.contains("{@link")) { - return true - } + if (lookbackSnippet.contains("{@link")) return true } return false From 95cd81acb51a57e219a348cea8e335df1e4e301d Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 11:19:52 +0800 Subject: [PATCH 096/117] Simplify ShadowJar --- .../gradle/plugins/shadow/tasks/ShadowJar.kt | 60 +++++++++---------- .../plugins/shadow/ShadowPropertiesTest.kt | 5 +- 2 files changed, 31 insertions(+), 34 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 ba39aaa9d4..b03f2fbe05 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 @@ -255,28 +255,22 @@ public abstract class ShadowJar : Jar() { * Defaults to * `/--sources.`. */ - @get:Optional + @get:Optional // `archiveFileName` may not have a value when the Java plugin is not applied. @get:OutputFile public open val archiveSourcesFile: RegularFileProperty = objectFactory .fileProperty() .convention( - generateSourcesJar.flatMap { generate -> - if (generate) { - destinationDirectory.file( - archiveFileName.map { name -> - val idx = name.lastIndexOf('.') - if (idx != -1) { - "${name.substring(0, idx)}-sources${name.substring(idx)}" - } else { - "$name-sources" - } - } - ) - } else { - project.provider { null } + destinationDirectory.file( + archiveFileName.map { name -> + val idx = name.lastIndexOf('.') + if (idx != -1) { + "${name.substring(0, idx)}-sources${name.substring(idx)}" + } else { + "$name-sources" + } } - } + ) ) /** @@ -627,21 +621,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() - } - if (unusedClasses.isNotEmpty()) { - logger.info("Found {} unused classes to drop for minimization.", unusedClasses.size) - } - this.unusedClasses = unusedClasses val actualTransformers = transformers.get().let { set -> if ( @@ -714,6 +693,23 @@ public abstract class ShadowJar : Jar() { } } + // We can't cache these classes due to CC serialization. + private val unusedClasses: Set + get() { + return if ( + _minimizeJar.get() && minimizeSpec.tool.get() == MinimizeTool.DEPENDENCY_ANALYZER + ) { + findUnusedClasses( + sourceSetsClassesDirs = sourceSetsClassesDirs, + classJars = apiJars, + toMinimize = toMinimize, + dependencies = includedDependencies, + ) + } else { + emptySet() + } + } + private fun addIncludedDependencies() { val isAar: File.() -> Boolean = { try { @@ -825,8 +821,6 @@ public abstract class ShadowJar : Jar() { ) } - private var unusedClasses: Set = emptySet() - private fun generateShadowedSourcesJar() { if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return generateSourcesJar( 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 c02f5a82f5..204f6d5556 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 @@ -164,7 +164,10 @@ class ShadowPropertiesTest { assertThat(relocationPrefix.get()).isEqualTo(ShadowBasePlugin.SHADOW) assertThat(configurations.get()).containsOnly(runtimeConfiguration) assertThat(generateSourcesJar.get()).isFalse() - assertThat(archiveSourcesFile.orNull).isNull() + 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).isEmpty() assertThat(includedSourcesJars.files).isEmpty() } From 5d50a5896849b99cb356e0edcccd0f80107ec70e Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 12:23:12 +0800 Subject: [PATCH 097/117] Improve ShadowedSourcesJar and tests --- .../gradle/plugins/shadow/BasePluginTest.kt | 9 +- .../shadow/internal/ShadowedSourcesJar.kt | 235 +++++++++++++++++ .../plugins/shadow/internal/SourcesJar.kt | 242 ------------------ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 12 +- .../shadow/internal/ShadowedSourcesJarTest.kt | 222 ++++++++++++++++ .../plugins/shadow/internal/SourcesJarTest.kt | 204 --------------- .../shadow/tasks/ShadowCopyActionTest.kt | 2 +- .../gradle/plugins/shadow/testkit/JarPath.kt | 9 +- 8 files changed, 476 insertions(+), 459 deletions(-) create mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt delete mode 100644 src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt create mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt delete mode 100644 src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.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 25803693f8..0c82c8893f 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 @@ -1,7 +1,6 @@ package com.github.jengelman.gradle.plugins.shadow import assertk.Assert -import assertk.all import assertk.assertions.isEqualTo import assertk.assertions.isNotNull import com.github.jengelman.gradle.plugins.shadow.ShadowApplicationPlugin.Companion.SHADOW_INSTALL_TASK_NAME @@ -14,12 +13,12 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.commonGradleArgs import com.github.jengelman.gradle.plugins.shadow.testkit.enableNoImplicitLookupInParentProjects import com.github.jengelman.gradle.plugins.shadow.testkit.gradleRunner import com.github.jengelman.gradle.plugins.shadow.testkit.requireResourceAsPath +import com.github.jengelman.gradle.plugins.shadow.testkit.useAll as testkitUseAll import com.github.jengelman.gradle.plugins.shadow.transformers.ResourceTransformer 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 import java.util.jar.JarEntry @@ -475,11 +474,7 @@ abstract class BasePluginTest { .trimMargin() } - fun Assert.useAll(body: Assert.() -> Unit) = all { - body() - // Close the resource after all assertions are done. - given { it.use(block = {}) } - } + fun Assert.useAll(body: Assert.() -> Unit) = testkitUseAll(body) fun Assert.taskOutcomeEquals(taskPath: String, expectedOutcome: TaskOutcome) { return transform { it.task(taskPath)?.outcome }.isNotNull().isEqualTo(expectedOutcome) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt new file mode 100644 index 0000000000..b02c164ce6 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt @@ -0,0 +1,235 @@ +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.io.InputStream +import org.apache.tools.zip.ZipOutputStream +import org.gradle.api.file.FileCollection +import org.vafer.jdeb.shaded.objectweb.asm.ClassReader +import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor +import org.vafer.jdeb.shaded.objectweb.asm.Opcodes + +internal fun generateSourcesJar( + sourcesJarFile: File, + zipOutStream: ZipOutputStream, + sourceSetsSourceDirs: FileCollection, + includedSourcesJars: Iterable, + classesDirs: Iterable, + dependencies: Iterable, + relocators: Iterable, + unusedClasses: Set, + preserveFileTimestamps: Boolean, +) = + try { + zipOutStream.use { zos -> + val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } + + val visitedFiles = mutableSetOf() + val sourceToClasses = + if (unusedClasses.isNotEmpty()) { + buildSourceToClassesMap(classesDirs = classesDirs, dependencies = dependencies) + } else { + emptyMap() + } + + val manifestEntry = "META-INF/MANIFEST.MF" + visitedFiles.add(manifestEntry) + zos.writeEntry( + name = manifestEntry, + preserveLastModified = preserveFileTimestamps, + unixMode = UnixMode.file(), + ) { + write("Manifest-Version: 1.0\n\n".toByteArray()) + } + + val filesWithRelPaths = mutableListOf>() + sourceSetsSourceDirs.asFileTree.visit { details -> + if (!details.isDirectory) { + filesWithRelPaths.add(details.file to details.relativePath.pathString) + } + } + + for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { + val isSource = relPath.isSourceFile() + if (isSource) { + if (isUnused(relPath, unusedClasses, sourceToClasses)) continue + val relocatedPath = relocators.relocateSourcePath(relPath) + if (visitedFiles.add(relocatedPath)) { + val text = file.readText() + val transformedText = relocators.remapSource(text) + val bytes = transformedText.toByteArray() + 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() + .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 + } + val isSource = name.isSourceFile() + if (isSource) { + if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach + val relocatedPath = relocators.relocateSourcePath(name) + if (visitedFiles.add(relocatedPath)) { + val text = getInputStream(entry).bufferedReader().use { it.readText() } + val transformedText = relocators.remapSource(text) + val bytes = transformedText.toByteArray() + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(bytes) + } + } + } else { + val relocatedPath = relocators.relocatePath(name) + if (visitedFiles.add(relocatedPath)) { + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = entry.time, + unixMode = UnixMode.file(), + ) { + write(getInputStream(entry).use { it.readBytes() }) + } + } + } + } + } + } + + val entries = zos.entries.map { it.name } + val added = entries.toMutableSet() + entries.forEach { name -> + name.parentDirectoryEntries().forEach { entryName -> + if (!added.add(entryName)) return@forEach + zos.writeEntry( + name = entryName, + preserveLastModified = preserveFileTimestamps, + unixMode = UnixMode.directory(), + ) + } + } + } + } catch (e: Exception) { + sourcesJarFile.delete() + gradleError("Could not create shadowed sources JAR '$sourcesJarFile'.", e) + } + +internal fun isUnused( + canonicalPath: String, + unusedClasses: Set, + sourceToClasses: Map>, +): Boolean { + if (unusedClasses.isEmpty()) return false + val classes = sourceToClasses[canonicalPath] ?: return false + return classes.isNotEmpty() && classes.all { it in unusedClasses } +} + +private fun buildSourceToClassesMap( + classesDirs: Iterable, + dependencies: Iterable, +): Map> { + val sourceToClasses = mutableMapOf>() + + fun InputStream.recordSourceMapping() = use { + try { + var internalName: String? = null + var sourceFile: String? = null + ClassReader(this) + .accept( + object : ClassVisitor(Opcodes.ASM9) { + override fun visit( + version: Int, + access: Int, + name: String, + signature: String?, + superName: String?, + interfaces: Array?, + ) { + internalName = name + super.visit(version, access, name, signature, superName, interfaces) + } + + override fun visitSource(source: String?, debug: String?) { + sourceFile = source + super.visitSource(source, debug) + } + }, + ClassReader.SKIP_CODE or ClassReader.SKIP_FRAMES, + ) + + val name = internalName ?: return@use + val source = sourceFile ?: return@use + val pkg = name.substringBeforeLast('/', "") + val canonicalSourcePath = if (pkg.isEmpty()) source else "$pkg/$source" + val className = name.replace('/', '.') + sourceToClasses.getOrPut(canonicalSourcePath) { mutableSetOf() }.add(className) + } catch (_: Exception) { + // Ignore invalid class files. + } + } + + classesDirs + .filter(File::isDirectory) + .flatMap(File::walk) + .filter { it.isFile && it.name.endsWith(".class") } + .forEach { file -> file.inputStream().recordSourceMapping() } + + dependencies + .filter(File::isFile) + .filter { + it.extension.equals("jar", ignoreCase = true) || it.extension.equals("zip", ignoreCase = true) + } + .forEach { file -> + try { + file.useZip { + entries() + .toList() + .filter { it.name.endsWith(".class") } + .forEach { entry -> getInputStream(entry).recordSourceMapping() } + } + } catch (_: Exception) { + // Ignore invalid archives. + } + } + + return sourceToClasses +} diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt deleted file mode 100644 index 035dbdcd99..0000000000 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt +++ /dev/null @@ -1,242 +0,0 @@ -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 org.gradle.api.file.FileCollection -import org.gradle.api.tasks.bundling.ZipEntryCompression -import org.vafer.jdeb.shaded.objectweb.asm.ClassReader -import org.vafer.jdeb.shaded.objectweb.asm.ClassVisitor -import org.vafer.jdeb.shaded.objectweb.asm.Opcodes - -internal fun generateSourcesJar( - sourcesJarFile: File, - sourceSetsSourceDirs: FileCollection, - includedSourcesJars: Iterable, - classesDirs: Iterable = emptyList(), - dependencies: Iterable = emptyList(), - relocators: Iterable, - unusedClasses: Set = emptySet(), - entryCompression: ZipEntryCompression, - isZip64: Boolean, - metadataCharset: String?, - preserveFileTimestamps: Boolean, -) { - val sourcesJars = includedSourcesJars.filter { it.exists() && it.isFile }.sortedBy { it.path } - - val visitedFiles = mutableSetOf() - val sourceToClasses = - if (unusedClasses.isNotEmpty()) { - buildSourceToClassesMap(classesDirs = classesDirs, dependencies = dependencies) - } else { - emptyMap() - } - - try { - sourcesJarFile.parentFile?.mkdirs() - sourcesJarFile - .createZipOutputStream( - entryCompression = entryCompression, - isZip64 = isZip64, - encoding = metadataCharset, - ) - .use { zos -> - val manifestEntry = "META-INF/MANIFEST.MF" - visitedFiles.add(manifestEntry) - zos.writeEntry( - name = manifestEntry, - preserveLastModified = preserveFileTimestamps, - unixMode = UnixMode.file(), - ) { - write("Manifest-Version: 1.0\n\n".toByteArray()) - } - - val filesWithRelPaths = mutableListOf>() - sourceSetsSourceDirs.asFileTree.visit { details -> - if (!details.isDirectory) { - filesWithRelPaths.add(details.file to details.relativePath.pathString) - } - } - - for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { - val isSource = relPath.isSourceFile() - if (isSource) { - if (isUnused(relPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocateSourcePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val text = file.readText() - val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray() - 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() - .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 - } - val isSource = name.isSourceFile() - if (isSource) { - if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocateSourcePath(name) - if (visitedFiles.add(relocatedPath)) { - val text = getInputStream(entry).bufferedReader().readText() - val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray() - 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().forEach { entryName -> - if (!added.add(entryName)) return@forEach - zos.writeEntry( - name = entryName, - preserveLastModified = preserveFileTimestamps, - unixMode = UnixMode.directory(), - ) - } - } - } - } catch (e: Exception) { - sourcesJarFile.delete() - gradleError("Could not create shadowed sources JAR '$sourcesJarFile'.", e) - } -} - -private fun buildSourceToClassesMap( - classesDirs: Iterable, - dependencies: Iterable, -): Map> { - val sourceToClasses = mutableMapOf>() - - fun processClassBytes(bytes: ByteArray) { - try { - var internalName: String? = null - var sourceFile: String? = null - ClassReader(bytes) - .accept( - object : ClassVisitor(Opcodes.ASM9) { - override fun visit( - version: Int, - access: Int, - name: String, - signature: String?, - superName: String?, - interfaces: Array?, - ) { - internalName = name - super.visit(version, access, name, signature, superName, interfaces) - } - - override fun visitSource(source: String?, debug: String?) { - sourceFile = source - super.visitSource(source, debug) - } - }, - ClassReader.SKIP_CODE or ClassReader.SKIP_FRAMES, - ) - - val name = internalName ?: return - val source = sourceFile ?: return - val pkg = name.substringBeforeLast('/', "") - val canonicalSourcePath = if (pkg.isEmpty()) source else "$pkg/$source" - val className = name.replace('/', '.') - sourceToClasses.getOrPut(canonicalSourcePath) { mutableSetOf() }.add(className) - } catch (_: Exception) { - // Ignore invalid class files - } - } - - for (dir in classesDirs.filter(File::isDirectory)) { - dir - .walkTopDown() - .filter { it.isFile && it.name.endsWith(".class") } - .forEach { file -> processClassBytes(file.readBytes()) } - } - - for (file in - dependencies.filter { it.isFile && (it.extension == "jar" || it.extension == "zip") }) { - try { - file.useZip { - entries() - .toList() - .filter { !it.isDirectory && it.name.endsWith(".class") } - .forEach { entry -> processClassBytes(getInputStream(entry).readBytes()) } - } - } catch (_: Exception) { - // Ignore invalid archives - } - } - - return sourceToClasses -} - -internal fun isUnused( - canonicalPath: String, - unusedClasses: Set, - sourceToClasses: Map>, -): Boolean { - if (unusedClasses.isEmpty()) return false - val classes = sourceToClasses[canonicalPath] ?: return false - return classes.isNotEmpty() && classes.all { it in unusedClasses } -} 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 b03f2fbe05..0ed8abf965 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowJar.kt @@ -823,17 +823,21 @@ public abstract class ShadowJar : Jar() { private fun generateShadowedSourcesJar() { if (!generateSourcesJar.get() || !archiveSourcesFile.isPresent) return + val sourcesJarFile = archiveSourcesFile.get().asFile.apply { parentFile?.mkdirs() } generateSourcesJar( - sourcesJarFile = archiveSourcesFile.get().asFile, + sourcesJarFile = sourcesJarFile, + zipOutStream = + sourcesJarFile.createZipOutputStream( + entryCompression = entryCompression, + isZip64 = isZip64, + encoding = metadataCharset, + ), sourceSetsSourceDirs = sourceSetsSourceDirs, includedSourcesJars = includedSourcesJars.files, classesDirs = sourceSetsClassesDirs.files, dependencies = includedDependencies.files, relocators = relocators.get() + packageRelocators, unusedClasses = unusedClasses, - entryCompression = entryCompression, - isZip64 = isZip64, - metadataCharset = metadataCharset, preserveFileTimestamps = isPreserveFileTimestamps, ) } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt new file mode 100644 index 0000000000..937ed96d0a --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt @@ -0,0 +1,222 @@ +package com.github.jengelman.gradle.plugins.shadow.internal + +import assertk.assertThat +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +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.testkit.JarPath +import com.github.jengelman.gradle.plugins.shadow.testkit.containsExactly +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.useAll +import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory +import java.io.File +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.createParentDirectories +import kotlin.io.path.writeBytes +import kotlin.io.path.writeText +import org.gradle.api.file.FileCollection +import org.gradle.api.tasks.bundling.ZipEntryCompression +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir + +class ShadowedSourcesJarTest { + @TempDir lateinit var tempDir: Path + + @Test + fun metadataCharsetDoesNotChangeSourceEncoding() { + val srcDir = tempDir.resolve("src").createDirectories() + val sourceBytes = "class Main { val message = \"你好\" }\n".toByteArray(StandardCharsets.UTF_8) + srcDir.resolve("Main.kt").writeBytes(sourceBytes) + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + metadataCharset = Charsets.US_ASCII.toString(), + ) + + assertThat(JarPath(outputJar)).useAll { + getBytes("Main.kt").isEqualTo(sourceBytes) + } + } + + @Test + fun isUnusedMatching() { + val unusedSet = + setOf( + "com.example.UnusedJava", + $$"com.example.UnusedJava$Inner", + "com.example.UnusedKtClass", + "com.example.DefaultFacadeKt", + "com.example.CustomFacade", + ) + val sourceToClasses = + mapOf( + "com/example/UnusedJava.java" to + setOf("com.example.UnusedJava", $$"com.example.UnusedJava$Inner"), + "com/example/PartiallyUsedJava.java" to + setOf("com.example.UnusedJava", "com.example.UsedHelper"), + "com/example/UsedJava.java" to setOf("com.example.UsedJava"), + "com/example/UnusedKtClass.kt" to setOf("com.example.UnusedKtClass"), + "com/example/DefaultFacade.kt" to setOf("com.example.DefaultFacadeKt"), + "com/example/Utils.kt" to setOf("com.example.CustomFacade"), + "com/example/MixedUtils.kt" to setOf("com.example.CustomFacade", "com.example.UsedClass"), + "Main.java" to setOf("Main"), + ) + + // All classes unused in file -> unused + assertThat(isUnused("com/example/UnusedJava.java", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/UnusedKtClass.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/DefaultFacade.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("com/example/Utils.kt", unusedSet, sourceToClasses)).isTrue() + assertThat(isUnused("Main.java", setOf("Main"), sourceToClasses)).isTrue() + + // At least one class is used in file -> NOT unused (kept!) + assertThat(isUnused("com/example/PartiallyUsedJava.java", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/MixedUtils.kt", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/UsedJava.java", unusedSet, sourceToClasses)).isFalse() + + // Unknown source file or empty unused set -> kept + assertThat(isUnused("com/example/Unknown.java", unusedSet, sourceToClasses)).isFalse() + assertThat(isUnused("com/example/UnusedJava.java", emptySet(), sourceToClasses)).isFalse() + assertThat(isUnused("Main.java", setOf("Other"), sourceToClasses)).isFalse() + } + + @Test + fun deterministicOrdering() { + val srcDir = tempDir.resolve("src").createDirectories() + srcDir.resolve("z/sub/Z.java").apply { + createParentDirectories() + writeText("package z.sub;\nclass Z {}") + } + srcDir.resolve("a/A.java").apply { + createParentDirectories() + writeText("package a;\nclass A {}") + } + srcDir.resolve("m/M.java").apply { + createParentDirectories() + writeText("package m;\nclass M {}") + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + ) + + assertThat(JarPath(outputJar)).useAll { + containsExactly( + "META-INF/MANIFEST.MF", + "a/A.java", + "m/M.java", + "z/sub/Z.java", + "META-INF/", + "a/", + "m/", + "z/", + "z/sub/", + ) + } + } + + @Test + fun respectsExcludedDirectory() { + val srcDir = tempDir.resolve("src").createDirectories() + srcDir.resolve("Excluded.java").writeText("public class Excluded {}") + + val fileTree = + testObjectFactory.fileCollection().from(srcDir).asFileTree.matching { + it.exclude("**/Excluded.java") + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(fileTree), + ) + + assertThat(JarPath(outputJar)).useAll { + containsOnly("META-INF/", "META-INF/MANIFEST.MF") + } + } + + @Test + fun mergesAndRelocatesIncludedSourcesJar() { + val srcDir = tempDir.resolve("src").createDirectories() + srcDir.resolve("com/example/Main.java").apply { + createParentDirectories() + writeText("package com.example;\npublic class Main {}") + } + + val depSourcesJar = tempDir.resolve("dep-sources.jar") + depSourcesJar + .toFile() + .createZipOutputStream( + entryCompression = ZipEntryCompression.STORED, + isZip64 = false, + encoding = null, + ) + .use { zos -> + zos.writeEntry("dep/Dep.java") { + write("package dep;\npublic class Dep {}".toByteArray()) + } + zos.writeEntry("dep/resource.txt") { + write("dep resource".toByteArray()) + } + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + includedSourcesJars = listOf(depSourcesJar.toFile()), + relocators = listOf(SimpleRelocator("dep", "shaded.dep")), + ) + + assertThat(JarPath(outputJar)).useAll { + containsExactly( + "META-INF/MANIFEST.MF", + "com/example/Main.java", + "shaded/dep/Dep.java", + "shaded/dep/resource.txt", + "META-INF/", + "com/", + "com/example/", + "shaded/", + "shaded/dep/", + ) + getContent("shaded/dep/Dep.java").isEqualTo("package shaded.dep;\npublic class Dep {}") + getContent("shaded/dep/resource.txt").isEqualTo("dep resource") + } + } +} + +private fun generateSourcesJar( + sourcesJarFile: File, + sourceSetsSourceDirs: FileCollection, + includedSourcesJars: Iterable = emptyList(), + relocators: Iterable = emptyList(), + metadataCharset: String? = null, +) = + generateSourcesJar( + sourcesJarFile = sourcesJarFile, + zipOutStream = + sourcesJarFile.createZipOutputStream( + entryCompression = ZipEntryCompression.STORED, + isZip64 = false, + encoding = metadataCharset, + ), + sourceSetsSourceDirs = sourceSetsSourceDirs, + includedSourcesJars = includedSourcesJars, + classesDirs = emptyList(), + dependencies = emptyList(), + relocators = relocators, + unusedClasses = emptySet(), + preserveFileTimestamps = true, + ) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt deleted file mode 100644 index f9c521f4eb..0000000000 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt +++ /dev/null @@ -1,204 +0,0 @@ -package com.github.jengelman.gradle.plugins.shadow.internal - -import assertk.assertFailure -import assertk.assertThat -import assertk.assertions.containsExactly -import assertk.assertions.containsOnly -import assertk.assertions.hasMessage -import assertk.assertions.isEqualTo -import assertk.assertions.isFalse -import assertk.assertions.isInstanceOf -import assertk.assertions.isTrue -import com.github.jengelman.gradle.plugins.shadow.util.testObjectFactory -import java.io.File -import java.nio.charset.StandardCharsets -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 SourcesJarTest { - - @Test - fun metadataCharsetDoesNotChangeSourceEncoding(@TempDir tempDir: File) { - val srcDir = tempDir.resolve("src").apply { mkdirs() } - val sourceBytes = "class Main { val message = \"你好\" }\n".toByteArray(StandardCharsets.UTF_8) - srcDir.resolve("Main.kt").writeBytes(sourceBytes) - - val outputJar = tempDir.resolve("output-sources.jar") - generateSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), - includedSourcesJars = emptyList(), - relocators = emptyList(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = "US-ASCII", - preserveFileTimestamps = true, - ) - - val archivedSource = - ZipFile(outputJar).use { zip -> zip.getInputStream(zip.getEntry("Main.kt")).readBytes() } - assertThat(archivedSource).isEqualTo(sourceBytes) - } - - @Test - fun createsParentDirectoryForOutput(@TempDir tempDir: File) { - val srcDir = tempDir.resolve("src").apply { mkdirs() } - srcDir.resolve("Main.java").writeText("class Main {}") - val outputJar = tempDir.resolve("nested/output-sources.jar") - - generateSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), - includedSourcesJars = emptyList(), - relocators = emptyList(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = null, - preserveFileTimestamps = true, - ) - - assertThat(outputJar.isFile).isTrue() - } - - @Test - fun isUnusedMatching() { - val unusedSet = - setOf( - "com.example.UnusedJava", - "com.example.UnusedJava\$Inner", - "com.example.UnusedKtClass", - "com.example.DefaultFacadeKt", - "com.example.CustomFacade", - ) - val sourceToClasses = - mapOf( - "com/example/UnusedJava.java" to - setOf("com.example.UnusedJava", "com.example.UnusedJava\$Inner"), - "com/example/PartiallyUsedJava.java" to - setOf("com.example.UnusedJava", "com.example.UsedHelper"), - "com/example/UsedJava.java" to setOf("com.example.UsedJava"), - "com/example/UnusedKtClass.kt" to setOf("com.example.UnusedKtClass"), - "com/example/DefaultFacade.kt" to setOf("com.example.DefaultFacadeKt"), - "com/example/Utils.kt" to setOf("com.example.CustomFacade"), - "com/example/MixedUtils.kt" to setOf("com.example.CustomFacade", "com.example.UsedClass"), - "Main.java" to setOf("Main"), - ) - - // All classes unused in file -> unused - assertThat(isUnused("com/example/UnusedJava.java", unusedSet, sourceToClasses)).isTrue() - assertThat(isUnused("com/example/UnusedKtClass.kt", unusedSet, sourceToClasses)).isTrue() - assertThat(isUnused("com/example/DefaultFacade.kt", unusedSet, sourceToClasses)).isTrue() - assertThat(isUnused("com/example/Utils.kt", unusedSet, sourceToClasses)).isTrue() - assertThat(isUnused("Main.java", setOf("Main"), sourceToClasses)).isTrue() - - // At least one class is used in file -> NOT unused (kept!) - assertThat(isUnused("com/example/PartiallyUsedJava.java", unusedSet, sourceToClasses)).isFalse() - assertThat(isUnused("com/example/MixedUtils.kt", unusedSet, sourceToClasses)).isFalse() - assertThat(isUnused("com/example/UsedJava.java", unusedSet, sourceToClasses)).isFalse() - - // Unknown source file or empty unused set -> kept - assertThat(isUnused("com/example/Unknown.java", unusedSet, sourceToClasses)).isFalse() - assertThat(isUnused("com/example/UnusedJava.java", emptySet(), sourceToClasses)).isFalse() - assertThat(isUnused("Main.java", setOf("Other"), sourceToClasses)).isFalse() - } - - @Test - fun deterministicOrdering(@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") - generateSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(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) - .containsExactly( - "META-INF/MANIFEST.MF", - "a/A.java", - "m/M.java", - "z/sub/Z.java", - "META-INF/", - "a/", - "m/", - "z/", - "z/sub/", - ) - } - - @Test - fun respectsExcludedDirectory(@TempDir tempDir: File) { - val srcDir = tempDir.resolve("src").apply { mkdirs() } - srcDir.resolve("Excluded.java").writeText("public class Excluded {}") - - val fileTree = - testObjectFactory.fileCollection().from(srcDir).asFileTree.matching { - it.exclude("**/Excluded.java") - } - - val outputJar = tempDir.resolve("output-sources.jar") - generateSourcesJar( - sourcesJarFile = outputJar, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(fileTree), - includedSourcesJars = emptyList(), - relocators = emptyList(), - unusedClasses = emptySet(), - entryCompression = ZipEntryCompression.DEFLATED, - isZip64 = false, - metadataCharset = null, - preserveFileTimestamps = true, - ) - - val entries = ZipFile(outputJar).use { zip -> zip.entries().toList().map { it.name } } - assertThat(entries).containsOnly("META-INF/", "META-INF/MANIFEST.MF") - } - - @Test - fun throwsGradleExceptionOnFailure(@TempDir tempDir: File) { - val invalidFile = tempDir.resolve("not-a-file").apply { mkdirs() } - val srcDir = - tempDir.resolve("src").apply { - mkdirs() - resolve("Main.java").writeText("public class Main {}") - } - - assertFailure { - generateSourcesJar( - sourcesJarFile = invalidFile, - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(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'.") - } -} diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt index f0cf101e1c..b0246233f1 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/tasks/ShadowCopyActionTest.kt @@ -189,7 +189,7 @@ class ShadowCopyActionTest { zipFile = zipFile, zipOutStream = zipFile.createZipOutputStream( - entryCompression = ZipEntryCompression.DEFLATED, + entryCompression = ZipEntryCompression.STORED, isZip64 = isZip64, encoding = null, ), 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 cb7d8b8e8e..f50e9e6299 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 @@ -1,6 +1,7 @@ package com.github.jengelman.gradle.plugins.shadow.testkit import assertk.Assert +import assertk.all import assertk.assertions.containsAtLeast import assertk.assertions.containsExactly import assertk.assertions.containsNone @@ -122,6 +123,12 @@ fun Assert.runMain( os.toString().invariantEolString } -fun Assert.toEntries() = transform { actual -> +fun Assert.useAll(body: Assert.() -> Unit) = all { + body() + // Close the resource after all assertions are done. + given { it.use(block = {}) } +} + +private fun Assert.toEntries() = transform { actual -> actual.entries().toList().map { it.name } } From 08a13e2828608bfeb9932f1c77bc41032aff0874 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 13:01:37 +0800 Subject: [PATCH 098/117] TODO ShadowSourcesPublishArtifact --- .../github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt | 1 + 1 file changed, 1 insertion(+) 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 3396cfb452..bc66f8fd06 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 @@ -241,6 +241,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } } +// TODO: https://github.com/gradle/gradle/issues/31474 private class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : PublishArtifact { override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() From af63b6c2a1d708020af7156f6b0e2457a37469da Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 13:11:09 +0800 Subject: [PATCH 099/117] Clean up SourceRemapperTest --- .../plugins/shadow/internal/SourceRemapperTest.kt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt index 4f7ee99fb9..7cdc1aca99 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapperTest.kt @@ -25,7 +25,7 @@ class SourceRemapperTest { } @Test - fun relocateSourcePathWithClassInclude() { + fun pathWithClassInclude() { val relocator = SimpleRelocator( "pkg", @@ -46,7 +46,7 @@ class SourceRemapperTest { } @Test - fun relocateSourcePathWithClassExclude() { + fun pathWithClassExclude() { val relocator = SimpleRelocator( "pkg", @@ -60,7 +60,7 @@ class SourceRemapperTest { } @Test - fun relocateSourcePathWithClassOnlyRelocator() { + fun pathWithClassOnlyRelocator() { val classOnlyRelocator = object : Relocator by DefaultRelocator { override fun canRelocateClass(className: String) = className.startsWith("custom.pkg.") @@ -79,7 +79,7 @@ class SourceRemapperTest { } @Test - fun relocateSourcePathWithNonSourceFileDelegatesToRelocatePath() { + fun pathWithNonSourceFileDelegatesToRelocatePath() { val relocators = listOf(SimpleRelocator("pkg", "hidden.pkg")) // Resource files and class files are handled via relocatePath @@ -90,7 +90,7 @@ class SourceRemapperTest { } @Test - fun relocateSourcePathWithMultipleRelocators() { + fun pathWithMultipleRelocators() { val r1 = SimpleRelocator("pkg.one", "shaded.one") val r2 = SimpleRelocator("pkg.two", "shaded.two") val relocators = listOf(r1, r2) @@ -102,7 +102,7 @@ class SourceRemapperTest { } @Test - fun relocateSourcePathWithEmptyRelocators() { + fun pathWithEmptyRelocators() { assertThat(emptyList().relocateSourcePath("com/example/Foo.java")) .isEqualTo("com/example/Foo.java") } From 0ec7d1ed621cd0c4795a8fe3d2fcfd4fb0ae1e0d Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 22:18:16 +0800 Subject: [PATCH 100/117] Only register archiveSourcesFile as output when generateSourcesJar is enabled --- .../gradle/plugins/shadow/JavaPluginsTest.kt | 23 +++++++++++++ .../gradle/plugins/shadow/tasks/ShadowJar.kt | 32 ++++++++++++------- .../plugins/shadow/ShadowPropertiesTest.kt | 11 ++++--- 3 files changed, 51 insertions(+), 15 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/JavaPluginsTest.kt index 8901476be5..74e4abafad 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 @@ -5,6 +5,7 @@ import assertk.assertThat import assertk.assertions.contains import assertk.assertions.containsAtLeast import assertk.assertions.containsMatch +import assertk.assertions.containsOnly import assertk.assertions.doesNotContain import assertk.assertions.isEqualTo import assertk.assertions.isNotEmpty @@ -25,9 +26,11 @@ import com.github.jengelman.gradle.plugins.shadow.testkit.getMainAttr import com.github.jengelman.gradle.plugins.shadow.testkit.getStream import com.github.jengelman.gradle.plugins.shadow.testkit.runMain import com.github.jengelman.gradle.plugins.shadow.util.prependText +import java.nio.file.Path import kotlin.io.path.appendText import kotlin.io.path.deleteExisting import kotlin.io.path.invariantSeparatorsPathString +import kotlin.io.path.listDirectoryEntries import kotlin.io.path.name import kotlin.io.path.outputStream import kotlin.io.path.relativeTo @@ -1519,6 +1522,26 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun shadowJarOutputsOnlyContainShadowedJarByDefault() { + val syncShadowJar = "syncShadowJar" + projectScript.appendText( + """ + |tasks.register('$syncShadowJar', Sync) { + | // `singleFile` fails if the sources JAR is registered as an output when it's disabled. + | from $shadowJarTask.map { it.outputs.files.singleFile } + | into layout.buildDirectory.dir('synced') + |} + """ + .trimMargin() + ) + + runWithSuccess(syncShadowJar) + + assertThat(path("build/synced").listDirectoryEntries().map(Path::name)) + .containsOnly("my-1.0-all.jar") + } + private fun dependencies(configuration: String, vararg flags: String): String { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } 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 0ed8abf965..71b7d0b48a 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 @@ -253,24 +253,34 @@ public abstract class ShadowJar : Jar() { * The destination location of the companion shadowed sources JAR. * * Defaults to - * `/--sources.`. + * `/--sources.` when + * [generateSourcesJar] is enabled, and has no value otherwise. */ - @get:Optional // `archiveFileName` may not have a value when the Java plugin is not applied. + @get:Optional @get:OutputFile public open 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" - } + generateSourcesJar.flatMap { generate -> + if (generate) { + destinationDirectory.file( + archiveFileName.map { name -> + val idx = name.lastIndexOf('.') + if (idx != -1) { + "${name.substring(0, idx)}-sources${name.substring(idx)}" + } else { + "$name-sources" + } + } + ) + } else { + // Leave it absent so it isn't registered as a task output when sources JAR generation + // is disabled, keeping `outputs.files` to the shadowed JAR only. + // TODO: replace with `providers.absent()` once the min Gradle version is 9.9. + project.provider { null } } - ) + } ) /** 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 204f6d5556..ab3c576ae6 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 @@ -164,10 +164,8 @@ 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(archiveSourcesFile.orNull).isNull() + assertThat(outputs.files.singleFile).isEqualTo(archiveFile.get().asFile) assertThat(sourceSetsSourceDirs.files).isEmpty() assertThat(includedSourcesJars.files).isEmpty() } @@ -188,6 +186,11 @@ class ShadowPropertiesTest { ) isEqualTo(projectDir.resolve("build/libs/my-project-1.0.0-all-sources.jar")) } + assertThat(shadowJarTask.outputs.files.files) + .containsOnly( + shadowJarTask.archiveFile.get().asFile, + shadowJarTask.archiveSourcesFile.get().asFile, + ) val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") assertThat(shadowJarTask.sourceSetsSourceDirs.files) .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) From 320e4bbda9147d8b9a12a8c89d8722a2619ee07c Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 22:59:48 +0800 Subject: [PATCH 101/117] Align ShadowSourcesPublishArtifact with ArchivePublishArtifact Include `archiveAppendix` in the artifact name, and skip publishing the sources JAR when `shadowJar` is disabled, which previously failed with a missing file while generating the module metadata. --- .../gradle/plugins/shadow/PublishingTest.kt | 33 +++++++++++++++++++ .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 14 ++++++-- .../plugins/shadow/ShadowPropertiesTest.kt | 3 ++ 3 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index bb02854070..9d5c25dbb3 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 @@ -375,6 +375,39 @@ class PublishingTest : BasePluginTest() { .isEqualTo("maven-1.0-shaded-sources.jar") } + @Test + fun dontPublishShadowJarAndSourcesWhenShadowJarDisabled() { + projectScript.appendText( + publishConfiguration( + projectBlock = + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin(), + shadowBlock = + """ + |enabled = false + """ + .trimMargin(), + publicationsBlock = + """ + |shadow(MavenPublication) { + | from components.shadow + |} + """ + .trimMargin(), + ) + ) + + publish() + + val artifactRoot = "my/maven/1.0" + assertThat(repoPath(artifactRoot).entries) + .containsOnly(*withChecksums("maven-1.0.module", "maven-1.0.pom")) + } + @Test fun publishJavaComponentWithShadowAndSourcesVariants() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index bc66f8fd06..1d936bef81 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 @@ -16,7 +16,6 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration -import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -242,9 +241,15 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } // TODO: https://github.com/gradle/gradle/issues/31474 +// Mirrors Gradle's `ArchivePublishArtifact`, which is used for the shadowed JAR. +@Suppress("InternalGradleApiUsage") // For skipping publishing when the task is disabled. private class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : - PublishArtifact { - override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() + org.gradle.api.internal.artifacts.PublishArtifactInternal { + override fun getName(): String { + val baseName = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() + val appendix = shadowJarTask.flatMap { it.archiveAppendix }.orNull + return if (appendix.isNullOrEmpty()) baseName else "$baseName-$appendix" + } override fun getExtension(): String = shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" @@ -263,4 +268,7 @@ private class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvid override fun getBuildDependencies(): TaskDependency = TaskDependency { setOf(@Suppress("EagerGradleConfiguration") shadowJarTask.get()) } + + override fun shouldBePublished(): Boolean = + @Suppress("EagerGradleConfiguration") shadowJarTask.get().enabled } 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 ab3c576ae6..db8b4266cd 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 @@ -216,6 +216,9 @@ class ShadowPropertiesTest { shadowJarTask.archiveExtension.set("zip") assertThat(name).isEqualTo("renamed") assertThat(extension).isEqualTo("zip") + + shadowJarTask.archiveAppendix.set("appendix") + assertThat(name).isEqualTo("renamed-appendix") } } From 0abfeb53c6797011220744141088f07498a755f6 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:04:23 +0800 Subject: [PATCH 102/117] Preserve non-UTF-8 source bytes in shadowed sources JAR Sources were always decoded and re-encoded as UTF-8, replacing non-UTF-8 bytes with U+FFFD. Fall back to ISO-8859-1 for sources that are not valid UTF-8, and keep the original bytes when nothing is relocated. --- .../shadow/internal/ShadowedSourcesJar.kt | 27 ++++++++--- .../shadow/internal/ShadowedSourcesJarTest.kt | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt index b02c164ce6..a8e9f03829 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt @@ -4,6 +4,8 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.io.InputStream +import java.nio.ByteBuffer +import java.nio.charset.CharacterCodingException import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileCollection import org.vafer.jdeb.shaded.objectweb.asm.ClassReader @@ -56,9 +58,7 @@ internal fun generateSourcesJar( if (isUnused(relPath, unusedClasses, sourceToClasses)) continue val relocatedPath = relocators.relocateSourcePath(relPath) if (visitedFiles.add(relocatedPath)) { - val text = file.readText() - val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray() + val bytes = relocators.remapSourceBytes(file.readBytes()) zos.writeEntry( name = relocatedPath, preserveLastModified = preserveFileTimestamps, @@ -106,9 +106,8 @@ internal fun generateSourcesJar( if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocateSourcePath(name) if (visitedFiles.add(relocatedPath)) { - val text = getInputStream(entry).bufferedReader().use { it.readText() } - val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray() + val bytes = + relocators.remapSourceBytes(getInputStream(entry).use { it.readBytes() }) zos.writeEntry( name = relocatedPath, preserveLastModified = preserveFileTimestamps, @@ -153,6 +152,22 @@ internal fun generateSourcesJar( gradleError("Could not create shadowed sources JAR '$sourcesJarFile'.", e) } +/** + * Remaps source content without assuming its encoding. UTF-8 is tried first, and other encodings + * fall back to ISO-8859-1, which maps each byte to a char losslessly, while package names to + * relocate are ASCII. Bytes are kept as-is if nothing is relocated. + */ +private fun Iterable.remapSourceBytes(bytes: ByteArray): ByteArray { + val (text, charset) = + try { + Charsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString() to Charsets.UTF_8 + } catch (_: CharacterCodingException) { + String(bytes, Charsets.ISO_8859_1) to Charsets.ISO_8859_1 + } + val transformedText = remapSource(text) + return if (transformedText == text) bytes else transformedText.toByteArray(charset) +} + internal fun isUnused( canonicalPath: String, unusedClasses: Set, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt index 937ed96d0a..a943c8bdd2 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt @@ -46,6 +46,54 @@ class ShadowedSourcesJarTest { } } + @Test + fun nonUtf8SourceBytesArePreserved() { + val latin1 = StandardCharsets.ISO_8859_1 + val srcDir = tempDir.resolve("src").createDirectories() + srcDir + .resolve("foo/Main.java") + .createParentDirectories() + .writeBytes("package foo;\nclass Main { String s = \"café\"; }".toByteArray(latin1)) + srcDir + .resolve("bar/Other.java") + .createParentDirectories() + .writeBytes("package bar;\nclass Other { String s = \"naïve\"; }".toByteArray(latin1)) + + val depSourcesJar = tempDir.resolve("dep-sources.jar") + depSourcesJar + .toFile() + .createZipOutputStream( + entryCompression = ZipEntryCompression.STORED, + isZip64 = false, + encoding = null, + ) + .use { zos -> + zos.writeEntry("dep/Dep.java") { + write("package dep;\npublic class Dep { String s = \"über\"; }".toByteArray(latin1)) + } + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + includedSourcesJars = listOf(depSourcesJar.toFile()), + relocators = + listOf(SimpleRelocator("foo", "shaded.foo"), SimpleRelocator("dep", "shaded.dep")), + ) + + assertThat(JarPath(outputJar)).useAll { + getBytes("shaded/foo/Main.java") + .isEqualTo("package shaded.foo;\nclass Main { String s = \"café\"; }".toByteArray(latin1)) + getBytes("bar/Other.java") + .isEqualTo("package bar;\nclass Other { String s = \"naïve\"; }".toByteArray(latin1)) + getBytes("shaded/dep/Dep.java") + .isEqualTo( + "package shaded.dep;\npublic class Dep { String s = \"über\"; }".toByteArray(latin1) + ) + } + } + @Test fun isUnusedMatching() { val unusedSet = From 67f77050fc3c4ff77d842ee30d2cf75faa38bb6a Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:11:38 +0800 Subject: [PATCH 103/117] Document that source changes rerun shadowJar when generateSourcesJar is enabled --- docs/publishing/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 44c479991f..94951b332b 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -843,6 +843,12 @@ To publish shadowed artifacts as the primary publication: > (`true` by default, or `false` when `withSourcesJar(publish = false)` is configured). > You can also explicitly toggle generation via `generateSourcesJar = true` (or `--generate-sources-jar`). +> [!NOTE] +> The shadowed sources JAR is generated by the `shadowJar` task itself, so it can reuse the same relocation, +> dependency filtering, and minimization configuration. As a result, when `generateSourcesJar` is enabled, source files +> become inputs of `shadowJar`, and any change to them (even a comment that doesn't affect the compiled classes) +> reruns `shadowJar` and rebuilds the shadowed JAR as well. + ### Customizing the Sources Archive File The companion shadowed sources JAR output location is configured via From 487f3b151f824cce30d74ccd80a6485f12c79906 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:21:48 +0800 Subject: [PATCH 104/117] Revert "Preserve non-UTF-8 source bytes in shadowed sources JAR" This reverts commit 0abfeb53c6797011220744141088f07498a755f6. --- .../shadow/internal/ShadowedSourcesJar.kt | 27 +++-------- .../shadow/internal/ShadowedSourcesJarTest.kt | 48 ------------------- 2 files changed, 6 insertions(+), 69 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt index a8e9f03829..b02c164ce6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt @@ -4,8 +4,6 @@ import com.github.jengelman.gradle.plugins.shadow.relocation.Relocator import com.github.jengelman.gradle.plugins.shadow.relocation.relocatePath import java.io.File import java.io.InputStream -import java.nio.ByteBuffer -import java.nio.charset.CharacterCodingException import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileCollection import org.vafer.jdeb.shaded.objectweb.asm.ClassReader @@ -58,7 +56,9 @@ internal fun generateSourcesJar( if (isUnused(relPath, unusedClasses, sourceToClasses)) continue val relocatedPath = relocators.relocateSourcePath(relPath) if (visitedFiles.add(relocatedPath)) { - val bytes = relocators.remapSourceBytes(file.readBytes()) + val text = file.readText() + val transformedText = relocators.remapSource(text) + val bytes = transformedText.toByteArray() zos.writeEntry( name = relocatedPath, preserveLastModified = preserveFileTimestamps, @@ -106,8 +106,9 @@ internal fun generateSourcesJar( if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach val relocatedPath = relocators.relocateSourcePath(name) if (visitedFiles.add(relocatedPath)) { - val bytes = - relocators.remapSourceBytes(getInputStream(entry).use { it.readBytes() }) + val text = getInputStream(entry).bufferedReader().use { it.readText() } + val transformedText = relocators.remapSource(text) + val bytes = transformedText.toByteArray() zos.writeEntry( name = relocatedPath, preserveLastModified = preserveFileTimestamps, @@ -152,22 +153,6 @@ internal fun generateSourcesJar( gradleError("Could not create shadowed sources JAR '$sourcesJarFile'.", e) } -/** - * Remaps source content without assuming its encoding. UTF-8 is tried first, and other encodings - * fall back to ISO-8859-1, which maps each byte to a char losslessly, while package names to - * relocate are ASCII. Bytes are kept as-is if nothing is relocated. - */ -private fun Iterable.remapSourceBytes(bytes: ByteArray): ByteArray { - val (text, charset) = - try { - Charsets.UTF_8.newDecoder().decode(ByteBuffer.wrap(bytes)).toString() to Charsets.UTF_8 - } catch (_: CharacterCodingException) { - String(bytes, Charsets.ISO_8859_1) to Charsets.ISO_8859_1 - } - val transformedText = remapSource(text) - return if (transformedText == text) bytes else transformedText.toByteArray(charset) -} - internal fun isUnused( canonicalPath: String, unusedClasses: Set, diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt index a943c8bdd2..937ed96d0a 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt @@ -46,54 +46,6 @@ class ShadowedSourcesJarTest { } } - @Test - fun nonUtf8SourceBytesArePreserved() { - val latin1 = StandardCharsets.ISO_8859_1 - val srcDir = tempDir.resolve("src").createDirectories() - srcDir - .resolve("foo/Main.java") - .createParentDirectories() - .writeBytes("package foo;\nclass Main { String s = \"café\"; }".toByteArray(latin1)) - srcDir - .resolve("bar/Other.java") - .createParentDirectories() - .writeBytes("package bar;\nclass Other { String s = \"naïve\"; }".toByteArray(latin1)) - - val depSourcesJar = tempDir.resolve("dep-sources.jar") - depSourcesJar - .toFile() - .createZipOutputStream( - entryCompression = ZipEntryCompression.STORED, - isZip64 = false, - encoding = null, - ) - .use { zos -> - zos.writeEntry("dep/Dep.java") { - write("package dep;\npublic class Dep { String s = \"über\"; }".toByteArray(latin1)) - } - } - - val outputJar = tempDir.resolve("output-sources.jar") - generateSourcesJar( - sourcesJarFile = outputJar.toFile(), - sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), - includedSourcesJars = listOf(depSourcesJar.toFile()), - relocators = - listOf(SimpleRelocator("foo", "shaded.foo"), SimpleRelocator("dep", "shaded.dep")), - ) - - assertThat(JarPath(outputJar)).useAll { - getBytes("shaded/foo/Main.java") - .isEqualTo("package shaded.foo;\nclass Main { String s = \"café\"; }".toByteArray(latin1)) - getBytes("bar/Other.java") - .isEqualTo("package bar;\nclass Other { String s = \"naïve\"; }".toByteArray(latin1)) - getBytes("shaded/dep/Dep.java") - .isEqualTo( - "package shaded.dep;\npublic class Dep { String s = \"über\"; }".toByteArray(latin1) - ) - } - } - @Test fun isUnusedMatching() { val unusedSet = From 10db2bb069181924f8c6d96a5d25097faa067bb9 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:21:48 +0800 Subject: [PATCH 105/117] Revert "Align ShadowSourcesPublishArtifact with ArchivePublishArtifact" This reverts commit 320e4bbda9147d8b9a12a8c89d8722a2619ee07c. --- .../gradle/plugins/shadow/PublishingTest.kt | 33 ------------------- .../gradle/plugins/shadow/ShadowJavaPlugin.kt | 14 ++------ .../plugins/shadow/ShadowPropertiesTest.kt | 3 -- 3 files changed, 3 insertions(+), 47 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 9d5c25dbb3..bb02854070 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 @@ -375,39 +375,6 @@ class PublishingTest : BasePluginTest() { .isEqualTo("maven-1.0-shaded-sources.jar") } - @Test - fun dontPublishShadowJarAndSourcesWhenShadowJarDisabled() { - projectScript.appendText( - publishConfiguration( - projectBlock = - """ - |java { - | withSourcesJar() - |} - """ - .trimMargin(), - shadowBlock = - """ - |enabled = false - """ - .trimMargin(), - publicationsBlock = - """ - |shadow(MavenPublication) { - | from components.shadow - |} - """ - .trimMargin(), - ) - ) - - publish() - - val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries) - .containsOnly(*withChecksums("maven-1.0.module", "maven-1.0.pom")) - } - @Test fun publishJavaComponentWithShadowAndSourcesVariants() { projectScript.appendText( diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt index 1d936bef81..bc66f8fd06 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 @@ -16,6 +16,7 @@ import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.api.artifacts.ConfigurationContainer import org.gradle.api.artifacts.ConsumableConfiguration +import org.gradle.api.artifacts.PublishArtifact import org.gradle.api.attributes.Bundling import org.gradle.api.attributes.Category import org.gradle.api.attributes.DocsType @@ -241,15 +242,9 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } // TODO: https://github.com/gradle/gradle/issues/31474 -// Mirrors Gradle's `ArchivePublishArtifact`, which is used for the shadowed JAR. -@Suppress("InternalGradleApiUsage") // For skipping publishing when the task is disabled. private class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvider) : - org.gradle.api.internal.artifacts.PublishArtifactInternal { - override fun getName(): String { - val baseName = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() - val appendix = shadowJarTask.flatMap { it.archiveAppendix }.orNull - return if (appendix.isNullOrEmpty()) baseName else "$baseName-$appendix" - } + PublishArtifact { + override fun getName(): String = shadowJarTask.flatMap { it.archiveBaseName }.orNull.orEmpty() override fun getExtension(): String = shadowJarTask.flatMap { it.archiveExtension }.orNull ?: "jar" @@ -268,7 +263,4 @@ private class ShadowSourcesPublishArtifact(private val shadowJarTask: TaskProvid override fun getBuildDependencies(): TaskDependency = TaskDependency { setOf(@Suppress("EagerGradleConfiguration") shadowJarTask.get()) } - - override fun shouldBePublished(): Boolean = - @Suppress("EagerGradleConfiguration") shadowJarTask.get().enabled } 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 db8b4266cd..ab3c576ae6 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 @@ -216,9 +216,6 @@ class ShadowPropertiesTest { shadowJarTask.archiveExtension.set("zip") assertThat(name).isEqualTo("renamed") assertThat(extension).isEqualTo("zip") - - shadowJarTask.archiveAppendix.set("appendix") - assertThat(name).isEqualTo("renamed-appendix") } } From 008d6d53b2d4bcbeff05dd139abc775a3874a398 Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:27:06 +0800 Subject: [PATCH 106/117] Correct shadowed sources JAR docs and document its limitations Also fix the broken blockquote in the R8 shadowed sources JAR note. --- docs/configuration/minimizing/README.md | 7 +++---- docs/publishing/README.md | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 904f0e7772..fc798d18be 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -135,10 +135,9 @@ rules published in dependency JARs, for example under `META-INF/proguard`. > **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. +> (`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. diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 94951b332b..71470e7ebd 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -589,9 +589,12 @@ 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. +- Relocated paths for source files laid out in their package directories. Other source files keep their original + relative paths, such as Kotlin Multiplatform (KMP) dependency sources under `commonMain/` or `jvmMain/`. +- Automatic filtering: dependencies excluded in `dependencies { exclude(...) }` are excluded from the shadowed sources + JAR as well. When minimizing with the default dependency analyzer (`minimize()`), source files of removed classes are + also excluded, as long as they are laid out in their package directories. Minimizing with R8 does not filter the + shadowed sources JAR. ### Publishing with `withSourcesJar()` @@ -849,6 +852,16 @@ To publish shadowed artifacts as the primary publication: > become inputs of `shadowJar`, and any change to them (even a comment that doesn't affect the compiled classes) > reruns `shadowJar` and rebuilds the shadowed JAR as well. +> [!NOTE] +> The shadowed sources JAR has a few limitations compared to the shadowed JAR: +> +> - `include(...)` and `exclude(...)` patterns configured on `shadowJar` are not applied to the shadowed sources JAR, +> so sources of excluded classes are still included. +> - When multiple source files share the same path, only the first one is kept, regardless of `duplicatesStrategy` and +> `failOnDuplicateEntries`. Project sources are added first, then dependency sources. For example, in KMP projects, +> `commonMain/kotlin/foo/Foo.kt` and `jvmMain/kotlin/foo/Foo.kt` (such as `expect` and `actual` declarations) share +> the path `foo/Foo.kt`, and KMP dependencies may share paths like `commonMain/Annotations.kt`. + ### Customizing the Sources Archive File The companion shadowed sources JAR output location is configured via From 1782d8ccaf9763a6b44266e4a1f075843d71bcea Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:48:18 +0800 Subject: [PATCH 107/117] Clean up ShadowPropertiesTest --- .../plugins/shadow/ShadowPropertiesTest.kt | 31 +++++++++---------- 1 file changed, 15 insertions(+), 16 deletions(-) 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 ab3c576ae6..06688cac3e 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 @@ -179,24 +179,23 @@ class ShadowPropertiesTest { plugins.apply(JavaPlugin::class.java) javaPluginExtension.withSourcesJar() val shadowJarTask = tasks.shadowJar.get() - assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() - assertThat(shadowJarTask.archiveSourcesFile.get().asFile).all { - isEqualTo( - shadowJarTask.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")) + with(shadowJarTask) { + assertThat(generateSourcesJar.get()).isTrue() + 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(outputs.files.files) + .containsOnly( + archiveFile.get().asFile, + archiveSourcesFile.get().asFile, + ) + val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") + assertThat(sourceSetsSourceDirs.files) + .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) } - assertThat(shadowJarTask.outputs.files.files) - .containsOnly( - shadowJarTask.archiveFile.get().asFile, - shadowJarTask.archiveSourcesFile.get().asFile, - ) - val mainSourceSet = javaPluginExtension.sourceSets.getByName("main") - assertThat(shadowJarTask.sourceSetsSourceDirs.files) - .containsOnly(*mainSourceSet.allSource.files.toTypedArray()) - val shadowSourcesElements = configurations.shadowSourcesElements.get() - with(shadowSourcesElements.artifacts.single()) { + with(project.configurations.shadowSourcesElements.get().artifacts.single()) { assertThat(classifier).isEqualTo("all-sources") assertThat(name).isEqualTo("my-project") assertThat(extension).isEqualTo("jar") From 6c2e7feb5acc96a287bac84f37e2f88dd9157bbf Mon Sep 17 00:00:00 2001 From: Goooler Date: Thu, 24 Sep 2026 23:55:46 +0800 Subject: [PATCH 108/117] Exclude module-info.java from shadowed sources JAR --- .../shadow/internal/ShadowedSourcesJar.kt | 6 +++ .../shadow/internal/ShadowedSourcesJarTest.kt | 39 +++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt index b02c164ce6..dce7ff5244 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt @@ -51,6 +51,7 @@ internal fun generateSourcesJar( } for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { + if (relPath.isModuleInfo) continue val isSource = relPath.isSourceFile() if (isSource) { if (isUnused(relPath, unusedClasses, sourceToClasses)) continue @@ -94,6 +95,7 @@ internal fun generateSourcesJar( val name = entry.name if ( name == "META-INF/MANIFEST.MF" || + name.isModuleInfo || name.endsWith(".class") || name.startsWith("META-INF/INDEX.LIST") || (name.startsWith("META-INF/") && @@ -233,3 +235,7 @@ private fun buildSourceToClassesMap( return sourceToClasses } + +// Aligned with the default exclusion of `module-info.class` from the shadowed JAR. +private val String.isModuleInfo: Boolean + get() = substringAfterLast('/') == "module-info.java" diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt index 937ed96d0a..2565219a54 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt @@ -20,6 +20,7 @@ import kotlin.io.path.createDirectories import kotlin.io.path.createParentDirectories import kotlin.io.path.writeBytes import kotlin.io.path.writeText +import org.apache.tools.zip.ZipOutputStream import org.gradle.api.file.FileCollection import org.gradle.api.tasks.bundling.ZipEntryCompression import org.junit.jupiter.api.Test @@ -146,6 +147,44 @@ class ShadowedSourcesJarTest { } } + @Test + fun excludesModuleInfo() { + val srcDir = tempDir.resolve("src").createDirectories() + srcDir.resolve("module-info.java").writeText("module my.module {}") + srcDir.resolve("com/example/Main.java").apply { + createParentDirectories() + writeText("package com.example;\npublic class Main {}") + } + + val depSourcesJar = tempDir.resolve("dep-sources.jar") + ZipOutputStream(depSourcesJar.toFile()).use { zos -> + zos.writeEntry("module-info.java") { write("module dep {}".toByteArray()) } + zos.writeEntry("jvmMain/module-info.java") { write("module dep.jvm {}".toByteArray()) } + zos.writeEntry("dep/Dep.java") { + write("package dep;\npublic class Dep {}".toByteArray()) + } + } + + val outputJar = tempDir.resolve("output-sources.jar") + generateSourcesJar( + sourcesJarFile = outputJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + includedSourcesJars = listOf(depSourcesJar.toFile()), + ) + + assertThat(JarPath(outputJar)).useAll { + containsOnly( + "META-INF/MANIFEST.MF", + "com/example/Main.java", + "dep/Dep.java", + "META-INF/", + "com/", + "com/example/", + "dep/", + ) + } + } + @Test fun mergesAndRelocatesIncludedSourcesJar() { val srcDir = tempDir.resolve("src").createDirectories() From 82b11cc811c5b2d908e8a631fd7f7e6b6a80a419 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 00:36:16 +0800 Subject: [PATCH 109/117] Simplify and speed up shadowed sources JAR processing - Deduplicate entry writing in generateSourcesJar. - Remove the redundant slash branch in extractSourceSubpatterns, as converting a pattern and its prefix to dot or slash notation keeps `startsWith` the same. - Remove the unused default offset from matchesSubpattern. - Skip unrelated sources early in SimpleRelocator.applyToSourceContent, avoiding building subpatterns and regexes for sources that don't reference the pattern. Remapping 300 Guava sources with 1001 relocators went from ~25s to ~1.5s. --- .../shadow/internal/ShadowedSourcesJar.kt | 109 ++++++------------ .../shadow/relocation/SimpleRelocator.kt | 36 ++---- 2 files changed, 47 insertions(+), 98 deletions(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt index dce7ff5244..9dd449fcdd 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt @@ -50,89 +50,52 @@ internal fun generateSourcesJar( } } - for ((file, relPath) in filesWithRelPaths.sortedBy { it.second }) { - if (relPath.isModuleInfo) continue - val isSource = relPath.isSourceFile() - if (isSource) { - if (isUnused(relPath, unusedClasses, sourceToClasses)) continue - val relocatedPath = relocators.relocateSourcePath(relPath) - if (visitedFiles.add(relocatedPath)) { - val text = file.readText() - val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray() - 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) - } + // Relocates and writes a project or dependency source file, or a resource. + fun addEntry(name: String, lastModified: Long, readBytes: () -> ByteArray) { + if (name.isModuleInfo) return + val isSource = name.isSourceFile() + if (isSource && isUnused(name, unusedClasses, sourceToClasses)) return + val relocatedPath = + if (isSource) relocators.relocateSourcePath(name) else relocators.relocatePath(name) + if (!visitedFiles.add(relocatedPath)) return + val bytes = + if (isSource) { + relocators.remapSource(readBytes().decodeToString()).toByteArray() + } else { + readBytes() } + zos.writeEntry( + name = relocatedPath, + preserveLastModified = preserveFileTimestamps, + lastModified = lastModified, + unixMode = UnixMode.file(), + ) { + write(bytes) } } + filesWithRelPaths + .sortedBy { it.second } + .forEach { (file, relPath) -> + addEntry(relPath, file.lastModified()) { file.readBytes() } + } + sourcesJars.forEach { jarFile -> jarFile.useZip { entries() .toList() - .filterNot { it.isDirectory } - .sortedBy { it.name } - .forEach { entry -> + .filterNot { entry -> val name = entry.name - if ( + entry.isDirectory || name == "META-INF/MANIFEST.MF" || - name.isModuleInfo || - 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 = name.isSourceFile() - if (isSource) { - if (isUnused(name, unusedClasses, sourceToClasses)) return@forEach - val relocatedPath = relocators.relocateSourcePath(name) - if (visitedFiles.add(relocatedPath)) { - val text = getInputStream(entry).bufferedReader().use { it.readText() } - val transformedText = relocators.remapSource(text) - val bytes = transformedText.toByteArray() - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(bytes) - } - } - } else { - val relocatedPath = relocators.relocatePath(name) - if (visitedFiles.add(relocatedPath)) { - zos.writeEntry( - name = relocatedPath, - preserveLastModified = preserveFileTimestamps, - lastModified = entry.time, - unixMode = UnixMode.file(), - ) { - write(getInputStream(entry).use { it.readBytes() }) - } - } - } + name.endsWith(".class") || + name.startsWith("META-INF/INDEX.LIST") || + (name.startsWith("META-INF/") && + (name.endsWith(".SF") || name.endsWith(".DSA") || name.endsWith(".RSA"))) + } + .sortedBy { it.name } + .forEach { entry -> + addEntry(entry.name, entry.time) { getInputStream(entry).use { it.readBytes() } } } } } 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 fa11c6e353..b6e689967f 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 @@ -108,6 +108,8 @@ constructor( override fun applyToSourceContent(sourceContent: String): String { if (rawString || pattern.isEmpty()) return sourceContent + // Fast path to skip building subpatterns and regexes for unrelated sources. + if (pattern !in sourceContent && pathPattern !in sourceContent) return sourceContent val sourceIncludes = extractSourceSubpatterns(includes, pattern) val sourceExcludes = extractSourceSubpatterns(excludes, pattern) // Relocate package and class references in dot notation (e.g. "org.foo.Bar"). @@ -249,43 +251,27 @@ constructor( return result.toString() } + /** + * Extracts the parts of [patterns] after [patternPrefix] in both dot and slash notations, as + * source contents may reference classes (e.g. "org.foo.Bar") or paths (e.g. "org/foo/Bar"). + */ fun extractSourceSubpatterns(patterns: Set, patternPrefix: String): Set { if (patternPrefix.isEmpty()) return emptySet() val result = mutableSetOf() val dotPrefix = patternPrefix.replace('/', '.') - val slashPrefix = patternPrefix.replace('.', '/') val trailingWildcardRegex = "[./][*]+$".toRegex() for (pat in patterns) { val dotPat = pat.replace('/', '.') - if (dotPat.startsWith(dotPrefix)) { - val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") - if (sub.isEmpty()) { - result.add("") - } else { - result.add(sub) - result.add(sub.replace('.', '/')) - } - } - val slashPat = pat.replace('.', '/') - if (slashPat.startsWith(slashPrefix)) { - val sub = slashPat.substring(slashPrefix.length).replaceFirst(trailingWildcardRegex, "") - if (sub.isEmpty()) { - result.add("") - } else { - result.add(sub) - result.add(sub.replace('/', '.')) - } - } + if (!dotPat.startsWith(dotPrefix)) continue + val sub = dotPat.substring(dotPrefix.length).replaceFirst(trailingWildcardRegex, "") + result.add(sub) + result.add(sub.replace('.', '/')) } return result } - fun matchesSubpattern( - content: CharSequence, - offset: Int = 0, - subpattern: String, - ): Boolean { + fun matchesSubpattern(content: CharSequence, offset: Int, subpattern: String): Boolean { val subLen = subpattern.length if (offset + subLen > content.length) return false for (i in 0 until subLen) { From 1bf1107b78d73a080aeafbcbabf41ceb121a804d Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 00:07:34 +0800 Subject: [PATCH 110/117] Relocate leading-slash resource paths in shadowed sources References like `getResource("/org/foo/x.properties")` are relocated in class files, as `canRelocatePath` strips the leading slash, but were kept as-is in the shadowed sources. --- .../shadow/relocation/SimpleRelocator.kt | 11 +++++++-- .../shadow/relocation/SimpleRelocatorTest.kt | 24 +++++++++++++++++++ 2 files changed, 33 insertions(+), 2 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 b6e689967f..5dee1b9949 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 @@ -305,8 +305,15 @@ constructor( if (prevChar == '/') { val beforeSlash = if (prevIndex > 0) sourceContent[prevIndex - 1] else null // Only reject if pattern does not contain '.' and the slash is an actual path delimiter - // (not closing a block comment '*/' or a single-line comment '//') - if (!pattern.contains('.') && beforeSlash != '*' && beforeSlash != '/') return false + // (not a leading slash in a string literal like `getResource("/org/foo/x")`, which is + // relocated in class files as well, nor closing a block comment '*/' or a single-line + // comment '//') + val isLeadingSlash = beforeSlash == null || beforeSlash == '"' || beforeSlash == '\'' + if ( + !pattern.contains('.') && !isLeadingSlash && beforeSlash != '*' && beforeSlash != '/' + ) { + return false + } } } diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt index c0bc5e0758..3cfb4ca28c 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocatorTest.kt @@ -440,6 +440,30 @@ class SimpleRelocatorTest { @JvmStatic fun sourceRelocationProvider(): List = listOf( + Arguments.of( + "resource paths with leading slash", + SimpleRelocator("org.foo", "shaded.org.foo"), + """ + |String a = "org/foo/a.properties"; + |String b = "/org/foo/b.properties"; + |getClass().getResource("/org/foo/c.properties"); + |String d = '/org/foo/d.properties'; + |String e = "com/example/org/foo/e.properties"; + |String f = "../org/foo/f.properties"; + |String g = "my-lib/org/foo/g.properties"; + """ + .trimMargin(), + """ + |String a = "shaded/org/foo/a.properties"; + |String b = "/shaded/org/foo/b.properties"; + |getClass().getResource("/shaded/org/foo/c.properties"); + |String d = '/shaded/org/foo/d.properties'; + |String e = "com/example/org/foo/e.properties"; + |String f = "../org/foo/f.properties"; + |String g = "my-lib/org/foo/g.properties"; + """ + .trimMargin(), + ), Arguments.of( "prefix collision with included class", SimpleRelocator( From 84222701bc304aff13c88195eae8e49c7523a34a Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 00:33:01 +0800 Subject: [PATCH 111/117] Keep package declarations consistent with relocated source paths With class-level includes or excludes, a source file's path is relocated per class like class files, but its `package` declaration can't match those patterns. An excluded class kept its path but got a relocated package, and an included class got the opposite. Set the declaration from the relocated path for sources laid out in their package directories. --- .../shadow/internal/ShadowedSourcesJar.kt | 4 +- .../plugins/shadow/internal/SourceRemapper.kt | 25 +++++++++++ .../shadow/internal/ShadowedSourcesJarTest.kt | 44 +++++++++++++++++++ 3 files changed, 72 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt index 9dd449fcdd..595cd23eb3 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJar.kt @@ -60,7 +60,9 @@ internal fun generateSourcesJar( if (!visitedFiles.add(relocatedPath)) return val bytes = if (isSource) { - relocators.remapSource(readBytes().decodeToString()).toByteArray() + relocators + .remapSourceFile(readBytes().decodeToString(), name, relocatedPath) + .toByteArray() } else { readBytes() } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt index e1af945e92..be0a792ac8 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourceRemapper.kt @@ -13,6 +13,26 @@ internal fun Iterable.remapSource(sourceContent: String): String { return content } +/** + * Remaps the content of the source file at [path], which is relocated to [relocatedPath]. + * + * If the file is laid out in its package directory, its `package` declaration is set to match + * [relocatedPath], which is decided per class like class files, as the declaration alone can't + * match class-level includes and excludes. + */ +internal fun Iterable.remapSourceFile( + sourceContent: String, + path: String, + relocatedPath: String, +): String { + val content = remapSource(sourceContent) + val originalPackage = packageRegex.find(sourceContent)?.groupValues?.get(1) ?: return content + val relocatedPackage = relocatedPath.packageOfPath + if (originalPackage != path.packageOfPath || relocatedPackage.isEmpty()) return content + val packageRange = packageRegex.find(content)?.groups?.get(1)?.range ?: return content + return content.replaceRange(packageRange, relocatedPackage) +} + /** * Relocates a source file path by stripping its extension before matching against class/path * relocators, ensuring class-level include/exclude patterns work symmetrically with binary classes. @@ -43,3 +63,8 @@ internal fun Iterable.relocateSourcePath(path: String): String { internal fun String.isSourceFile(): Boolean { return endsWith(".java") || endsWith(".kt") || endsWith(".groovy") || endsWith(".scala") } + +private val packageRegex = Regex("""^\s*package\s+([\w.]+)""", RegexOption.MULTILINE) + +private val String.packageOfPath: String + get() = substringBeforeLast('/', "").replace('/', '.') diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt index 2565219a54..5b4cb50116 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/ShadowedSourcesJarTest.kt @@ -147,6 +147,50 @@ class ShadowedSourcesJarTest { } } + @Test + fun packageDeclarationFollowsClassIncludesAndExcludes() { + val srcDir = tempDir.resolve("src").createDirectories() + srcDir.resolve("org/foo/Bar.java").apply { + createParentDirectories() + writeText("package org.foo;\npublic class Bar {}") + } + srcDir.resolve("org/foo/Baz.java").apply { + createParentDirectories() + writeText("package org.foo;\nimport org.foo.Bar;\npublic class Baz {}") + } + // Not laid out in its package directory, so its package declaration is left as-is. + srcDir.resolve("Flat.kt").writeText("package org.foo\nclass Flat") + + val excludeJar = tempDir.resolve("exclude-sources.jar") + generateSourcesJar( + sourcesJarFile = excludeJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + relocators = + listOf(SimpleRelocator("org.foo", "shaded.org.foo", excludes = listOf("org.foo.Bar"))), + ) + assertThat(JarPath(excludeJar)).useAll { + getContent("org/foo/Bar.java").isEqualTo("package org.foo;\npublic class Bar {}") + getContent("shaded/org/foo/Baz.java") + .isEqualTo("package shaded.org.foo;\nimport org.foo.Bar;\npublic class Baz {}") + getContent("Flat.kt").isEqualTo("package shaded.org.foo\nclass Flat") + } + + val includeJar = tempDir.resolve("include-sources.jar") + generateSourcesJar( + sourcesJarFile = includeJar.toFile(), + sourceSetsSourceDirs = testObjectFactory.fileCollection().from(srcDir), + relocators = + listOf(SimpleRelocator("org.foo", "shaded.org.foo", includes = listOf("org.foo.Bar"))), + ) + assertThat(JarPath(includeJar)).useAll { + getContent("shaded/org/foo/Bar.java") + .isEqualTo("package shaded.org.foo;\npublic class Bar {}") + getContent("org/foo/Baz.java") + .isEqualTo("package org.foo;\nimport shaded.org.foo.Bar;\npublic class Baz {}") + getContent("Flat.kt").isEqualTo("package org.foo\nclass Flat") + } + } + @Test fun excludesModuleInfo() { val srcDir = tempDir.resolve("src").createDirectories() From cb62c0347cb9661fc121346152c4be7c8cffdaa5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 01:18:52 +0800 Subject: [PATCH 112/117] Publish shadowed artifacts via a dedicated publication in KMP docs KGP's `jvm` publication publishes the non-shadowed JAR, so attaching the shadowed sources JAR to it would publish mismatched sources and may clash with KGP's own sources classifier. Publish the shadowed JAR and its sources together in a dedicated publication instead. --- docs/publishing/README.md | 32 ++++++++++++------- .../gradle/plugins/shadow/PublishingTest.kt | 1 - 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 71470e7ebd..c93a80cae8 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -915,7 +915,9 @@ You can also customize the source inputs included in the companion sources JAR u ### 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: +the `jvm` publication), which publish the original, non-shadowed artifacts. Shadow doesn't publish the shadowed JAR or +the shadowed sources JAR automatically in KMP projects, so create a dedicated publication for them. Don't attach the +shadowed sources JAR to KGP's `jvm` publication, as it would not match the non-shadowed JAR published there: === ":material-language-kotlin: build.gradle.kts" @@ -930,13 +932,17 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the jvm() } + val shadowJar = tasks.named("shadowJar") { + archiveClassifier = "" + } + publishing { publications { - withType().configureEach { - if (name == "jvm") { - artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) { - classifier = "sources" - } + create("shadow") { + artifactId = "my-library-all" + artifact(shadowJar) + artifact(shadowJar.flatMap { it.archiveSourcesFile }) { + classifier = "sources" } } } @@ -959,13 +965,17 @@ the `jvm` publication). You can attach the shadowed sources JAR artifact to the jvm() } + def shadowJar = tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { + archiveClassifier = '' + } + publishing { publications { - withType(MavenPublication).configureEach { - if (name == 'jvm') { - artifact(tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar).flatMap { it.archiveSourcesFile }) { - classifier = 'sources' - } + shadow(MavenPublication) { + artifactId = 'my-library-all' + artifact(shadowJar) + artifact(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 bb02854070..949bd6c055 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 @@ -829,7 +829,6 @@ class PublishingTest : BasePluginTest() { |} |$shadowJarTask { | archiveClassifier = '' - | generateSourcesJar = true |} |publishing { | repositories { From 140a35985cec16b430890c70963ce0dfe20e4314 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 01:28:23 +0800 Subject: [PATCH 113/117] Remove publishing shadowed JAR from java component as replacement Keeping `jar` enabled while `shadowJar` writes the same file in `build/libs` has no ordering between them, so the published JAR depended on which task finished last, which made its functional test flaky. The overlapping outputs also disable caching for both tasks. Keep publishing from the `shadow` component as the only documented replacement approach. --- docs/publishing/README.md | 88 +------------------ .../gradle/plugins/shadow/PublishingTest.kt | 65 -------------- 2 files changed, 3 insertions(+), 150 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index c93a80cae8..6af1ceb089 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -681,11 +681,9 @@ coordinate collisions within the same publication, the shadowed sources variant When configuring `shadowJar` to replace the standard JAR (`archiveClassifier = ""`), the companion shadowed sources JAR automatically uses the standard `sources` classifier. -To publish shadowed artifacts as the primary publication: - -1. **Publish from `components["shadow"]` (Recommended)**: Publish the `shadow` component directly in your Maven - publication, and disable standard archive tasks to prevent destination file collisions in `build/libs` (disabling - `sourcesJar` is only needed when `java.withSourcesJar()` is enabled): +To publish shadowed artifacts as the primary publication, publish the `shadow` component directly in your Maven +publication, and disable standard archive tasks to prevent destination file collisions in `build/libs` (disabling +`sourcesJar` is only needed when `java.withSourcesJar()` is enabled): === ":material-language-kotlin: build.gradle.kts" @@ -757,86 +755,6 @@ To publish shadowed artifacts as the primary publication: } ``` -2. **Publish from `components["java"]`**: If publishing `from(components["java"])`, Gradle ties the component's - primary artifact to the `jar` task, so `tasks.jar` must remain enabled (the `shadowJar` task will overwrite its output - in `build/libs`). To avoid publishing standard variants alongside shadowed ones in Gradle Module Metadata and POM, - you must explicitly skip them using `withVariantsFromConfiguration` (skipping `sourcesElements` is only needed when - `java.withSourcesJar()` is enabled, after which `sourcesJar` can also be disabled): - -=== ":material-language-kotlin: build.gradle.kts" - - ```kotlin - plugins { - java - `maven-publish` - id("com.gradleup.shadow") - } - - java { - withSourcesJar() - } - - // Only needed when java.withSourcesJar() is enabled. - tasks.named("sourcesJar") { - enabled = false - } - - tasks.shadowJar { - archiveClassifier = "" - } - - components.named("java") { - withVariantsFromConfiguration(configurations["runtimeElements"]) { skip() } - // Only needed when java.withSourcesJar() is enabled. - withVariantsFromConfiguration(configurations["sourcesElements"]) { skip() } - } - - publishing { - publications { - create("shadow") { - from(components["java"]) - } - } - } - ``` - -=== ":simple-apachegroovy: build.gradle" - - ```groovy - plugins { - id 'java' - id 'maven-publish' - id 'com.gradleup.shadow' - } - - java { - withSourcesJar() - } - - // Only needed when java.withSourcesJar() is enabled. - tasks.named('sourcesJar', Jar) { - enabled = false - } - - tasks.named('shadowJar', com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar) { - archiveClassifier = '' - } - - components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { - withVariantsFromConfiguration(configurations.runtimeElements) { skip() } - // Only needed when java.withSourcesJar() is enabled. - withVariantsFromConfiguration(configurations.sourcesElements) { skip() } - } - - publishing { - publications { - shadow(MavenPublication) { - from components.java - } - } - } - ``` - > [!NOTE] > Generating the companion shadowed sources JAR is controlled by [`generateSourcesJar`][ShadowJar.generateSourcesJar]. > In Java projects, it defaults to `true` when `java.withSourcesJar()` is enabled, and `false` otherwise to avoid diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 949bd6c055..451971e0e9 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 @@ -412,70 +411,6 @@ class PublishingTest : BasePluginTest() { ) } - @Test - fun publishShadowJarInsteadOfJarFromJavaComponent() { - projectScript.appendText( - publishConfiguration( - projectBlock = - """ - |java { - | withSourcesJar() - |} - |$sourcesJarTask { - | enabled = false - |} - |components.named('java', org.gradle.api.component.AdhocComponentWithVariants) { - | withVariantsFromConfiguration(configurations.runtimeElements) { skip() } - | withVariantsFromConfiguration(configurations.sourcesElements) { skip() } - |} - """ - .trimMargin(), - shadowBlock = - """ - |archiveClassifier = '' - """ - .trimMargin(), - publicationsBlock = - """ - |shadow(MavenPublication) { - | from components.java - |} - """ - .trimMargin(), - ) - ) - - publish() - - val artifactRoot = "my/maven/1.0" - assertThat(repoPath(artifactRoot).entries) - .containsOnly( - *withChecksums( - "maven-1.0.jar", - "maven-1.0-sources.jar", - "maven-1.0.module", - "maven-1.0.pom", - ) - ) - assertShadowJarCommon("$artifactRoot/maven-1.0.jar") - assertThat(repoPom("$artifactRoot/maven-1.0.pom")).all { - transform { it.dependencies.map(Dependency::coordinate) }.containsOnly("my:b:1.0") - transform { it.dependencies.map { dep -> dep.scope to dep.isOptional } } - .single() - .isEqualTo("compile" to true) - } - repoGmm("$artifactRoot/maven-1.0.module").let { gmm -> - assertThat(gmm.variantNames) - .containsOnly( - API_ELEMENTS_CONFIGURATION_NAME, - SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME, - SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME, - ) - assertShadowVariantCommon(gmm) - assertShadowSourcesVariantCommon(gmm) - } - } - @Test fun dontPublishSourcesWhenGenerateSourcesJarDisabled() { projectScript.appendText( From 700f7a346467b22905455fe3e391e4f0f2d7ffb5 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 01:30:17 +0800 Subject: [PATCH 114/117] Enable sources JAR only in multi-project tests that need it Add a `withSourcesJar` parameter to writeClientAndServerModules instead of always enabling `withSourcesJar()`, so unrelated multi-project tests keep covering the default path without generating shadowed sources JARs. --- .../gradle/plugins/shadow/BasePluginTest.kt | 25 +++++++++++++------ .../gradle/plugins/shadow/FilteringTest.kt | 9 ++++--- .../gradle/plugins/shadow/JavaPluginsTest.kt | 2 +- 3 files changed, 25 insertions(+), 11 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 0c82c8893f..51ccd215d3 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 @@ -263,7 +263,22 @@ abstract class BasePluginTest { return "$basePath.class" } - fun writeClientAndServerModules(clientShadowed: Boolean = false, serverShadowBlock: String = "") { + fun writeClientAndServerModules( + clientShadowed: Boolean = false, + serverShadowBlock: String = "", + withSourcesJar: Boolean = false, + ) { + val javaBlock = + if (withSourcesJar) { + """ + |java { + | withSourcesJar() + |} + """ + .trimMargin() + } else { + "" + } settingsScript.appendText( """ |include 'client', 'server' @@ -284,9 +299,7 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} - |java { - | withSourcesJar() - |} + |$javaBlock |dependencies { | implementation 'junit:junit:3.8.2' |} @@ -308,9 +321,7 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} - |java { - | withSourcesJar() - |} + |$javaBlock |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 003a336af6..9e81055740 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 @@ -137,7 +137,8 @@ class FilteringTest : BasePluginTest() { | exclude($clientProject) |} """ - .trimMargin() + .trimMargin(), + withSourcesJar = true, ) runWithSuccess(serverShadowJarPath) @@ -154,7 +155,8 @@ class FilteringTest : BasePluginTest() { | exclude(project(':client')) |} """ - .trimMargin() + .trimMargin(), + withSourcesJar = true, ) path("client/build.gradle").appendText("version = '1.0.0+1'") @@ -172,7 +174,8 @@ class FilteringTest : BasePluginTest() { | exclude { it.moduleGroup == 'junit' } |} """ - .trimMargin() + .trimMargin(), + withSourcesJar = true, ) runWithSuccess(serverShadowJarPath) 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 74e4abafad..bde9f0b3ed 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 @@ -146,7 +146,7 @@ class JavaPluginsTest : BasePluginTest() { @Test fun includeProjectDependencies() { - writeClientAndServerModules() + writeClientAndServerModules(withSourcesJar = true) runWithSuccess(serverShadowJarPath) From 7ff014ef7eec33241cf2c91a758bbf08100deebc Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 01:30:33 +0800 Subject: [PATCH 115/117] Remove unused artifactGJar from BasePluginTest --- .../github/jengelman/gradle/plugins/shadow/BasePluginTest.kt | 4 ---- 1 file changed, 4 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 51ccd215d3..a451b51a53 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 @@ -60,9 +60,6 @@ abstract class BasePluginTest { lateinit var artifactBJar: Path private set - lateinit var artifactGJar: Path - private set - val projectScript: Path get() = path("build.gradle") @@ -90,7 +87,6 @@ abstract class BasePluginTest { 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 From 8ec4e185170bd519a99312b8ca6375af02353333 Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 01:36:41 +0800 Subject: [PATCH 116/117] Polish publishing documentation --- docs/publishing/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 6af1ceb089..c2e1283820 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -575,8 +575,8 @@ 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`. +Since 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 @@ -770,7 +770,7 @@ publication, and disable standard archive tasks to prevent destination file coll > become inputs of `shadowJar`, and any change to them (even a comment that doesn't affect the compiled classes) > reruns `shadowJar` and rebuilds the shadowed JAR as well. -> [!NOTE] +> [!WARNING] > The shadowed sources JAR has a few limitations compared to the shadowed JAR: > > - `include(...)` and `exclude(...)` patterns configured on `shadowJar` are not applied to the shadowed sources JAR, From a6a6468b3abc24abaf5f925dc3faf6308492b09b Mon Sep 17 00:00:00 2001 From: Goooler Date: Fri, 25 Sep 2026 01:37:15 +0800 Subject: [PATCH 117/117] Use trimMargin for multiline string in JavaPluginsTest --- .../jengelman/gradle/plugins/shadow/JavaPluginsTest.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 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 bde9f0b3ed..8df3bf5d28 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 @@ -1472,10 +1472,10 @@ class JavaPluginsTest : BasePluginTest() { insert( "ext/Ext.java", """ - package ext; - public class Ext {} + |package ext; + |public class Ext {} """ - .trimIndent(), + .trimMargin(), ) }