普通文本  |  472行  |  17.94 KB

/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import android.support.checkapi.ApiXmlConversionTask
import android.support.checkapi.CheckApiTask
import android.support.checkapi.UpdateApiTask
import android.support.doclava.DoclavaMultilineJavadocOptionFileOption
import android.support.doclava.DoclavaTask
import android.support.jdiff.JDiffTask

import org.gradle.api.InvalidUserDataException

import groovy.io.FileType

import java.util.regex.Matcher
import java.util.regex.Pattern

// Set up platform API files for federation.
if (project.androidApiTxt != null) {
    task generateSdkApi(type: Copy) {
        description = 'Copies the API files for the current SDK.'

        // Export the API files so this looks like a DoclavaTask.
        ext.apiFile = new File(project.docsDir, 'release/sdk_current.txt')
        ext.removedApiFile = new File(project.docsDir, 'release/sdk_removed.txt')

        from project.androidApiTxt.absolutePath
        into apiFile.parent
        rename { apiFile.name }

        // Register the fake removed file as an output.
        outputs.file removedApiFile

        doLast {
            removedApiFile.createNewFile()
        }
    }
} else {
    task generateSdkApi(type: DoclavaTask, dependsOn: [configurations.doclava]) {
        description = 'Generates API files for the current SDK.'

        docletpath = configurations.doclava.resolve()
        destinationDir = project.docsDir

        classpath = project.androidJar
        source zipTree(project.androidSrcJar)

        apiFile = new File(project.docsDir, 'release/sdk_current.txt')
        removedApiFile = new File(project.docsDir, 'release/sdk_removed.txt')
        generateDocs = false

        options {
            addStringOption "stubpackages", "android.*"
        }
    }
}

// Generates online docs.
task generateDocs(type: DoclavaTask, dependsOn: [configurations.doclava, generateSdkApi]) {
    def offlineDocs = project.docs.offline
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = 'Generates d.android.com-style documentation. To generate offline docs use ' +
            '\'-PofflineDocs=true\' parameter.'

    docletpath = configurations.doclava.resolve()
    destinationDir = new File(project.docsDir, offlineDocs ? "offline" : "online")

    // Base classpath is Android SDK, sub-projects add their own.
    classpath = project.ext.androidJar

    def hdfOption = new DoclavaMultilineJavadocOptionFileOption('hdf')
    hdfOption.add(
            ['android.whichdoc', 'online'],
            ['android.hasSamples', 'true'],
            ['dac', 'true'])

    def federateOption = new DoclavaMultilineJavadocOptionFileOption('federate')
    federateOption.add(['Android', 'https://developer.android.com'])

    def federationapiOption = new DoclavaMultilineJavadocOptionFileOption('federationapi')
    federationapiOption.add(['Android', generateSdkApi.apiFile.absolutePath])

    // Track API change history.
    def apiFilePattern = /(\d+\.\d+\.\d).txt/
    def sinceOption = new DoclavaMultilineJavadocOptionFileOption('since')
    File apiDir = new File(supportRootFolder, 'api')
    apiDir.eachFileMatch FileType.FILES, ~apiFilePattern, { File apiFile ->
        def apiLevel = (apiFile.name =~ apiFilePattern)[0][1]
        sinceOption.add([apiFile.absolutePath, apiLevel])
    }

    // Default hidden errors + hidden superclass (111) and
    // deprecation mismatch (113) to match framework docs.
    final def hidden = [105, 106, 107, 111, 112, 113, 115, 116, 121]

    doclavaErrors = (101..122) - hidden
    doclavaWarnings = []
    doclavaHidden += hidden

    options {
        addStringOption "templatedir",
                "${supportRootFolder}/../../external/doclava/res/assets/templates-sdk"
        addStringOption "stubpackages", "android.support.*"
        addStringOption "samplesdir", "${supportRootFolder}/samples"
        addOption federateOption
        addOption federationapiOption
        addOption hdfOption
        addOption sinceOption

        // Specific to reference docs.
        if (!offlineDocs) {
            addStringOption "toroot", "/"
            addBooleanOption "devsite", true
            addStringOption "dac_libraryroot", project.docs.dac.libraryroot
            addStringOption "dac_dataname", project.docs.dac.dataname
        }
    }

    exclude '**/BuildConfig.java'
}

