1/* 2 * Copyright (C) 2017 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 17import android.support.checkapi.ApiXmlConversionTask 18import android.support.checkapi.CheckApiTask 19import android.support.checkapi.UpdateApiTask 20import android.support.doclava.DoclavaMultilineJavadocOptionFileOption 21import android.support.doclava.DoclavaTask 22import android.support.jdiff.JDiffTask 23 24import org.gradle.api.InvalidUserDataException 25 26import groovy.io.FileType 27 28import java.util.regex.Matcher 29import java.util.regex.Pattern 30 31// Set up platform API files for federation. 32if (project.androidApiTxt != null) { 33 task generateSdkApi(type: Copy) { 34 description = 'Copies the API files for the current SDK.' 35 36 // Export the API files so this looks like a DoclavaTask. 37 ext.apiFile = new File(project.docsDir, 'release/sdk_current.txt') 38 ext.removedApiFile = new File(project.docsDir, 'release/sdk_removed.txt') 39 40 from project.androidApiTxt.absolutePath 41 into apiFile.parent 42 rename { apiFile.name } 43 44 // Register the fake removed file as an output. 45 outputs.file removedApiFile 46 47 doLast { 48 removedApiFile.createNewFile() 49 } 50 } 51} else { 52 task generateSdkApi(type: DoclavaTask, dependsOn: [configurations.doclava]) { 53 description = 'Generates API files for the current SDK.' 54 55 docletpath = configurations.doclava.resolve() 56 destinationDir = project.docsDir 57 58 classpath = project.androidJar 59 source zipTree(project.androidSrcJar) 60 61 apiFile = new File(project.docsDir, 'release/sdk_current.txt') 62 removedApiFile = new File(project.docsDir, 'release/sdk_removed.txt') 63 generateDocs = false 64 65 options { 66 addStringOption "stubpackages", "android.*" 67 } 68 } 69} 70 71// Generates online docs. 72task generateDocs(type: DoclavaTask, dependsOn: [configurations.doclava, generateSdkApi]) { 73 def offlineDocs = project.docs.offline 74 group = JavaBasePlugin.DOCUMENTATION_GROUP 75 description = 'Generates d.android.com-style documentation. To generate offline docs use ' + 76 '\'-PofflineDocs=true\' parameter.' 77 78 docletpath = configurations.doclava.resolve() 79 destinationDir = new File(project.docsDir, offlineDocs ? "offline" : "online") 80 81 // Base classpath is Android SDK, sub-projects add their own. 82 classpath = project.ext.androidJar 83 84 def hdfOption = new DoclavaMultilineJavadocOptionFileOption('hdf') 85 hdfOption.add( 86 ['android.whichdoc', 'online'], 87 ['android.hasSamples', 'true'], 88 ['dac', 'true']) 89 90 def federateOption = new DoclavaMultilineJavadocOptionFileOption('federate') 91 federateOption.add(['Android', 'https://developer.android.com']) 92 93 def federationapiOption = new DoclavaMultilineJavadocOptionFileOption('federationapi') 94 federationapiOption.add(['Android', generateSdkApi.apiFile.absolutePath]) 95 96 // Track API change history. 97 def apiFilePattern = /(\d+\.\d+\.\d).txt/ 98 def sinceOption = new DoclavaMultilineJavadocOptionFileOption('since') 99 File apiDir = new File(supportRootFolder, 'api') 100 apiDir.eachFileMatch FileType.FILES, ~apiFilePattern, { File apiFile -> 101 def apiLevel = (apiFile.name =~ apiFilePattern)[0][1] 102 sinceOption.add([apiFile.absolutePath, apiLevel]) 103 } 104 105 // Default hidden errors + hidden superclass (111) and 106 // deprecation mismatch (113) to match framework docs. 107 final def hidden = [105, 106, 107, 111, 112, 113, 115, 116, 121] 108 109 doclavaErrors = (101..122) - hidden 110 doclavaWarnings = [] 111 doclavaHidden += hidden 112 113 options { 114 addStringOption "templatedir", 115 "${supportRootFolder}/../../external/doclava/res/assets/templates-sdk" 116 addStringOption "stubpackages", "android.support.*" 117 addStringOption "samplesdir", "${supportRootFolder}/samples" 118 addOption federateOption 119 addOption federationapiOption 120 addOption hdfOption 121 addOption sinceOption 122 123 // Specific to reference docs. 124 if (!offlineDocs) { 125 addStringOption "toroot", "/" 126 addBooleanOption "devsite", true 127 addStringOption "dac_libraryroot", project.docs.dac.libraryroot 128 addStringOption "dac_dataname", project.docs.dac.dataname 129 } 130 } 131 132 exclude '**/BuildConfig.java' 133} 134 135// Generates a distribution artifact for online docs. 136task distDocs(type: Zip, dependsOn: generateDocs) { 137 group = JavaBasePlugin.DOCUMENTATION_GROUP 138 description = 'Generates distribution artifact for d.android.com-style documentation.' 139 140 from generateDocs.destinationDir 141 destinationDir project.distDir 142 baseName = "android-support-docs" 143 version = project.buildNumber 144 145 doLast { 146 logger.lifecycle("'Wrote API reference to ${archivePath}") 147 } 148} 149 150def MSG_HIDE_API = 151 "If you are adding APIs that should be excluded from the public API surface,\n" + 152 "consider using package or private visibility. If the API must have public\n" + 153 "visibility, you may exclude it from public API by using the @hide javadoc\n" + 154 "annotation paired with the @RestrictTo(LIBRARY_GROUP) code annotation." 155 156// Check that the API we're building hasn't broken compatibility with the 157// previously released version. These types of changes are forbidden. 158def CHECK_API_CONFIG_RELEASE = [ 159 onFailMessage: 160 "Compatibility with previously released public APIs has been broken. Please\n" + 161 "verify your change with Support API Council and provide error output,\n" + 162 "including the error messages and associated SHAs.\n" + 163 "\n" + 164 "If you are removing APIs, they must be deprecated first before being removed\n" + 165 "in a subsequent release.\n" + 166 "\n" + MSG_HIDE_API, 167 errors: (7..18), 168 warnings: [], 169 hidden: (2..6) + (19..30) 170] 171 172// Check that the API we're building hasn't changed from the development 173// version. These types of changes require an explicit API file update. 174def CHECK_API_CONFIG_DEVELOP = [ 175 onFailMessage: 176 "Public API definition has changed. Please run ./gradlew updateApi to confirm\n" + 177 "these changes are intentional by updating the public API definition.\n" + 178 "\n" + MSG_HIDE_API, 179 errors: (2..30)-[22], 180 warnings: [], 181 hidden: [22] 182] 183 184// This is a patch or finalized release. Check that the API we're building 185// hasn't changed from the current. 186def CHECK_API_CONFIG_PATCH = [ 187 onFailMessage: 188 "Public API definition may not change in finalized or patch releases.\n" + 189 "\n" + MSG_HIDE_API, 190 errors: (2..30)-[22], 191 warnings: [], 192 hidden: [22] 193] 194 195CheckApiTask createCheckApiTask(String taskName, def checkApiConfig, File oldApi, File newApi, 196 File whitelist = null) { 197 return tasks.create(name: taskName, type: CheckApiTask.class) { 198 doclavaClasspath = generateApi.docletpath 199 200 onFailMessage = checkApiConfig.onFailMessage 201 checkApiErrors = checkApiConfig.errors 202 checkApiWarnings = checkApiConfig.warnings 203 checkApiHidden = checkApiConfig.hidden 204 205 newApiFile = newApi 206 oldApiFile = oldApi 207 newRemovedApiFile = new File(project.docsDir, 'release/removed.txt') 208 oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt') 209 210 whitelistErrorsFile = whitelist 211 212 doFirst { 213 logger.lifecycle "Verifying ${newApi.name} against ${oldApi.name}..." 214 } 215 } 216} 217 218// Generates API files. 219task generateApi(type: DoclavaTask, dependsOn: configurations.doclava) { 220 docletpath = configurations.doclava.resolve() 221 destinationDir = project.docsDir 222 223 // Base classpath is Android SDK, sub-projects add their own. 224 classpath = project.ext.androidJar 225 226 apiFile = new File(project.docsDir, 'release/current.txt') 227 removedApiFile = new File(project.docsDir, 'release/removed.txt') 228 generateDocs = false 229 230 options { 231 addStringOption "templatedir", 232 "${supportRootFolder}/../../external/doclava/res/assets/templates-sdk" 233 addStringOption "stubpackages", "android.support.*" 234 } 235 exclude '**/BuildConfig.java' 236 exclude '**/R.java' 237} 238 239/** 240 * Returns the most recent API, optionally restricting to APIs before 241 * <code>beforeApi</code>. 242 * 243 * @param refApi the reference API version, ex. 25.0.0-SNAPSHOT 244 * @return the most recently released API file 245 */ 246File getApiFile(String refApi = supportVersion, boolean previous = false, boolean release = false) { 247 def refMatcher = refApi =~ /^(\d+)\.(\d+)\.(\d+)(-.+)?$/ 248 def refMajor = refMatcher[0][1] as int 249 def refMinor = refMatcher[0][2] as int 250 def refPatch = refMatcher[0][3] as int 251 def refExtra = refMatcher[0][4] 252 253 File apiDir = new File(ext.supportRootFolder, 'api') 254 255 if (!previous) { 256 // If this is a patch or release version, ignore the extra. 257 return new File(apiDir, "$refMajor.$refMinor.0" + 258 (refPatch || release ? "" : refExtra) + ".txt") 259 } 260 261 File lastFile = null 262 def lastMajor 263 def lastMinor 264 265 // Only look at released versions and snapshots thereof, ex. X.Y.0.txt or X.Y.0-SNAPSHOT.txt. 266 apiDir.eachFileMatch FileType.FILES, ~/(\d+)\.(\d+)\.0(-SNAPSHOT)?\.txt/, { File file -> 267 def matcher = file.name =~ /(\d+)\.(\d+)\.0(-SNAPSHOT)?\.txt/ 268 def major = matcher[0][1] as int 269 def minor = matcher[0][2] as int 270 271 if (lastFile == null || major > lastMajor || (major == lastMajor && minor > lastMinor)) { 272 if (refMajor > major || (refMajor == major && refMinor > minor)) { 273 lastFile = file 274 lastMajor = major; 275 lastMinor = minor; 276 } 277 } 278 } 279 280 return lastFile 281} 282 283String stripExtension(String fileName) { 284 return fileName[0..fileName.lastIndexOf('.')-1] 285} 286 287// Make sure the API surface has not broken since the last release. 288def isPatchVersion = supportVersion ==~ /\d+\.\d+.[1-9]\d*(-.+)?/ 289def isSnapshotVersion = supportVersion ==~ /\d+\.\d+.\d+-SNAPSHOT/ 290def previousApiFile = getApiFile(project.supportVersion, !isPatchVersion) 291def whitelistFile = new File( 292 previousApiFile.parentFile, stripExtension(previousApiFile.name) + ".ignore") 293def checkApiRelease = createCheckApiTask("checkApiRelease", CHECK_API_CONFIG_RELEASE, 294 previousApiFile, generateApi.apiFile, whitelistFile).dependsOn(generateApi) 295 296// Allow a comma-delimited list of whitelisted errors. 297if (project.hasProperty("ignore")) { 298 checkApiRelease.whitelistErrors = ignore.split(',') 299} 300 301// Check whether the development API surface has changed. 302def verifyConfig = isPatchVersion != 0 ? CHECK_API_CONFIG_DEVELOP : CHECK_API_CONFIG_PATCH; 303def checkApi = createCheckApiTask("checkApi", verifyConfig, getApiFile(), generateApi.apiFile) 304 .dependsOn(generateApi, checkApiRelease) 305 306checkApi.group JavaBasePlugin.VERIFICATION_GROUP 307checkApi.description 'Verify the API surface.' 308 309rootProject.createArchive.dependsOn checkApi 310 311task verifyUpdateApiAllowed() { 312 // This could be moved to doFirst inside updateApi, but using it as a 313 // dependency with no inputs forces it to run even when updateApi is a 314 // no-op. 315 doLast { 316 if (isPatchVersion) { 317 throw new GradleException("Public APIs may not be modified in patch releases.") 318 } else if (isSnapshotVersion && getApiFile(supportVersion, false, true).exists()) { 319 throw new GradleException("Inconsistent version. Public API file already exists.") 320 } else if (!isSnapshotVersion && getApiFile().exists() && !project.hasProperty("force")) { 321 throw new GradleException("Public APIs may not be modified in finalized releases.") 322 } 323 } 324} 325 326task updateApi(type: UpdateApiTask, dependsOn: [checkApiRelease, verifyUpdateApiAllowed]) { 327 group JavaBasePlugin.VERIFICATION_GROUP 328 description 'Updates the candidate API file to incorporate valid changes.' 329 newApiFile = checkApiRelease.newApiFile 330 oldApiFile = getApiFile() 331 newRemovedApiFile = new File(project.docsDir, 'release/removed.txt') 332 oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt') 333 whitelistErrors = checkApiRelease.whitelistErrors 334 whitelistErrorsFile = checkApiRelease.whitelistErrorsFile 335} 336 337/** 338 * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly 339 * defined using -PtoAPi=<file>) to XML format for use by JDiff. 340 */ 341task newApiXml(type: ApiXmlConversionTask, dependsOn: configurations.doclava) { 342 classpath configurations.doclava.resolve() 343 344 if (project.hasProperty("toApi")) { 345 // Use an explicit API file. 346 inputApiFile = new File(rootProject.ext.supportRootFolder, "api/${toApi}.txt") 347 } else { 348 // Use the current API file (e.g. current.txt). 349 inputApiFile = generateApi.apiFile 350 dependsOn generateApi 351 } 352 353 int lastDot = inputApiFile.name.lastIndexOf('.') 354 outputApiXmlFile = new File(project.docsDir, 355 "release/" + inputApiFile.name.substring(0, lastDot) + ".xml") 356} 357 358/** 359 * Converts the <code>fromApi</code>.txt file (or the most recently released 360 * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format 361 * for use by JDiff. 362 */ 363task oldApiXml(type: ApiXmlConversionTask, dependsOn: configurations.doclava) { 364 classpath configurations.doclava.resolve() 365 366 if (project.hasProperty("fromApi")) { 367 // Use an explicit API file. 368 inputApiFile = new File(rootProject.ext.supportRootFolder, "api/${fromApi}.txt") 369 } else if (project.hasProperty("toApi") && toApi.matches(~/(\d+\.){2}\d+/)) { 370 // If toApi matches released API (X.Y.Z) format, use the most recently 371 // released API file prior to toApi. 372 inputApiFile = getApiFile(toApi, true) 373 } else { 374 // Use the most recently released API file. 375 inputApiFile = getApiFile(); 376 } 377 378 int lastDot = inputApiFile.name.lastIndexOf('.') 379 outputApiXmlFile = new File(project.docsDir, 380 "release/" + inputApiFile.name.substring(0, lastDot) + ".xml") 381} 382 383/** 384 * Generates API diffs. 385 * <p> 386 * By default, diffs are generated for the delta between current.txt and the 387 * next most recent X.Y.Z.txt API file. Behavior may be changed by specifying 388 * one or both of -PtoApi and -PfromApi. 389 * <p> 390 * If both fromApi and toApi are specified, diffs will be generated for 391 * fromApi -> toApi. For example, 25.0.0 -> 26.0.0 diffs could be generated by 392 * using: 393 * <br><code> 394 * ./gradlew generateDiffs -PfromApi=25.0.0 -PtoApi=26.0.0 395 * </code> 396 * <p> 397 * If only toApi is specified, it MUST be specified as X.Y.Z and diffs will be 398 * generated for (release before toApi) -> toApi. For example, 24.2.0 -> 25.0.0 399 * diffs could be generated by using: 400 * <br><code> 401 * ./gradlew generateDiffs -PtoApi=25.0.0 402 * </code> 403 * <p> 404 * If only fromApi is specified, diffs will be generated for fromApi -> current. 405 * For example, lastApiReview -> current diffs could be generated by using: 406 * <br><code> 407 * ./gradlew generateDiffs -PfromApi=lastApiReview 408 * </code> 409 * <p> 410 */ 411task generateDiffs(type: JDiffTask, dependsOn: [configurations.jdiff, configurations.doclava, 412 oldApiXml, newApiXml, generateDocs]) { 413 // Base classpath is Android SDK, sub-projects add their own. 414 classpath = project.ext.androidJar 415 416 // JDiff properties. 417 oldApiXmlFile = oldApiXml.outputApiXmlFile 418 newApiXmlFile = newApiXml.outputApiXmlFile 419 newJavadocPrefix = "../../../../reference/" 420 421 String newApi = newApiXmlFile.name 422 int lastDot = newApi.lastIndexOf('.') 423 newApi = newApi.substring(0, lastDot) 424 425 // Javadoc properties. 426 docletpath = configurations.jdiff.resolve() 427 destinationDir = new File(project.docsDir, "online/sdk/support_api_diff/$newApi") 428 title = "Support Library API Differences Report" 429 430 exclude '**/BuildConfig.java' 431 exclude '**/R.java' 432} 433 434// configuration file for setting up api diffs and api docs 435void registerForDocsTask(Task task, Project subProject, releaseVariant) { 436 task.dependsOn releaseVariant.javaCompile 437 task.source { 438 def buildConfig = fileTree(releaseVariant.getGenerateBuildConfig().sourceOutputDir) 439 return releaseVariant.javaCompile.source.minus(buildConfig) + 440 fileTree(releaseVariant.aidlCompile.sourceOutputDir) + 441 fileTree(releaseVariant.outputs[0].processResources.sourceOutputDir) 442 } 443 task.classpath += files{releaseVariant.javaCompile.classpath.files} + 444 files(releaseVariant.javaCompile.destinationDir) 445} 446 447// configuration file for setting up api diffs and api docs 448void registerJavaProjectForDocsTask(Task task, Project subProject, javaCompileTask) { 449 task.dependsOn javaCompileTask 450 task.source javaCompileTask.source 451 task.classpath += files(javaCompileTask.classpath) + 452 files(javaCompileTask.destinationDir) 453} 454 455subprojects { subProject -> 456 subProject.afterEvaluate { p -> 457 if (!p.hasProperty("noDocs") || !p.noDocs) { 458 if (p.hasProperty('android') && p.android.hasProperty('libraryVariants')) { 459 p.android.libraryVariants.all { v -> 460 if (v.name == 'release') { 461 registerForDocsTask(rootProject.generateDocs, p, v) 462 registerForDocsTask(rootProject.generateApi, p, v) 463 registerForDocsTask(rootProject.generateDiffs, p, v) 464 } 465 } 466 } else if (p.hasProperty("compileJava")) { 467 registerJavaProjectForDocsTask(rootProject.generateDocs, p, p.compileJava) 468 } 469 } 470 } 471} 472