|
4 | 4 |
|
5 | 5 | package ktorbuild.internal.publish
|
6 | 6 |
|
7 |
| -import ktorbuild.internal.gradle.directoryProvider |
8 |
| -import ktorbuild.internal.gradle.regularFileProvider |
9 |
| -import ktorbuild.internal.publish.TestRepository.locateTestRepository |
| 7 | +import ktorbuild.internal.gradle.maybeNamed |
10 | 8 | import org.gradle.api.DefaultTask
|
11 | 9 | import org.gradle.api.GradleException
|
12 | 10 | import org.gradle.api.Project
|
13 |
| -import org.gradle.api.file.DirectoryProperty |
14 | 11 | import org.gradle.api.file.RegularFileProperty
|
| 12 | +import org.gradle.api.provider.ListProperty |
15 | 13 | import org.gradle.api.provider.Property
|
16 |
| -import org.gradle.api.publish.PublishingExtension |
17 |
| -import org.gradle.api.tasks.* |
| 14 | +import org.gradle.api.provider.Provider |
| 15 | +import org.gradle.api.publish.maven.MavenPublication |
| 16 | +import org.gradle.api.publish.maven.tasks.PublishToMavenRepository |
| 17 | +import org.gradle.api.tasks.Input |
| 18 | +import org.gradle.api.tasks.Internal |
| 19 | +import org.gradle.api.tasks.OutputFile |
| 20 | +import org.gradle.api.tasks.TaskAction |
18 | 21 | import org.gradle.api.tasks.options.Option
|
19 |
| -import org.gradle.kotlin.dsl.configure |
| 22 | +import org.gradle.kotlin.dsl.assign |
20 | 23 | import org.gradle.language.base.plugins.LifecycleBasePlugin
|
21 | 24 | import java.io.File
|
22 | 25 |
|
23 | 26 | /**
|
24 | 27 | * Task that verifies the list of published artifacts matches the expected artifacts.
|
| 28 | + * Should be used together with exactly one publishing task. |
25 | 29 | *
|
26 |
| - * The expected artifact list is stored in a [artifactsDump], and this task ensures |
27 |
| - * consistency between the expected and the actual artifacts found in the [repositoryDirectory]. |
| 30 | + * Examples: |
| 31 | + * ``` |
| 32 | + * // Validate JS artifacts |
| 33 | + * ./gradlew validatePublishedArtifacts publishJsPublications |
| 34 | + * |
| 35 | + * // Save dump of JVM and common artifacts |
| 36 | + * ./gradlew validatePublishedArtifacts --dump publishJvmAndCommonPublications |
| 37 | + * |
| 38 | + * // Run validation against single project |
| 39 | + * ./gradlew validatePublishedArtifacts :ktor-io:publishJvmAndCommonPublications |
28 | 40 | *
|
29 |
| - * By default, `artifactsDump` is located at `<rootProject>/gradle/artifacts.txt` and [TestRepository] location is used |
30 |
| - * as a `repositoryDirectory`. |
| 41 | + * // Dump artifacts locally (switched publishing repository to MavenLocal) |
| 42 | + * ./gradlew validatePublishedArtifacts --dump publishJvmAndCommonPublications -Prepository=MavenLocal |
| 43 | + * ``` |
| 44 | + * |
| 45 | + * The expected artifact list is stored in a [artifactsDump], and this task ensures |
| 46 | + * consistency between the expected and the actual artifacts published by the task. |
| 47 | + * `artifactsDump` for the task is located at `<rootProject>/gradle/artifacts/<taskName>.txt`. |
31 | 48 | *
|
32 | 49 | * Initially cherry-picked from kotlinx.coroutines build scripts
|
33 | 50 | * See: https://github.com/Kotlin/kotlinx.serialization/blob/v1.8.0/buildSrc/src/main/kotlin/publishing-check-conventions.gradle.kts
|
34 | 51 | */
|
35 | 52 | @Suppress("LeakingThis")
|
36 | 53 | internal abstract class ValidatePublishedArtifactsTask : DefaultTask() {
|
37 | 54 |
|
38 |
| - @get:InputDirectory |
39 |
| - @get:PathSensitive(PathSensitivity.RELATIVE) |
40 |
| - abstract val repositoryDirectory: DirectoryProperty |
41 |
| - |
42 |
| - @get:Input |
43 |
| - abstract val packageName: Property<String> |
44 |
| - |
45 | 55 | @get:Option(option = DUMP_OPTION, description = "Dumps the list of published artifacts to a file")
|
46 | 56 | @get:Input
|
47 | 57 | abstract val dump: Property<Boolean>
|
48 | 58 |
|
| 59 | + @get:Input |
| 60 | + protected abstract val publishedArtifacts: ListProperty<String> |
| 61 | + |
| 62 | + @get:Internal |
| 63 | + protected abstract val publishTaskName: Property<String> |
| 64 | + |
49 | 65 | @get:OutputFile
|
50 |
| - abstract val artifactsDump: RegularFileProperty |
| 66 | + protected abstract val artifactsDump: RegularFileProperty |
51 | 67 |
|
52 | 68 | init {
|
53 | 69 | group = LifecycleBasePlugin.VERIFICATION_GROUP
|
54 | 70 | description = "Checks that the list of published artifacts matches the expected one"
|
55 | 71 |
|
56 |
| - outputs.cacheIf("Enable cache only when collecting a dump") { dump.get() } |
| 72 | + outputs.cacheIf("Isn't worth caching") { false } |
| 73 | + dump.convention(false) |
| 74 | + configurePublishTaskName() |
| 75 | + |
| 76 | + // Initialize the publishedArtifacts list to empty by default |
| 77 | + publishedArtifacts.convention(emptyList()) |
57 | 78 |
|
58 |
| - with(project) { |
59 |
| - repositoryDirectory.convention(directoryProvider { locateTestRepository() }) |
60 |
| - artifactsDump.convention(regularFileProvider { rootDir.resolve("gradle/artifacts.txt") }) |
61 |
| - packageName.convention(rootProject.group.toString()) |
| 79 | + // Collect artifacts from the all publishing tasks in the task graph |
| 80 | + project.gradle.taskGraph.whenReady { |
| 81 | + val publishTasks = allTasks.filterIsInstance<PublishToMavenRepository>() |
| 82 | + if (publishTasks.isNotEmpty()) { |
| 83 | + publishTasks.forEach { publishTask -> |
| 84 | + publishedArtifacts.addAll(publishTask.publication.formatArtifacts()) |
| 85 | + } |
| 86 | + } |
| 87 | + } |
| 88 | + } |
| 89 | + |
| 90 | + private fun configurePublishTaskName() { |
| 91 | + val taskNames = project.gradle.startParameter.taskNames.filterNot { name -> |
| 92 | + // Filter out the validation task itself and task parameters |
| 93 | + name == NAME || name.startsWith("-") |
| 94 | + } |
| 95 | + |
| 96 | + // Find publish tasks among the specified tasks |
| 97 | + val publishTasks = taskNames.filter { it.contains("publish", ignoreCase = true) } |
| 98 | + |
| 99 | + if (publishTasks.isEmpty()) { |
| 100 | + // If no publish task is specified, use a default name for the artifacts dump |
| 101 | + publishTaskName.set("defaultPublish") |
| 102 | + artifactsDump.set(project.rootDir.resolve("gradle/artifacts/defaultPublish.txt")) |
| 103 | + return |
| 104 | + } |
| 105 | + |
| 106 | + // Use the first publish task if multiple are specified |
| 107 | + publishTaskName.set(publishTasks.first()) |
| 108 | + artifactsDump = publishTaskName.map { taskName -> |
| 109 | + val sanitizedTaskName = taskName.replace(":", "_") |
| 110 | + project.rootDir.resolve("gradle/artifacts/${sanitizedTaskName}.txt") |
| 111 | + } |
| 112 | + } |
| 113 | + |
| 114 | + fun dependsOn(publishedProjects: Provider<List<Project>>) = with(project) { |
| 115 | + // Handle the case when an absolute task path is specified |
| 116 | + val taskName = publishTaskName.get() |
| 117 | + if (taskName.startsWith(":")) { |
| 118 | + val publishTask = tasks.getByPath(taskName) |
| 119 | + dependsOn(publishTask) |
| 120 | + return |
| 121 | + } |
| 122 | + |
| 123 | + publishedProjects.get().forEach { project -> |
| 124 | + val task = project.tasks.maybeNamed(taskName) |
| 125 | + if (task != null) { |
| 126 | + dependsOn(task) |
| 127 | + } |
62 | 128 | }
|
63 |
| - dump.convention(false) |
64 | 129 | }
|
65 | 130 |
|
66 | 131 | @TaskAction
|
67 |
| - fun validate() { |
68 |
| - val packagePath = packageName.get().replace(".", "/") |
69 |
| - val actualArtifacts = repositoryDirectory.get().asFile |
70 |
| - .resolve(packagePath) |
71 |
| - .list() |
72 |
| - ?.toSortedSet() |
73 |
| - .orEmpty() |
| 132 | + fun runValidation() { |
| 133 | + publishedArtifacts.finalizeValue() |
| 134 | + |
| 135 | + val actualArtifacts = publishedArtifacts.get().toSortedSet() |
74 | 136 | val dumpFile = artifactsDump.get().asFile
|
75 | 137 |
|
| 138 | + // If no artifacts were collected and we're not in dump mode, skip validation |
| 139 | + if (actualArtifacts.isEmpty() && !dump.get()) { |
| 140 | + logger.lifecycle("No artifacts were collected for validation. Skipping.") |
| 141 | + return |
| 142 | + } |
| 143 | + |
76 | 144 | if (dump.get()) {
|
77 |
| - if (!dumpFile.exists()) dumpFile.createNewFile() |
| 145 | + if (!dumpFile.exists()) { |
| 146 | + dumpFile.parentFile.mkdirs() |
| 147 | + dumpFile.createNewFile() |
| 148 | + } |
78 | 149 |
|
79 | 150 | dumpFile.bufferedWriter().use { writer ->
|
80 | 151 | actualArtifacts.forEach { artifact -> writer.appendLine(artifact) }
|
@@ -117,20 +188,6 @@ internal abstract class ValidatePublishedArtifactsTask : DefaultTask() {
|
117 | 188 | }
|
118 | 189 | }
|
119 | 190 |
|
120 |
| -internal object TestRepository { |
121 |
| - const val NAME = "test" |
122 |
| - |
123 |
| - fun Project.configureTestRepository() { |
124 |
| - extensions.configure<PublishingExtension> { |
125 |
| - repositories { |
126 |
| - maven { |
127 |
| - name = NAME |
128 |
| - setUrl(locateTestRepository()) |
129 |
| - } |
130 |
| - } |
131 |
| - } |
132 |
| - } |
133 |
| - |
134 |
| - fun Project.locateTestRepository(): File = |
135 |
| - rootDir.resolve("build/${ValidatePublishedArtifactsTask.NAME}Repository") |
136 |
| -} |
| 191 | +private fun MavenPublication.formatArtifacts(): List<String> = |
| 192 | + artifacts.map { "${groupId}:${artifactId}/${it.classifier.orEmpty()}.${it.extension}" } |
| 193 | + .ifEmpty { listOf("$groupId:$artifactId") } |
0 commit comments