1 /*
2  * 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.ApiBaselinesLocation
21 import androidx.build.checkapi.ApiLocation
22 import java.io.File
23 import javax.inject.Inject
24 import org.gradle.api.file.FileCollection
25 import org.gradle.api.provider.Property
26 import org.gradle.api.tasks.CacheableTask
27 import org.gradle.api.tasks.Input
28 import org.gradle.api.tasks.InputFiles
29 import org.gradle.api.tasks.OutputFile
30 import org.gradle.api.tasks.OutputFiles
31 import org.gradle.api.tasks.PathSensitive
32 import org.gradle.api.tasks.PathSensitivity
33 import org.gradle.api.tasks.TaskAction
34 import org.gradle.workers.WorkerExecutor
35 
36 @CacheableTask
37 internal abstract class UpdateApiLintBaselineTask
38 @Inject
39 constructor(workerExecutor: WorkerExecutor) : SourceMetalavaTask(workerExecutor) {
40     init {
41         group = "API"
42         description =
43             "Updates an API lint baseline file (api/api_lint.ignore) to match the " +
44                 "current set of violations. Only use a baseline " +
45                 "if you are in a library without Android dependencies, or when enabling a new " +
46                 "lint check, and it is prohibitively expensive / not possible to fix the errors " +
47                 "generated by enabling this lint check. "
48     }
49 
getOutputApiLintBaselinenull50     @OutputFile fun getOutputApiLintBaseline(): File = baselines.get().apiLintFile
51 
52     @TaskAction
53     fun updateBaseline() {
54         check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." }
55         val baselineFile = baselines.get().apiLintFile
56         val checkArgs =
57             getGenerateApiArgs(
58                 createProjectXmlFile(),
59                 sourcePaths.files.filter { it.exists() },
60                 null,
61                 GenerateApiMode.PublicApi,
62                 ApiLintMode.CheckBaseline(baselineFile, targetsJavaConsumers.get()),
63                 // API version history doesn't need to be generated
64                 emptyList(),
65                 manifestPath.orNull?.asFile?.absolutePath
66             )
67         val args = checkArgs + getCommonBaselineUpdateArgs(baselineFile)
68 
69         runWithArgs(args)
70     }
71 }
72 
73 @CacheableTask
74 abstract class IgnoreApiChangesTask @Inject constructor(workerExecutor: WorkerExecutor) :
75     MetalavaTask(workerExecutor) {
<lambda>null76     init {
77         description =
78             "Updates an API tracking baseline file (api/X.Y.Z.ignore) to match the " +
79                 "current set of violations"
80     }
81 
82     // The API that the library is supposed to be compatible with
83     @get:Input abstract val referenceApi: Property<ApiLocation>
84 
85     @get:Input abstract val api: Property<ApiLocation>
86 
87     // The baseline files (api/*.*.*.ignore) to update
88     @get:Input abstract val baselines: Property<ApiBaselinesLocation>
89 
90     // Version for the current API surface.
91     @get:Input abstract val version: Property<Version>
92 
93     @[InputFiles PathSensitive(PathSensitivity.RELATIVE)]
getTaskInputsnull94     fun getTaskInputs(): List<File> {
95         val referenceApiLocation = referenceApi.get()
96         return listOf(referenceApiLocation.publicApiFile, referenceApiLocation.restrictedApiFile)
97     }
98 
99     // Declaring outputs prevents Gradle from rerunning this task if the inputs haven't changed
100     @OutputFiles
getTaskOutputsnull101     fun getTaskOutputs(): List<File>? {
102         val apiBaselinesLocation = baselines.get()
103         return listOf(apiBaselinesLocation.publicApiFile, apiBaselinesLocation.restrictedApiFile)
104     }
105 
106     @TaskAction
execnull107     fun exec() {
108         check(bootClasspath.files.isNotEmpty()) { "Android boot classpath not set." }
109 
110         val apiLocation = api.get()
111         val referenceApiLocation = referenceApi.get()
112         val freezeApis = shouldFreezeApis(referenceApiLocation.version(), version.get())
113         updateBaseline(
114             apiLocation.publicApiFile,
115             referenceApiLocation.publicApiFile,
116             baselines.get().publicApiFile,
117             false,
118             freezeApis
119         )
120         if (referenceApiLocation.restrictedApiFile.exists()) {
121             updateBaseline(
122                 apiLocation.restrictedApiFile,
123                 referenceApiLocation.restrictedApiFile,
124                 baselines.get().restrictedApiFile,
125                 true,
126                 freezeApis
127             )
128         }
129     }
130 
131     // Updates the contents of baselineFile to specify an exception for every API present in apiFile
132     // but not
133     // present in the current source path
updateBaselinenull134     private fun updateBaseline(
135         api: File,
136         prevApi: File,
137         baselineFile: File,
138         processRestrictedApis: Boolean,
139         freezeApis: Boolean,
140     ) {
141         val args = getCommonBaselineUpdateArgs(bootClasspath, dependencyClasspath, baselineFile)
142         args +=
143             listOf(
144                 "--baseline",
145                 baselineFile.toString(),
146                 "--check-compatibility:api:released",
147                 prevApi.toString(),
148                 "--source-files",
149                 api.toString()
150             )
151         if (freezeApis) {
152             args += listOf("--error-category", "Compatibility")
153         }
154         if (processRestrictedApis) {
155             args +=
156                 listOf(
157                     "--show-annotation",
158                     "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
159                         "LIBRARY_GROUP)",
160                     "--show-annotation",
161                     "androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
162                         "LIBRARY_GROUP_PREFIX)",
163                     "--show-unannotated"
164                 )
165         }
166         runWithArgs(args)
167     }
168 }
169 
getCommonBaselineUpdateArgsnull170 private fun getCommonBaselineUpdateArgs(
171     bootClasspath: FileCollection,
172     dependencyClasspath: FileCollection,
173     baselineFile: File
174 ): MutableList<String> {
175     val args =
176         mutableListOf(
177             "--classpath",
178             (bootClasspath.files + dependencyClasspath.files).joinToString(File.pathSeparator)
179         )
180     args += getCommonBaselineUpdateArgs(baselineFile)
181     return args
182 }
183 
getCommonBaselineUpdateArgsnull184 private fun getCommonBaselineUpdateArgs(baselineFile: File): List<String> {
185     // Create the baseline file if it does exist, as Metalava cannot handle non-existent files.
186     baselineFile.createNewFile()
187     return mutableListOf(
188         "--update-baseline",
189         baselineFile.toString(),
190         "--pass-baseline-updates",
191         "--delete-empty-baselines",
192         "--format=v4"
193     )
194 }
195