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