1 /*
<lambda>null2  * Copyright 2019 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 package androidx.build.metalava
18 
19 import androidx.build.Version
20 import androidx.build.checkapi.ApiLocation
21 import androidx.build.getLibraryByName
22 import androidx.build.logging.TERMINAL_RED
23 import androidx.build.logging.TERMINAL_RESET
24 import java.io.ByteArrayOutputStream
25 import java.io.File
26 import javax.inject.Inject
27 import org.gradle.api.Project
28 import org.gradle.api.file.FileCollection
29 import org.gradle.api.provider.ListProperty
30 import org.gradle.api.provider.Property
31 import org.gradle.api.provider.SetProperty
32 import org.gradle.process.ExecOperations
33 import org.gradle.workers.WorkAction
34 import org.gradle.workers.WorkParameters
35 import org.gradle.workers.WorkerExecutor
36 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
37 
38 // MetalavaRunner stores common configuration for executing Metalava
39 
40 fun runMetalavaWithArgs(
41     metalavaClasspath: FileCollection,
42     args: List<String>,
43     k2UastEnabled: Boolean,
44     kotlinSourceLevel: KotlinVersion,
45     workerExecutor: WorkerExecutor,
46 ) {
47     val allArgs =
48         args +
49             listOf(
50                 "--hide",
51                 // Removing final from a method does not cause compatibility issues for AndroidX.
52                 "RemovedFinalStrict",
53                 "--error",
54                 "UnresolvedImport",
55                 "--kotlin-source",
56                 kotlinSourceLevel.version,
57 
58                 // Metalava arguments to suppress compatibility checks for experimental API
59                 // surfaces.
60                 "--suppress-compatibility-meta-annotation",
61                 "androidx.annotation.RequiresOptIn",
62                 "--suppress-compatibility-meta-annotation",
63                 "kotlin.RequiresOptIn",
64 
65                 // Skip reading comments in Metalava for two reasons:
66                 // - We prefer for developers to specify api information via annotations instead
67                 //   of just javadoc comments (like @hide)
68                 // - This allows us to improve cacheability of Metalava tasks
69                 "--ignore-comments",
70                 "--hide",
71                 "DeprecationMismatch",
72                 "--hide",
73                 "DocumentExceptions",
74 
75                 // Don't track annotations that aren't needed for review or checking compat.
76                 "--exclude-annotation",
77                 "androidx.annotation.ReplaceWith",
78             )
79     val workQueue = workerExecutor.processIsolation()
80     workQueue.submit(MetalavaWorkAction::class.java) { parameters ->
81         parameters.args.set(allArgs)
82         parameters.metalavaClasspath.set(metalavaClasspath.files)
83         parameters.k2UastEnabled.set(k2UastEnabled)
84     }
85 }
86 
87 interface MetalavaParams : WorkParameters {
88     val args: ListProperty<String>
89     val metalavaClasspath: SetProperty<File>
90     val k2UastEnabled: Property<Boolean>
91 }
92 
93 abstract class MetalavaWorkAction @Inject constructor(private val execOperations: ExecOperations) :
94     WorkAction<MetalavaParams> {
executenull95     override fun execute() {
96         val outputStream = ByteArrayOutputStream()
97         var successful = false
98         val k2UastArg =
99             listOfNotNull(
100                 // Enable Android Lint infrastructure used by Metalava to use K2 UAST
101                 // (also historically known as FIR) when running Metalava for this module.
102                 "--Xuse-k2-uast".takeIf { parameters.k2UastEnabled.get() }
103             )
104         try {
105             execOperations.javaexec {
106                 // Intellij core reflects into java.util.ResourceBundle
107                 it.jvmArgs = listOf("--add-opens", "java.base/java.util=ALL-UNNAMED")
108                 it.systemProperty("java.awt.headless", "true")
109                 it.classpath(parameters.metalavaClasspath.get())
110                 it.mainClass.set("com.android.tools.metalava.Driver")
111                 it.args = parameters.args.get() + k2UastArg
112                 it.setStandardOutput(outputStream)
113                 it.setErrorOutput(outputStream)
114             }
115             successful = true
116         } finally {
117             if (!successful) {
118                 System.err.println(outputStream.toString(Charsets.UTF_8))
119             }
120         }
121     }
122 }
123 
Projectnull124 fun Project.getMetalavaClasspath(): FileCollection {
125     val configuration =
126         configurations.detachedConfiguration(dependencies.create(getLibraryByName("metalava")))
127     return project.files(configuration)
128 }
129 
getApiLintArgsnull130 fun getApiLintArgs(targetsJavaConsumers: Boolean): List<String> {
131     val args =
132         mutableListOf(
133             "--api-lint",
134             "--hide",
135             listOf(
136                     // The list of checks that are hidden as they are not useful in androidx
137                     "Enum", // Enums are allowed to be use in androidx
138                     "CallbackInterface", // With target Java 8, we have default methods
139                     "ProtectedMember", // We allow using protected members in androidx
140                     "ManagerLookup", // Managers in androidx are not the same as platform services
141                     "ManagerConstructor",
142                     "RethrowRemoteException", // This check is for calls into system_server
143                     "PackageLayering", // This check is not relevant to androidx.* code.
144                     "UserHandle", // This check is not relevant to androidx.* code.
145                     "ParcelableList", // This check is only relevant to android platform that has
146                     // managers.
147 
148                     // List of checks that have bugs, but should be enabled once fixed.
149                     "StaticUtils", // b/135489083
150                     "StartWithLower", // b/135710527
151 
152                     // The list of checks that are API lint warnings and are yet to be enabled
153                     "SamShouldBeLast",
154 
155                     // We should only treat these as warnings
156                     "IntentBuilderName",
157                     "OnNameExpected",
158                     "UserHandleName"
159                 )
160                 .joinToString(),
161             "--error",
162             listOf(
163                     "AllUpper",
164                     "GetterSetterNames",
165                     "MinMaxConstant",
166                     "TopLevelBuilder",
167                     "BuilderSetStyle",
168                     "MissingBuildMethod",
169                     "SetterReturnsThis",
170                     "OverlappingConstants",
171                     "ListenerLast",
172                     "ExecutorRegistration",
173                     "StreamFiles",
174                     "AbstractInner",
175                     "NotCloseable",
176                     "MethodNameTense",
177                     "UseIcu",
178                     "NoByteOrShort",
179                     "GetterOnBuilder",
180                     "CallbackMethodName",
181                     "StaticFinalBuilder",
182                     "MissingGetterMatchingBuilder",
183                     "HiddenSuperclass",
184                     "KotlinOperator",
185                     "DataClassDefinition",
186                 )
187                 .joinToString()
188         )
189     // Acronyms that can be used in their all-caps form. "SQ" is included to allow "SQLite".
190     val allowedAcronyms = listOf("SQL", "SQ", "URL", "EGL", "GL", "KHR")
191     for (acronym in allowedAcronyms) {
192         args.add("--api-lint-allowed-acronym")
193         args.add(acronym)
194     }
195     val javaOnlyIssues = listOf("MissingJvmstatic", "ArrayReturn", "ValueClassDefinition")
196     val javaOnlyErrorLevel =
197         if (targetsJavaConsumers) {
198             "--error"
199         } else {
200             "--hide"
201         }
202     args.add(javaOnlyErrorLevel)
203     args.add(javaOnlyIssues.joinToString())
204     return args
205 }
206 
207 /** Returns the args needed to generate a version history JSON from the previous API files. */
getGenerateApiLevelsArgsnull208 internal fun getGenerateApiLevelsArgs(
209     apiDir: File,
210     apiFiles: List<File>,
211     currentVersion: Version,
212     outputLocation: File
213 ): List<String> {
214     return buildList {
215         add("--generate-api-version-history")
216         add(outputLocation.absolutePath)
217         add("--current-version")
218         add(currentVersion.toString())
219         if (apiFiles.isNotEmpty()) {
220             add("--api-version-signature-files")
221             add(apiFiles.joinToString(":"))
222             add("--api-version-signature-pattern")
223             // Select the version from the files. The `*` wildcard matches and ignores any
224             // pre-release suffix.
225             add("$apiDir/{version:major.minor.patch}*.txt")
226         }
227     }
228 }
229 
230 sealed class GenerateApiMode {
231     object PublicApi : GenerateApiMode()
232 
233     object AllRestrictedApis : GenerateApiMode()
234 
235     object RestrictToLibraryGroupPrefixApis : GenerateApiMode()
236 }
237 
238 sealed class ApiLintMode {
239     class CheckBaseline(val apiLintBaseline: File, val targetsJavaConsumers: Boolean) :
240         ApiLintMode()
241 
242     object Skip : ApiLintMode()
243 }
244 
245 /**
246  * Generates all of the specified api files, as well as a version history JSON for the public API.
247  */
generateApinull248 internal fun generateApi(
249     metalavaClasspath: FileCollection,
250     projectXml: File,
251     sourcePaths: Collection<File>,
252     apiLocation: ApiLocation,
253     apiLintMode: ApiLintMode,
254     includeRestrictToLibraryGroupApis: Boolean,
255     apiLevelsArgs: List<String>,
256     k2UastEnabled: Boolean,
257     kotlinSourceLevel: KotlinVersion,
258     workerExecutor: WorkerExecutor,
259     pathToManifest: String? = null,
260 ) {
261     val generateApiConfigs: MutableList<Pair<GenerateApiMode, ApiLintMode>> =
262         mutableListOf(GenerateApiMode.PublicApi to apiLintMode)
263 
264     @Suppress("LiftReturnOrAssignment")
265     if (includeRestrictToLibraryGroupApis) {
266         generateApiConfigs += GenerateApiMode.AllRestrictedApis to ApiLintMode.Skip
267     } else {
268         generateApiConfigs += GenerateApiMode.RestrictToLibraryGroupPrefixApis to ApiLintMode.Skip
269     }
270 
271     generateApiConfigs.forEach { (generateApiMode, apiLintMode) ->
272         generateApi(
273             metalavaClasspath,
274             projectXml,
275             sourcePaths,
276             apiLocation,
277             generateApiMode,
278             apiLintMode,
279             apiLevelsArgs,
280             k2UastEnabled,
281             kotlinSourceLevel,
282             workerExecutor,
283             pathToManifest
284         )
285     }
286 }
287 
288 /**
289  * Gets arguments for generating the specified api file (and a version history JSON if the
290  * [generateApiMode] is [GenerateApiMode.PublicApi].
291  */
generateApinull292 private fun generateApi(
293     metalavaClasspath: FileCollection,
294     projectXml: File,
295     sourcePaths: Collection<File>,
296     outputLocation: ApiLocation,
297     generateApiMode: GenerateApiMode,
298     apiLintMode: ApiLintMode,
299     apiLevelsArgs: List<String>,
300     k2UastEnabled: Boolean,
301     kotlinSourceLevel: KotlinVersion,
302     workerExecutor: WorkerExecutor,
303     pathToManifest: String? = null
304 ) {
305     val args =
306         getGenerateApiArgs(
307             projectXml,
308             sourcePaths,
309             outputLocation,
310             generateApiMode,
311             apiLintMode,
312             apiLevelsArgs,
313             pathToManifest
314         )
315     runMetalavaWithArgs(metalavaClasspath, args, k2UastEnabled, kotlinSourceLevel, workerExecutor)
316 }
317 
318 /**
319  * Generates the specified api file, and a version history JSON if the [generateApiMode] is
320  * [GenerateApiMode.PublicApi].
321  */
getGenerateApiArgsnull322 fun getGenerateApiArgs(
323     projectXml: File,
324     sourcePaths: Collection<File>,
325     outputLocation: ApiLocation?,
326     generateApiMode: GenerateApiMode,
327     apiLintMode: ApiLintMode,
328     apiLevelsArgs: List<String>,
329     pathToManifest: String? = null
330 ): List<String> {
331     // generate public API txt
332     val args =
333         mutableListOf(
334             "--source-path",
335             sourcePaths.filter { it.exists() }.joinToString(File.pathSeparator),
336             "--project",
337             projectXml.path
338         )
339 
340     args += listOf("--format=v4", "--warnings-as-errors")
341 
342     pathToManifest?.let { args += listOf("--manifest", pathToManifest) }
343 
344     if (outputLocation != null) {
345         when (generateApiMode) {
346             is GenerateApiMode.PublicApi -> {
347                 args += listOf("--api", outputLocation.publicApiFile.toString())
348                 // Generate API levels just for the public API
349                 args += apiLevelsArgs
350             }
351             is GenerateApiMode.AllRestrictedApis,
352             GenerateApiMode.RestrictToLibraryGroupPrefixApis -> {
353                 args += listOf("--api", outputLocation.restrictedApiFile.toString())
354             }
355         }
356     }
357 
358     when (generateApiMode) {
359         is GenerateApiMode.PublicApi -> {
360             args += listOf("--hide-annotation", "androidx.annotation.RestrictTo")
361             args += listOf("--show-unannotated")
362         }
363         is GenerateApiMode.AllRestrictedApis,
364         GenerateApiMode.RestrictToLibraryGroupPrefixApis -> {
365             // Despite being hidden we still track the following:
366             // * @RestrictTo(Scope.LIBRARY_GROUP_PREFIX): inter-library APIs
367             // * @PublishedApi: needs binary stability for inline methods
368             // * @RestrictTo(Scope.LIBRARY_GROUP): APIs between libraries in non-atomic groups
369             args +=
370                 listOf(
371                     // hide RestrictTo(LIBRARY), use --show-annotation for RestrictTo with
372                     // specific arguments
373                     "--hide-annotation",
374                     "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)",
375                     "--show-annotation",
376                     "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
377                         "LIBRARY_GROUP_PREFIX)",
378                     "--show-annotation",
379                     "kotlin.PublishedApi",
380                     "--show-unannotated"
381                 )
382             if (generateApiMode is GenerateApiMode.AllRestrictedApis) {
383                 args +=
384                     listOf(
385                         "--show-annotation",
386                         "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
387                             "LIBRARY_GROUP)"
388                     )
389             } else {
390                 args +=
391                     listOf(
392                         "--hide-annotation",
393                         "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
394                             "LIBRARY_GROUP)"
395                     )
396             }
397         }
398     }
399 
400     when (apiLintMode) {
401         is ApiLintMode.CheckBaseline -> {
402             args += getApiLintArgs(apiLintMode.targetsJavaConsumers)
403             if (apiLintMode.apiLintBaseline.exists()) {
404                 args += listOf("--baseline", apiLintMode.apiLintBaseline.toString())
405             }
406             args.addAll(
407                 listOf(
408                     "--error",
409                     "ReferencesDeprecated",
410                     "--error-message:api-lint",
411                     """
412     ${TERMINAL_RED}Your change has API lint issues. Fix the code according to the messages above.$TERMINAL_RESET
413 
414     If a check is broken, suppress it in code in Kotlin with @Suppress("id")/@get:Suppress("id")
415     and in Java with @SuppressWarnings("id") and file bug to
416     https://issuetracker.google.com/issues/new?component=739152&template=1344623
417 
418     If you are doing a refactoring or suppression above does not work, use ./gradlew updateApiLintBaseline
419 """
420                 )
421             )
422         }
423         is ApiLintMode.Skip -> {
424             args.addAll(
425                 listOf(
426                     "--hide",
427                     "UnhiddenSystemApi",
428                     "--hide",
429                     "ReferencesHidden",
430                     "--hide",
431                     "ReferencesDeprecated"
432                 )
433             )
434         }
435     }
436 
437     return args
438 }
439