// Generates a distribution artifact for online docs.
task distDocs(type: Zip, dependsOn: generateDocs) {
    group = JavaBasePlugin.DOCUMENTATION_GROUP
    description = 'Generates distribution artifact for d.android.com-style documentation.'

    from generateDocs.destinationDir
    destinationDir project.distDir
    baseName = "android-support-docs"
    version = project.buildNumber

    doLast {
        logger.lifecycle("'Wrote API reference to ${archivePath}")
    }
}

def MSG_HIDE_API =
        "If you are adding APIs that should be excluded from the public API surface,\n" +
        "consider using package or private visibility. If the API must have public\n" +
        "visibility, you may exclude it from public API by using the @hide javadoc\n" +
        "annotation paired with the @RestrictTo(LIBRARY_GROUP) code annotation."

// Check that the API we're building hasn't broken compatibility with the
// previously released version. These types of changes are forbidden.
def CHECK_API_CONFIG_RELEASE = [
    onFailMessage:
            "Compatibility with previously released public APIs has been broken. Please\n" +
            "verify your change with Support API Council and provide error output,\n" +
            "including the error messages and associated SHAs.\n" +
            "\n" +
            "If you are removing APIs, they must be deprecated first before being removed\n" +
            "in a subsequent release.\n" +
            "\n" + MSG_HIDE_API,
    errors: (7..18),
    warnings: [],
    hidden: (2..6) + (19..30)
]

// Check that the API we're building hasn't changed from the development
// version. These types of changes require an explicit API file update.
def CHECK_API_CONFIG_DEVELOP = [
    onFailMessage:
            "Public API definition has changed. Please run ./gradlew updateApi to confirm\n" +
            "these changes are intentional by updating the public API definition.\n" +
            "\n" + MSG_HIDE_API,
    errors: (2..30)-[22],
    warnings: [],
    hidden: [22]
]

// This is a patch or finalized release. Check that the API we're building
// hasn't changed from the current.
def CHECK_API_CONFIG_PATCH = [
    onFailMessage:
            "Public API definition may not change in finalized or patch releases.\n" +
            "\n" + MSG_HIDE_API,
    errors: (2..30)-[22],
    warnings: [],
    hidden: [22]
]

CheckApiTask createCheckApiTask(String taskName, def checkApiConfig, File oldApi, File newApi,
                                File whitelist = null) {
    return tasks.create(name: taskName, type: CheckApiTask.class) {
        doclavaClasspath = generateApi.docletpath

        onFailMessage = checkApiConfig.onFailMessage
        checkApiErrors = checkApiConfig.errors
        checkApiWarnings = checkApiConfig.warnings
        checkApiHidden = checkApiConfig.hidden

        newApiFile = newApi
        oldApiFile = oldApi
        newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
        oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt')

        whitelistErrorsFile = whitelist

        doFirst {
            logger.lifecycle "Verifying ${newApi.name} against ${oldApi.name}..."
        }
    }
}

// Generates API files.
task generateApi(type: DoclavaTask, dependsOn: configurations.doclava) {
    docletpath = configurations.doclava.resolve()
    destinationDir = project.docsDir

    // Base classpath is Android SDK, sub-projects add their own.
    classpath = project.ext.androidJar

    apiFile = new File(project.docsDir, 'release/current.txt')
    removedApiFile = new File(project.docsDir, 'release/removed.txt')
    generateDocs = false

    options {
        addStringOption "templatedir",
                "${supportRootFolder}/../../external/doclava/res/assets/templates-sdk"
        addStringOption "stubpackages", "android.support.*"
    }
    exclude '**/BuildConfig.java'
    exclude '**/R.java'
}

/**
 * Returns the most recent API, optionally restricting to APIs before
 * <code>beforeApi</code>.
 *
 * @param refApi the reference API version, ex. 25.0.0-SNAPSHOT
 * @return the most recently released API file
 */
File getApiFile(String refApi = supportVersion, boolean previous = false, boolean release = false) {
    def refMatcher = refApi =~ /^(\d+)\.(\d+)\.(\d+)(-.+)?$/
    def refMajor = refMatcher[0][1] as int
    def refMinor = refMatcher[0][2] as int
    def refPatch = refMatcher[0][3] as int
    def refExtra = refMatcher[0][4]

    File apiDir = new File(ext.supportRootFolder, 'api')

    if (!previous) {
        // If this is a patch or release version, ignore the extra.
        return new File(apiDir, "$refMajor.$refMinor.0" +
                (refPatch || release ? "" : refExtra) + ".txt")
    }

    File lastFile = null
    def lastMajor
    def lastMinor

    // Only look at released versions and snapshots thereof, ex. X.Y.0.txt or X.Y.0-SNAPSHOT.txt.
    apiDir.eachFileMatch FileType.FILES, ~/(\d+)\.(\d+)\.0(-SNAPSHOT)?\.txt/, { File file ->
        def matcher = file.name =~ /(\d+)\.(\d+)\.0(-SNAPSHOT)?\.txt/
        def major = matcher[0][1] as int
        def minor = matcher[0][2] as int

        if (lastFile == null || major > lastMajor || (major == lastMajor && minor > lastMinor)) {
            if (refMajor > major || (refMajor == major && refMinor > minor)) {
                lastFile = file
                lastMajor = major;
                lastMinor = minor;
            }
        }
    }

    return lastFile
}

