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