1 /*
<lambda>null2  * Copyright 2024 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.sources
18 
19 import androidx.build.LazyInputsCopyTask
20 import androidx.build.capitalize
21 import androidx.build.dackka.DokkaAnalysisPlatform
22 import androidx.build.dackka.docsPlatform
23 import androidx.build.hasAndroidMultiplatformPlugin
24 import androidx.build.multiplatformExtension
25 import androidx.build.registerAsComponentForPublishing
26 import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
27 import com.android.build.api.variant.LibraryAndroidComponentsExtension
28 import com.android.build.api.variant.LibraryVariant
29 import com.google.gson.GsonBuilder
30 import org.gradle.api.DefaultTask
31 import org.gradle.api.GradleException
32 import org.gradle.api.Project
33 import org.gradle.api.attributes.Bundling
34 import org.gradle.api.attributes.Category
35 import org.gradle.api.attributes.DocsType
36 import org.gradle.api.attributes.Usage
37 import org.gradle.api.file.DuplicatesStrategy
38 import org.gradle.api.file.RegularFileProperty
39 import org.gradle.api.plugins.JavaPluginExtension
40 import org.gradle.api.provider.Provider
41 import org.gradle.api.tasks.CacheableTask
42 import org.gradle.api.tasks.Input
43 import org.gradle.api.tasks.OutputFile
44 import org.gradle.api.tasks.TaskAction
45 import org.gradle.api.tasks.TaskProvider
46 import org.gradle.api.tasks.bundling.Jar
47 import org.gradle.kotlin.dsl.named
48 import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
49 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation.Companion.MAIN_COMPILATION_NAME
50 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
51 import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
52 
53 /** Sets up a source jar task for an Android library project. */
54 fun Project.configureSourceJarForAndroid(
55     libraryVariant: LibraryVariant,
56     samplesProjects: MutableCollection<Project>
57 ) {
58     val allSources =
59         project.files(libraryVariant.sources.java?.all) +
60             project.files(libraryVariant.sources.kotlin?.all)
61     val sourceJar =
62         tasks.register("sourceJar${libraryVariant.name.capitalize()}", Jar::class.java) { task ->
63             task.archiveClassifier.set("sources")
64             task.from(allSources)
65             task.exclude { it.file.path.contains("generated") }
66             // Do not allow source files with duplicate names, information would be lost
67             // otherwise.
68             task.duplicatesStrategy = DuplicatesStrategy.FAIL
69         }
70     registerSourcesVariant(sourceJar)
71     registerSamplesLibraries(samplesProjects)
72 
73     // b/272214715
74     configurations.whenObjectAdded {
75         if (it.name == "debugSourcesElements" || it.name == "releaseSourcesElements") {
76             it.artifacts.whenObjectAdded { _ ->
77                 it.attributes.attribute(
78                     DocsType.DOCS_TYPE_ATTRIBUTE,
79                     project.objects.named(DocsType::class.java, "fake-sources")
80                 )
81             }
82         }
83     }
84 
85     val disableNames =
86         setOf(
87             "releaseSourcesJar",
88         )
89     disableUnusedSourceJarTasks(disableNames)
90 }
91 
Projectnull92 fun Project.configureMultiplatformSourcesForAndroid(
93     variantName: String,
94     target: KotlinMultiplatformAndroidLibraryTarget,
95     samplesProjects: MutableCollection<Project>
96 ) {
97     val sourceJar =
98         tasks.register("sourceJar${variantName.capitalize()}", Jar::class.java) { task ->
99             task.archiveClassifier.set("sources")
100             target.mainCompilation().allKotlinSourceSets.forEach { sourceSet ->
101                 task.from(sourceSet.kotlin.srcDirs) { copySpec -> copySpec.into(sourceSet.name) }
102             }
103             task.duplicatesStrategy = DuplicatesStrategy.FAIL
104         }
105     registerSourcesVariant(sourceJar)
106     registerSamplesLibraries(samplesProjects)
107 }
108 
109 /** Sets up a source jar task for a Java library project. */
Projectnull110 fun Project.configureSourceJarForJava(samplesProjects: MutableCollection<Project>) {
111     val sourceJar =
112         tasks.register("sourceJar", Jar::class.java) { task ->
113             task.archiveClassifier.set("sources")
114 
115             // Do not allow source files with duplicate names, information would be lost otherwise.
116             // Different sourceSets in KMP should use different platform infixes, see b/203764756
117             task.duplicatesStrategy = DuplicatesStrategy.FAIL
118 
119             extensions.findByType(JavaPluginExtension::class.java)?.let { javaExtension ->
120                 // Since KotlinPlugin applies JavaPlugin, it's possible for JavaPlugin to exist, but
121                 // not to have "main".  Eventually, we should stop expecting to grab sourceSets by
122                 // name
123                 // (b/235828421)
124                 javaExtension.sourceSets.findByName("main")?.let {
125                     task.from(it.allSource.sourceDirectories)
126                 }
127             }
128 
129             extensions.findByType(KotlinMultiplatformExtension::class.java)?.let { kmpExtension ->
130                 for (sourceSetName in listOf("commonMain", "jvmMain")) {
131                     kmpExtension.sourceSets.findByName(sourceSetName)?.let { sourceSet ->
132                         task.from(sourceSet.kotlin.sourceDirectories)
133                     }
134                 }
135             }
136         }
137     registerSourcesVariant(sourceJar)
138     registerSamplesLibraries(samplesProjects)
139 
140     val disableNames =
141         setOf(
142             "kotlinSourcesJar",
143         )
144     disableUnusedSourceJarTasks(disableNames)
145 }
146 
Projectnull147 fun Project.configureSourceJarForMultiplatform() {
148     val kmpExtension =
149         multiplatformExtension
150             ?: throw GradleException(
151                 "Unable to find multiplatform extension while configuring multiplatform source JAR"
152             )
153     val metadataFile = layout.buildDirectory.file(PROJECT_STRUCTURE_METADATA_FILEPATH)
154     val multiplatformMetadataTask =
155         tasks.register("createMultiplatformMetadata", CreateMultiplatformMetadata::class.java) {
156             it.metadataFile.set(metadataFile)
157             it.sourceSetMetadata = project.provider { createSourceSetMetadata(kmpExtension) }
158         }
159     val sourceJar =
160         tasks.register("multiplatformSourceJar", Jar::class.java) { task ->
161             task.dependsOn(multiplatformMetadataTask)
162             task.archiveClassifier.set("multiplatform-sources")
163 
164             // Do not allow source files with duplicate names, information would be lost otherwise.
165             // Different sourceSets in KMP should use different platform infixes, see b/203764756
166             task.duplicatesStrategy = DuplicatesStrategy.FAIL
167             kmpExtension.targets
168                 // Filter out sources from stub targets as they are not intended to be documented
169                 .filterNot { it.name in setOfStubTargets }
170                 .flatMap { it.mainCompilation().allKotlinSourceSets }
171                 .toSet()
172                 // Sort sourceSets to ensure child sourceSets come after their parents, b/404784813
173                 .sortedWith(compareBy({ it.dependsOn.size }, { it.name }))
174                 .forEach { sourceSet ->
175                     task.from(sourceSet.kotlin.srcDirs) { copySpec ->
176                         copySpec.into(sourceSet.name)
177                     }
178                 }
179             task.metaInf.from(metadataFile)
180         }
181     registerMultiplatformSourcesVariant(sourceJar)
182 
183     val disableNames =
184         setOf(
185             "kotlinSourcesJar",
186         )
187     disableUnusedSourceJarTasks(disableNames)
188 }
189 
Projectnull190 fun Project.disableUnusedSourceJarTasks(disableNames: Set<String>) {
191     project.tasks.configureEach { task ->
192         if (disableNames.contains(task.name)) {
193             task.enabled = false
194         }
195     }
196 }
197 
198 internal val Project.multiplatformUsage
199     get() = objects.named<Usage>("androidx-multiplatform-docs")
200 
Projectnull201 private fun Project.registerMultiplatformSourcesVariant(sourceJar: TaskProvider<Jar>) =
202     registerSourcesVariant(kmpSourcesConfigurationName, sourceJar, multiplatformUsage)
203 
204 private fun Project.registerSourcesVariant(sourceJar: TaskProvider<Jar>) =
205     registerSourcesVariant(sourcesConfigurationName, sourceJar, objects.named(Usage.JAVA_RUNTIME))
206 
207 private fun Project.registerSourcesVariant(
208     configurationName: String,
209     sourceJar: TaskProvider<Jar>,
210     usage: Usage,
211 ) =
212     configurations.create(configurationName) { gradleVariant ->
213         gradleVariant.isVisible = false
214         gradleVariant.isCanBeResolved = false
215         gradleVariant.attributes.attribute(Usage.USAGE_ATTRIBUTE, usage)
216         gradleVariant.attributes.attribute(
217             Category.CATEGORY_ATTRIBUTE,
218             objects.named<Category>(Category.DOCUMENTATION)
219         )
220         gradleVariant.attributes.attribute(
221             Bundling.BUNDLING_ATTRIBUTE,
222             objects.named<Bundling>(Bundling.EXTERNAL)
223         )
224         gradleVariant.attributes.attribute(
225             DocsType.DOCS_TYPE_ATTRIBUTE,
226             objects.named<DocsType>(DocsType.SOURCES)
227         )
228         gradleVariant.outgoing.artifact(sourceJar)
229         registerAsComponentForPublishing(gradleVariant)
230     }
231 
232 /**
233  * Finds the main compilation for a source set, usually called 'main' but for android we need to
234  * search for 'release' instead.
235  */
mainCompilationnull236 private fun KotlinTarget.mainCompilation() =
237     compilations.findByName(MAIN_COMPILATION_NAME) ?: compilations.getByName("release")
238 
239 /**
240  * Writes a metadata file to the given [metadataFile] location for all multiplatform Kotlin source
241  * sets including their dependencies and analysisPlatform. This is consumed when we are reading
242  * source JARs so that we can pass the correct inputs to Dackka.
243  */
244 @CacheableTask
245 abstract class CreateMultiplatformMetadata : DefaultTask() {
246     @Input lateinit var sourceSetMetadata: Provider<Map<String, Any>>
247 
248     @get:OutputFile abstract val metadataFile: RegularFileProperty
249 
250     @TaskAction
251     fun execute() {
252         metadataFile.get().asFile.apply {
253             parentFile.mkdirs()
254             createNewFile()
255             val gson = GsonBuilder().setPrettyPrinting().create()
256             writeText(gson.toJson(sourceSetMetadata.get()))
257         }
258     }
259 }
260 
createSourceSetMetadatanull261 fun createSourceSetMetadata(kmpExtension: KotlinMultiplatformExtension): Map<String, Any> {
262     val commonMain = kmpExtension.sourceSets.getByName("commonMain")
263     val sourceSetsByName =
264         mutableMapOf(
265             "commonMain" to
266                 mapOf(
267                     "name" to commonMain.name,
268                     "dependencies" to commonMain.dependsOn.map { it.name }.sorted(),
269                     "analysisPlatform" to DokkaAnalysisPlatform.COMMON.jsonName
270                 )
271         )
272     kmpExtension.targets.forEach { target ->
273         // Skip adding entries for stub targets are they are not intended to be documented
274         if (target.name in setOfStubTargets) return@forEach
275         target.mainCompilation().allKotlinSourceSets.forEach {
276             sourceSetsByName.getOrPut(it.name) {
277                 mapOf(
278                     "name" to it.name,
279                     "dependencies" to it.dependsOn.map { it.name }.sorted(),
280                     "analysisPlatform" to target.docsPlatform().jsonName
281                 )
282             }
283         }
284     }
285     return mapOf("sourceSets" to sourceSetsByName.keys.sorted().map { sourceSetsByName[it] })
286 }
287 
Projectnull288 private fun Project.registerSamplesLibraries(samplesProjects: MutableCollection<Project>) =
289     samplesProjects.forEach {
290         dependencies.add("samples", it)
291         // this publishing variant is used in non-KMP projects and non-KMP source jars of KMP
292         // projects
293         val publishingVariants = mutableListOf<String>()
294         val hasAndroidMultiplatformPlugin = hasAndroidMultiplatformPlugin()
295         publishingVariants.add(sourcesConfigurationName)
296         project.multiplatformExtension?.let { ext ->
297             val hasAndroidJvmTarget =
298                 ext.targets.any { target -> target.platformType == KotlinPlatformType.androidJvm }
299             publishingVariants += kmpSourcesConfigurationName // used for KMP source jars
300             // used for --android source jars of KMP projects
301             if (hasAndroidMultiplatformPlugin) {
302                 publishingVariants += "$androidMultiplatformSourcesConfigurationName-published"
303             } else if (hasAndroidJvmTarget) {
304                 publishingVariants += "release${sourcesConfigurationName.capitalize()}"
305             }
306         }
307         updateCopySampleSourceJarsTaskWithVariant(publishingVariants)
308     }
309 
310 /**
311  * Updates the published variants with the output of [LazyInputsCopyTask]. This function must be
312  * called in the stack of [LibraryAndroidComponentsExtension.onVariants] as at that stage,
313  * [AndroidXExtension.samplesProjects] would be populated.
314  */
Projectnull315 private fun Project.updateCopySampleSourceJarsTaskWithVariant(publishingVariants: List<String>) {
316     val copySampleJarTask = tasks.named("copySampleSourceJars", LazyInputsCopyTask::class.java)
317     val configuredVariants = mutableListOf<String>()
318     configurations.configureEach { config ->
319         if (config.name in publishingVariants) {
320             // Register the sample source jar as an outgoing artifact of the publishing variant
321             config.outgoing.artifact(copySampleJarTask.flatMap { it.destinationJar }) {
322                 // The only place where this classifier is load-bearing is when we filter sample
323                 // source jars out in our AndroidXDocsImplPlugin.configureUnzipJvmSourcesTasks
324                 it.classifier = "samples-sources"
325             }
326             configuredVariants.add(config.name)
327         }
328     }
329     // Check that all the variants are configured because we only configure when the name matches
330     // and could fail silently if we never see a matching configuration
331     gradle.taskGraph.whenReady {
332         if (!configuredVariants.containsAll(publishingVariants)) {
333             val unconfiguredVariants =
334                 (publishingVariants.toSet() - configuredVariants.toSet()).joinToString(", ")
335             throw GradleException(
336                 "Sample source jar tasks were not configured for $unconfiguredVariants"
337             )
338         }
339     }
340 }
341 
342 /**
343  * Set of targets are there to serve as stubs, but are not expected to be consumed by library
344  * consumers.
345  */
346 private val setOfStubTargets = setOf("commonStubs", "jvmStubs", "linuxx64Stubs")
347 
348 internal const val PROJECT_STRUCTURE_METADATA_FILENAME = "kotlin-project-structure-metadata.json"
349 
350 private const val PROJECT_STRUCTURE_METADATA_FILEPATH =
351     "project_structure_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME"
352 
353 internal const val sourcesConfigurationName = "sourcesElements"
354 private const val androidMultiplatformSourcesConfigurationName = "androidSourcesElements"
355 private const val kmpSourcesConfigurationName = "androidxSourcesElements"
356