blob: d39bffdcbe70ec36aa859550881a33c8f8b67e37 [file] [log] [blame]
/*
* 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.LibraryVersions
import android.support.Version
import android.support.checkapi.ApiXmlConversionTask
import android.support.checkapi.CheckApiTask
import android.support.checkapi.UpdateApiTask
import android.support.doclava.DoclavaTask
import android.support.jdiff.JDiffTask
import groovy.io.FileType
import groovy.transform.Field
// 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.*"
}
}
}
// configuration file for setting up api diffs and api docs
void registerAndroidProjectForDocsTask(Task task, releaseVariant) {
task.dependsOn releaseVariant.javaCompile
task.source {
return releaseVariant.javaCompile.source +
fileTree(releaseVariant.aidlCompile.sourceOutputDir) +
fileTree(releaseVariant.outputs[0].processResources.sourceOutputDir)
}
task.classpath += releaseVariant.getCompileClasspath(null) +
files(releaseVariant.javaCompile.destinationDir)
}
// configuration file for setting up api diffs and api docs
void registerJavaProjectForDocsTask(Task task, javaCompileTask) {
task.dependsOn javaCompileTask
task.source javaCompileTask.source
task.classpath += files(javaCompileTask.classpath) +
files(javaCompileTask.destinationDir)
}
// Generates online docs.
task generateDocs(type: DoclavaTask, dependsOn: [configurations.doclava, generateSdkApi]) {
ext.artifacts = []
ext.sinces = []
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
// 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
// Track API change history prior to split versioning.
def apiFilePattern = /(\d+\.\d+\.\d).txt/
File apiDir = new File(supportRootFolder, 'api')
apiDir.eachFileMatch FileType.FILES, ~apiFilePattern, { File apiFile ->
def apiLevel = (apiFile.name =~ apiFilePattern)[0][1]
sinces.add([apiFile.absolutePath, apiLevel])
}
options {
addStringOption "templatedir",
"${supportRootFolder}/../../external/doclava/res/assets/templates-sdk"
addStringOption "stubpackages", "android.support.*"
addStringOption "samplesdir", "${supportRootFolder}/samples"
addMultilineMultiValueOption("federate").setValue([
['Android', 'https://developer.android.com']
])
addMultilineMultiValueOption("federationapi").setValue([
['Android', generateSdkApi.apiFile.absolutePath]
])
addMultilineMultiValueOption("hdf").setValue([
['android.whichdoc', 'online'],
['android.hasSamples', 'true'],
['dac', 'true']
])
// 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'
doFirst {
if (artifacts.size() > 0) {
options.addMultilineMultiValueOption("artifact").setValue(artifacts)
}
if (sinces.size() > 0) {
options.addMultilineMultiValueOption("since").setValue(sinces)
}
}
}
// 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}")
}
}
@Field 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.
@Field 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.
@Field 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.
@Field 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(Project project, String taskName, def checkApiConfig,
File oldApi, File newApi, File whitelist = null) {
return project.tasks.create(name: taskName, type: CheckApiTask.class) {
doclavaClasspath = project.generateApi.docletpath
onFailMessage = checkApiConfig.onFailMessage
checkApiErrors = checkApiConfig.errors
checkApiWarnings = checkApiConfig.warnings
checkApiHidden = checkApiConfig.hidden
newApiFile = newApi
oldApiFile = oldApi
whitelistErrorsFile = whitelist
doFirst {
logger.lifecycle "Verifying ${newApi.name} against ${oldApi ? oldApi.name : "nothing"}..."
}
}
}
DoclavaTask createGenerateApiTask(Project project) {
// Generates API files
return project.tasks.create(name: "generateApi", type: DoclavaTask.class,
dependsOn: configurations.doclava) {
docletpath = configurations.doclava.resolve()
destinationDir = project.docsDir
// Base classpath is Android SDK, sub-projects add their own.
classpath = rootProject.ext.androidJar
apiFile = new File(project.docsDir, 'release/' + project.name + '/current.txt')
generateDocs = false
options {
addBooleanOption "stubsourceonly", true
}
exclude '**/BuildConfig.java'
exclude '**/R.java'
}
}
/**
* Returns the API file for the specified reference version.
*
* @param refApi the reference API version, ex. 25.0.0-SNAPSHOT
* @return the most recently released API file
*/
File getApiFile(File rootDir, Version refVersion, boolean forceRelease = false) {
File apiDir = new File(rootDir, 'api')
if (!refVersion.isSnapshot() || forceRelease) {
// Release API file is always X.Y.0.txt.
return new File(apiDir, "$refVersion.major.$refVersion.minor.0.txt")
}
// Non-release API file is always current.txt.
return new File(apiDir, 'current.txt')
}
File getLastReleasedApiFile(File rootFolder, String refApi) {
Version refVersion = new Version(refApi)
File apiDir = new File(rootFolder, 'api')
File lastFile = null
Version lastVersion = null
// Only look at released versions and snapshots thereof, ex. X.Y.0.txt.
apiDir.eachFileMatch FileType.FILES, ~/(\d+)\.(\d+)\.0\.txt/, { File file ->
Version version = new Version(stripExtension(file.name))
if ((lastFile == null || lastVersion < version) && version < refVersion) {
lastFile = file
lastVersion = version
}
}
return lastFile
}
boolean hasApiFolder(Project project) {
new File(project.projectDir, "api").exists()
}
String stripExtension(String fileName) {
return fileName[0..fileName.lastIndexOf('.') - 1]
}
void initializeApiChecksForProject(Project project) {
if (!project.hasProperty("docsDir")) {
project.ext.docsDir = new File(rootProject.docsDir, project.name)
}
def artifact = project.group + ":" + project.name + ":" + project.version
def version = new Version(project.version)
def workingDir = project.projectDir
DoclavaTask generateApi = createGenerateApiTask(project)
createVerifyUpdateApiAllowedTask(project)
// Make sure the API surface has not broken since the last release.
File lastReleasedApiFile = getLastReleasedApiFile(workingDir, project.version)
def whitelistFile = lastReleasedApiFile == null ? null : new File(
lastReleasedApiFile.parentFile, stripExtension(lastReleasedApiFile.name) + ".ignore")
def checkApiRelease = createCheckApiTask(project, "checkApiRelease", CHECK_API_CONFIG_RELEASE,
lastReleasedApiFile, 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 = version.isPatch() ? CHECK_API_CONFIG_PATCH : CHECK_API_CONFIG_DEVELOP
File currentApiFile = getApiFile(workingDir, new Version(project.version))
def checkApi = createCheckApiTask(project, "checkApi", verifyConfig,
currentApiFile, project.generateApi.apiFile)
.dependsOn(generateApi, checkApiRelease)
checkApi.group JavaBasePlugin.VERIFICATION_GROUP
checkApi.description 'Verify the API surface.'
createUpdateApiTask(project)
createNewApiXmlTask(project)
createOldApiXml(project)
createGenerateDiffsTask(project)
// Track API change history.
def apiFilePattern = /(\d+\.\d+\.\d).txt/
File apiDir = new File(project.projectDir, 'api')
apiDir.eachFileMatch FileType.FILES, ~apiFilePattern, { File apiFile ->
def apiLevel = (apiFile.name =~ apiFilePattern)[0][1]
rootProject.generateDocs.sinces.add([apiFile.absolutePath, apiLevel])
}
// Associate current API surface with the Maven artifact.
rootProject.generateDocs.artifacts.add([generateApi.apiFile.absolutePath, artifact])
rootProject.generateDocs.dependsOn generateApi
rootProject.createArchive.dependsOn checkApi
}
Task createVerifyUpdateApiAllowedTask(Project project) {
project.tasks.create(name: "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 {
def rootFolder = project.projectDir
Version version = new Version(project.version)
if (version.isPatch()) {
throw new GradleException("Public APIs may not be modified in patch releases.")
} else if (version.isSnapshot() && getApiFile(rootFolder, version, true).exists()) {
throw new GradleException("Inconsistent version. Public API file already exists.")
} else if (!version.isSnapshot() && getApiFile(rootFolder, version).exists()
&& !project.hasProperty("force")) {
throw new GradleException("Public APIs may not be modified in finalized releases.")
}
}
}
}
UpdateApiTask createUpdateApiTask(Project project) {
project.tasks.create(name: "updateApi", type: UpdateApiTask,
dependsOn: [project.checkApiRelease, project.verifyUpdateApiAllowed]) {
group JavaBasePlugin.VERIFICATION_GROUP
description 'Updates the candidate API file to incorporate valid changes.'
newApiFile = project.checkApiRelease.newApiFile
oldApiFile = getApiFile(project.projectDir, new Version(project.version))
whitelistErrors = project.checkApiRelease.whitelistErrors
whitelistErrorsFile = project.checkApiRelease.whitelistErrorsFile
doFirst {
// Replace the expected whitelist with the detected whitelist.
whitelistErrors = project.checkApiRelease.detectedWhitelistErrors
}
}
}
/**
* Converts the <code>toApi</code>.txt file (or current.txt if not explicitly
* defined using -PtoApi=<file>) to XML format for use by JDiff.
*/
ApiXmlConversionTask createNewApiXmlTask(Project project) {
project.tasks.create(name: "newApiXml", type: ApiXmlConversionTask, dependsOn: configurations.doclava) {
classpath configurations.doclava.resolve()
if (project.hasProperty("toApi")) {
// Use an explicit API file.
inputApiFile = new File(project.projectDir, "api/${toApi}.txt")
} else {
// Use the current API file (e.g. current.txt).
inputApiFile = project.generateApi.apiFile
dependsOn project.generateApi
}
outputApiXmlFile = new File(project.docsDir,
"release/" + stripExtension(inputApiFile.name) + ".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.
*/
ApiXmlConversionTask createOldApiXml(Project project) {
project.tasks.create(name: "oldApiXml", type: ApiXmlConversionTask, dependsOn: configurations.doclava) {
classpath configurations.doclava.resolve()
def rootFolder = project.projectDir
if (project.hasProperty("fromApi")) {
// Use an explicit API file.
inputApiFile = new File(rootFolder, "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 = getLastReleasedApiFile(rootFolder, toApi)
} else {
// Use the most recently released API file.
inputApiFile = getApiFile(rootFolder, new Version(project.version))
}
outputApiXmlFile = new File(project.docsDir,
"release/" + stripExtension(inputApiFile.name) + ".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>
*/
JDiffTask createGenerateDiffsTask(Project project) {
project.tasks.create(name: "generateDiffs", type: JDiffTask,
dependsOn: [configurations.jdiff, configurations.doclava,
project.oldApiXml, project.newApiXml, rootProject.generateDocs]) {
// Base classpath is Android SDK, sub-projects add their own.
classpath = rootProject.ext.androidJar
// JDiff properties.
oldApiXmlFile = project.oldApiXml.outputApiXmlFile
newApiXmlFile = project.newApiXml.outputApiXmlFile
String newApi = newApiXmlFile.name
int lastDot = newApi.lastIndexOf('.')
newApi = newApi.substring(0, lastDot)
if (project == rootProject) {
newJavadocPrefix = "../../../../reference/"
destinationDir = new File(rootProject.docsDir, "online/sdk/support_api_diff/$newApi")
} else {
newJavadocPrefix = "../../../../../reference/"
destinationDir = new File(rootProject.docsDir,
"online/sdk/support_api_diff/$project.name/$newApi")
}
// Javadoc properties.
docletpath = configurations.jdiff.resolve()
title = "Support&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;Report"
exclude '**/BuildConfig.java'
exclude '**/R.java'
}
}
boolean hasJavaSources(releaseVariant) {
def fs = releaseVariant.javaCompile.source.filter { file ->
file.name != "R.java" && file.name != "BuildConfig.java"
}
return !fs.isEmpty();
}
subprojects { subProject ->
subProject.afterEvaluate { project ->
if (project.hasProperty("noDocs") && project.noDocs) {
return
}
if (project.hasProperty('android') && project.android.hasProperty('libraryVariants')) {
project.android.libraryVariants.all { variant ->
if (variant.name == 'release') {
registerAndroidProjectForDocsTask(rootProject.generateDocs, variant)
if (rootProject.tasks.findByPath("generateApi")) {
registerAndroidProjectForDocsTask(rootProject.generateApi, variant)
registerAndroidProjectForDocsTask(rootProject.generateDiffs, variant)
}
if (!hasJavaSources(variant)) {
return
}
if (!hasApiFolder(project)) {
logger.warn("Project $project.name doesn't have an api folder, " +
"ignoring API tasks")
return
}
initializeApiChecksForProject(project)
registerAndroidProjectForDocsTask(project.generateApi, variant)
registerAndroidProjectForDocsTask(project.generateDiffs, variant)
}
}
} else if (project.hasProperty("compileJava")) {
registerJavaProjectForDocsTask(rootProject.generateDocs, project.compileJava)
if (!hasApiFolder(project)) {
logger.warn("Project $project.name doesn't have an api folder, " +
"ignoring API tasks")
return
}
initializeApiChecksForProject(project)
registerJavaProjectForDocsTask(project.generateApi, project.compileJava)
registerJavaProjectForDocsTask(project.generateDiffs, project.compileJava)
}
}
}