-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathProjectExtensions.kt
393 lines (377 loc) · 17.8 KB
/
ProjectExtensions.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
package org.danilopianini.gradle.mavencentral
import io.github.gradlenexus.publishplugin.internal.StagingRepository.State.CLOSED
import kotlinx.coroutines.runBlocking
import org.danilopianini.gradle.mavencentral.MavenPublicationExtensions.signingTasks
import org.danilopianini.gradle.mavencentral.PublishPortalDeployment.Companion.DROP_TASK_NAME
import org.danilopianini.gradle.mavencentral.PublishPortalDeployment.Companion.RELEASE_TASK_NAME
import org.gradle.api.Action
import org.gradle.api.DefaultTask
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.provider.Property
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.tasks.PublishToMavenRepository
import org.gradle.api.publish.plugins.PublishingPlugin
import org.gradle.jvm.tasks.Jar
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.create
import org.gradle.kotlin.dsl.property
import org.gradle.kotlin.dsl.register
import org.gradle.kotlin.dsl.the
import org.gradle.kotlin.dsl.withType
import org.gradle.plugins.signing.Sign
import org.jetbrains.kotlin.gradle.dsl.KotlinJsProjectExtension
import kotlin.reflect.KClass
internal object ProjectExtensions {
/**
* The id of the Kotlin/JS plugin.
*/
private const val KOTLIN_JS_PLUGIN = "org.jetbrains.kotlin.js"
/**
* The `jsSourcesJar` [Task] of a Kotlin/JS project.
*/
internal val Project.jsSourcesJar: Jar? get() = tasks.withType<Jar>().findByName("jsSourcesJar")
/**
* The `kotlinSourcesJar` [Task] of a Kotlin/JS or Kotlin/JVM project.
*/
private val Project.kotlinSourcesJar: Jar? get() = tasks.withType<Jar>().findByName("kotlinSourcesJar")
/**
* The list of default sources Jar [Task]s: it may include [kotlinSourcesJar] and [jsSourcesJar],
* if they are non-null.
*/
internal val Project.sourcesJarTasks: List<Jar> get() = listOfNotNull(jsSourcesJar, kotlinSourcesJar)
/**
* Executes an action on Kotlin/JS projects only.
*/
internal fun Project.ifKotlinJsProject(action: Action<Plugin<*>>) {
plugins.withId(KOTLIN_JS_PLUGIN, action)
}
/**
* Configures the provided task to include the `main` source set of a Kotlin/JS project.
* The configuration does nothing if the provided task not of type [SourceJar].
*/
fun Project.configureJavadocJarTaskForKtJs(sourcesJarTask: Task) {
ifKotlinJsProject { _ ->
configure<KotlinJsProjectExtension> {
sourceSets.getByName("main") {
(sourcesJarTask as? SourceJar)?.run {
sourceSet(it.kotlin)
sourceSet(it.resources)
} ?: logger.warn(
"source sets of task {} not configured because it is not of type {}",
sourcesJarTask.name,
SourceJar::class.java.name,
)
}
}
}
}
/**
* Reifies this repository setup onto every [PublishingExtension] configuration of the provided [Project].
*/
fun Project.configureRepository(repoToConfigure: Repository) {
extensions.configure(PublishingExtension::class) { publishing ->
publishing.repositories { repository ->
repository.maven { mavenArtifactRepository ->
mavenArtifactRepository.name = repoToConfigure.name
mavenArtifactRepository.url = repoToConfigure.url.get()
if (mavenArtifactRepository.url.scheme != "file") {
mavenArtifactRepository.credentials { credentials ->
credentials.username = repoToConfigure.user.orNull
credentials.password = repoToConfigure.password.orNull
}
}
tasks.withType(PublishToMavenRepository::class) {
if (it.repository == mavenArtifactRepository) {
it.doFirst {
warnIfCredentialsAreMissing(repoToConfigure)
}
}
}
}
}
}
if (repoToConfigure.nexusUrl != null) {
configureNexusRepository(repoToConfigure, repoToConfigure.nexusUrl)
}
}
private fun Project.configureNexusRepository(
repoToConfigure: Repository,
nexusUrl: String,
) {
val repoName = repoToConfigure.name
val nexusClient =
rootProject.registerTaskIfNeeded(
"createNexusClientFor$repoName",
InitializeNexusClient::class,
repoToConfigure,
nexusUrl,
) as InitializeNexusClient
/*
* Creates a new staging repository on the Nexus server, or fetches an existing one if the repoId is known.
*/
val createStagingRepository =
rootProject.registerTaskIfNeeded(
"createStagingRepositoryOn$repoName",
) {
val stagingRepoIdsFileName = "staging-repo-ids.properties"
val stagingRepoIdsFile =
rootProject.layout.buildDirectory.map { it.asFile.resolve(stagingRepoIdsFileName) }
outputs.file(stagingRepoIdsFile)
dependsOn(nexusClient)
doLast {
rootProject.warnIfCredentialsAreMissing(repoToConfigure)
nexusClient.nexusClient.repoUrl // triggers the initialization of a repository
val repoId = nexusClient.nexusClient.repoId
// Write the staging repository ID to build/staging-repo-ids.properties file
stagingRepoIdsFile.get().appendText("$repoName=$repoId" + System.lineSeparator())
logger.lifecycle("Append repo name {} to file {}", repoId, stagingRepoIdsFile.get().path)
}
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Creates a new Nexus staging repository on $repoName."
}
/*
* Collector of all upload tasks. Actual uploads are defined at the bottom.
* Requires the creation of the staging repository.
*/
val uploadAllPublications =
tasks.register("uploadAllPublicationsTo${repoName}Nexus") {
it.dependsOn(createStagingRepository)
it.group = PublishingPlugin.PUBLISH_TASK_GROUP
it.description = "Uploads all publications to a staging repository on $repoName."
}
/*
* Closes the staging repository. If it's closed already, skips the operation.
* Runs after all uploads. Requires the creation of the staging repository.
*/
val closeStagingRepository =
rootProject.registerTaskIfNeeded("closeStagingRepositoryOn$repoName") {
doLast {
with(nexusClient.nexusClient) {
when (client.getStagingRepositoryStateById(repoId).state) {
CLOSED -> logger.warn("The staging repository is already closed. Skipping.")
else -> close()
}
}
}
dependsOn(createStagingRepository)
mustRunAfter(uploadAllPublications)
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Closes the Nexus repository on $repoName."
}
/*
* Releases the staging repository. Requires closing.
*/
val release =
rootProject.registerTaskIfNeeded("releaseStagingRepositoryOn${repoToConfigure.name}") {
doLast { nexusClient.nexusClient.release() }
dependsOn(closeStagingRepository)
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Releases the Nexus repo on ${repoToConfigure.name}. " +
"Mutually exclusive with dropStagingRepositoryOn${repoToConfigure.name}."
}
/*
* Drops the staging repository.
* Requires the creation of the staging repository.
* It must run after all uploads.
* If closing is requested as well, drop must run after it.
*/
val drop =
rootProject.registerTaskIfNeeded("dropStagingRepositoryOn${repoToConfigure.name}") {
doLast { nexusClient.nexusClient.drop() }
dependsOn(createStagingRepository)
mustRunAfter(uploadAllPublications)
mustRunAfter(closeStagingRepository)
group = PublishingPlugin.PUBLISH_TASK_GROUP
description =
"Drops the Nexus repo on ${repoToConfigure.name}. Incompatible with releasing the same repo."
}
/*
* Checks that only release or drop are selected for execution, as they are mutually exclusive.
*/
gradle.taskGraph.whenReady {
if (it.hasTask(release) && it.hasTask(drop)) {
error("Mutually exclusive tasks '${release.name}' and '${drop.name}' both selected for execution")
}
}
the<PublishingExtension>().publications.withType<MavenPublication>().configureEach { publication ->
val publicationName = publication.name.replaceFirstChar(Char::titlecase)
project.tasks
.register<PublishToMavenRepository>(
"upload${publicationName}To${repoToConfigure.name}Nexus",
).configure { uploadTask ->
uploadTask.repository =
project.repositories.maven { repo ->
repo.name = repoToConfigure.name
repo.url = project.uri(repoToConfigure.url)
repo.credentials {
it.username = repoToConfigure.user.orNull
it.password = repoToConfigure.password.orNull
}
}
uploadTask.publication = publication
publication.signingTasks(project).forEach { uploadTask.dependsOn(it) }
tasks.withType<Sign>().forEach { uploadTask.mustRunAfter(it) }
/*
* We need to make sure that the staging repository is created before we upload anything.
* We also need to make sure that the staging repository is closed *after* we upload
* We also need to make sure that the staging repository is dropped *after* we upload
* Releasing does not need to be explicitly ordered, as it will be performed after closing
*/
uploadTask.dependsOn(createStagingRepository)
uploadAllPublications.get().dependsOn(uploadTask)
closeStagingRepository.mustRunAfter(uploadTask)
drop.mustRunAfter(uploadTask)
uploadTask.doFirst {
warnIfCredentialsAreMissing(repoToConfigure)
uploadTask.repository.url = nexusClient.nexusClient.repoUrl
}
uploadTask.group = PublishingPlugin.PUBLISH_TASK_GROUP
uploadTask.description = "Uploads the $publicationName publication " +
"to a staging repository on ${repoToConfigure.name} (${repoToConfigure.url.orNull})."
}
}
}
internal inline fun <reified T> Project.propertyWithDefault(default: T?): Property<T> =
objects.property<T>().apply { convention(default) }
internal inline fun <reified T> Project.propertyWithDefaultProvider(noinline default: () -> T?): Property<T> =
objects.property<T>().apply { convention(provider(default)) }
fun <T : Task> Project.registerTaskIfNeeded(
name: String,
type: KClass<T>,
vararg parameters: Any = emptyArray(),
configuration: T.() -> Unit = { },
): Task = tasks.findByName(name) ?: tasks.create(name, type, *parameters).apply(configuration)
fun Project.registerTaskIfNeeded(
name: String,
vararg parameters: Any = emptyArray(),
configuration: DefaultTask.() -> Unit = { },
): Task =
registerTaskIfNeeded(
name = name,
type = DefaultTask::class,
parameters = parameters,
configuration = configuration,
)
internal fun Project.addSourcesArtifactIfNeeded(
publication: MavenPublication,
sourcesJarTask: Task,
) {
if (sourcesJarTask is SourceJar) {
if (jsSourcesJar == null) {
publication.artifact(sourcesJarTask)
logger.info(
"add sources jar artifact to publication {} from task {}",
publication.name,
sourcesJarTask.name,
)
} else {
/*
* This is a hack for Kotlin/JS projects.
* These projects already contain tasks named "jsSourcesJar" and "kotlinSourcesJar", generating the
* same jar "<project.name>-js-<project.version>-sources.jar".
* In particular, task kotlinSourcesJar is automatically registered as an artifact to Maven publications
* when they are created. So, adding further sources-jar-generating tasks it troublesome in this
* situation. The following code simply removes the "-js" appendix from the jar file name,
* hence making the jar compliant with Maven Central.
*/
ifKotlinJsProject { _ ->
project.sourcesJarTasks.forEach {
it.archiveAppendix.set("")
// Better telling the user the plugin is changing the behaviour of default tasks
project.logger.lifecycle(
"remove '-js' appendix from sources jar generated by task {}",
it.name,
)
}
}
}
}
}
private fun Project.warnIfCredentialsAreMissing(repository: Repository) {
if (repository.user.orNull == null) {
logger.warn(
"No username configured for repository {} at {}.",
repository.name,
repository.url,
)
}
if (repository.password.orNull == null) {
logger.warn(
"No password configured for user {} on repository {} at {}.",
repository.user.orNull,
repository.name,
repository.url,
)
}
}
internal fun Project.setupMavenCentralPortal() {
configureRepository(Repository.projectLocalRepository(project))
val zipMavenCentralPortal =
tasks.register<ZipMavenCentralPortalPublication>(
checkNotNull(ZipMavenCentralPortalPublication::class.simpleName)
.replaceFirstChar { it.lowercase() },
)
val portalDeployment =
PublishPortalDeployment(
project = project,
baseUrl = "https://central.sonatype.com/",
user =
project.propertyWithDefaultProvider {
System.getenv("MAVEN_CENTRAL_PORTAL_USERNAME")
?: project.properties["mavenCentralPortalUsername"]?.toString()
?: project.properties["centralPortalUsername"]?.toString()
?: project.properties["centralUsername"]?.toString()
},
password =
project.propertyWithDefaultProvider {
System.getenv("MAVEN_CENTRAL_PORTAL_PASSWORD")
?: project.properties["mavenCentralPortalPassword"]?.toString()
?: project.properties["centralPortalPassword"]?.toString()
?: project.properties["centralPassword"]?.toString()
},
zipTask = zipMavenCentralPortal,
)
val validate =
tasks.register(PublishPortalDeployment.VALIDATE_TASK_NAME) { validate ->
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Validates the Maven Central Portal publication, uploading if needed"
validate.mustRunAfter(zipMavenCentralPortal)
validate.doLast {
runBlocking {
portalDeployment.validate()
}
}
}
tasks.register(DROP_TASK_NAME) { drop ->
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Drops the Maven Central Portal publication"
drop.mustRunAfter(validate)
drop.mustRunAfter(zipMavenCentralPortal)
drop.doLast {
runBlocking {
portalDeployment.drop()
}
}
}
tasks.register(RELEASE_TASK_NAME) { release ->
group = PublishingPlugin.PUBLISH_TASK_GROUP
description = "Releases the Maven Central Portal publication"
release.mustRunAfter(validate)
release.mustRunAfter(zipMavenCentralPortal)
release.doLast {
runBlocking {
portalDeployment.release()
}
}
}
gradle.taskGraph.whenReady { taskGraph ->
val allTasks = taskGraph.allTasks.map { it.name }.toSet()
check(RELEASE_TASK_NAME !in allTasks || DROP_TASK_NAME !in allTasks) {
"Task $RELEASE_TASK_NAME and $DROP_TASK_NAME cannot be executed together"
}
}
}
}