String stripExtension(String fileName) {
    return fileName[0..fileName.lastIndexOf('.')-1]
}

// Make sure the API surface has not broken since the last release.
def isPatchVersion = supportVersion ==~ /\d+\.\d+.[1-9]\d*(-.+)?/
def isSnapshotVersion = supportVersion ==~ /\d+\.\d+.\d+-SNAPSHOT/
def previousApiFile = getApiFile(project.supportVersion, !isPatchVersion)
def whitelistFile = new File(
        previousApiFile.parentFile, stripExtension(previousApiFile.name) + ".ignore")
def checkApiRelease = createCheckApiTask("checkApiRelease", CHECK_API_CONFIG_RELEASE,
        previousApiFile, generateApi.apiFile, whitelistFile).dependsOn(generateApi)

// Allow a comma-delimited list of whitelisted errors.
if (project.hasProperty("ignore")) {
    checkApiRelease.whitelistErrors = ignore.split(',')
}

// Check whether the development API surface has changed.
def verifyConfig = isPatchVersion != 0 ? CHECK_API_CONFIG_DEVELOP : CHECK_API_CONFIG_PATCH;
def checkApi = createCheckApiTask("checkApi", verifyConfig, getApiFile(), generateApi.apiFile)
        .dependsOn(generateApi, checkApiRelease)

checkApi.group JavaBasePlugin.VERIFICATION_GROUP
checkApi.description 'Verify the API surface.'

rootProject.createArchive.dependsOn checkApi

task verifyUpdateApiAllowed() {
    // This could be moved to doFirst inside updateApi, but using it as a
    // dependency with no inputs forces it to run even when updateApi is a
    // no-op.
    doLast {
        if (isPatchVersion) {
            throw new GradleException("Public APIs may not be modified in patch releases.")
        } else if (isSnapshotVersion && getApiFile(supportVersion, false, true).exists()) {
            throw new GradleException("Inconsistent version. Public API file already exists.")
        } else if (!isSnapshotVersion && getApiFile().exists() && !project.hasProperty("force")) {
            throw new GradleException("Public APIs may not be modified in finalized releases.")
        }
    }
}

task updateApi(type: UpdateApiTask, dependsOn: [checkApiRelease, verifyUpdateApiAllowed]) {
    group JavaBasePlugin.VERIFICATION_GROUP
    description 'Updates the candidate API file to incorporate valid changes.'
    newApiFile = checkApiRelease.newApiFile
    oldApiFile = getApiFile()
    newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
    oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt')
    whitelistErrors = checkApiRelease.whitelistErrors
    whitelistErrorsFile = checkApiRelease.whitelistErrorsFile
}

/**
 * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly
 * defined using -PtoAPi=<file>) to XML format for use by JDiff.
 */
task newApiXml(type: ApiXmlConversionTask, dependsOn: configurations.doclava) {
    classpath configurations.doclava.resolve()

    if (project.hasProperty("toApi")) {
        // Use an explicit API file.
        inputApiFile = new File(rootProject.ext.supportRootFolder, "api/${toApi}.txt")
    } else {
        // Use the current API file (e.g. current.txt).
        inputApiFile = generateApi.apiFile
        dependsOn generateApi
    }

    int lastDot = inputApiFile.name.lastIndexOf('.')
    outputApiXmlFile = new File(project.docsDir,
            "release/" + inputApiFile.name.substring(0, lastDot) + ".xml")
}

/**
 * Converts the <code>fromApi</code>.txt file (or the most recently released
 * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format
 * for use by JDiff.
 */
