1 /*
<lambda>null2 * Copyright 2020 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.docs
18
19 import androidx.build.configureTaskTimeouts
20 import androidx.build.dackka.DackkaTask
21 import androidx.build.dackka.GenerateMetadataTask
22 import androidx.build.defaultAndroidConfig
23 import androidx.build.getAndroidJar
24 import androidx.build.getBuildId
25 import androidx.build.getCheckoutRoot
26 import androidx.build.getDistributionDirectory
27 import androidx.build.getKeystore
28 import androidx.build.getLibraryByName
29 import androidx.build.getSupportRootFolder
30 import androidx.build.metalava.versionMetadataUsage
31 import androidx.build.sources.PROJECT_STRUCTURE_METADATA_FILENAME
32 import androidx.build.sources.multiplatformUsage
33 import androidx.build.versionCatalog
34 import androidx.build.workaroundPrebuiltTakingPrecedenceOverProject
35 import com.android.build.api.attributes.BuildTypeAttr
36 import com.android.build.api.dsl.LibraryExtension
37 import com.android.build.gradle.LibraryPlugin
38 import com.google.gson.GsonBuilder
39 import java.io.File
40 import java.io.FileNotFoundException
41 import java.time.Duration
42 import java.time.LocalDateTime
43 import java.util.concurrent.TimeUnit
44 import javax.inject.Inject
45 import org.gradle.api.DefaultTask
46 import org.gradle.api.Plugin
47 import org.gradle.api.Project
48 import org.gradle.api.Task
49 import org.gradle.api.artifacts.ComponentMetadataContext
50 import org.gradle.api.artifacts.ComponentMetadataRule
51 import org.gradle.api.artifacts.Configuration
52 import org.gradle.api.attributes.Attribute
53 import org.gradle.api.attributes.Bundling
54 import org.gradle.api.attributes.Category
55 import org.gradle.api.attributes.DocsType
56 import org.gradle.api.attributes.LibraryElements
57 import org.gradle.api.attributes.Usage
58 import org.gradle.api.file.ArchiveOperations
59 import org.gradle.api.file.Directory
60 import org.gradle.api.file.DirectoryProperty
61 import org.gradle.api.file.DuplicatesStrategy
62 import org.gradle.api.file.FileCollection
63 import org.gradle.api.file.FileSystemOperations
64 import org.gradle.api.file.FileTree
65 import org.gradle.api.file.ProjectLayout
66 import org.gradle.api.file.RegularFile
67 import org.gradle.api.file.RegularFileProperty
68 import org.gradle.api.model.ObjectFactory
69 import org.gradle.api.plugins.JavaBasePlugin
70 import org.gradle.api.provider.Property
71 import org.gradle.api.provider.Provider
72 import org.gradle.api.tasks.CacheableTask
73 import org.gradle.api.tasks.Classpath
74 import org.gradle.api.tasks.InputFiles
75 import org.gradle.api.tasks.Internal
76 import org.gradle.api.tasks.OutputDirectory
77 import org.gradle.api.tasks.OutputFile
78 import org.gradle.api.tasks.PathSensitive
79 import org.gradle.api.tasks.PathSensitivity
80 import org.gradle.api.tasks.Sync
81 import org.gradle.api.tasks.TaskAction
82 import org.gradle.api.tasks.TaskProvider
83 import org.gradle.api.tasks.bundling.Zip
84 import org.gradle.api.tasks.testing.Test
85 import org.gradle.kotlin.dsl.all
86 import org.gradle.kotlin.dsl.getByType
87 import org.gradle.kotlin.dsl.named
88 import org.gradle.kotlin.dsl.register
89 import org.gradle.work.DisableCachingByDefault
90
91 /**
92 * Plugin that allows to build documentation for a given set of prebuilt and tip of tree projects.
93 */
94 abstract class AndroidXDocsImplPlugin : Plugin<Project> {
95 lateinit var docsType: String
96 lateinit var docsSourcesConfiguration: Configuration
97 lateinit var multiplatformDocsSourcesConfiguration: Configuration
98 lateinit var samplesSourcesConfiguration: Configuration
99 lateinit var versionMetadataConfiguration: Configuration
100 lateinit var dependencyClasspath: FileCollection
101
102 @get:Inject abstract val archiveOperations: ArchiveOperations
103
104 override fun apply(project: Project) {
105 docsType = project.name.removePrefix("docs-")
106 project.plugins.configureEach { plugin ->
107 when (plugin) {
108 is LibraryPlugin -> {
109 val libraryExtension = project.extensions.getByType<LibraryExtension>()
110 libraryExtension.compileSdk =
111 project.defaultAndroidConfig.latestStableCompileSdk
112 libraryExtension.buildToolsVersion =
113 project.defaultAndroidConfig.buildToolsVersion
114
115 // Use a local debug keystore to avoid build server issues.
116 val debugSigningConfig = libraryExtension.signingConfigs.getByName("debug")
117 debugSigningConfig.storeFile = project.getKeystore()
118 libraryExtension.buildTypes.configureEach { buildType ->
119 // Sign all the builds (including release) with debug key
120 buildType.signingConfig = debugSigningConfig
121 }
122 }
123 }
124 }
125 disableUnneededTasks(project)
126 createConfigurations(project)
127 val buildOnServer =
128 project.tasks.register<DocsBuildOnServer>("buildOnServer") {
129 buildId = getBuildId()
130 docsType = this@AndroidXDocsImplPlugin.docsType
131 distributionDirectory = project.getDistributionDirectory()
132 }
133
134 val unzippedDeprecatedSamplesSources =
135 project.layout.buildDirectory.dir("unzippedDeprecatedSampleSources")
136 val deprecatedUnzipSamplesTask =
137 configureUnzipTask(
138 project,
139 "unzipSampleSourcesDeprecated",
140 unzippedDeprecatedSamplesSources,
141 samplesSourcesConfiguration
142 )
143 val unzippedKmpSamplesSourcesDirectory =
144 project.layout.buildDirectory.dir("unzippedMultiplatformSampleSources")
145 val unzippedJvmSamplesSourcesDirectory =
146 project.layout.buildDirectory.dir("unzippedJvmSampleSources")
147 val unzippedJvmSourcesDirectory = project.layout.buildDirectory.dir("unzippedJvmSources")
148 val unzippedMultiplatformSourcesDirectory =
149 project.layout.buildDirectory.dir("unzippedMultiplatformSources")
150 val mergedProjectMetadata =
151 project.layout.buildDirectory.file(
152 "project_metadata/$PROJECT_STRUCTURE_METADATA_FILENAME"
153 )
154 val (unzipJvmSourcesTask, unzipJvmSamplesTask) =
155 configureUnzipJvmSourcesTasks(
156 project,
157 unzippedJvmSourcesDirectory,
158 unzippedJvmSamplesSourcesDirectory,
159 docsSourcesConfiguration
160 )
161 val configureMultiplatformSourcesTask =
162 configureMultiplatformInputsTasks(
163 project,
164 unzippedMultiplatformSourcesDirectory,
165 unzippedKmpSamplesSourcesDirectory,
166 multiplatformDocsSourcesConfiguration,
167 mergedProjectMetadata
168 )
169
170 configureDackka(
171 project = project,
172 unzippedJvmSourcesDirectory = unzippedJvmSourcesDirectory,
173 unzippedMultiplatformSourcesDirectory = unzippedMultiplatformSourcesDirectory,
174 unzipJvmSourcesTask = unzipJvmSourcesTask,
175 configureMultiplatformSourcesTask = configureMultiplatformSourcesTask,
176 unzippedDeprecatedSamplesSources = unzippedDeprecatedSamplesSources,
177 unzipDeprecatedSamplesTask = deprecatedUnzipSamplesTask,
178 unzippedJvmSamplesSources = unzippedJvmSamplesSourcesDirectory,
179 unzipJvmSamplesTask = unzipJvmSamplesTask,
180 unzippedKmpSamplesSources = unzippedKmpSamplesSourcesDirectory,
181 dependencyClasspath = dependencyClasspath,
182 buildOnServer = buildOnServer,
183 docsConfiguration = docsSourcesConfiguration,
184 multiplatformDocsConfiguration = multiplatformDocsSourcesConfiguration,
185 mergedProjectMetadata = mergedProjectMetadata
186 )
187
188 project.configureTaskTimeouts()
189 project.workaroundPrebuiltTakingPrecedenceOverProject()
190 }
191
192 /**
193 * Creates and configures a task that will build a list of all sources for projects in
194 * [docsConfiguration] configuration, resolve them and put them to [destinationDirectory].
195 */
196 private fun configureUnzipTask(
197 project: Project,
198 taskName: String,
199 destinationDirectory: Provider<Directory>,
200 docsConfiguration: Configuration
201 ): TaskProvider<Sync> {
202 return project.tasks.register(taskName, Sync::class.java) { task ->
203 val sources = docsConfiguration.incoming.artifactView {}.files
204 // Store archiveOperations into a local variable to prevent access to the plugin
205 // during the task execution, as that breaks configuration caching.
206 val localVar = archiveOperations
207 task.from(
208 sources.elements.map { jars ->
209 jars.map { jar ->
210 localVar.zipTree(jar).matching {
211 // Filter out files that documentation tools cannot process.
212 it.exclude("**/*.MF")
213 it.exclude("**/*.aidl")
214 it.exclude("**/META-INF/**")
215 it.exclude("**/OWNERS")
216 it.exclude("**/package.html")
217 it.exclude("**/*.md")
218 }
219 }
220 }
221 )
222 task.into(destinationDirectory)
223 // TODO(123020809) remove this filter once it is no longer necessary to prevent Dokka
224 // from failing
225 val regex = Regex("@attr ref ([^*]*)styleable#([^_*]*)_([^*]*)$")
226 task.filter { line -> regex.replace(line, "{@link $1attr#$3}") }
227 }
228 }
229
230 /**
231 * Creates and configures a task that builds a list of select sources from jars and places them
232 * in [sourcesDestinationDirectory], partitioning samples into [samplesDestinationDirectory].
233 *
234 * This is a modified version of [configureUnzipTask], customized for Dackka usage.
235 */
236 private fun configureUnzipJvmSourcesTasks(
237 project: Project,
238 sourcesDestinationDirectory: Provider<Directory>,
239 samplesDestinationDirectory: Provider<Directory>,
240 docsConfiguration: Configuration
241 ): Pair<TaskProvider<Sync>, TaskProvider<Sync>> {
242 val pairProvider =
243 docsConfiguration.incoming
244 .artifactView {}
245 .files
246 .elements
247 .map {
248 it.map { it.asFile }.toSortedSet().partition { "samples" !in it.toString() }
249 }
250 return project.tasks.register("unzipJvmSources", Sync::class.java) { task ->
251 // Store archiveOperations into a local variable to prevent access to the plugin
252 // during the task execution, as that breaks configuration caching.
253 val localVar = archiveOperations
254 val tempDir = project.layout.buildDirectory.dir("tmp/JvmSources").get().asFile
255 // Get rid of stale files in the directory
256 tempDir.deleteRecursively()
257 task.into(sourcesDestinationDirectory)
258 task.from(
259 pairProvider
260 .map { it.first }
261 .map {
262 it.map { jar ->
263 localVar
264 .zipTree(jar)
265 .matching { it.exclude("**/META-INF/MANIFEST.MF") }
266 .rewriteImageTagsAndCopy(tempDir)
267 tempDir
268 }
269 }
270 )
271 // Files with the same path in different source jars of the same library will lead to
272 // some classes/methods not appearing in the docs.
273 task.duplicatesStrategy = DuplicatesStrategy.WARN
274 } to
275 project.tasks.register("unzipSampleSources", Sync::class.java) { task ->
276 // Store archiveOperations into a local variable to prevent access to the plugin
277 // during the task execution, as that breaks configuration caching.
278 val localVar = archiveOperations
279 val tempDir = project.layout.buildDirectory.dir("tmp/SampleSources").get().asFile
280 // Get rid of stale files in the directory
281 tempDir.deleteRecursively()
282 task.into(samplesDestinationDirectory)
283 task.from(
284 pairProvider
285 .map { it.second }
286 .map {
287 it.map { jar ->
288 localVar
289 .zipTree(jar)
290 .matching { it.exclude("**/META-INF/MANIFEST.MF") }
291 .rewriteImageTagsAndCopy(tempDir)
292 tempDir
293 }
294 }
295 )
296 // We expect this to happen when multiple libraries use the same sample, e.g.
297 // paging.
298 task.duplicatesStrategy = DuplicatesStrategy.INCLUDE
299 }
300 }
301
302 /**
303 * Creates multiple tasks to unzip multiplatform sources and merge their metadata to be used as
304 * input for Dackka. Returns a single umbrella task which depends on the others.
305 */
306 private fun configureMultiplatformInputsTasks(
307 project: Project,
308 unzippedMultiplatformSourcesDirectory: Provider<Directory>,
309 unzippedMultiplatformSamplesDirectory: Provider<Directory>,
310 multiplatformDocsSourcesConfiguration: Configuration,
311 mergedProjectMetadata: Provider<RegularFile>
312 ): TaskProvider<MergeMultiplatformMetadataTask> {
313 val tempMultiplatformMetadataDirectory =
314 project.layout.buildDirectory.dir("tmp/multiplatformMetadataFiles")
315 // unzip the sources into source folder and metadata files into folders per project
316 val unzipMultiplatformSources =
317 project.tasks.register(
318 "unzipMultiplatformSources",
319 UnzipMultiplatformSourcesTask::class.java
320 ) {
321 it.inputJars.set(
322 multiplatformDocsSourcesConfiguration.incoming.artifactView {}.files
323 )
324 it.metadataOutput.set(tempMultiplatformMetadataDirectory)
325 it.sourceOutput.set(unzippedMultiplatformSourcesDirectory)
326 it.samplesOutput.set(unzippedMultiplatformSamplesDirectory)
327 }
328 // merge all the metadata files from the individual project dirs
329 return project.tasks.register(
330 "mergeMultiplatformMetadata",
331 MergeMultiplatformMetadataTask::class.java
332 ) {
333 it.mergedProjectMetadata.set(mergedProjectMetadata)
334 it.inputDirectory.set(unzipMultiplatformSources.flatMap { it.metadataOutput })
335 }
336 }
337
338 /**
339 * The following configurations are created to build a list of projects that need to be
340 * documented and should be used from build.gradle of docs projects for the following:
341 * - docs(project(":foo:foo") or docs("androidx.foo:foo:1.0.0") for docs sources
342 * - samples(project(":foo:foo-samples") or samples("androidx.foo:foo-samples:1.0.0") for
343 * samples sources
344 * - stubs(project(":foo:foo-stubs")) - stubs needed for a documented library
345 */
346 private fun createConfigurations(project: Project) {
347 project.dependencies.components.all<SourcesVariantRule>()
348 val docsConfiguration =
349 project.configurations.create("docs") {
350 it.isCanBeResolved = false
351 it.isCanBeConsumed = false
352 }
353 // This exists for libraries that are deprecated or not hosted in the AndroidX repo
354 val docsWithoutApiSinceConfiguration =
355 project.configurations.create("docsWithoutApiSince") {
356 it.isCanBeResolved = false
357 it.isCanBeConsumed = false
358 }
359 val multiplatformDocsConfiguration =
360 project.configurations.create("kmpDocs") {
361 it.isCanBeResolved = false
362 it.isCanBeConsumed = false
363 }
364 val samplesConfiguration =
365 project.configurations.create("samples") {
366 it.isCanBeResolved = false
367 it.isCanBeConsumed = false
368 }
369 val stubsConfiguration =
370 project.configurations.create("stubs") {
371 it.isCanBeResolved = false
372 it.isCanBeConsumed = false
373 }
374
375 fun Configuration.setResolveSources() {
376 isTransitive = false
377 isCanBeConsumed = false
378 attributes {
379 it.attribute(
380 Usage.USAGE_ATTRIBUTE,
381 project.objects.named<Usage>(Usage.JAVA_RUNTIME)
382 )
383 it.attribute(
384 Category.CATEGORY_ATTRIBUTE,
385 project.objects.named<Category>(Category.DOCUMENTATION)
386 )
387 it.attribute(
388 DocsType.DOCS_TYPE_ATTRIBUTE,
389 project.objects.named<DocsType>(DocsType.SOURCES)
390 )
391 it.attribute(
392 LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
393 project.objects.named<LibraryElements>(LibraryElements.JAR)
394 )
395 }
396 }
397 docsSourcesConfiguration =
398 project.configurations.create("docs-sources") {
399 it.setResolveSources()
400 it.extendsFrom(docsConfiguration, docsWithoutApiSinceConfiguration)
401 }
402 multiplatformDocsSourcesConfiguration =
403 project.configurations.create("multiplatform-docs-sources") { configuration ->
404 configuration.isTransitive = false
405 configuration.isCanBeConsumed = false
406 configuration.attributes {
407 it.attribute(Usage.USAGE_ATTRIBUTE, project.multiplatformUsage)
408 it.attribute(
409 Category.CATEGORY_ATTRIBUTE,
410 project.objects.named<Category>(Category.DOCUMENTATION)
411 )
412 it.attribute(
413 DocsType.DOCS_TYPE_ATTRIBUTE,
414 project.objects.named<DocsType>(DocsType.SOURCES)
415 )
416 it.attribute(
417 LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE,
418 project.objects.named<LibraryElements>(LibraryElements.JAR)
419 )
420 }
421 configuration.extendsFrom(multiplatformDocsConfiguration)
422 }
423 samplesSourcesConfiguration =
424 project.configurations.create("samples-sources") {
425 it.setResolveSources()
426 it.extendsFrom(samplesConfiguration)
427 }
428
429 versionMetadataConfiguration =
430 project.configurations.create("library-version-metadata") {
431 it.isTransitive = false
432 it.isCanBeConsumed = false
433
434 it.attributes.attribute(Usage.USAGE_ATTRIBUTE, project.versionMetadataUsage)
435 it.attributes.attribute(
436 Category.CATEGORY_ATTRIBUTE,
437 project.objects.named<Category>(Category.DOCUMENTATION)
438 )
439 it.attributes.attribute(
440 Bundling.BUNDLING_ATTRIBUTE,
441 project.objects.named<Bundling>(Bundling.EXTERNAL)
442 )
443
444 it.extendsFrom(docsConfiguration, multiplatformDocsConfiguration)
445 }
446
447 fun Configuration.setResolveClasspathForUsage(usage: String) {
448 isCanBeConsumed = false
449 attributes {
450 it.attribute(Usage.USAGE_ATTRIBUTE, project.objects.named<Usage>(usage))
451 it.attribute(
452 Category.CATEGORY_ATTRIBUTE,
453 project.objects.named<Category>(Category.LIBRARY)
454 )
455 it.attribute(
456 BuildTypeAttr.ATTRIBUTE,
457 project.objects.named<BuildTypeAttr>("release")
458 )
459 }
460 extendsFrom(
461 docsConfiguration,
462 samplesConfiguration,
463 stubsConfiguration,
464 docsWithoutApiSinceConfiguration
465 )
466 }
467
468 // Build a compile & runtime classpaths for needed for documenting the libraries
469 // from the configurations above.
470 val docsCompileClasspath =
471 project.configurations.create("docs-compile-classpath") {
472 it.setResolveClasspathForUsage(Usage.JAVA_API)
473 }
474 val docsRuntimeClasspath =
475 project.configurations.create("docs-runtime-classpath") {
476 it.setResolveClasspathForUsage(Usage.JAVA_RUNTIME)
477 }
478 // TODO: Set to DEFAULT when b/407754247 is fixed
479 val kotlinDefaultCatalogVersion = androidx.build.KotlinTarget.KOTLIN_1_8.catalogVersion
480 val kotlinLatest = project.versionCatalog.findVersion(kotlinDefaultCatalogVersion).get()
481 listOf(docsCompileClasspath, docsRuntimeClasspath).forEach { config ->
482 config.resolutionStrategy {
483 it.eachDependency { details ->
484 if (details.requested.group == "org.jetbrains.kotlin") {
485 details.useVersion(kotlinLatest.requiredVersion)
486 }
487 }
488 }
489 }
490 dependencyClasspath =
491 docsCompileClasspath.incoming
492 .artifactView {
493 it.attributes.attribute(
494 Attribute.of("artifactType", String::class.java),
495 "android-classes"
496 )
497 }
498 .files +
499 docsRuntimeClasspath.incoming
500 .artifactView {
501 it.attributes.attribute(
502 Attribute.of("artifactType", String::class.java),
503 "android-classes"
504 )
505 }
506 .files
507 }
508
509 private fun configureDackka(
510 project: Project,
511 unzippedJvmSourcesDirectory: Provider<Directory>,
512 unzippedMultiplatformSourcesDirectory: Provider<Directory>,
513 unzipJvmSourcesTask: TaskProvider<Sync>,
514 configureMultiplatformSourcesTask: TaskProvider<MergeMultiplatformMetadataTask>,
515 unzippedDeprecatedSamplesSources: Provider<Directory>,
516 unzipDeprecatedSamplesTask: TaskProvider<Sync>,
517 unzippedJvmSamplesSources: Provider<Directory>,
518 unzipJvmSamplesTask: TaskProvider<Sync>,
519 unzippedKmpSamplesSources: Provider<Directory>,
520 dependencyClasspath: FileCollection,
521 buildOnServer: TaskProvider<*>,
522 docsConfiguration: Configuration,
523 multiplatformDocsConfiguration: Configuration,
524 mergedProjectMetadata: Provider<RegularFile>
525 ) {
526 val generatedDocsDir = project.layout.buildDirectory.dir("docs")
527
528 val dackkaConfiguration =
529 project.configurations.create("dackka") {
530 it.dependencies.add(project.dependencies.create(project.getLibraryByName("dackka")))
531 it.isCanBeConsumed = false
532 }
533
534 val generateMetadataTask =
535 project.tasks.register("generateMetadata", GenerateMetadataTask::class.java) { task ->
536 val artifacts = docsConfiguration.incoming.artifacts.resolvedArtifacts
537 task.getArtifactIds().set(artifacts.map { result -> result.map { it.id } })
538 task.getArtifactFiles().set(artifacts.map { result -> result.map { it.file } })
539 val multiplatformArtifacts =
540 multiplatformDocsConfiguration.incoming.artifacts.resolvedArtifacts
541 task
542 .getMultiplatformArtifactIds()
543 .set(multiplatformArtifacts.map { result -> result.map { it.id } })
544 task
545 .getMultiplatformArtifactFiles()
546 .set(multiplatformArtifacts.map { result -> result.map { it.file } })
547 task.destinationFile.set(getMetadataRegularFile(project))
548 }
549
550 val metricsFile = project.layout.buildDirectory.file("build-metrics.json")
551 val projectName = project.name
552
553 val dackkaTask =
554 project.tasks.register("docs", DackkaTask::class.java) { task ->
555 var taskStartTime: LocalDateTime? = null
556 task.argsJsonFile.set(
557 File(project.getDistributionDirectory(), "dackkaArgs-${project.name}.json")
558 )
559 task.apply {
560 // Remove once there is property version of Copy#destinationDir
561 // Use samplesDir.set(unzipSamplesTask.flatMap { it.destinationDirectory })
562 // https://github.com/gradle/gradle/issues/25824
563 dependsOn(unzipJvmSourcesTask)
564 dependsOn(unzipJvmSamplesTask)
565 dependsOn(unzipDeprecatedSamplesTask)
566 dependsOn(configureMultiplatformSourcesTask)
567
568 description =
569 "Generates reference documentation using a Google devsite Dokka" +
570 " plugin. Places docs in ${generatedDocsDir.get()}"
571 group = JavaBasePlugin.DOCUMENTATION_GROUP
572
573 dackkaClasspath.from(project.files(dackkaConfiguration))
574 destinationDir.set(generatedDocsDir)
575 frameworkSamplesDir.set(File(project.getSupportRootFolder(), "samples"))
576 samplesDeprecatedDir.set(unzippedDeprecatedSamplesSources)
577 samplesJvmDir.set(unzippedJvmSamplesSources)
578 samplesKmpDir.set(unzippedKmpSamplesSources)
579 jvmSourcesDir.set(unzippedJvmSourcesDirectory)
580 multiplatformSourcesDir.set(unzippedMultiplatformSourcesDirectory)
581 projectListsDirectory.set(
582 File(project.getSupportRootFolder(), "docs-public/package-lists")
583 )
584 dependenciesClasspath.from(
585 dependencyClasspath +
586 project.getAndroidJar(
587 project.defaultAndroidConfig.latestStableCompileSdk
588 ) +
589 project.getExtraCommonDependencies()
590 )
591 excludedPackages.set(hiddenPackages.toSet())
592 excludedPackagesForJava.set(hiddenPackagesJava)
593 excludedPackagesForKotlin.set(emptySet())
594 libraryMetadataFile.set(generateMetadataTask.flatMap { it.destinationFile })
595 projectStructureMetadataFile.set(mergedProjectMetadata)
596 // See go/dackka-source-link for details on these links.
597 baseSourceLink.set("https://cs.android.com/search?q=file:%s+class:%s")
598 baseFunctionSourceLink.set(
599 "https://cs.android.com/search?q=file:%s+function:%s"
600 )
601 basePropertySourceLink.set("https://cs.android.com/search?q=file:%s+symbol:%s")
602 annotationsNotToDisplay.set(hiddenAnnotations)
603 annotationsNotToDisplayJava.set(hiddenAnnotationsJava)
604 annotationsNotToDisplayKotlin.set(hiddenAnnotationsKotlin)
605 hidingAnnotations.set(annotationsToHideApis)
606 nullabilityAnnotations.set(validNullabilityAnnotations)
607 versionMetadataFiles.from(
608 versionMetadataConfiguration.incoming.artifactView {}.files
609 )
610 task.doFirst { taskStartTime = LocalDateTime.now() }
611 task.doLast {
612 val cpus =
613 try {
614 ProcessBuilder("lscpu")
615 .start()
616 .apply { waitFor(100L, TimeUnit.MILLISECONDS) }
617 .inputStream
618 .bufferedReader()
619 .readLines()
620 .filter { it.startsWith("CPU(s):") }
621 .singleOrNull()
622 ?.split(" ")
623 ?.last()
624 ?.toInt()
625 } catch (e: java.io.IOException) {
626 null
627 } // not running on linux
628 if (cpus != 64) { // Keep stddev of build metrics low b/334867245
629 println("$cpus cpus, so not storing build metrics.")
630 return@doLast
631 }
632 println("$cpus cpus, so storing build metrics.")
633 val taskEndTime = LocalDateTime.now()
634 val duration = Duration.between(taskStartTime, taskEndTime).toMillis()
635 metricsFile
636 .get()
637 .asFile
638 .writeText("{ \"${projectName}_docs_execution_duration\": $duration }")
639 }
640 }
641 }
642
643 val zipTask =
644 project.tasks.register("zipDocs", Zip::class.java) { task ->
645 task.apply {
646 from(dackkaTask.flatMap { it.destinationDir })
647
648 val baseName = "docs-$docsType"
649 val buildId = getBuildId()
650 archiveBaseName.set(baseName)
651 archiveVersion.set(buildId)
652 destinationDirectory.set(project.getDistributionDirectory())
653 group = JavaBasePlugin.DOCUMENTATION_GROUP
654
655 val filePath = "${project.getDistributionDirectory().canonicalPath}/"
656 val fileName = "$baseName-$buildId.zip"
657 val destinationFile = filePath + fileName
658 description =
659 "Zips Java and Kotlin documentation (generated via Dackka in the" +
660 " style of d.android.com) into $destinationFile"
661 }
662 }
663 buildOnServer.configure { it.dependsOn(zipTask) }
664 }
665
666 /**
667 * Replace all tests etc with empty task, so we don't run anything it is more effective then
668 * task.enabled = false, because we avoid executing deps as well
669 */
670 private fun disableUnneededTasks(project: Project) {
671 var reentrance = false
672 project.tasks.whenTaskAdded { task ->
673 if (
674 task is Test ||
675 task.name.startsWith("assemble") ||
676 task.name == "lint" ||
677 task.name == "lintDebug" ||
678 task.name == "lintAnalyzeDebug" ||
679 task.name == "transformDexArchiveWithExternalLibsDexMergerForPublicDebug" ||
680 task.name == "transformResourcesWithMergeJavaResForPublicDebug" ||
681 task.name == "checkPublicDebugDuplicateClasses"
682 ) {
683 if (!reentrance) {
684 reentrance = true
685 project.tasks.named(task.name) {
686 it.actions = emptyList()
687 it.dependsOn(emptyList<Task>())
688 }
689 reentrance = false
690 }
691 }
692 }
693 }
694 }
695
696 @DisableCachingByDefault(because = "Doesn't benefit from caching")
697 open class DocsBuildOnServer : DefaultTask() {
698 @Internal lateinit var docsType: String
699 @Internal lateinit var buildId: String
700 @Internal lateinit var distributionDirectory: File
701
702 @[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
getRequiredFilesnull703 fun getRequiredFiles(): List<File> {
704 return listOf(
705 File(distributionDirectory, "docs-$docsType-$buildId.zip"),
706 )
707 }
708
709 @TaskAction
checkAllBuildOutputsnull710 fun checkAllBuildOutputs() {
711 val missingFiles = mutableListOf<String>()
712 getRequiredFiles().forEach { file ->
713 if (!file.exists()) {
714 missingFiles.add(file.path)
715 }
716 }
717
718 if (missingFiles.isNotEmpty()) {
719 val missingFileString = missingFiles.reduce { acc, s -> "$acc, $s" }
720 throw FileNotFoundException("buildOnServer required output missing: $missingFileString")
721 }
722 }
723 }
724
725 /**
726 * Adapter rule to handles prebuilt dependencies that do not use Gradle Metadata (only pom). We
727 * create a new variant sources that we can later use in the same way we do for tip of tree projects
728 * and prebuilts with Gradle Metadata.
729 */
730 abstract class SourcesVariantRule : ComponentMetadataRule {
731 @get:Inject abstract val objects: ObjectFactory
732
executenull733 override fun execute(context: ComponentMetadataContext) {
734 context.details.maybeAddVariant("sources", "runtime") {
735 it.attributes {
736 it.attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
737 it.attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.DOCUMENTATION))
738 it.attribute(DocsType.DOCS_TYPE_ATTRIBUTE, objects.named(DocsType.SOURCES))
739 }
740 it.withFiles {
741 it.removeAllFiles()
742 it.addFile("${context.details.id.name}-${context.details.id.version}-sources.jar")
743 }
744 }
745 }
746 }
747
748 /**
749 * Location of the library metadata JSON file that's used by Dackka, represented as a [RegularFile]
750 */
getMetadataRegularFilenull751 private fun getMetadataRegularFile(project: Project): Provider<RegularFile> =
752 project.layout.buildDirectory.file("AndroidXLibraryMetadata.json")
753
754 // List of packages to exclude from both Java and Kotlin refdoc generation
755 private val hiddenPackages =
756 listOf(
757 "androidx.camera.camera2.impl",
758 "androidx.camera.camera2.internal.*",
759 "androidx.camera.core.impl.*",
760 "androidx.camera.core.internal.*",
761 "androidx.core.internal",
762 "androidx.preference.internal",
763 "androidx.wear.internal.widget.drawer",
764 "androidx.webkit.internal",
765 "androidx.work.impl.*"
766 )
767
768 // Set of packages to exclude from Java refdoc generation
769 private val hiddenPackagesJava =
770 setOf(
771 "androidx.*compose.*",
772 "androidx.*glance.*",
773 "androidx\\.tv\\..*",
774 )
775
776 // List of annotations which should not be displayed in the docs
777 private val hiddenAnnotations: List<String> =
778 listOf(
779 // This information is compose runtime implementation details; not useful for most, those
780 // who
781 // would want it should look at source
782 "androidx.compose.runtime.Stable",
783 "androidx.compose.runtime.Immutable",
784 "androidx.compose.runtime.ReadOnlyComposable",
785 // This opt-in requirement is non-propagating so developers don't need to know about it
786 // https://kotlinlang.org/docs/opt-in-requirements.html#non-propagating-opt-in
787 "androidx.annotation.OptIn",
788 "kotlin.OptIn",
789 // This annotation is used mostly in paging, and was removed at the request of the paging
790 // team
791 "androidx.annotation.CheckResult",
792 // This annotation is generated upstream. Dokka uses it for signature serialization. It
793 // doesn't
794 // seem useful for developers
795 "kotlin.ParameterName",
796 // This annotations is not useful for developers but right now is @ShowAnnotation?
797 "kotlin.js.JsName",
798 // This annotation is intended to target the compiler and is general not useful for devs.
799 "java.lang.Override",
800 // This annotation is used by the room processor and isn't useful for developers
801 "androidx.room.Ignore"
802 )
803
804 val validNullabilityAnnotations =
805 listOf(
806 "org.jspecify.annotations.NonNull",
807 "org.jspecify.annotations.Nullable",
808 "androidx.annotation.Nullable",
809 "android.annotation.Nullable",
810 "androidx.annotation.NonNull",
811 "android.annotation.NonNull",
812 // Required by media3
813 "org.checkerframework.checker.nullness.qual.Nullable",
814 )
815
816 // Annotations which should not be displayed in the Kotlin docs, in addition to hiddenAnnotations
817 private val hiddenAnnotationsKotlin: List<String> = listOf("kotlin.ExtensionFunctionType")
818
819 // Annotations which should not be displayed in the Java docs, in addition to hiddenAnnotations
820 private val hiddenAnnotationsJava: List<String> = emptyList()
821
822 // Annotations which mean the elements they are applied to should be hidden from the docs
823 private val annotationsToHideApis: List<String> =
824 listOf(
825 "androidx.annotation.RestrictTo",
826 // Appears in androidx.test sources
827 "dagger.internal.DaggerGenerated",
828 )
829
830 /** Data class that matches JSON structure of kotlin source set metadata */
831 data class ProjectStructureMetadata(var sourceSets: List<SourceSetMetadata>)
832
833 data class SourceSetMetadata(
834 val name: String,
835 val analysisPlatform: String,
836 var dependencies: List<String>
837 )
838
839 @CacheableTask
840 abstract class UnzipMultiplatformSourcesTask() : DefaultTask() {
841
842 @get:Classpath abstract val inputJars: Property<FileCollection>
843
844 @get:OutputDirectory abstract val metadataOutput: DirectoryProperty
845
846 @get:OutputDirectory abstract val sourceOutput: DirectoryProperty
847
848 @get:OutputDirectory abstract val samplesOutput: DirectoryProperty
849
850 @get:Inject abstract val fileSystemOperations: FileSystemOperations
851
852 @get:Inject abstract val archiveOperations: ArchiveOperations
853
854 @get:Inject abstract val projectLayout: ProjectLayout
855
856 @TaskAction
857 fun execute() {
858 val tempSourcesDir =
859 projectLayout.buildDirectory.dir("tmp/MultiplatformSources").get().asFile
860 val tempSamplesDir =
861 projectLayout.buildDirectory.dir("tmp/MultiplatformSamples").get().asFile
862 // Get rid of stale files in the directories
863 tempSourcesDir.deleteRecursively()
864 tempSamplesDir.deleteRecursively()
865
866 val (sources, samples) =
867 inputJars
868 .get()
869 .associate { it.name to archiveOperations.zipTree(it) }
870 .toSortedMap()
871 // Now that we publish sample jars, they can get confused with normal source
872 // jars. We want to handle sample jars separately, so filter by the name.
873 .partition { name -> "samples" !in name }
874
875 sources.values.forEach { fileTree ->
876 fileTree.matching { it.exclude("META-INF/*") }.rewriteImageTagsAndCopy(tempSourcesDir)
877 }
878 fileSystemOperations.sync {
879 it.duplicatesStrategy = DuplicatesStrategy.FAIL
880 it.from(tempSourcesDir)
881 it.into(sourceOutput)
882 it.exclude("META-INF/*")
883 }
884
885 samples.values.forEach { fileTree ->
886 fileTree.matching { it.exclude("META-INF/*") }.rewriteImageTagsAndCopy(tempSamplesDir)
887 }
888 fileSystemOperations.sync {
889 // Some libraries share samples, e.g. paging. This can be an issue if and only if the
890 // consumer libraries have pinned samples version or are not in an atomic group.
891 // We don't have anything matching this case now, but should enforce better. b/334825580
892 it.duplicatesStrategy = DuplicatesStrategy.INCLUDE
893 it.from(tempSamplesDir)
894 it.into(samplesOutput)
895 it.exclude("META-INF/*")
896 }
897 sources.forEach { (name, fileTree) ->
898 fileSystemOperations.sync {
899 it.from(fileTree)
900 it.into(metadataOutput.file(name))
901 it.include("META-INF/*")
902 }
903 }
904 }
905 }
906
partitionnull907 private fun <K, V> Map<K, V>.partition(condition: (K) -> Boolean): Pair<Map<K, V>, Map<K, V>> =
908 this.toList().partition { (k, _) -> condition(k) }.let { it.first.toMap() to it.second.toMap() }
909
910 /** Merges multiplatform metadata files created by [CreateMultiplatformMetadata] */
911 @CacheableTask
912 abstract class MergeMultiplatformMetadataTask : DefaultTask() {
913
914 @get:InputFiles
915 @get:PathSensitive(PathSensitivity.RELATIVE)
916 abstract val inputDirectory: DirectoryProperty
917 @get:OutputFile abstract val mergedProjectMetadata: RegularFileProperty
918
919 @TaskAction
executenull920 fun execute() {
921 val mergedMetadata = ProjectStructureMetadata(sourceSets = listOf())
922 inputDirectory
923 .get()
924 .asFile
925 .walkTopDown()
926 .filter { file -> file.name == PROJECT_STRUCTURE_METADATA_FILENAME }
927 .forEach { metaFile ->
928 val gson = GsonBuilder().create()
929 val metadata =
930 gson.fromJson(metaFile.readText(), ProjectStructureMetadata::class.java)
931 mergedMetadata.merge(metadata)
932 }
933 val gson = GsonBuilder().setPrettyPrinting().create()
934 // Sort sourceSets to ensure that child sourceSets come after their parents, b/404784813
935 // Also ensure deterministic order--mergedMetadata.merge() uses .toSet() to deduplicate.
936 mergedMetadata.sourceSets =
937 mergedMetadata.sourceSets.sortedWith(compareBy({ it.dependencies.size }, { it.name }))
938 val json = gson.toJson(mergedMetadata)
939 mergedProjectMetadata.get().asFile.apply {
940 parentFile.mkdirs()
941 createNewFile()
942 writeText(json)
943 }
944 }
945
mergenull946 private fun ProjectStructureMetadata.merge(metadata: ProjectStructureMetadata) {
947 val originalSourceSets = this.sourceSets
948 metadata.sourceSets.forEach { newSourceSet ->
949 val existingSourceSet = originalSourceSets.find { it.name == newSourceSet.name }
950 if (existingSourceSet != null) {
951 existingSourceSet.dependencies =
952 (newSourceSet.dependencies + existingSourceSet.dependencies).toSet().toList()
953 } else {
954 sourceSets += listOf(newSourceSet)
955 }
956 }
957 }
958 }
959
Projectnull960 private fun Project.getPrebuiltsExternalPath() =
961 File(project.getCheckoutRoot(), "prebuilts/androidx/external/")
962
963 private val PLATFORMS =
964 listOf("linuxx64", "macosarm64", "macosx64", "iosx64", "iossimulatorarm64", "iosarm64")
965
966 private fun Project.getExtraCommonDependencies(): FileCollection =
967 files(
968 arrayOf(
969 File(
970 getPrebuiltsExternalPath(),
971 "org/jetbrains/kotlinx/kotlinx-coroutines-core/1.6.4/" +
972 "kotlinx-coroutines-core-1.6.4.jar"
973 ),
974 File(
975 getPrebuiltsExternalPath(),
976 "org/jetbrains/kotlinx/atomicfu/0.17.0/atomicfu-0.17.0.jar"
977 ),
978 File(getPrebuiltsExternalPath(), "com/squareup/okio/okio-jvm/3.1.0/okio-jvm-3.1.0.jar")
979 ) +
980 PLATFORMS.map {
981 File(
982 getPrebuiltsExternalPath(),
983 "com/squareup/okio/okio-$it/3.1.0/okio-$it-3.1.0.klib"
984 )
985 }
986 )
987
FileTreenull988 private fun FileTree.rewriteImageTagsAndCopy(destinationDir: File) {
989 visit { fileDetails ->
990 if (!fileDetails.isDirectory) {
991 val targetFile = File(destinationDir, fileDetails.relativePath.pathString)
992 targetFile.parentFile.mkdirs()
993
994 if (fileDetails.file.extension == "kt") {
995 val content = fileDetails.file.readText()
996 val updatedContent = rewriteLinks(content)
997 targetFile.writeText(updatedContent)
998 } else {
999 fileDetails.file.copyTo(targetFile, overwrite = true)
1000 }
1001 }
1002 }
1003 }
1004
1005 /**
1006 * Rewrites multi-line markdown links ![]()to a single-line format. Work-around for b/350055200.
1007 *
1008 * Example transformation:
1009 * ```
1010 * Original: 
1012 * Result: 
1013 * ```
1014 */
rewriteLinksnull1015 internal fun rewriteLinks(content: String): String =
1016 markdownLinksRegex.replace(content) { matchResult ->
1017 val exclamationMark = matchResult.groupValues[1]
1018 val linkText = matchResult.groupValues[2].replace("\n", " ").replace(" * ", "").trim()
1019 val url = matchResult.groupValues[3].trim()
1020 "$exclamationMark[$linkText]($url)"
1021 }
1022
1023 /**
1024 * Regular expression to match markdown link syntax, supporting both standard links and image links.
1025 *
1026 * The pattern matches:
1027 * - Optional `!` at the beginning for image links.
1028 * - Link text enclosed in square brackets `[ ... ]`, allowing for whitespace around the text.
1029 * - URL in parentheses `( ... )`, allowing for whitespace around the URL.
1030 */
1031 private val markdownLinksRegex = Regex("""(!?)\[\s*([^\[\]]+?)\s*]\(\s*([^(]+?)\s*\)""")
1032