diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc196d0d..1c5717d28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Support manifest header relocation via configurable `attributesToRelocate` property. - Allow disabling default ProGuard rules in R8 minimization with `R8Spec.useDefaultRules`. ([#2252](https://github.com/GradleUp/shadow/pull/2252)) - Allow passing classpath files to R8 minimization with `R8Spec.classpath`. ([#2255](https://github.com/GradleUp/shadow/pull/2255)) +- Support shadowed sources JAR. ([#2265](https://github.com/GradleUp/shadow/pull/2265)) ### Changed diff --git a/api/shadow.api b/api/shadow.api index 5ce64b4d7..4428364a4 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 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; @@ -267,7 +270,9 @@ 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; public fun getJavaLauncher ()Lorg/gradle/api/provider/Property; public fun getMainClass ()Lorg/gradle/api/provider/Property; @@ -279,6 +284,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/build.gradle.kts b/build.gradle.kts index bb05feec5..b5de68aba 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -140,6 +140,7 @@ dependencies { testPluginRuntimeOnly(libs.foojayResolver) testPluginRuntimeOnly(libs.pluginPublish) + testPluginRuntimeOnly(libs.dokka) lintChecks(libs.androidx.gradlePluginLints) } diff --git a/docs/configuration/minimizing/README.md b/docs/configuration/minimizing/README.md index 094136074..5a1f3acad 100644 --- a/docs/configuration/minimizing/README.md +++ b/docs/configuration/minimizing/README.md @@ -107,6 +107,16 @@ rules published in dependency JARs, for example under `META-INF/proguard`. > Alternatively, if you use [R8 Repackaging][r8-repackaging] (e.g. `-repackageclasses`), R8 applies embedded rules > natively without needing rule rewriting. +> [!NOTE] +> **Shadowed Sources JAR and R8** +> +> R8 operates directly on compiled JVM bytecode rather than source code. When minimizing with R8 (`minimize { r8 { ... } }`), +> Shadow cannot determine which source files correspond to classes removed by R8. Therefore, the shadowed sources JAR will +> contain all relocated source files without responding to R8 shrinking results. +> +> If you need unused source files to be filtered out of the shadowed sources JAR, use the default dependency analyzer +> minimization (`minimize()`) instead. + === ":material-language-kotlin: build.gradle.kts" ```kotlin diff --git a/docs/getting-started/README.md b/docs/getting-started/README.md index f75096494..fed55c39a 100644 --- a/docs/getting-started/README.md +++ b/docs/getting-started/README.md @@ -137,8 +137,11 @@ 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 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]). + [`maven-publish`][maven-publish]), including the `shadowSourcesElements` variant when `java.withSourcesJar()` is enabled. ## ShadowJar Command Line options @@ -155,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. @@ -175,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/kotlin-plugins/README.md b/docs/kotlin-plugins/README.md index e97c39ceb..c1bac80a3 100644 --- a/docs/kotlin-plugins/README.md +++ b/docs/kotlin-plugins/README.md @@ -139,6 +139,9 @@ automatically configure additional tasks for bundling the shadowed JAR for its ` } ``` +For details on publishing shadowed artifacts and sources JAR in KMP projects, see +[Publishing with Kotlin Multiplatform (KMP)][publishing-with-kmp]. + ## Kotlin Module Metadata Remapping Kotlin module metadata (`.kotlin_module`) files contain information about package parts and facades. When relocating @@ -169,4 +172,5 @@ To explicitly apply this remapping (recommended for future compatibility), add [KotlinModuleMetadataTransformer]: ../api/shadow/com.github.jengelman.gradle.plugins.shadow.transformers/-kotlin-module-metadata-transformer/index.html [dependency-on-the-standard-library]: https://kotlinlang.org/docs/gradle-configure-project.html#dependency-on-the-standard-library [publishing-libraries]: ../publishing/README.md +[publishing-with-kmp]: ../publishing/README.md#publishing-with-kotlin-multiplatform-kmp [running-applications]: ../application-plugin/README.md diff --git a/docs/publishing/README.md b/docs/publishing/README.md index 2ec088dfd..147227b6b 100644 --- a/docs/publishing/README.md +++ b/docs/publishing/README.md @@ -515,10 +515,289 @@ 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`. + +> [!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 +> 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 + +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" + + ```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') + } + ``` + +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 +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 { + withType().configureEach { + if (name == "jvm") { + artifact(tasks.named("shadowJar").flatMap { it.archiveSourcesFile }) { + classifier = "sources" + } + } + } + } + repositories { + maven("https://repo.myorg.com") + } + } + + tasks.named("shadowJar") { + generateSourcesJar = true + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'org.jetbrains.kotlin.multiplatform' + id 'com.gradleup.shadow' + id 'maven-publish' + } + + kotlin { + jvm() + } + + 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' + } + } + } + } + repositories { + 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 + +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.com.Example` instead of `com.Example`). + +=== ":material-language-kotlin: build.gradle.kts" + + ```kotlin + tasks.javadoc { + classpath = files(tasks.shadowJar.flatMap { it.archiveFile }) + source = zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile }) + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + tasks.named('javadoc', Javadoc) { + classpath = files(tasks.named('shadowJar').flatMap { it.archiveFile }) + source = zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + } + ``` + +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("org.jetbrains.dokka") + id("com.gradleup.shadow") + } + + val extractShadowedSources = tasks.register("extractShadowedSources") { + from(zipTree(tasks.shadowJar.flatMap { it.archiveSourcesFile })) + into(layout.buildDirectory.dir("extracted-shadowed-sources")) + } + + dokka { + dokkaSourceSets.configureEach { + classpath.setFrom(tasks.shadowJar.flatMap { it.archiveFile }) + sourceRoots.setFrom(extractShadowedSources.map { it.destinationDir }) + } + } + ``` + +=== ":simple-apachegroovy: build.gradle" + + ```groovy + plugins { + id 'org.jetbrains.kotlin.jvm' + id 'org.jetbrains.dokka' + id 'com.gradleup.shadow' + } + + tasks.register('extractShadowedSources', Sync) { + from zipTree(tasks.named('shadowJar').flatMap { it.archiveSourcesFile }) + into layout.buildDirectory.dir('extracted-shadowed-sources') + } + + dokka { + dokkaSourceSets.configureEach { + classpath.from(tasks.named('shadowJar').flatMap { it.archiveFile }) + sourceRoots.from(extractShadowedSources.map { it.destinationDir }) + } + } + ``` [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.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 [maven-publish]: https://docs.gradle.org/current/userguide/publishing_maven.html [gradle-plugin-publish-docs]: https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#shadow_dependencies [configuring-output-name]: ../configuration/README.md#configuring-output-name +[dokka]: https://kotlinlang.org/docs/dokka-introduction.html diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62a63a108..323d4df3f 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,6 +3,7 @@ minGradle = "9.4.0" kotlin = "2.4.10" moshi = "1.15.2" pluginPublish = "2.1.1" +dokka = "2.2.0" [libraries] apache-ant = "org.apache.ant:ant:1.10.17" @@ -22,6 +23,7 @@ foojayResolver = "org.gradle.toolchains.foojay-resolver-convention:org.gradle.to develocity = "com.gradle:develocity-gradle-plugin:4.5.0" kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } pluginPublish = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "pluginPublish" } +dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } androidx-gradlePluginLints = "androidx.lint:lint-gradle:1.0.0" # Dummy to get renovate updates, the version is used in rootProject build.gradle with spotless. @@ -34,7 +36,7 @@ assertk = "com.willowtreeapps.assertk:assertk:0.28.1" [plugins] kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } android-lint = "com.android.lint:9.4.0" -jetbrains-dokka = "org.jetbrains.dokka:2.2.0" +jetbrains-dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" } mavenPublish = "com.vanniktech.maven.publish:0.37.0" pluginPublish = { id = "com.gradle.plugin-publish", version.ref = "pluginPublish" } spotless = "com.diffplug.spotless:8.10.1" diff --git a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt index 244bc6851..64bf20ce0 100644 --- a/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt +++ b/src/documentTest/kotlin/com/github/jengelman/gradle/plugins/shadow/SnippetExecutable.kt @@ -51,6 +51,23 @@ sealed interface SnippetExecutable { """ .trimMargin() ) + // TODO: https://github.com/Kotlin/dokka/issues/4488 + projectRoot + .resolve("gradle.properties") + .writeText( + """ + |# Dokka 2.2.0 DGPv2 is the default, but the plugin still looks up these properties dynamically. + |# Setting them here avoids cross-project property lookups that break isolated projects. + |org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled + |org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true + |org.jetbrains.dokka.experimental.gradle.pluginMode.nowarn=true + |org.jetbrains.dokka.experimental.tryK2=true + |org.jetbrains.dokka.experimental.tryK2.noWarn=true + |org.jetbrains.dokka.experimental.tryK2.nowarn=true + |org.jetbrains.dokka.internal.enableWorkaroundKT80551=true + """ + .trimMargin() + ) val pluginsBlock = """ |plugins { diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt index 02c3b5feb..0c3b31f68 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/BasePluginTest.kt @@ -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 @@ -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") @@ -73,57 +76,22 @@ 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") + val outputServerShadowedSourcesJar: JarPath + get() = jarPath("server/build/libs/server-1.0-all-sources.jar") + @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) - } - bomModule("my", "bom", "1.0") { - addDependency(a) - addDependency(b) - addDependency(c) - addDependency(d) - addDependency(e) - addDependency(f) - } - } - 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) + artifactGJar = path("my/g/1.0/g-1.0.jar", parent = localRepo.root) } @BeforeEach @@ -295,6 +263,9 @@ abstract class BasePluginTest { .writeText( """ |${getDefaultProjectBuildScript("java")} + |java { + | withSourcesJar() + |} |dependencies { | implementation 'junit:junit:3.8.2' |} @@ -316,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 e378ef552..d349a07d8 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/FilteringTest.kt @@ -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 @@ -182,6 +184,15 @@ class FilteringTest : BasePluginTest() { loadClass("server.Server") } } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly( + "client/", + "server/", + "client/Client.java", + "server/Server.java", + *manifestEntries, + ) + } } @Test @@ -228,6 +239,36 @@ class FilteringTest : BasePluginTest() { } } + @Test + fun excludeDependencyFromSourcesJar() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + | implementation 'my:h:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + | 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) @@ -242,5 +283,8 @@ class FilteringTest : BasePluginTest() { loadClass("junit.framework.Test") } } + assertThat(outputServerShadowedSourcesJar).useAll { + 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 77a13bd40..979623bd8 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,6 +3,7 @@ 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 @@ -30,6 +31,8 @@ import kotlin.io.path.deleteExisting 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 @@ -127,6 +130,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. @@ -153,6 +158,15 @@ class JavaPluginsTest : BasePluginTest() { *manifestEntries, ) } + assertThat(outputServerShadowedSourcesJar).useAll { + containsOnly( + "client/", + "server/", + "client/Client.java", + "server/Server.java", + *manifestEntries, + ) + } } @Test @@ -1259,6 +1273,49 @@ class JavaPluginsTest : BasePluginTest() { } } + @Test + fun generateJavadocFromShadowedSourcesJar() { + path("src/main/java/my/Main.java") + .writeText( + """ + |package my; + |/** Main class doc */ + |public class Main { + | public static void main(String[] args) {} + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + | relocate 'g', 'shadow.g' + |} + |tasks.named('javadoc', Javadoc) { + | classpath = files($shadowJarTask.flatMap { it.archiveFile }) + | source = zipTree($shadowJarTask.flatMap { it.archiveSourcesFile }).matching { include('**/*.java') } + |} + """ + .trimMargin() + ) + + runWithSuccess("javadoc") + + val javadocDir = projectRoot.resolve("build/docs/javadoc") + 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 { return runWithSuccess("dependencies", "--configuration", configuration, *flags).output } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt index 5105912fa..76c98bdb3 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/KotlinPluginsTest.kt @@ -2,16 +2,21 @@ 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 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 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 +284,87 @@ 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 { + | id 'org.jetbrains.kotlin.jvm' + | id 'com.gradleup.shadow' + | id 'org.jetbrains.dokka' + |} + |dependencies { + | implementation 'my:g:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + | relocate 'g', 'shadow.g' + |} + |def extractShadowedSources = tasks.register('extractShadowedSources', Sync) { + | 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 }) + | } + |} + """ + .trimMargin() + ) + + runWithSuccess("dokkaGenerateHtml") + + val dokkaDir = projectRoot.resolve("build/dokka/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", + ) + } + + @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 { + | generateSourcesJar = true + | 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/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/MinimizeTest.kt index 5716568df..7239ad0e4 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 @@ -124,6 +124,47 @@ 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 { + | generateSourcesJar = true + | 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/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt index 4aaf12455..4c3fab5e3 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/PublishingTest.kt @@ -17,6 +17,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 @@ -29,12 +30,14 @@ 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 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 @@ -282,7 +285,67 @@ class PublishingTest : BasePluginTest() { ) 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) + } + + @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( + "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", + ) + 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) } @Test @@ -642,6 +705,90 @@ 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 = '' + | generateSourcesJar = true + |} + |publishing { + | repositories { + | maven { url = '${remoteRepoPath.toUri()}' } + | } + | publications { + | shadow(MavenPublication) { + | artifactId = 'my-all' + | artifact($shadowJarTask) + | artifact($shadowJarTask.flatMap { it.archiveSourcesFile }) { + | classifier = 'sources' + | } + | } + | } + |} + """ + .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" } } } @@ -702,10 +849,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") + } } } @@ -722,6 +871,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 +912,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 225c73d53..8f509f158 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,14 @@ 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 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 @@ -706,6 +710,227 @@ 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") + .writeText( + """ + |package my; + |import g.G; + |public class Main { + | G g; + |} + """ + .trimMargin() + ) + projectScript.appendText( + """ + |dependencies { + | implementation 'my:g:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + | relocate('g', 'shadow.g') + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + assertThat(outputShadowedSourcesJar).useAll { + containsOnly( + "my/", + "my/Main.java", + "shadow/", + "shadow/g/", + "shadow/g/G.java", + *manifestEntries, + ) + getContent("my/Main.java") + .isEqualTo( + """ + |package my; + |import shadow.g.G; + |public class Main { + | G g; + |} + """ + .trimMargin() + ) + getContent("shadow/g/G.java") + .isEqualTo( + """ + |package shadow.g; + |public class G {} + """ + .trimMargin() + ) + } + } + + @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", + *manifestEntries, + ) + } + } + + @Test + fun generateEmptyShadowedSourcesJarWhenNoSources() { + projectScript.appendText( + """ + |dependencies { + | implementation 'my:b:1.0' + |} + |$shadowJarTask { + | generateSourcesJar = true + |} + """ + .trimMargin() + ) + + runWithSuccess(shadowJarPath) + + 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 { + | 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", + *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 { + | 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", + *manifestEntries, + ) + getContent("shadow/ext/Ext.java") + .isEqualTo( + """ + |package shadow.ext; + |public class Ext {} + """ + .trimMargin() + ) + } + } + private companion object { @JvmStatic fun preserveLastModifiedProvider() = diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt index fa7eda57b..0643d431a 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/AppendableMavenRepository.kt @@ -97,9 +97,16 @@ class AppendableMavenRepository(val root: Path) { """ .trimMargin() } + val sourcesArtifactLine = + if (module.sourcesArtifactPath != null) { + "artifact('${module.sourcesArtifactPath}') { classifier = 'sources' }" + } else { + "" + } module.createMavenPublication( """ |artifact '${module.artifactPath}' + |$sourcesArtifactLine |pom.withXml { xml -> | def dependenciesNode = xml.asNode().get('dependencies') ?: xml.asNode().appendNode('dependencies') | $nodes @@ -199,6 +206,7 @@ class AppendableMavenRepository(val root: Path) { inner class JarModule(groupId: String, artifactId: String, version: String) : Module(groupId, artifactId, version) { private var existingJar: Path? = null + private var existingSourcesJar: Path? = null val artifactPath: String get() = @@ -210,6 +218,16 @@ class AppendableMavenRepository(val root: Path) { } ?.invariantSeparatorsPathString ?: error("No jar file provided for $coordinate") + val sourcesArtifactPath: String? + get() = + existingSourcesJar + ?.also { + check(it.exists() && it.isRegularFile()) { + "Sources jar file does not exist or is not a regular file: $it" + } + } + ?.invariantSeparatorsPathString + fun useJar(existingJar: Path) { this.existingJar = existingJar } @@ -218,6 +236,11 @@ class AppendableMavenRepository(val root: Path) { val jarPath = jarsDir.resolve("${coordinate.replace(':', '-')}.jar") existingJar = JarBuilder(jarPath).apply(builder).write() } + + fun buildSourcesJar(builder: JarBuilder.() -> Unit) { + val jarPath = jarsDir.resolve("${coordinate.replace(':', '-')}-sources.jar") + existingSourcesJar = JarBuilder(jarPath).apply(builder).write() + } } class BomModule(groupId: String, artifactId: String, version: String) : diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt index fef15ab37..4e888c1c2 100644 --- a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/GradleModuleMetadata.kt @@ -1,6 +1,7 @@ package com.github.jengelman.gradle.plugins.shadow.util import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME +import com.github.jengelman.gradle.plugins.shadow.ShadowJavaPlugin.Companion.SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.API_ELEMENTS_CONFIGURATION_NAME import org.gradle.api.plugins.JavaPlugin.RUNTIME_ELEMENTS_CONFIGURATION_NAME @@ -18,6 +19,9 @@ data class GradleModuleMetadata(private val variants: List) { val shadowRuntimeElementsVariant: Variant get() = variants.single { it.name == SHADOW_RUNTIME_ELEMENTS_CONFIGURATION_NAME } + val shadowSourcesElementsVariant: Variant + get() = variants.single { it.name == SHADOW_SOURCES_ELEMENTS_CONFIGURATION_NAME } + val variantNames: List get() = variants.map { it.name } diff --git a/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt new file mode 100644 index 000000000..26dfb6d0e --- /dev/null +++ b/src/functionalTest/kotlin/com/github/jengelman/gradle/plugins/shadow/util/LocalMavenRepository.kt @@ -0,0 +1,119 @@ +package com.github.jengelman.gradle.plugins.shadow.util + +import com.github.jengelman.gradle.plugins.shadow.BasePluginTest.Companion.createEmptyClassBytes +import java.nio.file.Path +import kotlin.io.path.createDirectories +import kotlin.io.path.createTempDirectory + +fun createDefaultLocalMavenRepository(junitJar: Path): AppendableMavenRepository { + return AppendableMavenRepository( + root = createTempDirectory().resolve("local-maven-repo").createDirectories() + ) + .apply { + jarModule("junit", "junit", "3.8.2") { useJar(junitJar) } + val a = + jarModule("my", "a", "1.0") { + buildJar { + insert("a.properties", "a") + insert("a2.properties", "a2") + } + } + val b = jarModule("my", "b", "1.0") { buildJar { insert("b.properties", "b") } } + val c = jarModule("my", "c", "1.0") { buildJar { insert("c.properties", "c") } } + val d = + jarModule("my", "d", "1.0") { + buildJar { insert("d.properties", "d") } + // Depends on c but c does not depend on d. + addDependency(c) + } + val e = + jarModule("my", "e", "1.0") { + buildJar { insert("e.properties", "e") } + // Circular dependency with f. + addDependency("my:f:1.0") + } + val f = + jarModule("my", "f", "1.0") { + buildJar { insert("f.properties", "f") } + // Circular dependency with e. + addDependency(e) + } + val g = + jarModule("my", "g", "1.0") { + buildJar { insert("g/G.class", createEmptyClassBytes("g/G")) } + buildSourcesJar { + insert( + "g/G.java", + """ + |package g; + |public class G {} + """ + .trimMargin(), + ) + } + } + val h = + jarModule("my", "h", "1.0") { + buildJar { + insert("h/H.class", createEmptyClassBytes("h/H")) + insert("h/UnusedH.class", createEmptyClassBytes("h/UnusedH")) + } + buildSourcesJar { + insert( + "h/H.java", + """ + |package h; + |public class H {} + """ + .trimMargin(), + ) + insert( + "h/UnusedH.java", + """ + |package h; + |public class UnusedH {} + """ + .trimMargin(), + ) + } + } + val k = + jarModule("my", "k", "1.0") { + buildJar { + insert("k/CustomUtils.class", createEmptyClassBytes("k/CustomUtils")) + insert("k/CustomUnusedUtils.class", createEmptyClassBytes("k/CustomUnusedUtils")) + } + buildSourcesJar { + insert( + "k/Utils.kt", + """ + |@file:JvmName("CustomUtils") + |package k + |fun util() {} + """ + .trimMargin(), + ) + insert( + "k/UnusedUtils.kt", + """ + |@file:JvmName("CustomUnusedUtils") + |package k + |fun unusedUtil() {} + """ + .trimMargin(), + ) + } + } + bomModule("my", "bom", "1.0") { + addDependency(a) + addDependency(b) + addDependency(c) + addDependency(d) + addDependency(e) + addDependency(f) + addDependency(g) + addDependency(h) + addDependency(k) + } + } +} 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 2bb9ef578..4da9fe347 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 ff017e8ca..9ed6780d7 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowJavaPlugin.kt @@ -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 @@ -23,6 +24,7 @@ import org.gradle.api.component.ConfigurationVariantDetails 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.bundling.Jar public abstract class ShadowJavaPlugin @@ -38,9 +40,14 @@ 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.generateSourcesJar.convention( + provider { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) != null } + ) task.configurations.convention(provider { listOf(runtimeConfiguration) }) } artifacts.add(configurations.shadow.name, taskProvider) @@ -53,14 +60,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), @@ -69,13 +71,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) + } + 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 { @@ -112,7 +127,12 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } protected open fun Project.configureComponents() { + val addIntoJavaComponent = shadow.addShadowVariantIntoJavaComponent val shadowRuntimeElements = configurations.shadowRuntimeElements + val shadowSourcesElements = configurations.shadowSourcesElements + // If `withSourcesJar` is present. + val sourcesElements = { configurations.findByName(SOURCES_ELEMENTS_CONFIGURATION_NAME) } + val shadowComponent = softwareComponentFactory.adhoc(COMPONENT_NAME) components.add(shadowComponent) shadowComponent.addVariants( @@ -121,11 +141,24 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl ) { mapToMavenScope("runtime") } + shadowComponent.addVariants( + outgoingConfiguration = shadowSourcesElements, + logger = logger, + shouldAdd = { sourcesElements() != null }, + ) + components.named("java", AdhocComponentWithVariants::class.java) { component -> component.addVariants( outgoingConfiguration = shadowRuntimeElements, logger = logger, - shouldAdd = shadow.addShadowVariantIntoJavaComponent::get, + shouldAdd = addIntoJavaComponent::get, + ) { + mapToOptional() + } + component.addVariants( + outgoingConfiguration = shadowSourcesElements, + logger = logger, + shouldAdd = { addIntoJavaComponent.get() && sourcesElements() != null }, ) { mapToOptional() } @@ -136,7 +169,7 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl outgoingConfiguration: NamedDomainObjectProvider, logger: Logger, shouldAdd: () -> Boolean = { true }, - action: ConfigurationVariantDetails.() -> Unit, + action: ConfigurationVariantDetails.() -> Unit = {}, ) { addVariantsFromConfiguration(outgoingConfiguration) { variant -> if (shouldAdd()) { @@ -149,16 +182,40 @@ constructor(private val softwareComponentFactory: SoftwareComponentFactory) : Pl } } + 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() {} 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/ShadowKmpPlugin.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt index c8bb3a257..68c1d389f 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowKmpPlugin.kt @@ -36,6 +36,9 @@ public abstract class ShadowKmpPlugin : Plugin { val kotlinJvmMain = target.compilations.named("main") registerShadowJarCommon(tasks.named(target.artifactsTaskName, Jar::class.java)) { task -> task.from(kotlinJvmMain.map { it.output.allOutputs }) + task.sourceSetsSourceDirs.convention( + kotlinJvmMain.map { it.allKotlinSourceSets.flatMap { ss -> ss.kotlin.srcDirs } } + ) task.configurations.convention( kotlinJvmMain .flatMap { configurations.named(it.runtimeDependencyConfigurationName) } diff --git a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt index c743b14b8..12edefe99 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/DefaultDependencyFilter.kt @@ -2,9 +2,19 @@ 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.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(project: Project) : +internal class DefaultDependencyFilter(@Transient private val project: Project) : DependencyFilter.AbstractDependencyFilter(project) { override fun resolve( dependencies: Set, @@ -19,4 +29,76 @@ 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 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 } + .toSet() + + val externalComponentIds = + componentIds + .filterIsInstance() + .filter { id -> + includes.any { + it.moduleGroup == id.group && + it.moduleName == id.module && + it.moduleVersion == id.version + } + } + .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 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 + } } 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 new file mode 100644 index 000000000..3a6e40c27 --- /dev/null +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJar.kt @@ -0,0 +1,209 @@ +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( + sourcesJarFile: File, + sourceSetsSourceDirs: Iterable, + includedSourcesJars: Iterable, + relocators: Iterable, + unusedClasses: Set = emptySet(), + entryCompression: ZipEntryCompression, + isZip64: Boolean, + metadataCharset: String?, + 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 + + try { + 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(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 + 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() + .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 = 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().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 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 isUnused( + fileName: String, + pkg: String, + text: String, + unusedClasses: Set, +): 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 +} + +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 85384f069..c9677d0d6 100644 --- a/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt +++ b/src/main/kotlin/com/github/jengelman/gradle/plugins/shadow/relocation/SimpleRelocator.kt @@ -127,10 +127,6 @@ constructor( return if (rawString) clazz else clazz.replaceFirst(pattern.toRegex(), shadedPattern) } - /** - * We don't call this function now, so we don't have to expose [sourcePackageExcludes] and - * [sourcePathExcludes] as inputs. - */ override fun applyToSourceContent(sourceContent: String): String { if (rawString) return sourceContent val content = 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 5a4348690..d6718f3ec 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,6 +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.getApiJars import com.github.jengelman.gradle.plugins.shadow.internal.gradleError import com.github.jengelman.gradle.plugins.shadow.internal.javaPluginExtension @@ -51,6 +52,7 @@ 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 @@ -60,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 @@ -188,6 +191,68 @@ 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:InputFiles + @get:PathSensitive(PathSensitivity.NONE) + 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() + + /** + * 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. + * + * Defaults to + * `/--sources.`. + */ + @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" + } + } + ) + ) + /** * Enables auto relocation of packages in the dependencies. * @@ -530,6 +595,7 @@ public abstract class ShadowJar : Jar() { addIncludedDependencies() injectManifestAttributes() super.copy() + generateShadowedSourcesJar() runR8Minimization() } @@ -549,6 +615,7 @@ public abstract class ShadowJar : Jar() { if (unusedClasses.isNotEmpty()) { logger.info("Found {} unused classes to drop for minimization.", unusedClasses.size) } + this.unusedClasses = unusedClasses val actualTransformers = transformers.get().let { set -> if ( @@ -732,6 +799,23 @@ public abstract class ShadowJar : Jar() { ) } + private var unusedClasses: Set = emptySet() + + private fun generateShadowedSourcesJar() { + if (!generateSourcesJar.get() || !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, + ) + } + public companion object { public const val SHADOW_JAR_TASK_NAME: String = "shadowJar" diff --git a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt index 91c70e5ab..04994e251 100644 --- a/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/ShadowPropertiesTest.kt @@ -4,6 +4,7 @@ import assertk.all import assertk.assertThat import assertk.assertions.containsNone import assertk.assertions.containsOnly +import assertk.assertions.isEmpty import assertk.assertions.isEqualTo import assertk.assertions.isFalse import assertk.assertions.isNotNull @@ -162,9 +163,28 @@ class ShadowPropertiesTest { assertThat(relocationPrefix.get()).isEqualTo(ShadowBasePlugin.SHADOW) assertThat(configurations.get()).containsOnly(runtimeConfiguration) + assertThat(generateSourcesJar.get()).isFalse() + assertThat(archiveSourcesFile.get().asFile).all { + isEqualTo(destinationDirectory.file("my-project-1.0.0-all-sources.jar").get().asFile) + isEqualTo(projectDir.resolve("build/libs/my-project-1.0.0-all-sources.jar")) + } + assertThat(sourceSetsSourceDirs.files) + .containsOnly( + *javaPluginExtension.sourceSets.getByName("main").allSource.srcDirs.toTypedArray() + ) + assertThat(includedSourcesJars.files).isEmpty() } } + @Test + fun applyJavaPluginWithSourcesJar() = + with(project) { + plugins.apply(JavaPlugin::class.java) + javaPluginExtension.withSourcesJar() + val shadowJarTask = tasks.shadowJar.get() + assertThat(shadowJarTask.generateSourcesJar.get()).isTrue() + } + @Test fun applyApplicationPlugin() = with(project) { 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 new file mode 100644 index 000000000..f02787e7d --- /dev/null +++ b/src/test/kotlin/com/github/jengelman/gradle/plugins/shadow/internal/SourcesJarTest.kt @@ -0,0 +1,282 @@ +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 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 = + 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( + "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() + 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") + } + + @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/", + ) + ) + } + + @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'.") + } +} diff --git a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt index 6ba359b9b..cb7d8b8e8 100644 --- a/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt +++ b/src/testKit/kotlin/com/github/jengelman/gradle/plugins/shadow/testkit/JarPath.kt @@ -122,6 +122,6 @@ fun Assert.runMain( os.toString().invariantEolString } -private fun Assert.toEntries() = transform { actual -> +fun Assert.toEntries() = transform { actual -> actual.entries().toList().map { it.name } }