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