diff_and_docs.gradle revision cec847e45396ec0e141c6e5be959d3020e9d4ea5
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.LibraryVersions 18import android.support.Version 19import android.support.checkapi.ApiXmlConversionTask 20import android.support.checkapi.CheckApiTask 21import android.support.checkapi.UpdateApiTask 22import android.support.doclava.DoclavaTask 23import android.support.jdiff.JDiffTask 24import groovy.io.FileType 25import groovy.transform.Field 26 27// Set up platform API files for federation. 28if (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 68void 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 80void 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. 88task 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. 158task 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 217CheckApiTask 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 238DoclavaTask 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 most recent API, optionally restricting to APIs before 260 * <code>beforeApi</code>. 261 * 262 * @param refApi the reference API version, ex. 25.0.0-SNAPSHOT 263 * @return the most recently released API file 264 */ 265File getApiFile(File rootFolder, String refApi, boolean release = false) { 266 Version refVersion = new Version(refApi) 267 File apiDir = new File(rootFolder, 'api') 268 // If this is a patch or release version, ignore the extra. 269 return new File(apiDir, "$refVersion.major.$refVersion.minor.0" + 270 (refVersion.patch || release ? "" : refVersion.extra) + ".txt") 271} 272 273File getPreviousApiFile(File rootFolder, String refApi) { 274 Version refVersion = new Version(refApi) 275 File apiDir = new File(rootFolder, 'api') 276 277 File lastFile = null 278 Version lastVersion = null 279 280 // Only look at released versions and snapshots thereof, ex. X.Y.0.txt or X.Y.0-SNAPSHOT.txt. 281 apiDir.eachFileMatch FileType.FILES, ~/(\d+)\.(\d+)\.0(-SNAPSHOT)?\.txt/, { File file -> 282 Version version = new Version(stripExtension(file.name)) 283 if ((lastFile == null || lastVersion < version) && version < refVersion) { 284 lastFile = file 285 lastVersion = version 286 } 287 } 288 289 return lastFile 290} 291 292boolean hasApiFolder(Project project) { 293 new File(project.projectDir, "api").exists() 294} 295 296String stripExtension(String fileName) { 297 return fileName[0..fileName.lastIndexOf('.') - 1] 298} 299 300void initializeApiChecksForProject(Project project) { 301 if (!project.hasProperty("docsDir")) { 302 project.ext.docsDir = new File(rootProject.docsDir, project.name) 303 } 304 def artifact = project.group + ":" + project.name + ":" + project.version 305 def version = new Version(project.version) 306 def workingDir = project.projectDir 307 308 DoclavaTask generateApi = createGenerateApiTask(project) 309 createVerifyUpdateApiAllowedTask(project) 310 311 // Make sure the API surface has not broken since the last release. 312 def previousApiFile = version.isPatch() ? getApiFile(workingDir, project.version) 313 : getPreviousApiFile(workingDir, project.version) 314 315 def whitelistFile = previousApiFile == null ? null : new File( 316 previousApiFile.parentFile, stripExtension(previousApiFile.name) + ".ignore") 317 def checkApiRelease = createCheckApiTask(project, "checkApiRelease", 318 CHECK_API_CONFIG_RELEASE, previousApiFile, generateApi.apiFile, whitelistFile) 319 .dependsOn(generateApi) 320 321 // Allow a comma-delimited list of whitelisted errors. 322 if (project.hasProperty("ignore")) { 323 checkApiRelease.whitelistErrors = ignore.split(',') 324 } 325 326 // Check whether the development API surface has changed. 327 def verifyConfig = version.isPatch() ? CHECK_API_CONFIG_DEVELOP : CHECK_API_CONFIG_PATCH 328 def checkApi = createCheckApiTask(project, "checkApi", verifyConfig, 329 getApiFile(workingDir, project.version), project.generateApi.apiFile) 330 .dependsOn(generateApi, checkApiRelease) 331 332 checkApi.group JavaBasePlugin.VERIFICATION_GROUP 333 checkApi.description 'Verify the API surface.' 334 335 createUpdateApiTask(project) 336 createNewApiXmlTask(project) 337 createOldApiXml(project) 338 createGenerateDiffsTask(project) 339 340 // Track API change history. 341 def apiFilePattern = /(\d+\.\d+\.\d).txt/ 342 File apiDir = new File(project.projectDir, 'api') 343 apiDir.eachFileMatch FileType.FILES, ~apiFilePattern, { File apiFile -> 344 def apiLevel = (apiFile.name =~ apiFilePattern)[0][1] 345 rootProject.generateDocs.sinces.add([apiFile.absolutePath, apiLevel]) 346 } 347 348 // Associate current API surface with the Maven artifact. 349 rootProject.generateDocs.artifacts.add([generateApi.apiFile.absolutePath, artifact]) 350 rootProject.generateDocs.dependsOn generateApi 351 352 rootProject.createArchive.dependsOn checkApi 353} 354 355Task createVerifyUpdateApiAllowedTask(Project project) { 356 project.tasks.create(name: "verifyUpdateApiAllowed") { 357 // This could be moved to doFirst inside updateApi, but using it as a 358 // dependency with no inputs forces it to run even when updateApi is a 359 // no-op. 360 doLast { 361 def rootFolder = project.projectDir 362 def versionString = project.version 363 def version = new Version(versionString) 364 365 if (version.isPatch()) { 366 throw new GradleException("Public APIs may not be modified in patch releases.") 367 } else if (version.isSnapshot() && getApiFile(rootFolder, versionString, true).exists()) { 368 throw new GradleException("Inconsistent version. Public API file already exists.") 369 } else if (!version.isSnapshot() && getApiFile(rootFolder, versionString).exists() 370 && !project.hasProperty("force")) { 371 throw new GradleException("Public APIs may not be modified in finalized releases.") 372 } 373 } 374 } 375} 376 377UpdateApiTask createUpdateApiTask(Project project) { 378 project.tasks.create(name: "updateApi", type: UpdateApiTask, 379 dependsOn: [project.checkApiRelease, project.verifyUpdateApiAllowed]) { 380 group JavaBasePlugin.VERIFICATION_GROUP 381 description 'Updates the candidate API file to incorporate valid changes.' 382 newApiFile = project.checkApiRelease.newApiFile 383 oldApiFile = getApiFile(project.projectDir, project.version) 384 whitelistErrors = project.checkApiRelease.whitelistErrors 385 whitelistErrorsFile = project.checkApiRelease.whitelistErrorsFile 386 } 387} 388 389/** 390 * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly 391 * defined using -PtoApi=<file>) to XML format for use by JDiff. 392 */ 393ApiXmlConversionTask createNewApiXmlTask(Project project) { 394 project.tasks.create(name: "newApiXml", type: ApiXmlConversionTask, dependsOn: configurations.doclava) { 395 classpath configurations.doclava.resolve() 396 397 if (project.hasProperty("toApi")) { 398 // Use an explicit API file. 399 inputApiFile = new File(project.projectDir, "api/${toApi}.txt") 400 } else { 401 // Use the current API file (e.g. current.txt). 402 inputApiFile = project.generateApi.apiFile 403 dependsOn project.generateApi 404 } 405 406 outputApiXmlFile = new File(project.docsDir, 407 "release/" + stripExtension(inputApiFile.name) + ".xml") 408 } 409} 410 411/** 412 * Converts the <code>fromApi</code>.txt file (or the most recently released 413 * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format 414 * for use by JDiff. 415 */ 416ApiXmlConversionTask createOldApiXml(Project project) { 417 project.tasks.create(name: "oldApiXml", type: ApiXmlConversionTask, dependsOn: configurations.doclava) { 418 classpath configurations.doclava.resolve() 419 420 def rootFolder = project.projectDir 421 if (project.hasProperty("fromApi")) { 422 // Use an explicit API file. 423 inputApiFile = new File(rootFolder, "api/${fromApi}.txt") 424 } else if (project.hasProperty("toApi") && toApi.matches(~/(\d+\.){2}\d+/)) { 425 // If toApi matches released API (X.Y.Z) format, use the most recently 426 // released API file prior to toApi. 427 inputApiFile = getPreviousApiFile(rootFolder, toApi) 428 } else { 429 // Use the most recently released API file. 430 inputApiFile = getApiFile(rootFolder, project.version); 431 } 432 433 outputApiXmlFile = new File(project.docsDir, 434 "release/" + stripExtension(inputApiFile.name) + ".xml") 435 } 436} 437 438/** 439 * Generates API diffs. 440 * <p> 441 * By default, diffs are generated for the delta between current.txt and the 442 * next most recent X.Y.Z.txt API file. Behavior may be changed by specifying 443 * one or both of -PtoApi and -PfromApi. 444 * <p> 445 * If both fromApi and toApi are specified, diffs will be generated for 446 * fromApi -> toApi. For example, 25.0.0 -> 26.0.0 diffs could be generated by 447 * using: 448 * <br><code> 449 * ./gradlew generateDiffs -PfromApi=25.0.0 -PtoApi=26.0.0 450 * </code> 451 * <p> 452 * If only toApi is specified, it MUST be specified as X.Y.Z and diffs will be 453 * generated for (release before toApi) -> toApi. For example, 24.2.0 -> 25.0.0 454 * diffs could be generated by using: 455 * <br><code> 456 * ./gradlew generateDiffs -PtoApi=25.0.0 457 * </code> 458 * <p> 459 * If only fromApi is specified, diffs will be generated for fromApi -> current. 460 * For example, lastApiReview -> current diffs could be generated by using: 461 * <br><code> 462 * ./gradlew generateDiffs -PfromApi=lastApiReview 463 * </code> 464 * <p> 465 */ 466JDiffTask createGenerateDiffsTask(Project project) { 467 project.tasks.create(name: "generateDiffs", type: JDiffTask, 468 dependsOn: [configurations.jdiff, configurations.doclava, 469 project.oldApiXml, project.newApiXml, rootProject.generateDocs]) { 470 // Base classpath is Android SDK, sub-projects add their own. 471 classpath = rootProject.ext.androidJar 472 473 // JDiff properties. 474 oldApiXmlFile = project.oldApiXml.outputApiXmlFile 475 newApiXmlFile = project.newApiXml.outputApiXmlFile 476 477 String newApi = newApiXmlFile.name 478 int lastDot = newApi.lastIndexOf('.') 479 newApi = newApi.substring(0, lastDot) 480 481 if (project == rootProject) { 482 newJavadocPrefix = "../../../../reference/" 483 destinationDir = new File(rootProject.docsDir, "online/sdk/support_api_diff/$newApi") 484 } else { 485 newJavadocPrefix = "../../../../../reference/" 486 destinationDir = new File(rootProject.docsDir, 487 "online/sdk/support_api_diff/$project.name/$newApi") 488 } 489 490 // Javadoc properties. 491 docletpath = configurations.jdiff.resolve() 492 title = "Support Library API Differences Report" 493 494 exclude '**/BuildConfig.java' 495 exclude '**/R.java' 496 } 497} 498 499boolean hasJavaSources(releaseVariant) { 500 def fs = releaseVariant.javaCompile.source.filter { file -> 501 file.name != "R.java" && file.name != "BuildConfig.java" 502 } 503 return !fs.isEmpty(); 504} 505 506subprojects { subProject -> 507 subProject.afterEvaluate { project -> 508 if (project.hasProperty("noDocs") && project.noDocs) { 509 return 510 } 511 if (project.hasProperty('android') && project.android.hasProperty('libraryVariants')) { 512 project.android.libraryVariants.all { variant -> 513 if (variant.name == 'release') { 514 registerAndroidProjectForDocsTask(rootProject.generateDocs, variant) 515 if (rootProject.tasks.findByPath("generateApi")) { 516 registerAndroidProjectForDocsTask(rootProject.generateApi, variant) 517 registerAndroidProjectForDocsTask(rootProject.generateDiffs, variant) 518 } 519 if (!hasJavaSources(variant)) { 520 return 521 } 522 if (!hasApiFolder(project)) { 523 logger.warn("Project $project.name doesn't have an api folder, " + 524 "ignoring API tasks") 525 return 526 } 527 initializeApiChecksForProject(project) 528 registerAndroidProjectForDocsTask(project.generateApi, variant) 529 registerAndroidProjectForDocsTask(project.generateDiffs, variant) 530 } 531 } 532 } else if (project.hasProperty("compileJava")) { 533 registerJavaProjectForDocsTask(rootProject.generateDocs, project.compileJava) 534 if (!hasApiFolder(project)) { 535 logger.warn("Project $project.name doesn't have an api folder, " + 536 "ignoring API tasks") 537 return 538 } 539 initializeApiChecksForProject(project) 540 registerJavaProjectForDocsTask(project.generateApi, project.compileJava) 541 registerJavaProjectForDocsTask(project.generateDiffs, project.compileJava) 542 } 543 } 544} 545