/* * 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 Library API Differences 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) } } } }