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