1 /*
<lambda>null2  * Copyright 2021 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.dackka
18 
19 import androidx.build.docs.ProjectStructureMetadata
20 import com.google.gson.GsonBuilder
21 import java.io.File
22 import javax.inject.Inject
23 import org.gradle.api.DefaultTask
24 import org.gradle.api.file.ConfigurableFileCollection
25 import org.gradle.api.file.DirectoryProperty
26 import org.gradle.api.file.FileCollection
27 import org.gradle.api.file.RegularFileProperty
28 import org.gradle.api.model.ObjectFactory
29 import org.gradle.api.provider.ListProperty
30 import org.gradle.api.provider.Property
31 import org.gradle.api.provider.SetProperty
32 import org.gradle.api.tasks.CacheableTask
33 import org.gradle.api.tasks.Classpath
34 import org.gradle.api.tasks.Input
35 import org.gradle.api.tasks.InputFile
36 import org.gradle.api.tasks.InputFiles
37 import org.gradle.api.tasks.Internal
38 import org.gradle.api.tasks.OutputDirectory
39 import org.gradle.api.tasks.OutputFile
40 import org.gradle.api.tasks.PathSensitive
41 import org.gradle.api.tasks.PathSensitivity
42 import org.gradle.api.tasks.TaskAction
43 import org.gradle.api.tasks.options.Option
44 import org.gradle.process.ExecOperations
45 import org.gradle.workers.WorkAction
46 import org.gradle.workers.WorkParameters
47 import org.gradle.workers.WorkerExecutor
48 
49 @CacheableTask
50 abstract class DackkaTask
51 @Inject
52 constructor(private val workerExecutor: WorkerExecutor, private val objects: ObjectFactory) :
53     DefaultTask() {
54 
55     @get:OutputFile abstract val argsJsonFile: RegularFileProperty
56 
57     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
58     abstract val projectStructureMetadataFile: RegularFileProperty
59 
60     // Classpath containing Dackka
61     @get:Classpath abstract val dackkaClasspath: ConfigurableFileCollection
62 
63     // Classpath containing dependencies of libraries needed to resolve types in docs
64     @get:[InputFiles Classpath]
65     abstract val dependenciesClasspath: ConfigurableFileCollection
66 
67     // Directory containing the code samples from framework
68     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
69     abstract val frameworkSamplesDir: DirectoryProperty
70 
71     // Directory containing the code samples derived via the old method. This will be removed
72     // as soon as all libraries have been published with samples. b/329424152
73     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
74     abstract val samplesDeprecatedDir: DirectoryProperty
75 
76     // Directory containing the code samples for non-KMP libraries
77     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
78     abstract val samplesJvmDir: DirectoryProperty
79 
80     // Directory containing the code samples for KMP libraries
81     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
82     abstract val samplesKmpDir: DirectoryProperty
83 
84     // Directory containing the JVM source code for Dackka to process
85     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
86     abstract val jvmSourcesDir: DirectoryProperty
87 
88     // Directory containing the multiplatform source code for Dackka to process
89     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
90     abstract val multiplatformSourcesDir: DirectoryProperty
91 
92     // Directory containing the package-lists
93     @get:[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
94     abstract val projectListsDirectory: DirectoryProperty
95 
96     // Location of generated reference docs
97     @get:OutputDirectory abstract val destinationDir: DirectoryProperty
98 
99     // Set of packages to exclude for refdoc generation for all languages
100     @get:Input abstract val excludedPackages: SetProperty<String>
101 
102     // Set of packages to exclude for Java refdoc generation
103     @get:Input abstract val excludedPackagesForJava: SetProperty<String>
104 
105     // Set of packages to exclude for Kotlin refdoc generation
106     @get:Input abstract val excludedPackagesForKotlin: SetProperty<String>
107 
108     @get:Input abstract val annotationsNotToDisplay: ListProperty<String>
109 
110     @get:Input abstract val annotationsNotToDisplayJava: ListProperty<String>
111 
112     @get:Input abstract val annotationsNotToDisplayKotlin: ListProperty<String>
113 
114     @get:Input abstract val hidingAnnotations: ListProperty<String>
115 
116     @get:Input abstract val nullabilityAnnotations: ListProperty<String>
117 
118     // Version metadata for apiSince, only marked as @InputFiles if includeVersionMetadata is true
119     @get:Internal abstract val versionMetadataFiles: ConfigurableFileCollection
120 
121     @InputFiles
122     @PathSensitive(PathSensitivity.NONE)
123     fun getOptionalVersionMetadataFiles(): ConfigurableFileCollection {
124         return if (includeVersionMetadata) {
125             versionMetadataFiles
126         } else {
127             objects.fileCollection()
128         }
129     }
130 
131     // Maps to the system variable LIBRARY_METADATA_FILE containing artifactID and other metadata
132     @get:[InputFile PathSensitive(PathSensitivity.NONE)]
133     abstract val libraryMetadataFile: RegularFileProperty
134 
135     // The base URLs to create source links for classes, functions, and properties, respectively, as
136     // format strings with placeholders for the file path and qualified class name, function name,
137     // or property name.
138     @get:Input abstract val baseSourceLink: Property<String>
139     @get:Input abstract val baseFunctionSourceLink: Property<String>
140     @get:Input abstract val basePropertySourceLink: Property<String>
141 
142     /**
143      * Option for whether to include apiSince metadata in the docs. Defaults to including metadata.
144      * Run with `--no-version-metadata` to avoid running `generateApi` before `docs`.
145      */
146     @get:Input
147     @set:Option(
148         option = "version-metadata",
149         description = "Include added-in/deprecated-in API version metadata"
150     )
151     var includeVersionMetadata: Boolean = true
152 
153     private fun sourceSets(): List<DokkaInputModels.SourceSet> {
154         val externalDocs =
155             externalLinks.map { (name, url) ->
156                 DokkaInputModels.GlobalDocsLink(
157                     url = url,
158                     packageListUrl =
159                         "file://${
160                             projectListsDirectory.get().asFile.absolutePath
161                         }/$name/package-list"
162                 )
163             }
164         val gson = GsonBuilder().create()
165         val multiplatformSourceSets =
166             projectStructureMetadataFile
167                 .get()
168                 .asFile
169                 .takeIf { it.exists() }
170                 ?.let { metadataFile ->
171                     val metadata =
172                         gson.fromJson(metadataFile.readText(), ProjectStructureMetadata::class.java)
173                     // Sort to ensure that child sourceSets come after their parents, b/404784813
174                     metadata.sourceSets
175                         .sortedWith(compareBy({ it.dependencies.size }, { it.name }))
176                         .mapNotNull { sourceSet ->
177                             val sourceDir =
178                                 multiplatformSourcesDir.get().asFile.resolve(sourceSet.name)
179                             if (!sourceDir.exists()) return@mapNotNull null
180                             val analysisPlatform =
181                                 DokkaAnalysisPlatform.valueOf(
182                                     sourceSet.analysisPlatform.uppercase()
183                                 )
184                             DokkaInputModels.SourceSet(
185                                 id = sourceSetIdForSourceSet(sourceSet.name),
186                                 displayName = sourceSet.name,
187                                 analysisPlatform = analysisPlatform.jsonName,
188                                 sourceRoots = objects.fileCollection().from(sourceDir),
189                                 // TODO(b/181224204): KMP samples aren't supported, dackka assumes
190                                 // all
191                                 // samples are in common
192                                 samples =
193                                     if (analysisPlatform == DokkaAnalysisPlatform.COMMON) {
194                                         objects
195                                             .fileCollection()
196                                             .from(
197                                                 samplesDeprecatedDir,
198                                                 samplesJvmDir,
199                                                 samplesKmpDir,
200                                                 frameworkSamplesDir.get().asFile
201                                             )
202                                     } else {
203                                         objects.fileCollection()
204                                     },
205                                 includes = objects.fileCollection().from(includesFiles(sourceDir)),
206                                 classpath = dependenciesClasspath,
207                                 externalDocumentationLinks = externalDocs,
208                                 dependentSourceSets =
209                                     sourceSet.dependencies.map { sourceSetIdForSourceSet(it) },
210                                 noJdkLink = !analysisPlatform.androidOrJvm(),
211                                 noAndroidSdkLink =
212                                     analysisPlatform != DokkaAnalysisPlatform.ANDROID,
213                                 noStdlibLink = false,
214                                 // Dackka source link configuration doesn't use the Dokka version
215                                 sourceLinks = emptyList()
216                             )
217                         }
218                 } ?: emptyList()
219         return listOf(
220             DokkaInputModels.SourceSet(
221                 id = sourceSetIdForSourceSet("main"),
222                 displayName = "main",
223                 analysisPlatform = "jvm",
224                 sourceRoots = objects.fileCollection().from(jvmSourcesDir),
225                 samples =
226                     objects
227                         .fileCollection()
228                         .from(
229                             samplesDeprecatedDir,
230                             samplesJvmDir,
231                             samplesKmpDir,
232                             frameworkSamplesDir.get().asFile
233                         ),
234                 includes = objects.fileCollection().from(includesFiles(jvmSourcesDir.get().asFile)),
235                 classpath = dependenciesClasspath,
236                 externalDocumentationLinks = externalDocs,
237                 dependentSourceSets = emptyList(),
238                 noJdkLink = false,
239                 noAndroidSdkLink = false,
240                 noStdlibLink = false,
241                 // Dackka source link configuration doesn't use the Dokka version
242                 sourceLinks = emptyList()
243             )
244         ) + multiplatformSourceSets
245     }
246 
247     // Documentation for Dackka command line usage and arguments can be found at
248     // https://kotlin.github.io/dokka/1.6.0/user_guide/cli/usage/
249     // Documentation for the DevsitePlugin arguments can be found at
250     // https://cs.android.com/androidx/platform/tools/dokka-devsite-plugin/+/master:src/main/java/com/google/devsite/DevsiteConfiguration.kt
251     private fun computeArguments(): File {
252         val gson = DokkaUtils.createGson()
253         val linksConfiguration = ""
254         val jsonMap =
255             mapOf(
256                 "outputDir" to destinationDir.get().asFile.path,
257                 "globalLinks" to linksConfiguration,
258                 "sourceSets" to sourceSets(),
259                 "offlineMode" to "true",
260                 "noJdkLink" to "true",
261                 "pluginsConfiguration" to
262                     listOf(
263                         mapOf(
264                             "fqPluginName" to "com.google.devsite.DevsitePlugin",
265                             "serializationFormat" to "JSON",
266                             // values is a JSON string
267                             "values" to
268                                 gson.toJson(
269                                     mapOf(
270                                         "projectPath" to "androidx",
271                                         "javaDocsPath" to "",
272                                         "kotlinDocsPath" to "kotlin",
273                                         "excludedPackages" to excludedPackages.get(),
274                                         "excludedPackagesForJava" to excludedPackagesForJava.get(),
275                                         "excludedPackagesForKotlin" to
276                                             excludedPackagesForKotlin.get(),
277                                         "libraryMetadataFilename" to
278                                             libraryMetadataFile.get().toString(),
279                                         "baseSourceLink" to baseSourceLink.get(),
280                                         "baseFunctionSourceLink" to baseFunctionSourceLink.get(),
281                                         "basePropertySourceLink" to basePropertySourceLink.get(),
282                                         "annotationsNotToDisplay" to annotationsNotToDisplay.get(),
283                                         "annotationsNotToDisplayJava" to
284                                             annotationsNotToDisplayJava.get(),
285                                         "annotationsNotToDisplayKotlin" to
286                                             annotationsNotToDisplayKotlin.get(),
287                                         "hidingAnnotations" to hidingAnnotations.get(),
288                                         "versionMetadataFilenames" to getVersionMetadataFiles(),
289                                         "validNullabilityAnnotations" to
290                                             nullabilityAnnotations.get(),
291                                     )
292                                 )
293                         )
294                     )
295             )
296 
297         val json = gson.toJson(jsonMap)
298         return argsJsonFile.get().asFile.apply { writeText(json) }
299     }
300 
301     /**
302      * If version metadata shouldn't be included in the docs, returns an empty list. Otherwise,
303      * returns the list of version metadata files after checking if they're all JSON. If version
304      * metadata does not exist for a project, it's possible that a configuration which isn't an
305      * exact match of the version metadata attributes to be selected as version metadata.
306      */
307     private fun getVersionMetadataFiles(): List<File> {
308         val (json, nonJson) =
309             getOptionalVersionMetadataFiles().files.partition { it.extension == "json" }
310         if (nonJson.isNotEmpty()) {
311             logger.error(
312                 "The following were resolved as version metadata files but are not JSON files. " +
313                     "If these projects do not have API tracking enabled (e.g. compiler plugin, " +
314                     "annotation processor, proto), they should not be included in the docs. " +
315                     "Remove the projects from `docs-public/build.gradle` and/or " +
316                     "`docs-tip-of-tree/build.gradle`.\n" +
317                     nonJson.joinToString("\n")
318             )
319         }
320         return json
321     }
322 
323     @TaskAction
324     fun generate() {
325         runDackkaWithArgs(
326             classpath = dackkaClasspath,
327             argsFile = computeArguments(),
328             workerExecutor = workerExecutor,
329         )
330     }
331 
332     companion object {
333         private val externalLinks =
334             mapOf(
335                 "coroutinesCore" to "https://kotlinlang.org/api/kotlinx.coroutines/",
336                 "android" to "https://developer.android.com/reference",
337                 "guava" to "https://guava.dev/releases/18.0/api/docs/",
338                 "kotlin" to "https://kotlinlang.org/api/latest/jvm/stdlib/",
339                 "junit" to "https://junit.org/junit4/javadoc/4.12/",
340                 "okio" to "https://square.github.io/okio/3.x/okio/",
341                 "protobuf" to "https://protobuf.dev/reference/java/api-docs/",
342                 "kotlinpoet" to "https://square.github.io/kotlinpoet/1.x/kotlinpoet/",
343                 "skiko" to "https://jetbrains.github.io/skiko/",
344                 "reactivex" to "https://reactivex.io/RxJava/2.x/javadoc/",
345                 "reactivex-rxjava3" to "http://reactivex.io/RxJava/3.x/javadoc/",
346                 "grpc" to "https://grpc.github.io/grpc-java/javadoc/",
347                 // From developer.android.com/reference/com/google/android/play/core/package-list
348                 "play" to "https://developer.android.com/reference/",
349                 // From developer.android.com/reference/com/google/android/material/package-list
350                 "material" to "https://developer.android.com/reference",
351                 "okhttp3" to "https://square.github.io/okhttp/5.x/",
352                 "truth" to "https://truth.dev/api/0.41/",
353                 // From developer.android.com/reference/android/support/wearable/package-list
354                 "wearable" to "https://developer.android.com/reference/",
355                 // Filtered to just java.awt and javax packages (base java packages are included in
356                 // the android package-list)
357                 "javase8" to "https://docs.oracle.com/javase/8/docs/api/",
358                 "javaee7" to "https://docs.oracle.com/javaee%2F7%2Fapi%2F%2F",
359                 "findbugs" to "https://www.javadoc.io/doc/com.google.code.findbugs/jsr305/latest/",
360                 // All package-lists below were created manually
361                 "mlkit" to "https://developers.google.com/android/reference/",
362                 "dagger" to "https://dagger.dev/api/latest/",
363                 "reactivestreams" to
364                     "https://www.reactive-streams.org/reactive-streams-1.0.4-javadoc/",
365                 "jetbrains-annotations" to
366                     "https://javadoc.io/doc/org.jetbrains/annotations/latest/",
367                 "auto-value" to
368                     "https://www.javadoc.io/doc/com.google.auto.value/auto-value/latest/",
369                 "robolectric" to "https://robolectric.org/javadoc/4.11/",
370                 "interactive-media" to
371                     "https://developers.google.com/interactive-media-ads/docs/sdks/android/" +
372                         "client-side/api/reference/com/google/ads/interactivemedia/v3",
373                 "errorprone" to "https://errorprone.info/api/latest/",
374                 "gms" to "https://developers.google.com/android/reference",
375                 "checkerframework" to "https://checkerframework.org/api/",
376                 "chromium" to
377                     "https://developer.android.com/develop/connectivity/cronet/reference/",
378                 "jspecify" to "https://jspecify.dev/docs/api/",
379             )
380     }
381 }
382 
383 interface DackkaParams : WorkParameters {
384     val args: ListProperty<String>
385     val classpath: SetProperty<File>
386 }
387 
runDackkaWithArgsnull388 fun runDackkaWithArgs(
389     classpath: FileCollection,
390     argsFile: File,
391     workerExecutor: WorkerExecutor,
392 ) {
393     val workQueue = workerExecutor.noIsolation()
394     workQueue.submit(DackkaWorkAction::class.java) { parameters ->
395         parameters.args.set(listOf(argsFile.path, "-loggingLevel", "WARN"))
396         parameters.classpath.set(classpath)
397     }
398 }
399 
400 abstract class DackkaWorkAction @Inject constructor(private val execOperations: ExecOperations) :
401     WorkAction<DackkaParams> {
executenull402     override fun execute() {
403         execOperations.javaexec {
404             it.mainClass.set("org.jetbrains.dokka.MainKt")
405             it.args = parameters.args.get()
406             it.classpath(parameters.classpath.get())
407         }
408     }
409 }
410 
includesFilesnull411 private fun includesFiles(sourceRoot: File): List<File> {
412     return sourceRoot.walkTopDown().filter { it.name.endsWith("documentation.md") }.toList()
413 }
414 
sourceSetIdForSourceSetnull415 private fun sourceSetIdForSourceSet(name: String): DokkaInputModels.SourceSetId {
416     return DokkaInputModels.SourceSetId(scopeId = "androidx", sourceSetName = name)
417 }
418