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&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;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