1/*
2 * Copyright 2018 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
17package androidx.build
18
19import androidx.build.Strategy.Prebuilts
20import androidx.build.Strategy.TipOfTree
21import androidx.build.checkapi.ApiXmlConversionTask
22import androidx.build.checkapi.CheckApiTask
23import androidx.build.checkapi.UpdateApiTask
24import androidx.build.doclava.DoclavaTask
25import androidx.build.doclava.DEFAULT_DOCLAVA_CONFIG
26import androidx.build.doclava.CHECK_API_CONFIG_DEVELOP
27import androidx.build.doclava.CHECK_API_CONFIG_RELEASE
28import androidx.build.doclava.CHECK_API_CONFIG_PATCH
29import androidx.build.doclava.ChecksConfig
30import androidx.build.docs.GenerateDocsTask
31import androidx.build.jdiff.JDiffTask
32import com.android.build.gradle.AppExtension
33import com.android.build.gradle.LibraryExtension
34import com.android.build.gradle.api.BaseVariant
35import com.android.build.gradle.api.LibraryVariant
36import org.gradle.api.GradleException
37import org.gradle.api.Project
38import org.gradle.api.Task
39import org.gradle.api.artifacts.Configuration
40import org.gradle.api.artifacts.ResolveException
41import org.gradle.api.file.FileCollection
42import org.gradle.api.file.FileTree
43import org.gradle.api.plugins.JavaBasePlugin
44import org.gradle.api.tasks.TaskContainer
45import org.gradle.api.tasks.bundling.Zip
46import org.gradle.api.tasks.compile.JavaCompile
47import org.gradle.api.tasks.javadoc.Javadoc
48import java.io.File
49import kotlin.collections.Collection
50import kotlin.collections.List
51import kotlin.collections.MutableMap
52import kotlin.collections.emptyList
53import kotlin.collections.filter
54import kotlin.collections.find
55import kotlin.collections.forEach
56import kotlin.collections.listOf
57import kotlin.collections.mapNotNull
58import kotlin.collections.minus
59import kotlin.collections.mutableMapOf
60import kotlin.collections.plus
61import kotlin.collections.set
62import kotlin.collections.toSet
63
64data class DacOptions(val libraryroot: String, val dataname: String)
65
66object DiffAndDocs {
67    private lateinit var anchorTask: Task
68    private var docsProject: Project? = null
69
70    private lateinit var rules: List<PublishDocsRules>
71    private val docsTasks: MutableMap<String, GenerateDocsTask> = mutableMapOf()
72
73    @JvmStatic
74    fun configureDiffAndDocs(
75        root: Project,
76        supportRootFolder: File,
77        dacOptions: DacOptions,
78        additionalRules: List<PublishDocsRules> = emptyList()
79    ): Task {
80        rules = additionalRules + TIP_OF_TREE
81        docsProject = root.findProject(":docs-fake")
82        anchorTask = root.tasks.create("anchorDocsTask")
83        val doclavaConfiguration = root.configurations.getByName("doclava")
84        val generateSdkApiTask = createGenerateSdkApiTask(root, doclavaConfiguration)
85        rules.forEach {
86            val task = createGenerateDocsTask(
87                    project = root, generateSdkApiTask = generateSdkApiTask,
88                    doclavaConfig = doclavaConfiguration,
89                    supportRootFolder = supportRootFolder, dacOptions = dacOptions,
90                    destDir = File(root.docsDir(), it.name),
91                    taskName = "${it.name}DocsTask")
92            docsTasks[it.name] = task
93            anchorTask.dependsOn(createDistDocsTask(root, task, it.name))
94        }
95
96        root.tasks.create("generateDocs").dependsOn(docsTasks[TIP_OF_TREE.name])
97
98        setupDocsProject()
99        return anchorTask
100    }
101
102    private fun prebuiltSources(
103        root: Project,
104        mavenId: String,
105        originName: String,
106        originRule: DocsRule
107    ): FileTree {
108        val configName = "docs-temp_$mavenId"
109        val configuration = root.configurations.create(configName)
110        root.dependencies.add(configName, mavenId)
111
112        val artifacts = try {
113            configuration.resolvedConfiguration.resolvedArtifacts
114        } catch (e: ResolveException) {
115            throw GradleException("Failed to find prebuilts for $mavenId. " +
116                    "A matching rule $originRule in docsRules(\"$originName\") " +
117                    "in PublishDocsRules.kt requires it. You should either add a prebuilt, " +
118                    "or add overriding \"ignore\" or \"tipOfTree\" rules", e)
119        }
120
121        val artifact = artifacts.find { it.moduleVersion.id.toString() == mavenId }
122                ?: throw GradleException()
123
124        val folder = artifact.file.parentFile
125        val tree = root.zipTree(File(folder, "${artifact.file.nameWithoutExtension}-sources.jar"))
126                    .matching {
127                        it.exclude("**/*.MF")
128                        it.exclude("**/*.aidl")
129                        it.exclude("**/*.html")
130                        it.exclude("**/*.kt")
131                    }
132        root.configurations.remove(configuration)
133        return tree
134    }
135
136    private fun setupDocsProject() {
137        docsProject?.afterEvaluate { docs ->
138            val appExtension = docs.extensions.findByType(AppExtension::class.java)
139                    ?: throw GradleException("Android app plugin is missing on docsProject")
140
141            rules.forEach { rule ->
142                appExtension.productFlavors.create(rule.name) {
143                    it.dimension = "library-group"
144                }
145            }
146            appExtension.applicationVariants.all { v ->
147                val task = docsTasks[v.flavorName]
148                if (v.buildType.name == "release" && task != null) {
149                    registerAndroidProjectForDocsTask(task, v)
150                    task.exclude { fileTreeElement ->
151                        fileTreeElement.path.endsWith(v.rFile())
152                    }
153                }
154            }
155        }
156
157        docsProject?.rootProject?.subprojects
158                ?.filter { docsProject != it }
159                ?.forEach { docsProject?.evaluationDependsOn(it.path) }
160    }
161
162    private fun registerPrebuilts(extension: SupportLibraryExtension) =
163            docsProject?.afterEvaluate { docs ->
164        val depHandler = docs.dependencies
165        val root = docs.rootProject
166        rules.forEach { rule ->
167            val resolvedRule = rule.resolve(extension)
168            val strategy = resolvedRule.strategy
169            if (strategy is Prebuilts) {
170                val dependency = strategy.dependency(extension)
171                depHandler.add("${rule.name}Implementation", dependency)
172                strategy.stubs?.forEach { path ->
173                    depHandler.add("${rule.name}CompileOnly", root.files(path))
174                }
175                docsTasks[rule.name]!!.source(prebuiltSources(root, dependency,
176                        rule.name, resolvedRule))
177            }
178        }
179    }
180
181    private fun tipOfTreeTasks(extension: SupportLibraryExtension, setup: (DoclavaTask) -> Unit) {
182        rules.filter { rule -> rule.resolve(extension).strategy == TipOfTree }
183                .mapNotNull { rule -> docsTasks[rule.name] }
184                .forEach(setup)
185    }
186
187    /**
188     * Registers a Java project for global docs generation, local API file generation, and
189     * local API diff generation tasks.
190     */
191    fun registerJavaProject(project: Project, extension: SupportLibraryExtension) {
192        if (!hasApiTasks(project, extension)) {
193            return
194        }
195        val compileJava = project.properties["compileJava"] as JavaCompile
196
197        registerPrebuilts(extension)
198
199        tipOfTreeTasks(extension) { task ->
200            registerJavaProjectForDocsTask(task, compileJava)
201        }
202
203        if (!project.hasApiFolder()) {
204            project.logger.info("Project ${project.name} doesn't have an api folder, " +
205                    "ignoring API tasks.")
206            return
207        }
208        val tasks = initializeApiChecksForProject(project)
209        registerJavaProjectForDocsTask(tasks.generateApi, compileJava)
210        registerJavaProjectForDocsTask(tasks.generateDiffs, compileJava)
211        setupDocsTasks(project, tasks)
212        anchorTask.dependsOn(tasks.checkApiTask)
213    }
214
215    /**
216     * Registers an Android project for global docs generation, local API file generation, and
217     * local API diff generation tasks.
218     */
219    fun registerAndroidProject(
220        project: Project,
221        library: LibraryExtension,
222        extension: SupportLibraryExtension
223    ) {
224        if (!hasApiTasks(project, extension)) {
225            return
226        }
227
228        registerPrebuilts(extension)
229
230        library.libraryVariants.all { variant ->
231            if (variant.name == "release") {
232                // include R.file generated for prebuilts
233                rules.filter { it.resolve(extension).strategy is Prebuilts }.forEach { rule ->
234                    docsTasks[rule.name]?.include { fileTreeElement ->
235                        fileTreeElement.path.endsWith(variant.rFile())
236                    }
237                }
238
239                tipOfTreeTasks(extension) { task ->
240                    registerAndroidProjectForDocsTask(task, variant)
241                }
242
243                if (!variant.hasJavaSources()) {
244                    return@all
245                }
246                if (!project.hasApiFolder()) {
247                    project.logger.info("Project ${project.name} doesn't have " +
248                            "an api folder, ignoring API tasks.")
249                    return@all
250                }
251                val tasks = initializeApiChecksForProject(project)
252                registerAndroidProjectForDocsTask(tasks.generateApi, variant)
253                registerAndroidProjectForDocsTask(tasks.generateDiffs, variant)
254                setupDocsTasks(project, tasks)
255                anchorTask.dependsOn(tasks.checkApiTask)
256            }
257        }
258    }
259
260    private fun setupDocsTasks(project: Project, tasks: Tasks) {
261        docsTasks.values.forEach { docs ->
262            tasks.generateDiffs.dependsOn(docs)
263            // Track API change history.
264            docs.addSinceFilesFrom(project.projectDir)
265            // Associate current API surface with the Maven artifact.
266            val artifact = "${project.group}:${project.name}:${project.version}"
267            docs.addArtifact(tasks.generateApi.apiFile!!.absolutePath, artifact)
268            docs.dependsOn(tasks.generateApi)
269        }
270    }
271}
272
273@Suppress("DEPRECATION")
274private fun LibraryVariant.hasJavaSources() = !javaCompile.source
275        .filter { file -> file.name != "R.java" && file.name != "BuildConfig.java" }
276        .isEmpty
277
278fun Project.hasApiFolder() = File(projectDir, "api").exists()
279
280private fun stripExtension(fileName: String) = fileName.substringBeforeLast('.')
281
282private fun getLastReleasedApiFile(rootFolder: File, refVersion: Version?): File? {
283    val apiDir = File(rootFolder, "api")
284    val lastFile = getLastReleasedApiFileFromDir(apiDir, refVersion)
285    if (lastFile != null) {
286        return lastFile
287    }
288
289    return null
290}
291
292private fun getLastReleasedApiFileFromDir(apiDir: File, refVersion: Version?): File? {
293    var lastFile: File? = null
294    var lastVersion: Version? = null
295    apiDir.listFiles().forEach { file ->
296        Version.parseOrNull(file)?.let { version ->
297            if ((lastFile == null || lastVersion!! < version) &&
298                    (refVersion == null || version < refVersion)) {
299                lastFile = file
300                lastVersion = version
301            }
302        }
303    }
304
305    return lastFile
306}
307
308private fun getApiFile(rootDir: File, refVersion: Version): File {
309    return getApiFile(rootDir, refVersion, false)
310}
311
312/**
313 * Returns the API file for the specified reference version.
314 *
315 * @param refVersion the reference API version, ex. 25.0.0-SNAPSHOT
316 * @return the most recently released API file
317 */
318private fun getApiFile(rootDir: File, refVersion: Version, forceRelease: Boolean = false): File {
319    val apiDir = File(rootDir, "api")
320
321    if (refVersion.isFinalApi() || forceRelease) {
322        // Release API file is always X.Y.0.txt.
323        return File(apiDir, "${refVersion.major}.${refVersion.minor}.0.txt")
324    }
325
326    // Non-release API file is always current.txt.
327    return File(apiDir, "current.txt")
328}
329
330// Generates API files
331private fun createGenerateApiTask(project: Project, docletpathParam: Collection<File>) =
332        project.tasks.createWithConfig("generateApi", DoclavaTask::class.java) {
333            setDocletpath(docletpathParam)
334            destinationDir = project.docsDir()
335            // Base classpath is Android SDK, sub-projects add their own.
336            classpath = androidJarFile(project)
337            apiFile = File(project.docsDir(), "release/${project.name}/current.txt")
338            generateDocs = false
339
340            coreJavadocOptions {
341                addBooleanOption("stubsourceonly", true)
342            }
343
344            exclude("**/BuildConfig.java")
345            exclude("**/R.java")
346        }
347
348private fun createCheckApiTask(
349    project: Project,
350    taskName: String,
351    docletpath: Collection<File>,
352    config: ChecksConfig,
353    oldApi: File?,
354    newApi: File,
355    whitelist: File? = null
356) =
357        project.tasks.createWithConfig(taskName, CheckApiTask::class.java) {
358            doclavaClasspath = docletpath
359            checksConfig = config
360            newApiFile = newApi
361            oldApiFile = oldApi
362            whitelistErrorsFile = whitelist
363            doFirst {
364                logger.lifecycle("Verifying ${newApi.name} " +
365                        "against ${oldApi?.name ?: "nothing"}...")
366            }
367        }
368
369/**
370 * Registers a Java project on the given Javadocs task.
371 * <p>
372 * <ul>
373 * <li>Sets up a dependency to ensure the project is compiled prior to running the task
374 * <li>Adds the project's source files to the Javadoc task's source files
375 * <li>Adds the project's compilation classpath (e.g. dependencies) to the task classpath to ensure
376 *     that references in the source files may be resolved
377 * <li>Adds the project's output artifacts to the task classpath to ensure that source references to
378 *     generated code may be resolved
379 * </ul>
380 */
381private fun registerJavaProjectForDocsTask(task: Javadoc, javaCompileTask: JavaCompile) {
382    task.dependsOn(javaCompileTask)
383    task.source(javaCompileTask.source)
384    val project = task.project
385    task.classpath += project.files(javaCompileTask.classpath) +
386            project.files(javaCompileTask.destinationDir)
387}
388
389/**
390 * Registers an Android project on the given Javadocs task.
391 * <p>
392 * @see #registerJavaProjectForDocsTask
393 */
394private fun registerAndroidProjectForDocsTask(task: Javadoc, releaseVariant: BaseVariant) {
395    // This code makes a number of unsafe assumptions about Android Gradle Plugin,
396    // and there's a good chance that this will break in the near future.
397    @Suppress("DEPRECATION")
398    task.dependsOn(releaseVariant.javaCompile)
399    task.include { fileTreeElement ->
400        fileTreeElement.name != "R.java" || fileTreeElement.path.endsWith(releaseVariant.rFile()) }
401    @Suppress("DEPRECATION")
402    task.source(releaseVariant.javaCompile.source)
403    @Suppress("DEPRECATION")
404    task.classpath += releaseVariant.getCompileClasspath(null) +
405            task.project.files(releaseVariant.javaCompile.destinationDir)
406}
407
408/**
409 * Constructs a new task to copy a generated API file to an appropriately-named "official" API file
410 * suitable for source control. This task should be called prior to source control check-in whenever
411 * the public API has been modified.
412 * <p>
413 * The output API file varies according to version:
414 * <ul>
415 * <li>Snapshot and pre-release versions (e.g. X.Y.Z-SNAPSHOT, X.Y.Z-alphaN) output to current.txt
416 * <li>Release versions (e.g. X.Y.Z) output to X.Y.0.txt, throwing an exception if the API has been
417 *     finalized and the file already exists
418 * </ul>
419 */
420private fun createUpdateApiTask(project: Project, checkApiRelease: CheckApiTask) =
421        project.tasks.createWithConfig("updateApi", UpdateApiTask::class.java) {
422            group = JavaBasePlugin.VERIFICATION_GROUP
423            description = "Updates the candidate API file to incorporate valid changes."
424            newApiFile = checkApiRelease.newApiFile
425            oldApiFile = getApiFile(project.projectDir, project.version())
426            whitelistErrors = checkApiRelease.whitelistErrors
427            whitelistErrorsFile = checkApiRelease.whitelistErrorsFile
428            doFirst {
429                val version = project.version()
430                if (!version.isFinalApi() &&
431                        getApiFile(project.projectDir, version, true).exists()) {
432                    throw GradleException("Inconsistent version. Public API file already exists.")
433                }
434                // Replace the expected whitelist with the detected whitelist.
435                whitelistErrors = checkApiRelease.detectedWhitelistErrors
436            }
437        }
438
439/**
440 * Converts the <code>fromApi</code>.txt file (or the most recently released
441 * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format
442 * for use by JDiff.
443 */
444private fun createOldApiXml(project: Project, doclavaConfig: Configuration) =
445        project.tasks.createWithConfig("oldApiXml", ApiXmlConversionTask::class.java) {
446            val toApi = project.processProperty("toApi")?.let {
447                Version.parseOrNull(it)
448            }
449            val fromApi = project.processProperty("fromApi")
450            classpath = project.files(doclavaConfig.resolve())
451            val rootFolder = project.projectDir
452            inputApiFile = if (fromApi != null) {
453                // Use an explicit API file.
454                File(rootFolder, "api/$fromApi.txt")
455            } else {
456                // Use the most recently released API file bounded by toApi.
457                getLastReleasedApiFile(rootFolder, toApi)
458            }
459
460            outputApiXmlFile = File(project.docsDir(),
461                    "release/${stripExtension(inputApiFile?.name ?: "creation")}.xml")
462
463            dependsOn(doclavaConfig)
464        }
465
466/**
467 * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly
468 * defined using -PtoApi=<file>) to XML format for use by JDiff.
469 */
470private fun createNewApiXmlTask(
471    project: Project,
472    generateApi: DoclavaTask,
473    doclavaConfig: Configuration
474) =
475        project.tasks.createWithConfig("newApiXml", ApiXmlConversionTask::class.java) {
476            classpath = project.files(doclavaConfig.resolve())
477            val toApi = project.processProperty("toApi")
478
479            if (toApi != null) {
480                // Use an explicit API file.
481                inputApiFile = File(project.projectDir, "api/$toApi.txt")
482            } else {
483                // Use the current API file (e.g. current.txt).
484                inputApiFile = generateApi.apiFile!!
485                dependsOn(generateApi, doclavaConfig)
486            }
487
488            outputApiXmlFile = File(project.docsDir(),
489                    "release/${stripExtension(inputApiFile?.name ?: "creation")}.xml")
490        }
491
492/**
493 * Generates API diffs.
494 * <p>
495 * By default, diffs are generated for the delta between current.txt and the
496 * next most recent X.Y.Z.txt API file. Behavior may be changed by specifying
497 * one or both of -PtoApi and -PfromApi.
498 * <p>
499 * If both fromApi and toApi are specified, diffs will be generated for
500 * fromApi -> toApi. For example, 25.0.0 -> 26.0.0 diffs could be generated by
501 * using:
502 * <br><code>
503 *   ./gradlew generateDiffs -PfromApi=25.0.0 -PtoApi=26.0.0
504 * </code>
505 * <p>
506 * If only toApi is specified, it MUST be specified as X.Y.Z and diffs will be
507 * generated for (release before toApi) -> toApi. For example, 24.2.0 -> 25.0.0
508 * diffs could be generated by using:
509 * <br><code>
510 *   ./gradlew generateDiffs -PtoApi=25.0.0
511 * </code>
512 * <p>
513 * If only fromApi is specified, diffs will be generated for fromApi -> current.
514 * For example, lastApiReview -> current diffs could be generated by using:
515 * <br><code>
516 *   ./gradlew generateDiffs -PfromApi=lastApiReview
517 * </code>
518 * <p>
519 */
520private fun createGenerateDiffsTask(
521    project: Project,
522    oldApiTask: ApiXmlConversionTask,
523    newApiTask: ApiXmlConversionTask,
524    jdiffConfig: Configuration
525): JDiffTask =
526        project.tasks.createWithConfig("generateDiffs", JDiffTask::class.java) {
527            // Base classpath is Android SDK, sub-projects add their own.
528            classpath = androidJarFile(project)
529
530            // JDiff properties.
531            oldApiXmlFile = oldApiTask.outputApiXmlFile
532            newApiXmlFile = newApiTask.outputApiXmlFile
533
534            val newApi = newApiXmlFile.name.substringBeforeLast('.')
535            val docsDir = project.rootProject.docsDir()
536
537            newJavadocPrefix = "../../../../../reference/"
538            destinationDir = File(docsDir, "online/sdk/support_api_diff/${project.name}/$newApi")
539
540            // Javadoc properties.
541            docletpath = jdiffConfig.resolve()
542            title = "Support&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;Report"
543
544            exclude("**/BuildConfig.java", "**/R.java")
545            dependsOn(oldApiTask, newApiTask, jdiffConfig)
546        }
547
548// Generates a distribution artifact for online docs.
549private fun createDistDocsTask(project: Project, generateDocs: DoclavaTask, ruleName: String = ""): Zip =
550        project.tasks.createWithConfig("dist${ruleName}Docs", Zip::class.java) {
551            dependsOn(generateDocs)
552            group = JavaBasePlugin.DOCUMENTATION_GROUP
553            description = "Generates distribution artifact for d.android.com-style documentation."
554            from(generateDocs.destinationDir)
555            baseName = "android-support-$ruleName-docs"
556            version = project.buildNumber()
557            destinationDir = project.distDir()
558            doLast {
559                logger.lifecycle("'Wrote API reference to $archivePath")
560            }
561        }
562
563/**
564 * Creates a task to generate an API file from the platform SDK's source and stub JARs.
565 * <p>
566 * This is useful for federating docs against the platform SDK when no API XML file is available.
567 */
568private fun createGenerateSdkApiTask(project: Project, doclavaConfig: Configuration): DoclavaTask =
569        project.tasks.createWithConfig("generateSdkApi", DoclavaTask::class.java) {
570            dependsOn(doclavaConfig)
571            description = "Generates API files for the current SDK."
572            setDocletpath(doclavaConfig.resolve())
573            destinationDir = project.docsDir()
574            classpath = androidJarFile(project)
575            source(project.zipTree(androidSrcJarFile(project)))
576            apiFile = sdkApiFile(project)
577            generateDocs = false
578            coreJavadocOptions {
579                addStringOption("stubpackages", "android.*")
580            }
581        }
582
583private val GENERATEDOCS_HIDDEN = listOf(105, 106, 107, 111, 112, 113, 115, 116, 121)
584private val GENERATE_DOCS_CONFIG = ChecksConfig(
585        warnings = emptyList(),
586        hidden = GENERATEDOCS_HIDDEN + DEFAULT_DOCLAVA_CONFIG.hidden,
587        errors = ((101..122) - GENERATEDOCS_HIDDEN)
588)
589
590private fun createGenerateDocsTask(
591    project: Project,
592    generateSdkApiTask: DoclavaTask,
593    doclavaConfig: Configuration,
594    supportRootFolder: File,
595    dacOptions: DacOptions,
596    destDir: File,
597    taskName: String = "generateDocs"
598): GenerateDocsTask =
599        project.tasks.createWithConfig(taskName, GenerateDocsTask::class.java) {
600            dependsOn(generateSdkApiTask, doclavaConfig)
601            group = JavaBasePlugin.DOCUMENTATION_GROUP
602            description = "Generates d.android.com-style documentation. To generate offline docs " +
603                    "use \'-PofflineDocs=true\' parameter."
604
605            setDocletpath(doclavaConfig.resolve())
606            val offline = project.processProperty("offlineDocs") != null
607            destinationDir = File(destDir, if (offline) "offline" else "online")
608            classpath = androidJarFile(project)
609            checksConfig = GENERATE_DOCS_CONFIG
610            addSinceFilesFrom(supportRootFolder)
611
612            coreJavadocOptions {
613                addStringOption("templatedir",
614                        "$supportRootFolder/../../external/doclava/res/assets/templates-sdk")
615                addStringOption("samplesdir", "$supportRootFolder/samples")
616                addMultilineMultiValueOption("federate").value = listOf(
617                        listOf("Android", "https://developer.android.com")
618                )
619                addMultilineMultiValueOption("federationapi").value = listOf(
620                        listOf("Android", generateSdkApiTask.apiFile?.absolutePath)
621                )
622                addMultilineMultiValueOption("hdf").value = listOf(
623                        listOf("android.whichdoc", "online"),
624                        listOf("android.hasSamples", "true"),
625                        listOf("dac", "true")
626                )
627
628                // Specific to reference docs.
629                if (!offline) {
630                    addStringOption("toroot", "/")
631                    addBooleanOption("devsite", true)
632                    addBooleanOption("yamlV2", true)
633                    addStringOption("dac_libraryroot", dacOptions.libraryroot)
634                    addStringOption("dac_dataname", dacOptions.dataname)
635                }
636
637                exclude("**/BuildConfig.java")
638            }
639
640            addArtifactsAndSince()
641        }
642
643private data class Tasks(
644    val generateApi: DoclavaTask,
645    val generateDiffs: JDiffTask,
646    val checkApiTask: CheckApiTask
647)
648
649private fun initializeApiChecksForProject(project: Project): Tasks {
650    if (!project.hasProperty("docsDir")) {
651        project.extensions.add("docsDir", File(project.rootProject.docsDir(), project.name))
652    }
653    val version = project.version()
654    val workingDir = project.projectDir
655
656    val doclavaConfiguration = project.rootProject.configurations.getByName("doclava")
657    val docletClasspath = doclavaConfiguration.resolve()
658    val generateApi = createGenerateApiTask(project, docletClasspath)
659    generateApi.dependsOn(doclavaConfiguration)
660
661    // Make sure the API surface has not broken since the last release.
662    val lastReleasedApiFile = getLastReleasedApiFile(workingDir, version)
663
664    val whitelistFile = lastReleasedApiFile?.let { apiFile ->
665        File(lastReleasedApiFile.parentFile, stripExtension(apiFile.name) + ".ignore")
666    }
667    val checkApiRelease = createCheckApiTask(project,
668            "checkApiRelease",
669            docletClasspath,
670            CHECK_API_CONFIG_RELEASE,
671            lastReleasedApiFile,
672            generateApi.apiFile!!,
673            whitelistFile)
674    checkApiRelease.dependsOn(generateApi)
675
676    // Allow a comma-delimited list of whitelisted errors.
677    if (project.hasProperty("ignore")) {
678        checkApiRelease.whitelistErrors = (project.properties["ignore"] as String)
679                .split(',').toSet()
680    }
681
682    // Check whether the development API surface has changed.
683    val verifyConfig = if (version.isPatch()) CHECK_API_CONFIG_PATCH else CHECK_API_CONFIG_DEVELOP
684    val currentApiFile = getApiFile(workingDir, version)
685    val checkApi = createCheckApiTask(project,
686            "checkApi",
687            docletClasspath,
688            verifyConfig,
689            currentApiFile,
690            generateApi.apiFile!!,
691            null)
692    checkApi.dependsOn(generateApi, checkApiRelease)
693
694    checkApi.group = JavaBasePlugin.VERIFICATION_GROUP
695    checkApi.description = "Verify the API surface."
696
697    val updateApiTask = createUpdateApiTask(project, checkApiRelease)
698    updateApiTask.dependsOn(checkApiRelease)
699    val newApiTask = createNewApiXmlTask(project, generateApi, doclavaConfiguration)
700    val oldApiTask = createOldApiXml(project, doclavaConfiguration)
701
702    val jdiffConfiguration = project.rootProject.configurations.getByName("jdiff")
703    val generateDiffTask = createGenerateDiffsTask(project,
704            oldApiTask,
705            newApiTask,
706            jdiffConfiguration)
707    return Tasks(generateApi, generateDiffTask, checkApi)
708}
709
710fun hasApiTasks(project: Project, extension: SupportLibraryExtension): Boolean {
711    if (!extension.publish) {
712        project.logger.info("Project ${project.name} is not published, ignoring API tasks.")
713        return false
714    }
715
716    if (!extension.generateDocs) {
717        project.logger.info("Project ${project.name} specified generateDocs = false, " +
718                "ignoring API tasks.")
719        return false
720    }
721    return true
722}
723
724private fun sdkApiFile(project: Project) = File(project.docsDir(), "release/sdk_current.txt")
725
726private fun <T : Task> TaskContainer.createWithConfig(
727    name: String,
728    taskClass: Class<T>,
729    config: T.() -> Unit
730) =
731        create(name, taskClass) { task -> task.config() }
732
733private fun androidJarFile(project: Project): FileCollection =
734        project.files(arrayOf(File(project.fullSdkPath(),
735                "platforms/android-${SupportConfig.CURRENT_SDK_VERSION}/android.jar")))
736
737private fun androidSrcJarFile(project: Project): File = File(project.fullSdkPath(),
738        "platforms/android-${SupportConfig.CURRENT_SDK_VERSION}/android-stubs-src.jar")
739
740private fun PublishDocsRules.resolve(extension: SupportLibraryExtension) =
741        resolve(extension.mavenGroup!!, extension.project.name)
742
743private fun Prebuilts.dependency(extension: SupportLibraryExtension) =
744        "${extension.mavenGroup}:${extension.project.name}:$version"
745
746private fun BaseVariant.rFile() = "${applicationId.replace('.', '/')}/R.java"
747
748// Nasty part. Get rid of that eventually!
749private fun Project.docsDir(): File = properties["docsDir"] as File
750
751private fun Project.fullSdkPath(): File = rootProject.properties["fullSdkPath"] as File
752
753private fun Project.version() = Version(project.version as String)
754
755private fun Project.buildNumber() = properties["buildNumber"] as String
756
757private fun Project.distDir(): File = rootProject.properties["distDir"] as File
758
759private fun Project.processProperty(name: String) =
760        if (hasProperty(name)) {
761            properties[name] as String
762        } else {
763            null
764        }
765