• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;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