Home | History | Annotate | Download | only in buildSrc
      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&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;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