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