diff --git a/api/dev-publish-plugin.api b/api/dev-publish-plugin.api index 6eeb2f1..d811b5f 100644 --- a/api/dev-publish-plugin.api +++ b/api/dev-publish-plugin.api @@ -20,6 +20,7 @@ public abstract class dev/adamko/gradle/dev_publish/DevPublishPluginExtension { public abstract class dev/adamko/gradle/dev_publish/data/PublicationData : org/gradle/api/Named { public abstract fun getArtifacts ()Lorg/gradle/api/file/ConfigurableFileCollection; + public abstract fun getGradleModuleMetadata ()Lorg/gradle/api/file/ConfigurableFileCollection; public abstract fun getIdentifier ()Lorg/gradle/api/provider/Property; public fun getName ()Ljava/lang/String; } diff --git a/src/main/kotlin/DevPublishPlugin.kt b/src/main/kotlin/DevPublishPlugin.kt index 123d620..f063bc5 100644 --- a/src/main/kotlin/DevPublishPlugin.kt +++ b/src/main/kotlin/DevPublishPlugin.kt @@ -14,6 +14,7 @@ import dev.adamko.gradle.dev_publish.utils.* import javax.inject.Inject import org.gradle.api.Plugin import org.gradle.api.Project +import org.gradle.api.file.FileCollection import org.gradle.api.file.FileSystemOperations import org.gradle.api.file.ProjectLayout import org.gradle.api.logging.Logging @@ -25,6 +26,7 @@ import org.gradle.api.publish.PublishingExtension import org.gradle.api.publish.maven.MavenPublication import org.gradle.api.publish.maven.plugins.MavenPublishPlugin import org.gradle.api.publish.maven.tasks.PublishToMavenRepository +import org.gradle.api.publish.tasks.GenerateModuleMetadata import org.gradle.api.services.BuildServiceRegistry import org.gradle.api.tasks.PathSensitivity.RELATIVE import org.gradle.kotlin.dsl.* @@ -138,7 +140,12 @@ constructor( publicationData.addAllLater(providers.provider { publications .withType() - .mapNotNull { createPublicationData(it) } + .mapNotNull { publication -> + createPublicationData( + project = project, + publication = publication, + ) + } }) } } @@ -173,12 +180,21 @@ constructor( val currentProjectDir = layout.projectDirectory - val publicationData = providers.provider { createPublicationData(publication) } + val publicationData = providers.provider { + createPublicationData( + project = project, + publication = publication, + ) + } + + inputs.files(publicationData.map { it.gradleModuleMetadata }) + .withPropertyName("devPubGradleModuleMetadata") + .withPathSensitivity(RELATIVE) val currentChecksum = providers.createPublicationChecksum { this.projectDir.set(currentProjectDir) - this.artifacts.from(publicationData.map { it.artifacts }) this.identifier.set(publicationData.flatMap { it.identifier }) + this.gradleModuleMetadata.from(publicationData.map { it.gradleModuleMetadata }) } val storedChecksum = providers.loadPublicationChecksum { @@ -244,6 +260,7 @@ constructor( /** Create an instance of [PublicationData] from [publication]. */ private fun createPublicationData( + project: Project, publication: MavenPublication?, ): PublicationData? { if (publication == null) { @@ -251,20 +268,32 @@ constructor( return null } - val artifacts = providers.provider { publication.artifacts } - .map { artifacts -> - objects.fileCollection() - .from(artifacts.map { it.file }) - .builtBy(artifacts) - } val identifier = providers.provider { publication.run { "$groupId:$artifactId:$version" } } + val gmm = getGmm(project, publication) + return objects.newInstance(publication.name).apply { this.identifier.set(identifier) - this.artifacts.from(artifacts) + this.gradleModuleMetadata.from(gmm) } } + + private fun getGmm( + project: Project, + publication: MavenPublication, + ): FileCollection { + val gmm = objects.fileCollection() + project.tasks + .withType() + .all { + if (name == publication.getGenerateModuleMetadataTaskName()) { + gmm.from(outputFile) + } + } + return gmm + } + companion object { const val DEV_PUB__EXTENSION_NAME = "devPublish" @@ -282,5 +311,8 @@ constructor( const val DEV_PUB__PUBLICATION_OUTGOING = "devPublicationConsumableElements" private val logger = Logging.getLogger(DevPublishService::class.java) + + private fun MavenPublication.getGenerateModuleMetadataTaskName(): String = + "generateMetadataFileFor${name.uppercaseFirstChar()}Publication" } } diff --git a/src/main/kotlin/data/PublicationData.kt b/src/main/kotlin/data/PublicationData.kt index fc358ad..3c3fd11 100644 --- a/src/main/kotlin/data/PublicationData.kt +++ b/src/main/kotlin/data/PublicationData.kt @@ -25,12 +25,27 @@ constructor( ) : Named { /** - * The artifacts inside a [MavenPublication]. + * The Gradle Module Metadata files that describe this publication. + * + * Typically, there should only be one GMM file, but the Gradle API does not have a stable way to access it. + * Therefore, we accept multiple files, in case this assumption will change in the future. * * @see MavenPublication.getArtifacts */ @get:InputFiles @get:PathSensitive(RELATIVE) + abstract val gradleModuleMetadata: ConfigurableFileCollection + + /** + * The artifacts inside a [MavenPublication]. + * + * This property is no longer used. Instead, the data inside the Gradle Module Metadata files is used. + * These files contain all details (including checksums) of the artifacts attached to the publication. + * + * @see MavenPublication.getArtifacts + */ + @get:Internal + @Deprecated("No longer used. Scheduled for removal in version 2.0.0.") abstract val artifacts: ConfigurableFileCollection /** diff --git a/src/main/kotlin/internal/checksums/CreatePublicationChecksum.kt b/src/main/kotlin/internal/checksums/CreatePublicationChecksum.kt index 6da6577..2ce3a51 100644 --- a/src/main/kotlin/internal/checksums/CreatePublicationChecksum.kt +++ b/src/main/kotlin/internal/checksums/CreatePublicationChecksum.kt @@ -9,30 +9,28 @@ internal abstract class CreatePublicationChecksum : ValueSource + val gradleModuleMetadata: ConfigurableFileCollection } override fun obtain(): String? { val identifier = parameters.identifier.get() - val artifactsChecksums = artifactsChecksums() + val gradleModuleMetadataChecksums = gradleModuleMetadataChecksums() return buildString { appendLine(identifier) appendLine("---") - artifactsChecksums.forEach { + gradleModuleMetadataChecksums.forEach { appendLine(it) } }.trim() } - private fun artifactsChecksums(): List { - val projectDir = parameters.projectDir.get().asFile - - return parameters.artifacts - .map { artifact -> - val artifactPath = artifact.relativeTo(projectDir).invariantSeparatorsPath - "${artifactPath}$FileChecksumSeparator${artifact.checksum()}" + private fun gradleModuleMetadataChecksums(): List { + return parameters.gradleModuleMetadata + .map { gmm -> + val gmmPath = gmm.relativeTo(parameters.projectDir.get().asFile).invariantSeparatorsPath + "${gmmPath}$FileChecksumSeparator${gmm.checksum()}" } .sorted() } diff --git a/src/main/kotlin/tasks/GeneratePublicationDataChecksumTask.kt b/src/main/kotlin/tasks/GeneratePublicationDataChecksumTask.kt index ed83004..57c7063 100644 --- a/src/main/kotlin/tasks/GeneratePublicationDataChecksumTask.kt +++ b/src/main/kotlin/tasks/GeneratePublicationDataChecksumTask.kt @@ -54,14 +54,14 @@ constructor( val currentProjectDir = layout.projectDirectory publicationData.forEach { data -> - logger.info("Creating publication data checksum for ${data.name} ${data.artifacts.asPath}") + logger.info("Creating publication data checksum for ${data.name} ${data.gradleModuleMetadata.asPath}") val checksumFile = tempDir.resolve(data.checksumFilename) val checksum = providers.createPublicationChecksum { this.projectDir.set(currentProjectDir) - this.artifacts.from(data.artifacts) this.identifier.set(data.identifier) + this.gradleModuleMetadata.from(data.gradleModuleMetadata) }.get() checksumFile.writeText(checksum) diff --git a/src/main/kotlin/utils/stdlibUtils.kt b/src/main/kotlin/utils/stdlibUtils.kt index 2b2fe42..4290138 100644 --- a/src/main/kotlin/utils/stdlibUtils.kt +++ b/src/main/kotlin/utils/stdlibUtils.kt @@ -3,3 +3,6 @@ package dev.adamko.gradle.dev_publish.utils /** Split a string to a [Pair], using [substringBefore] and [substringAfter] */ internal fun String.splitToPair(delimiter: String): Pair = substringBefore(delimiter) to substringAfter(delimiter, "") + +internal fun String.uppercaseFirstChar(): String = + replaceFirstChar { it.uppercase() } diff --git a/src/test/kotlin/BuildCacheTest.kt b/src/test/kotlin/BuildCacheTest.kt index 1fe95d3..6c4da97 100644 --- a/src/test/kotlin/BuildCacheTest.kt +++ b/src/test/kotlin/BuildCacheTest.kt @@ -40,8 +40,7 @@ class BuildCacheTest : FunSpec({ shouldNotHaveRunTask(":updateDevRepo") } - val initialBuildCacheSize = - expectedBuildCacheDir.walk().filter { it.isRegularFile() }.sumOf { it.fileSize() } + val initialBuildCacheSize = expectedBuildCacheDir.recursiveFileSize() project.runner .withArguments( @@ -104,9 +103,9 @@ class BuildCacheTest : FunSpec({ settingsGradleKts += """ | |buildCache { - | local { - | directory = file("local-cache").toURI() - | } + | local { + | directory = file("local-cache").toURI() + | } |} |""".trimMargin() diff --git a/src/test/kotlin/IncrementalBuildTest.kt b/src/test/kotlin/IncrementalBuildTest.kt new file mode 100644 index 0000000..aaea67b --- /dev/null +++ b/src/test/kotlin/IncrementalBuildTest.kt @@ -0,0 +1,154 @@ +package dev.adamko.gradle.dev_publish + +import dev.adamko.gradle.dev_publish.test_utils.* +import io.kotest.core.spec.style.FunSpec +import io.kotest.core.test.TestCaseOrder +import io.kotest.core.test.TestScope +import org.gradle.testkit.runner.TaskOutcome.SKIPPED +import org.gradle.testkit.runner.TaskOutcome.SUCCESS + +class IncrementalBuildTest : FunSpec({ + + context("check incremental build") { + val project = project() + + context("initial clean run") { + test("1st time - publish task should run successfully") { + project.runner.withArguments( + ":clean", + ":updateDevRepo", + ).build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SUCCESS) + } + } + + test("2nd time - publish task should be UP_TO_DATE") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SKIPPED) + } + } + } + + context("when dependency added") { + project.buildGradleKts = project.buildGradleKts.replace("// ", "/**/") + + test("1st time - publish task should run successfully") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SUCCESS) + } + } + + test("2nd time - publish task should be UP_TO_DATE") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SKIPPED) + } + } + } + + context("when dependency changes") { + project.buildGradleKts = project.buildGradleKts + .replace("/**/", "// ") + .replace("// ", "/**/") + + test("1st time - publish task should run successfully") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SUCCESS) + } + } + + test("2nd time - publish task should be UP_TO_DATE") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SKIPPED) + } + } + } + + context("when dependency removed - expect updateDevRepo re-runs") { + project.buildGradleKts = project.buildGradleKts + .replace("/**/", "// ") + + test("1st time - publish task should run successfully") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SUCCESS) + } + } + + test("2nd time - publish task should be UP_TO_DATE") { + project.runner + .withArguments(":updateDevRepo", "--info") + .forwardOutput() + .build { + shouldHaveTaskWithOutcome(":publishMavenJavaPublicationToDevPublishMavenRepository", SKIPPED) + } + } + } + } +}) { + + override fun testCaseOrder(): TestCaseOrder = TestCaseOrder.Sequential + + companion object { + + private fun TestScope.project(): GradleProjectTest = + gradleKtsProjectTest( + projectName = "single-module-project", + testProjectPath = testCase.descriptor.slashSeparatedPath(), + ) { + + buildGradleKts = """ + |plugins { + | kotlin("jvm") version embeddedKotlinVersion + | id("dev.adamko.dev-publish") version "+" + | `maven-publish` + |} + | + |group = "foo.project" + |version = "0.0.1" + | + |dependencies { + | //devPublication(project(":")) + |} + | + |publishing { + | publications { + | create("mavenJava") { + | from(components["java"]) + | } + | } + |} + | + |dependencies { + | // implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0") + | // implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.1") + |} + |""".trimMargin() + + gradleProperties = """ + |org.gradle.jvmargs=-Dfile.encoding=UTF-8 + |org.gradle.caching=false + |org.gradle.configuration-cache=true + |org.gradle.logging.level=info + |org.gradle.logging.stacktrace=full + |org.gradle.parallel=true + |""".trimMargin() + } + } +} diff --git a/src/test/kotlin/SingleProjectTest.kt b/src/test/kotlin/SingleProjectTest.kt index e1b9f04..a96b877 100644 --- a/src/test/kotlin/SingleProjectTest.kt +++ b/src/test/kotlin/SingleProjectTest.kt @@ -49,27 +49,27 @@ class SingleProjectTest : FunSpec({ ) { buildGradleKts = """ - plugins { - kotlin("jvm") version embeddedKotlinVersion - id("dev.adamko.dev-publish") version "+" - `maven-publish` - } - - group = "foo.project" - version = "0.0.1" - - dependencies { - //devPublication(project(":")) - } - - publishing { - publications { - create("mavenJava") { - from(components["java"]) - } - } - } - """.trimIndent() + |plugins { + | kotlin("jvm") version embeddedKotlinVersion + | id("dev.adamko.dev-publish") version "+" + | `maven-publish` + |} + | + |group = "foo.project" + |version = "0.0.1" + | + |dependencies { + | //devPublication(project(":")) + |} + | + |publishing { + | publications { + | create("mavenJava") { + | from(components["java"]) + | } + | } + |} + |""".trimMargin() } @Language("TEXT")