1 /*
<lambda>null2 * Copyright 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package androidx.build
18
19 import androidx.build.buildInfo.CreateLibraryBuildInfoFileTask
20 import androidx.build.checkapi.shouldConfigureApiTasks
21 import androidx.build.sources.sourcesConfigurationName
22 import com.android.build.api.dsl.LibraryExtension
23 import com.android.build.gradle.AppPlugin
24 import com.android.build.gradle.LibraryPlugin
25 import com.android.utils.childrenIterator
26 import com.android.utils.forEach
27 import com.google.gson.GsonBuilder
28 import com.google.gson.JsonObject
29 import com.google.gson.stream.JsonWriter
30 import java.io.File
31 import java.io.StringWriter
32 import org.dom4j.Element
33 import org.dom4j.Namespace
34 import org.dom4j.QName
35 import org.dom4j.io.XMLWriter
36 import org.dom4j.tree.DefaultText
37 import org.gradle.api.GradleException
38 import org.gradle.api.Project
39 import org.gradle.api.XmlProvider
40 import org.gradle.api.artifacts.Configuration
41 import org.gradle.api.component.ComponentWithVariants
42 import org.gradle.api.component.SoftwareComponent
43 import org.gradle.api.component.SoftwareComponentFactory
44 import org.gradle.api.internal.component.SoftwareComponentInternal
45 import org.gradle.api.internal.component.UsageContext
46 import org.gradle.api.plugins.JavaPlugin
47 import org.gradle.api.provider.Provider
48 import org.gradle.api.publish.PublishingExtension
49 import org.gradle.api.publish.maven.MavenPom
50 import org.gradle.api.publish.maven.MavenPublication
51 import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal
52 import org.gradle.api.publish.maven.tasks.GenerateMavenPom
53 import org.gradle.api.publish.tasks.GenerateModuleMetadata
54 import org.gradle.api.tasks.bundling.Zip
55 import org.gradle.kotlin.dsl.configure
56 import org.gradle.kotlin.dsl.create
57 import org.gradle.kotlin.dsl.findByType
58 import org.gradle.kotlin.dsl.named
59 import org.gradle.work.DisableCachingByDefault
60 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
61 import org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPluginWrapper
62 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
63 import org.jetbrains.kotlin.gradle.tooling.BuildKotlinToolingMetadataTask
64
65 fun Project.configureMavenArtifactUpload(
66 androidXExtension: AndroidXExtension,
67 androidXKmpExtension: AndroidXMultiplatformExtension,
68 componentFactory: SoftwareComponentFactory,
69 afterConfigure: () -> Unit
70 ) {
71 apply(mapOf("plugin" to "maven-publish"))
72 var registered = false
73 fun registerOnFirstPublishableArtifact(component: SoftwareComponent) {
74 if (!registered) {
75 configureComponentPublishing(
76 androidXExtension,
77 androidXKmpExtension,
78 component,
79 componentFactory,
80 afterConfigure
81 )
82 Release.register(this, androidXExtension)
83 registered = true
84 }
85 }
86 afterEvaluate {
87 if (!androidXExtension.shouldPublish()) {
88 return@afterEvaluate
89 }
90 components.configureEach { component ->
91 if (isValidReleaseComponent(component)) {
92 registerOnFirstPublishableArtifact(component)
93 }
94 }
95 }
96 // validate that all libraries that should be published actually get registered.
97 gradle.taskGraph.whenReady {
98 if (releaseTaskShouldBeRegistered(androidXExtension)) {
99 validateTaskIsRegistered(Release.PROJECT_ARCHIVE_ZIP_TASK_NAME)
100 }
101 if (buildInfoTaskShouldBeRegistered(androidXExtension)) {
102 if (!androidXExtension.isIsolatedProjectsEnabled()) {
103 validateTaskIsRegistered(CreateLibraryBuildInfoFileTask.TASK_NAME)
104 }
105 }
106 }
107 }
108
Projectnull109 private fun Project.validateTaskIsRegistered(taskName: String) =
110 tasks.findByName(taskName)
111 ?: throw GradleException(
112 "Project $name is configured for publishing, but a '$taskName' task was never " +
113 "registered. This is likely a bug in AndroidX plugin configuration."
114 )
115
116 private fun Project.releaseTaskShouldBeRegistered(extension: AndroidXExtension): Boolean {
117 if (plugins.hasPlugin(AppPlugin::class.java)) {
118 return false
119 }
120 if (!extension.shouldRelease() && !isSnapshotBuild()) {
121 return false
122 }
123 return extension.shouldPublish()
124 }
125
buildInfoTaskShouldBeRegisterednull126 private fun Project.buildInfoTaskShouldBeRegistered(extension: AndroidXExtension): Boolean {
127 if (plugins.hasPlugin(AppPlugin::class.java)) {
128 return false
129 }
130 return extension.shouldRelease()
131 }
132
133 /** Configure publishing for a [SoftwareComponent]. */
Projectnull134 private fun Project.configureComponentPublishing(
135 extension: AndroidXExtension,
136 androidxKmpExtension: AndroidXMultiplatformExtension,
137 component: SoftwareComponent,
138 componentFactory: SoftwareComponentFactory,
139 afterConfigure: () -> Unit
140 ) {
141 val androidxGroup = validateCoordinatesAndGetGroup(extension)
142 val projectArchiveDir =
143 File(getRepositoryDirectory(), "${androidxGroup.group.replace('.', '/')}/$name")
144 group = androidxGroup.group
145
146 /*
147 * Provides a set of maven coordinates (groupId:artifactId) of artifacts in AndroidX
148 * that are Android Libraries.
149 */
150 val androidLibrariesSetProvider: Provider<Set<String>> = provider {
151 val androidxAndroidProjects = mutableSetOf<String>()
152 // Check every project is the project map to see if they are an Android Library
153 val projectModules = extension.mavenCoordinatesToProjectPathMap
154 for ((mavenCoordinates, projectPath) in projectModules) {
155 project.findProject(projectPath)?.plugins?.let { plugins ->
156 if (plugins.hasPlugin(LibraryPlugin::class.java)) {
157 if (plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java)) {
158 // For KMP projects, android AAR is published under -android
159 androidxAndroidProjects.add("$mavenCoordinates-android")
160 } else {
161 androidxAndroidProjects.add(mavenCoordinates)
162 }
163 }
164 }
165 }
166 androidxAndroidProjects
167 }
168
169 configure<PublishingExtension> {
170 repositories {
171 it.maven { repo -> repo.setUrl(getRepositoryDirectory()) }
172 it.maven { repo -> repo.setUrl(getPerProjectRepositoryDirectory()) }
173 }
174 publications {
175 if (appliesJavaGradlePluginPlugin()) {
176 // The 'java-gradle-plugin' will also add to the 'pluginMaven' publication
177 it.create<MavenPublication>("pluginMaven")
178 tasks.getByName("publishPluginMavenPublicationToMavenRepository").doFirst {
179 removePreviouslyUploadedArchives(projectArchiveDir)
180 }
181 afterConfigure()
182 } else {
183 if (project.isMultiplatformPublicationEnabled()) {
184 configureMultiplatformPublication(componentFactory, afterConfigure)
185 } else {
186 it.create<MavenPublication>("maven") { from(component) }
187 tasks.getByName("publishMavenPublicationToMavenRepository").doFirst {
188 removePreviouslyUploadedArchives(projectArchiveDir)
189 }
190 afterConfigure()
191 }
192 }
193 }
194 publications.withType(MavenPublication::class.java).configureEach { publication ->
195 // Used to add buildId to Gradle module metadata set below
196 publication.withBuildIdentifier()
197 val isKmpAnchor = (publication.name == KMP_ANCHOR_PUBLICATION_NAME)
198 val pomPlatform = androidxKmpExtension.defaultPlatform
199 // b/297355397 If a kmp project has Android as the default platform, there might
200 // externally be legacy projects depending on its .pom
201 // We advertise a stub .aar in this .pom for backwards compatibility and
202 // add a dependency on the actual .aar
203 val addStubAar = isKmpAnchor && pomPlatform == PlatformIdentifier.ANDROID.id
204 val buildDir = project.layout.buildDirectory
205 if (addStubAar) {
206 val minSdk =
207 project.extensions.findByType<LibraryExtension>()?.defaultConfig?.minSdk
208 ?: extensions
209 .findByType<AndroidXMultiplatformExtension>()
210 ?.agpKmpExtension
211 ?.minSdk
212 ?: throw GradleException(
213 "Couldn't find valid Android extension to read minSdk from"
214 )
215 // create a unique namespace for this .aar, different from the android artifact
216 val stubNamespace =
217 project.group.toString().replace(':', '.') +
218 "." +
219 project.name.toString().replace('-', '.') +
220 ".anchor"
221 val unpackedStubAarTask =
222 tasks.register("unpackedStubAar", UnpackedStubAarTask::class.java) { aarTask ->
223 aarTask.aarPackage.set(stubNamespace)
224 aarTask.minSdkVersion.set(minSdk)
225 aarTask.outputDir.set(buildDir.dir("intermediates/stub-aar"))
226 }
227 val stubAarTask =
228 tasks.register("stubAar", ZipStubAarTask::class.java) { zipTask ->
229 zipTask.from(unpackedStubAarTask.flatMap { it.outputDir })
230 zipTask.destinationDirectory.set(buildDir.dir("outputs"))
231 zipTask.archiveExtension.set("aar")
232 }
233 publication.artifact(stubAarTask)
234 }
235
236 publication.pom { pom ->
237 if (addStubAar) {
238 pom.packaging = "aar"
239 }
240 addInformativeMetadata(extension, pom)
241 tweakDependenciesMetadata(
242 androidxGroup,
243 pom,
244 androidLibrariesSetProvider,
245 isKmpAnchor,
246 pomPlatform
247 )
248 }
249 }
250 }
251
252 // Workarounds for https://github.com/gradle/gradle/issues/20011
253 project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
254 task.doLast {
255 val metadataFile = task.outputFile.asFile.get()
256 val metadata = metadataFile.readText()
257 verifyGradleMetadata(metadata)
258 val sortedMetadata = sortGradleMetadataDependencies(metadata)
259
260 if (metadata != sortedMetadata) {
261 metadataFile.writeText(sortedMetadata)
262 }
263 }
264 }
265 project.tasks.withType(GenerateMavenPom::class.java).configureEach { task ->
266 task.doLast {
267 val pomFile = task.destination
268 val pom = pomFile.readText()
269 val sortedPom = sortPomDependencies(pom)
270
271 if (pom != sortedPom) {
272 pomFile.writeText(sortedPom)
273 }
274 }
275 }
276
277 // Workaround for https://github.com/gradle/gradle/issues/31218
278 project.tasks.withType(GenerateModuleMetadata::class.java).configureEach { task ->
279 task.doLast {
280 val metadata = task.outputFile.asFile.get()
281 val text = metadata.readText()
282 metadata.writeText(
283 text.replace("\"buildId\": .*".toRegex(), "\"buildId:\": \"${getBuildId()}\"")
284 )
285 }
286 }
287 }
288
289 private val ARTIFACT_ID = QName("artifactId", Namespace("", "http://maven.apache.org/POM/4.0.0"))
290
Elementnull291 private fun Element.textElements() = content().filterIsInstance<DefaultText>()
292
293 /** Looks for a dependencies XML element within [pom] and sorts its contents. */
294 fun sortPomDependencies(pom: String): String {
295 // Workaround for using the default namespace in dom4j.
296 val namespaceUris = mapOf("ns" to "http://maven.apache.org/POM/4.0.0")
297 val document = parseXml(pom, namespaceUris)
298
299 // For each <dependencies> element, sort the contained elements in-place.
300 document.rootElement.selectNodes("ns:dependencies").filterIsInstance<Element>().forEach {
301 element ->
302 val deps = element.elements()
303 val sortedDeps = deps.toSortedSet(compareBy { it.stringValue }).toList()
304 sortedDeps.map { // b/356612738 https://github.com/gradle/gradle/issues/30112
305 val itsArtifactId = it.element(ARTIFACT_ID)
306 if (itsArtifactId.stringValue.endsWith("-debug")) {
307 itsArtifactId.textElements().last().text =
308 itsArtifactId.textElements().last().text.removeSuffix("-debug")
309 } else if (itsArtifactId.stringValue.endsWith("-release")) {
310 itsArtifactId.textElements().last().text =
311 itsArtifactId.textElements().last().text.removeSuffix("-release")
312 }
313 }
314 // Content contains formatting nodes, so to avoid modifying those we replace
315 // each element with the sorted element from its respective index. Note this
316 // will not move adjacent elements, so any comments would remain in their
317 // original order.
318 element.content().replaceAll {
319 val index = sortedDeps.indexOf(it)
320 if (index >= 0) {
321 deps[index]
322 } else {
323 it
324 }
325 }
326 }
327
328 // Write to string. Note that this does not preserve the original indent level, but it
329 // does preserve line breaks -- not that any of this matters for client XML parsing.
330 val stringWriter = StringWriter()
331 XMLWriter(stringWriter).apply {
332 setIndentLevel(2)
333 write(document)
334 close()
335 }
336
337 return stringWriter.toString()
338 }
339
340 /** Looks for a dependencies JSON element within [metadata] and sorts its contents. */
sortGradleMetadataDependenciesnull341 fun sortGradleMetadataDependencies(metadata: String): String {
342 val gson = GsonBuilder().create()
343 val jsonObj = gson.fromJson(metadata, JsonObject::class.java)!!
344 jsonObj.getAsJsonArray("variants").forEach { entry ->
345 (entry as? JsonObject)?.getAsJsonArray("dependencies")?.let { jsonArray ->
346 val sortedSet = jsonArray.toSortedSet(compareBy { it.toString() })
347 jsonArray.removeAll { true }
348 sortedSet.forEach { element -> jsonArray.add(element) }
349 }
350 }
351
352 val stringWriter = StringWriter()
353 val jsonWriter = JsonWriter(stringWriter)
354 jsonWriter.setIndent(" ")
355 gson.toJson(jsonObj, jsonWriter)
356 return stringWriter.toString()
357 }
358
359 /**
360 * Checks the variants field in the metadata file has an entry containing "sourcesElements". All our
361 * publications must be published with a sources variant.
362 */
verifyGradleMetadatanull363 fun verifyGradleMetadata(metadata: String) {
364 val gson = GsonBuilder().create()
365 val jsonObj = gson.fromJson(metadata, JsonObject::class.java)!!
366 jsonObj.getAsJsonArray("variants").firstOrNull { variantElement ->
367 variantElement.asJsonObject
368 .get("name")
369 .asString
370 .contains(other = sourcesConfigurationName, ignoreCase = true)
371 } ?: throw Exception("The $sourcesConfigurationName variant must exist in the module file.")
372 }
373
Projectnull374 private fun Project.isMultiplatformPublicationEnabled(): Boolean {
375 return extensions.findByType<KotlinMultiplatformExtension>() != null
376 }
377
Projectnull378 private fun Project.configureMultiplatformPublication(
379 componentFactory: SoftwareComponentFactory,
380 afterConfigure: () -> Unit
381 ) {
382 val multiplatformExtension = extensions.findByType<KotlinMultiplatformExtension>()!!
383
384 multiplatformExtension.targets.configureEach { target ->
385 if (target is KotlinAndroidTarget) {
386 target.publishLibraryVariants(Release.DEFAULT_PUBLISH_CONFIG)
387 }
388 }
389
390 replaceBaseMultiplatformPublication(componentFactory, afterConfigure)
391 }
392
393 /**
394 * This was added because KMP did not include a sources configuration (b/235486368), so we replaced
395 * it with our own publication that includes it. This can be cleaned up now that the bug is fixed
396 * which is tracked here b/309641019
397 */
Projectnull398 private fun Project.replaceBaseMultiplatformPublication(
399 componentFactory: SoftwareComponentFactory,
400 afterConfigure: () -> Unit
401 ) {
402 val kotlinComponent = components.findByName("kotlin") as SoftwareComponentInternal
403 val sourcesElements = buildSet {
404 add("androidxSourcesElements")
405 // Wait for libraryVersionMetadata if it should exist because the project runs API tasks.
406 // There are some libraries (generated icons) that release without running API tasks.
407 if (androidXExtension.shouldConfigureApiTasks()) {
408 add("libraryVersionMetadata")
409 }
410 }
411 withSourcesComponents(componentFactory, sourcesElements) { sourcesComponents ->
412 configure<PublishingExtension> {
413 publications { pubs ->
414 pubs.create<MavenPublication>(KMP_ANCHOR_PUBLICATION_NAME) {
415 addKotlinToolingMetadataArtifact(this@replaceBaseMultiplatformPublication)
416 // Duplicate behavior from KMP plugin
417 // (https://cs.github.com/JetBrains/kotlin/blob/0c001cc9939a2ab11815263ed825c1096b3ce087/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/Publishing.kt#L42)
418 // Should be able to remove internal API usage once
419 // https://youtrack.jetbrains.com/issue/KT-36943 is fixed
420 (this as MavenPublicationInternal).publishWithOriginalFileName()
421
422 from(
423 object : ComponentWithVariants, SoftwareComponentInternal {
424 override fun getName(): String {
425 return KMP_ANCHOR_PUBLICATION_NAME
426 }
427
428 override fun getUsages(): MutableSet<out UsageContext> {
429 // Include sources artifact we built and root artifacts from kotlin
430 // plugin.
431 return (sourcesComponents.flatMap { it.usages } +
432 kotlinComponent.usages)
433 .toMutableSet()
434 }
435
436 override fun getVariants(): MutableSet<out SoftwareComponent> {
437 // Include all target-based variants from kotlin plugin.
438 return (kotlinComponent as ComponentWithVariants).variants
439 }
440 }
441 )
442 }
443
444 // mark original publication as an alias, so we do not try to publish it.
445 pubs.named("kotlinMultiplatform").configure {
446 it as MavenPublicationInternal
447 it.isAlias = true
448 }
449 }
450
451 disableBaseKmpPublications()
452 afterConfigure()
453 }
454 }
455 }
456
457 // https://github.com/JetBrains/kotlin/blob/1ff7ffbe618aa9fda68e23a7094b52f0be02f966/libraries/tools/kotlin-gradle-plugin/src/common/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/publishing/Publishing.kt#L61
MavenPublicationnull458 private fun MavenPublication.addKotlinToolingMetadataArtifact(project: Project) {
459 val buildKotlinToolingMetadataTask =
460 project.tasks.named<BuildKotlinToolingMetadataTask>("buildKotlinToolingMetadata")
461
462 artifact(buildKotlinToolingMetadataTask.map { it.outputFile }) { artifact ->
463 artifact.classifier = "kotlin-tooling-metadata"
464 artifact.builtBy(buildKotlinToolingMetadataTask)
465 }
466 }
467
468 /**
469 * If source configurations with the given names are currently in the project, or if they eventually
470 * gets added, run the given [action] with those configurations as software components.
471 */
Projectnull472 private fun Project.withSourcesComponents(
473 componentFactory: SoftwareComponentFactory,
474 names: Set<String>,
475 action: (List<SoftwareComponentInternal>) -> Unit
476 ) {
477 val targetConfigurations = mutableSetOf<Configuration>()
478 configurations.configureEach {
479 if (it.name in names) {
480 targetConfigurations.add(it)
481 if (targetConfigurations.size == names.size) {
482 action(
483 targetConfigurations.map { configuration ->
484 componentFactory.adhoc(configuration.name).apply {
485 addVariantsFromConfiguration(configuration) {}
486 } as SoftwareComponentInternal
487 }
488 )
489 }
490 }
491 }
492 }
493
494 /**
495 * Now that we have created our own publication that we want published, prevent the base publication
496 * from being published using the roll-up tasks. We should be able to remove this workaround when
497 * b/235486368 is fixed.
498 */
Projectnull499 private fun Project.disableBaseKmpPublications() {
500 listOf("publish", "publishToMavenLocal").forEach { taskName ->
501 tasks.named(taskName).configure { publishTask ->
502 publishTask.setDependsOn(
503 publishTask.dependsOn.filterNot {
504 (it as String).startsWith("publishKotlinMultiplatform")
505 }
506 )
507 }
508 }
509 }
510
Projectnull511 private fun Project.isValidReleaseComponent(component: SoftwareComponent) =
512 component.name == releaseComponentName()
513
514 private fun Project.releaseComponentName() =
515 when {
516 plugins.hasPlugin(KotlinMultiplatformPluginWrapper::class.java) -> "kotlin"
517 plugins.hasPlugin(JavaPlugin::class.java) -> "java"
518 else -> "release"
519 }
520
Projectnull521 private fun Project.validateCoordinatesAndGetGroup(extension: AndroidXExtension): LibraryGroup {
522 val mavenGroup = extension.mavenGroup
523 if (mavenGroup == null) {
524 val groupExplanation = extension.explainMavenGroup().joinToString("\n")
525 throw Exception("You must specify mavenGroup for $path :\n$groupExplanation")
526 }
527 val strippedGroupId = mavenGroup.group.substringAfterLast(".")
528 if (
529 !extension.bypassCoordinateValidation &&
530 mavenGroup.group.startsWith("androidx") &&
531 !name.startsWith(strippedGroupId)
532 ) {
533 throw Exception("Your artifactId must start with '$strippedGroupId'. (currently is $name)")
534 }
535 return mavenGroup
536 }
537
538 /**
539 * Delete any existing archives, so that developers don't get confused/surprised by the presence of
540 * old versions. Additionally, deleting old versions makes it more convenient to iterate over all
541 * existing archives without visiting archives having old versions too
542 */
removePreviouslyUploadedArchivesnull543 private fun removePreviouslyUploadedArchives(projectArchiveDir: File) {
544 projectArchiveDir.deleteRecursively()
545 }
546
Projectnull547 private fun Project.addInformativeMetadata(extension: AndroidXExtension, pom: MavenPom) {
548 pom.name.set(extension.name)
549 pom.description.set(provider { extension.description })
550 pom.url.set(
551 provider {
552 fun defaultUrl() =
553 "https://developer.android.com/jetpack/androidx/releases/" +
554 extension.mavenGroup!!.group.removePrefix("androidx.").replace(".", "-") +
555 "#" +
556 extension.project.version()
557 getAlternativeProjectUrl() ?: defaultUrl()
558 }
559 )
560 pom.inceptionYear.set(provider { extension.inceptionYear })
561 pom.licenses { licenses ->
562 licenses.license { license ->
563 license.name.set(extension.license.name)
564 license.url.set(extension.license.url)
565 license.distribution.set("repo")
566 }
567
568 for (extraLicense in extension.getExtraLicenses()) {
569 licenses.license { license ->
570 license.name.set(provider { extraLicense.name })
571 license.url.set(provider { extraLicense.url })
572 license.distribution.set("repo")
573 }
574 }
575 }
576 pom.scm { scm ->
577 scm.url.set("https://cs.android.com/androidx/platform/frameworks/support")
578 scm.connection.set(ANDROID_GIT_URL)
579 }
580 pom.organization { org -> org.name.set("The Android Open Source Project") }
581 pom.developers { devs ->
582 devs.developer { dev -> dev.name.set("The Android Open Source Project") }
583 }
584 }
585
tweakDependenciesMetadatanull586 private fun tweakDependenciesMetadata(
587 mavenGroup: LibraryGroup,
588 pom: MavenPom,
589 androidLibrariesSetProvider: Provider<Set<String>>,
590 kmpAnchor: Boolean,
591 pomPlatform: String?
592 ) {
593 pom.withXml { xml ->
594 // The following code depends on getProjectsMap which is only available late in
595 // configuration at which point Java Library plugin's variants are not allowed to be
596 // modified. TODO remove the use of getProjectsMap and move to earlier configuration.
597 // For more context see:
598 // https://android-review.googlesource.com/c/platform/frameworks/support/+/1144664/8/buildSrc/src/main/kotlin/androidx/build/MavenUploadHelper.kt#177
599 assignSingleVersionDependenciesInGroupForPom(xml, mavenGroup)
600 assignAarDependencyTypes(xml, androidLibrariesSetProvider.get())
601 ensureConsistentJvmSuffix(xml)
602
603 if (kmpAnchor && pomPlatform != null) {
604 insertDefaultMultiplatformDependencies(xml, pomPlatform)
605 }
606 }
607 }
608
609 // TODO(aurimas): remove this when Gradle bug is fixed.
610 // https://github.com/gradle/gradle/issues/3170
assignAarDependencyTypesnull611 fun assignAarDependencyTypes(xml: XmlProvider, androidLibrariesSet: Set<String>) {
612 val xmlElement = xml.asElement()
613 val dependencies = xmlElement.find { it.nodeName == "dependencies" } as? org.w3c.dom.Element
614
615 dependencies?.getElementsByTagName("dependency")?.forEach { dependency ->
616 val groupId =
617 dependency.find { it.nodeName == "groupId" }?.textContent
618 ?: throw IllegalArgumentException("Failed to locate groupId node")
619 val artifactId =
620 dependency.find { it.nodeName == "artifactId" }?.textContent
621 ?: throw IllegalArgumentException("Failed to locate artifactId node")
622 if (androidLibrariesSet.contains("$groupId:$artifactId")) {
623 dependency.appendElement("type", "aar")
624 }
625 }
626 }
627
insertDefaultMultiplatformDependenciesnull628 fun insertDefaultMultiplatformDependencies(xml: XmlProvider, platformId: String) {
629 val xmlElement = xml.asElement()
630 val groupId =
631 xmlElement.find { it.nodeName == "groupId" }?.textContent
632 ?: throw IllegalArgumentException("Failed to locate groupId node")
633 val artifactId =
634 xmlElement.find { it.nodeName == "artifactId" }?.textContent
635 ?: throw IllegalArgumentException("Failed to locate artifactId node")
636 val version =
637 xmlElement.find { it.nodeName == "version" }?.textContent
638 ?: throw IllegalArgumentException("Failed to locate version node")
639
640 // Find the top-level <dependencies> element or add one if there are no other dependencies.
641 val dependencies =
642 xmlElement.find { it.nodeName == "dependencies" }
643 ?: xmlElement.appendElement("dependencies")
644 dependencies.appendElement("dependency").apply {
645 appendElement("groupId", groupId)
646 appendElement("artifactId", "$artifactId-$platformId")
647 appendElement("version", version)
648 if (platformId == PlatformIdentifier.ANDROID.id) {
649 appendElement("type", "aar")
650 }
651 appendElement("scope", "compile")
652 }
653 }
654
orgnull655 private fun org.w3c.dom.Node.appendElement(
656 tagName: String,
657 textValue: String? = null
658 ): org.w3c.dom.Element {
659 val element = ownerDocument.createElement(tagName)
660 appendChild(element)
661
662 if (textValue != null) {
663 val textNode = ownerDocument.createTextNode(textValue)
664 element.appendChild(textNode)
665 }
666
667 return element
668 }
669
orgnull670 private fun org.w3c.dom.Node.find(predicate: (org.w3c.dom.Node) -> Boolean): org.w3c.dom.Node? {
671 val iterator = childrenIterator()
672 while (iterator.hasNext()) {
673 val node = iterator.next()
674 if (predicate(node)) {
675 return node
676 }
677 }
678 return null
679 }
680
681 /**
682 * Modifies the given .pom to specify that every dependency in <group> refers to a single version
683 * and can't be automatically promoted to a new version. This will replace, for example, a version
684 * string of "1.0" with a version string of "[1.0]"
685 *
686 * Note: this is not enforced in Gradle nor in plain Maven (without the Enforcer plugin)
687 * (https://github.com/gradle/gradle/issues/8297)
688 */
assignSingleVersionDependenciesInGroupForPomnull689 fun assignSingleVersionDependenciesInGroupForPom(xml: XmlProvider, mavenGroup: LibraryGroup) {
690 if (!mavenGroup.requireSameVersion) {
691 return
692 }
693
694 val dependencies =
695 xml.asElement().find { it.nodeName == "dependencies" } as? org.w3c.dom.Element ?: return
696
697 dependencies.getElementsByTagName("dependency").forEach { dependency ->
698 val groupId =
699 dependency.find { it.nodeName == "groupId" }?.textContent
700 ?: throw IllegalArgumentException("Failed to locate groupId node")
701 if (groupId == mavenGroup.group) {
702 val versionNode =
703 dependency.find { it.nodeName == "version" }
704 ?: throw IllegalArgumentException("Failed to locate version node")
705 val version = versionNode.textContent
706 if (isVersionRange(version)) {
707 throw GradleException("Unsupported version '$version': already is a version range")
708 }
709 val pinnedVersion = "[$version]"
710 versionNode.textContent = pinnedVersion
711 }
712 }
713 }
714
isVersionRangenull715 private fun isVersionRange(text: String): Boolean {
716 return text.contains("[") ||
717 text.contains("]") ||
718 text.contains("(") ||
719 text.contains(")") ||
720 text.contains(",")
721 }
722
723 /**
724 * Ensures that artifactIds are consistent when using configuration caching. A workaround for
725 * https://github.com/gradle/gradle/issues/18369
726 */
ensureConsistentJvmSuffixnull727 fun ensureConsistentJvmSuffix(xml: XmlProvider) {
728 val dependencies =
729 xml.asElement().find { it.nodeName == "dependencies" } as? org.w3c.dom.Element ?: return
730
731 dependencies.getElementsByTagName("dependency").forEach { dependency ->
732 val artifactId =
733 dependency.find { it.nodeName == "artifactId" }
734 ?: throw IllegalArgumentException("Failed to locate artifactId node")
735 // kotlinx-coroutines-core is only a .pom and only depends on kotlinx-coroutines-core-jvm,
736 // so the two artifacts should be approximately equivalent. However,
737 // when loading from configuration cache, Gradle often returns a different resolution.
738 // We replace it here to ensure consistency and predictability, and
739 // to avoid having to rerun any zip tasks that include it
740 if (artifactId.textContent == "kotlinx-coroutines-core-jvm") {
741 artifactId.textContent = "kotlinx-coroutines-core"
742 }
743 }
744 }
745
Projectnull746 private fun Project.appliesJavaGradlePluginPlugin() = pluginManager.hasPlugin("java-gradle-plugin")
747
748 private const val ANDROID_GIT_URL =
749 "scm:git:https://android.googlesource.com/platform/frameworks/support"
750
751 internal const val KMP_ANCHOR_PUBLICATION_NAME = "androidxKmp"
752
753 @DisableCachingByDefault(because = "Not worth caching")
754 internal abstract class ZipStubAarTask : Zip()
755