task oldApiXml(type: ApiXmlConversionTask, dependsOn: configurations.doclava) {
    classpath configurations.doclava.resolve()

    if (project.hasProperty("fromApi")) {
        // Use an explicit API file.
        inputApiFile = new File(rootProject.ext.supportRootFolder, "api/${fromApi}.txt")
    } else if (project.hasProperty("toApi") && toApi.matches(~/(\d+\.){2}\d+/)) {
        // If toApi matches released API (X.Y.Z) format, use the most recently
        // released API file prior to toApi.
        inputApiFile = getApiFile(toApi, true)
    } else {
        // Use the most recently released API file.
        inputApiFile = getApiFile();
    }

    int lastDot = inputApiFile.name.lastIndexOf('.')
    outputApiXmlFile = new File(project.docsDir,
            "release/" + inputApiFile.name.substring(0, lastDot) + ".xml")
}

/**
 * Generates API diffs.
 * <p>
 * By default, diffs are generated for the delta between current.txt and the
 * next most recent X.Y.Z.txt API file. Behavior may be changed by specifying
 * one or both of -PtoApi and -PfromApi.
 * <p>
 * If both fromApi and toApi are specified, diffs will be generated for
 * fromApi -> toApi. For example, 25.0.0 -> 26.0.0 diffs could be generated by
 * using:
 * <br><code>
 *   ./gradlew generateDiffs -PfromApi=25.0.0 -PtoApi=26.0.0
 * </code>
 * <p>
 * If only toApi is specified, it MUST be specified as X.Y.Z and diffs will be
 * generated for (release before toApi) -> toApi. For example, 24.2.0 -> 25.0.0
 * diffs could be generated by using:
 * <br><code>
 *   ./gradlew generateDiffs -PtoApi=25.0.0
 * </code>
 * <p>
 * If only fromApi is specified, diffs will be generated for fromApi -> current.
 * For example, lastApiReview -> current diffs could be generated by using:
 * <br><code>
 *   ./gradlew generateDiffs -PfromApi=lastApiReview
 * </code>
 * <p>
 */
task generateDiffs(type: JDiffTask, dependsOn: [configurations.jdiff, configurations.doclava,
                                                oldApiXml, newApiXml, generateDocs]) {
    // Base classpath is Android SDK, sub-projects add their own.
    classpath = project.ext.androidJar

    // JDiff properties.
    oldApiXmlFile = oldApiXml.outputApiXmlFile
    newApiXmlFile = newApiXml.outputApiXmlFile
    newJavadocPrefix = "../../../../reference/"

    String newApi = newApiXmlFile.name
    int lastDot = newApi.lastIndexOf('.')
    newApi = newApi.substring(0, lastDot)

    // Javadoc properties.
    docletpath = configurations.jdiff.resolve()
    destinationDir = new File(project.docsDir, "online/sdk/support_api_diff/$newApi")
    title = "Support&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;Report"

    exclude '**/BuildConfig.java'
    exclude '**/R.java'
}

// configuration file for setting up api diffs and api docs
void registerForDocsTask(Task task, Project subProject, releaseVariant) {
    task.dependsOn releaseVariant.javaCompile
    task.source {
        def buildConfig = fileTree(releaseVariant.getGenerateBuildConfig().sourceOutputDir)
        return releaseVariant.javaCompile.source.minus(buildConfig) +
                fileTree(releaseVariant.aidlCompile.sourceOutputDir) +
                fileTree(releaseVariant.outputs[0].processResources.sourceOutputDir)
    }
    task.classpath += files{releaseVariant.javaCompile.classpath.files} +
            files(releaseVariant.javaCompile.destinationDir)
}

// configuration file for setting up api diffs and api docs
void registerJavaProjectForDocsTask(Task task, Project subProject, javaCompileTask) {
    task.dependsOn javaCompileTask
    task.source javaCompileTask.source
    task.classpath += files(javaCompileTask.classpath) +
            files(javaCompileTask.destinationDir)
}

subprojects { subProject ->
    subProject.afterEvaluate { p ->
        if (!p.hasProperty("noDocs") || !p.noDocs) {
            if (p.hasProperty('android') && p.android.hasProperty('libraryVariants')) {
                p.android.libraryVariants.all { v ->
                    if (v.name == 'release') {
                        registerForDocsTask(rootProject.generateDocs, p, v)
                        registerForDocsTask(rootProject.generateApi, p, v)
                        registerForDocsTask(rootProject.generateDiffs, p, v)
                    }
                }
            } else if (p.hasProperty("compileJava")) {
                registerJavaProjectForDocsTask(rootProject.generateDocs, p, p.compileJava)
            }
        }
    }
}