diff_and_docs.gradle revision cef9526cf31392b0e267a962944b119280654279
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
28// Set up platform API files for federation.
29if (project.androidApiTxt != null) {
30    task generateSdkApi(type: Copy) {
31        description = 'Copies the API files for the current SDK.'
32
33        // Export the API files so this looks like a DoclavaTask.
34        ext.apiFile = new File(project.docsDir, 'release/sdk_current.txt')
35        ext.removedApiFile = new File(project.docsDir, 'release/sdk_removed.txt')
36
37        from project.androidApiTxt.absolutePath
38        into apiFile.parent
39        rename { apiFile.name }
40
41        // Register the fake removed file as an output.
42        outputs.file removedApiFile
43
44        doLast {
45            removedApiFile.createNewFile()
46        }
47    }
48} else {
49    task generateSdkApi(type: DoclavaTask, dependsOn: [configurations.doclava]) {
50        description = 'Generates API files for the current SDK.'
51
52        docletpath = configurations.doclava.resolve()
53        destinationDir = project.docsDir
54
55        classpath = project.androidJar
56        source zipTree(project.androidSrcJar)
57
58        apiFile = new File(project.docsDir, 'release/sdk_current.txt')
59        removedApiFile = new File(project.docsDir, 'release/sdk_removed.txt')
60        generateDocs = false
61
62        options {
63            addStringOption "stubpackages", "android.*"
64        }
65    }
66}
67
68// Generates online docs.
69task generateDocs(type: DoclavaTask, dependsOn: [configurations.doclava, generateSdkApi]) {
70    group = JavaBasePlugin.DOCUMENTATION_GROUP
71    description = 'Generates d.android.com-style documentation.'
72
73    docletpath = configurations.doclava.resolve()
74    destinationDir = new File(project.docsDir, "online")
75
76    // Base classpath is Android SDK, sub-projects add their own.
77    classpath = project.ext.androidJar
78
79    def hdfOption = new DoclavaMultilineJavadocOptionFileOption('hdf')
80    hdfOption.add(
81            ['android.whichdoc', 'online'],
82            ['android.hasSamples', 'true'])
83
84    def federateOption = new DoclavaMultilineJavadocOptionFileOption('federate')
85    federateOption.add(['Android', 'https://developer.android.com'])
86
87    def federationapiOption = new DoclavaMultilineJavadocOptionFileOption('federationapi')
88    federationapiOption.add(['Android', generateSdkApi.apiFile.absolutePath])
89
90    // Track API change history.
91    def apiFilePattern = /(\d+\.\d+\.\d).txt/
92    def sinceOption = new DoclavaMultilineJavadocOptionFileOption('since')
93    File apiDir = new File(supportRootFolder, 'api')
94    apiDir.eachFileMatch FileType.FILES, ~apiFilePattern, { File apiFile ->
95        def apiLevel = (apiFile.name =~ apiFilePattern)[0][1]
96        sinceOption.add([apiFile.absolutePath, apiLevel])
97    }
98
99    // Default hidden errors + hidden superclass (111) and
100    // deprecation mismatch (113) to match framework docs.
101    final def hidden = [105, 107, 111, 112, 113, 115, 116, 121]
102
103    doclavaErrors = (101..122) - hidden
104    doclavaWarnings = []
105    doclavaHidden += hidden
106
107    options {
108        addStringOption "templatedir",
109                "${supportRootFolder}/../../external/doclava/res/assets/templates-sdk"
110        addStringOption "stubpackages", "android.support.*"
111        addStringOption "samplesdir", "${supportRootFolder}/samples"
112        addOption federateOption
113        addOption federationapiOption
114        addOption hdfOption
115        addOption sinceOption
116
117        // Specific to reference docs.
118        addStringOption "toroot", "/"
119        addBooleanOption "devsite", true
120        addBooleanOption "androidSupportRef", true
121    }
122
123    exclude '**/BuildConfig.java'
124}
125
126// Generates a distribution artifact for online docs.
127task distDocs(type: Zip, dependsOn: generateDocs) {
128    group = JavaBasePlugin.DOCUMENTATION_GROUP
129    description = 'Generates distribution artifact for d.android.com-style documentation.'
130
131    from generateDocs.destinationDir
132    destinationDir project.distDir
133    baseName = "android-support-docs"
134    version = project.buildNumber
135
136    doLast {
137        logger.lifecycle("'Wrote API reference to ${archivePath}")
138    }
139}
140
141// Generates API files.
142task generateApi(type: DoclavaTask, dependsOn: configurations.doclava) {
143    docletpath = configurations.doclava.resolve()
144    destinationDir = project.docsDir
145
146    // Base classpath is Android SDK, sub-projects add their own.
147    classpath = project.ext.androidJar
148
149    apiFile = new File(project.docsDir, 'release/current.txt')
150    removedApiFile = new File(project.docsDir, 'release/removed.txt')
151    generateDocs = false
152
153    options {
154        addStringOption "templatedir",
155                "${supportRootFolder}/../../external/doclava/res/assets/templates-sdk"
156        addStringOption "stubpackages", "android.support.*"
157    }
158    exclude '**/BuildConfig.java'
159    exclude '**/R.java'
160}
161
162// Copies generated API files to current version.
163task updateApi(type: UpdateApiTask, dependsOn: generateApi) {
164    group JavaBasePlugin.VERIFICATION_GROUP
165    description 'Invoke Doclava\'s ApiCheck tool to update current.txt based on current changes.'
166    newApiFile = new File(project.docsDir, 'release/current.txt')
167    oldApiFile = new File(supportRootFolder, 'api/current.txt')
168    newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
169    oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt')
170}
171
172// Finalizes the API file for a release version.
173task finalizeApi(type: Copy, dependsOn: updateApi) {
174    group JavaBasePlugin.VERIFICATION_GROUP
175    description 'Finalize the API definition for the current release.'
176
177    def apiVersion = project.supportVersion;
178    if (project.hasProperty("revision")) {
179        apiVersion = revision;
180    }
181
182    File currentApiFile = new File(project.rootDir, 'api/current.txt')
183    File finalizedApiFile = new File(currentApiFile.parentFile, "${apiVersion}.txt")
184
185    from currentApiFile.absolutePath
186    into finalizedApiFile.parent
187    rename { finalizedApiFile.name }
188
189    doFirst {
190        // Verify this is a proper release version.
191        if (!(apiVersion ==~ /^\d+\.\d+\.\d+$/)) {
192            throw new InvalidUserDataException("${apiVersion} is not valid release version format. "
193                    + "Use -Prevision=X.Y.Z to specify an explicit version.")
194        }
195
196        // Verify that we're not accidentally overwriting an existing API file.
197        if (finalizedApiFile.exists() && !(project.hasProperty("overwrite") && overwrite)) {
198            throw new InvalidUserDataException("Version ${apiVersion} has already been "
199                    + "finalized. Use -Poverwrite=true to overwrite.")
200        }
201    }
202
203    doLast {
204        project.logger.warn("Wrote ${finalizedApiFile.getParentFile().name}/"
205                + "${finalizedApiFile.name} API file.")
206    }
207}
208
209// Checks generated API files against current version.
210task checkApi(type: CheckApiTask, dependsOn: generateApi) {
211    doclavaClasspath = generateApi.docletpath
212
213    checkApiTaskPath = name
214    updateApiTaskPath = updateApi.name
215
216    // Check that the API we're building hasn't changed from the development
217    // version. These typed of changes require an explicit API file update.
218    checkApiErrors = (2..30)-[22]
219    checkApiWarnings = []
220    checkApiHidden = [22]
221
222    newApiFile = new File(project.docsDir, 'release/current.txt')
223    oldApiFile = new File(supportRootFolder, 'api/current.txt')
224    newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
225    oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt')
226}
227
228rootProject.createArchive.dependsOn checkApi
229
230
231// Checks generated API files against current version.
232task checkApiStable(type: CheckApiTask, dependsOn: generateApi) {
233    doclavaClasspath = generateApi.docletpath
234
235    checkApiTaskPath = name
236    updateApiTaskPath = updateApi.name
237
238    // Check that the API we're building hasn't broken the last-released
239    // library version. These types of changes are forbidden.
240    checkApiErrors = (7..18)
241    checkApiWarnings = [23, 24]
242    checkApiHidden = (2..6) + (19..22) + (25..30)
243
244    newApiFile = new File(project.docsDir, 'release/current.txt')
245    oldApiFile = getReleasedApiFile()
246    newRemovedApiFile = new File(project.docsDir, 'release/removed.txt')
247    oldRemovedApiFile = new File(supportRootFolder, 'api/removed.txt')
248}
249
250checkApi.dependsOn checkApiStable
251
252
253/**
254 * Converts the <code>toApi</code>.txt file (or current.txt if not explicitly
255 * defined using -PtoAPi=<file>) to XML format for use by JDiff.
256 */
257task newApiXml(type: ApiXmlConversionTask, dependsOn: configurations.doclava) {
258    classpath configurations.doclava.resolve()
259
260    if (project.hasProperty("toApi")) {
261        // Use an explicit API file.
262        inputApiFile = new File(rootProject.ext.supportRootFolder, "api/${toApi}.txt")
263    } else {
264        // Use the current API file (e.g. current.txt).
265        inputApiFile = generateApi.apiFile
266        dependsOn generateApi
267    }
268
269    int lastDot = inputApiFile.name.lastIndexOf('.')
270    outputApiXmlFile = new File(project.docsDir,
271            "release/" + inputApiFile.name.substring(0, lastDot) + ".xml")
272}
273
274/**
275 * Converts the <code>fromApi</code>.txt file (or the most recently released
276 * X.Y.Z.txt if not explicitly defined using -PfromAPi=<file>) to XML format
277 * for use by JDiff.
278 */
279task oldApiXml(type: ApiXmlConversionTask, dependsOn: configurations.doclava) {
280    classpath configurations.doclava.resolve()
281
282    if (project.hasProperty("fromApi")) {
283        // Use an explicit API file.
284        inputApiFile = new File(rootProject.ext.supportRootFolder, "api/${fromApi}.txt")
285    } else if (project.hasProperty("toApi") && toApi.matches(~/(\d+\.){2}\d+/)) {
286        // If toApi matches released API (X.Y.Z) format, use the most recently
287        // released API file prior to toApi.
288        inputApiFile = getReleasedApiFile(toApi)
289    } else {
290        // Use the most recently released API file.
291        inputApiFile = getReleasedApiFile();
292    }
293
294    int lastDot = inputApiFile.name.lastIndexOf('.')
295    outputApiXmlFile = new File(project.docsDir,
296            "release/" + inputApiFile.name.substring(0, lastDot) + ".xml")
297}
298
299/**
300 * Generates API diffs.
301 * <p>
302 * By default, diffs are generated for the delta between current.txt and the
303 * next most recent X.Y.Z.txt API file. Behavior may be changed by specifying
304 * one or both of -PtoApi and -PfromApi.
305 * <p>
306 * If both fromApi and toApi are specified, diffs will be generated for
307 * fromApi -> toApi. For example, 25.0.0 -> 26.0.0 diffs could be generated by
308 * using:
309 * <br><code>
310 *   ./gradlew generateDiffs -PfromApi=25.0.0 -PtoApi=26.0.0
311 * </code>
312 * <p>
313 * If only toApi is specified, it MUST be specified as X.Y.Z and diffs will be
314 * generated for (release before toApi) -> toApi. For example, 24.2.0 -> 25.0.0
315 * diffs could be generated by using:
316 * <br><code>
317 *   ./gradlew generateDiffs -PtoApi=25.0.0
318 * </code>
319 * <p>
320 * If only fromApi is specified, diffs will be generated for fromApi -> current.
321 * For example, lastApiReview -> current diffs could be generated by using:
322 * <br><code>
323 *   ./gradlew generateDiffs -PfromApi=lastApiReview
324 * </code>
325 * <p>
326 */
327task generateDiffs(type: JDiffTask, dependsOn: [configurations.jdiff, configurations.doclava,
328                                                oldApiXml, newApiXml, generateDocs]) {
329    // Base classpath is Android SDK, sub-projects add their own.
330    classpath = project.ext.androidJar
331
332    // JDiff properties.
333    oldApiXmlFile = oldApiXml.outputApiXmlFile
334    newApiXmlFile = newApiXml.outputApiXmlFile
335    newJavadocPrefix = "../../../../reference/"
336
337    String newApi = newApiXmlFile.name
338    int lastDot = newApi.lastIndexOf('.')
339    newApi = newApi.substring(0, lastDot)
340
341    // Javadoc properties.
342    docletpath = configurations.jdiff.resolve()
343    destinationDir = new File(project.docsDir, "online/sdk/support_api_diff/$newApi")
344    title = "Support&nbsp;Library&nbsp;API&nbsp;Differences&nbsp;Report"
345
346    exclude '**/BuildConfig.java'
347    exclude '**/R.java'
348}
349
350/**
351 * Returns the most recently released API, optionally restricting to APIs
352 * before <code>beforeApi</code>.
353 *
354 * @param beforeApi the API to find an API file before, ex. 25.0.0
355 * @return the most recently released API file
356 */
357File getReleasedApiFile(String beforeApi = null) {
358    String beforeApiFileName = beforeApi != null ? beforeApi + ".txt" : null
359    File lastReleasedApiFile = null
360    File apiDir = new File(ext.supportRootFolder, 'api')
361
362    apiDir.eachFileMatch FileType.FILES, ~/(\d+\.){3}txt/, { File apiFile ->
363        // Is the current API file newer than the last one we saw?
364        if (lastReleasedApiFile == null || apiFile.name > lastReleasedApiFile.name) {
365            // Is the current API file older than the "before" API?
366            if (beforeApiFileName == null || apiFile.name < beforeApiFileName) {
367                lastReleasedApiFile = apiFile
368            }
369        }
370    }
371
372    return lastReleasedApiFile
373}
374
375// configuration file for setting up api diffs and api docs
376void registerForDocsTask(Task task, Project subProject, releaseVariant) {
377    task.dependsOn releaseVariant.javaCompile
378    task.source {
379        def buildConfig = fileTree(releaseVariant.getGenerateBuildConfig().sourceOutputDir)
380        return releaseVariant.javaCompile.source.minus(buildConfig) +
381                fileTree(releaseVariant.aidlCompile.sourceOutputDir) +
382                fileTree(releaseVariant.outputs[0].processResources.sourceOutputDir)
383    }
384    task.classpath += files(releaseVariant.javaCompile.classpath) +
385            files(releaseVariant.javaCompile.destinationDir)
386}
387
388// configuration file for setting up api diffs and api docs
389void registerJavaProjectForDocsTask(Task task, Project subProject, javaCompileTask) {
390    task.dependsOn javaCompileTask
391    task.source javaCompileTask.source
392    task.classpath += files(javaCompileTask.classpath) +
393            files(javaCompileTask.destinationDir)
394}
395
396subprojects { subProject ->
397    subProject.afterEvaluate { p ->
398        if (!p.hasProperty("noDocs") || !p.noDocs) {
399            if (p.hasProperty('android') && p.android.hasProperty('libraryVariants')) {
400                p.android.libraryVariants.all { v ->
401                    if (v.name == 'release') {
402                        registerForDocsTask(rootProject.generateDocs, p, v)
403                        registerForDocsTask(rootProject.generateApi, p, v)
404                        registerForDocsTask(rootProject.generateDiffs, p, v)
405                    }
406                }
407            } else if (p.hasProperty("compileJava")) {
408                registerJavaProjectForDocsTask(rootProject.generateDocs, p, p.compileJava)
409            }
410        }
411    }
412}
413