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