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.checkapi.SourceSetInputs
22 import androidx.build.checkapi.getApiFileVersion
23 import androidx.build.checkapi.getRequiredCompatibilityApiLocation
24 import androidx.build.checkapi.getVersionedApiLocation
25 import androidx.build.checkapi.isValidArtifactVersion
26 import androidx.build.getAndroidJar
27 import androidx.build.getCheckoutRoot
28 import java.io.File
29 import javax.inject.Inject
30 import org.gradle.api.DefaultTask
31 import org.gradle.api.Project
32 import org.gradle.api.file.FileCollection
33 import org.gradle.api.internal.artifacts.ivyservice.TypedResolveException
34 import org.gradle.api.provider.Property
35 import org.gradle.api.tasks.CacheableTask
36 import org.gradle.api.tasks.Input
37 import org.gradle.api.tasks.TaskAction
38 import org.gradle.api.tasks.options.Option
39 import org.gradle.api.tasks.util.PatternFilterable
40 import org.gradle.workers.WorkerExecutor
41 import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
42 
43 /** Generate API signature text files using previously built .jar/.aar artifacts. */
44 @CacheableTask
45 abstract class RegenerateOldApisTask
46 @Inject
47 constructor(private val workerExecutor: WorkerExecutor) : DefaultTask() {
48 
49     @Input var generateRestrictToLibraryGroupAPIs = true
50 
51     @get:Input abstract val kotlinSourceLevel: Property<KotlinVersion>
52 
53     @get:Input
54     @set:Option(
55         option = "compat-version",
56         description = "Regenerate just the signature file needed for compatibility checks"
57     )
58     var compatVersion: Boolean = false
59 
60     @TaskAction
61     fun exec() {
62         val groupId = project.group.toString()
63         val artifactId = project.name
64         val internalPrebuiltsDir = File(project.getCheckoutRoot(), "prebuilts/androidx/internal")
65         val projectPrebuiltsDir =
66             File(internalPrebuiltsDir, groupId.replace(".", "/") + "/" + artifactId)
67         if (compatVersion) {
68             regenerateCompatVersion(groupId, artifactId, projectPrebuiltsDir)
69         } else {
70             regenerateAllVersions(groupId, artifactId, projectPrebuiltsDir)
71         }
72     }
73 
74     /**
75      * Attempts to regenerate the API file for all previous versions by listing the prebuilt
76      * versions that exist and regenerating each one which already has an existing signature file.
77      */
78     private fun regenerateAllVersions(
79         groupId: String,
80         artifactId: String,
81         projectPrebuiltsDir: File,
82     ) {
83         val artifactVersions = listVersions(projectPrebuiltsDir)
84 
85         var prevApiFileVersion = getApiFileVersion(project.version as Version)
86         for (artifactVersion in artifactVersions.reversed()) {
87             val apiFileVersion = getApiFileVersion(artifactVersion)
88             // If two artifacts correspond to the same API file, don't regenerate the
89             // same api file again
90             if (apiFileVersion != prevApiFileVersion) {
91                 val location = project.getVersionedApiLocation(apiFileVersion)
92                 regenerate(project.rootProject, groupId, artifactId, artifactVersion, location)
93                 prevApiFileVersion = apiFileVersion
94             }
95         }
96     }
97 
98     /**
99      * Regenerates just the signature file used for compatibility checks against the current
100      * version. If prebuilts for that version don't exist (since prebuilts for betas are sometimes
101      * deleted), attempts to use prebuilts for the corresponding stable version, which should have
102      * the same API surface.
103      */
104     private fun regenerateCompatVersion(
105         groupId: String,
106         artifactId: String,
107         projectPrebuiltsDir: File,
108     ) {
109         val location =
110             project.getRequiredCompatibilityApiLocation()
111                 ?: run {
112                     logger.warn("No required compat location for $groupId:$artifactId")
113                     return
114                 }
115         val compatVersion = location.version()!!
116 
117         if (!tryRegenerate(projectPrebuiltsDir, groupId, artifactId, compatVersion, location)) {
118             val stable = compatVersion.copy(extra = null)
119             logger.warn("No prebuilts for version $compatVersion, trying with $stable")
120             if (!tryRegenerate(projectPrebuiltsDir, groupId, artifactId, stable, location)) {
121                 logger.error("Could not regenerate $compatVersion")
122             }
123         }
124     }
125 
126     /**
127      * If prebuilts exists for the [version], runs [regenerate] and returns true, otherwise returns
128      * false.
129      */
130     private fun tryRegenerate(
131         projectPrebuiltsDir: File,
132         groupId: String,
133         artifactId: String,
134         version: Version,
135         location: ApiLocation,
136     ): Boolean {
137         if (File(projectPrebuiltsDir, version.toString()).exists()) {
138             regenerate(project.rootProject, groupId, artifactId, version, location)
139             return true
140         }
141         return false
142     }
143 
144     // Returns all (valid) artifact versions that appear to exist in <dir>
145     private fun listVersions(dir: File): List<Version> {
146         val pathNames: Array<String> = dir.list() ?: arrayOf()
147         val files = pathNames.map { name -> File(dir, name) }
148         val subdirs = files.filter { child -> child.isDirectory() }
149         val versions = subdirs.map { child -> Version(child.name) }
150         val validVersions = versions.filter { v -> isValidArtifactVersion(v) }
151         return validVersions.sorted()
152     }
153 
154     private fun regenerate(
155         runnerProject: Project,
156         groupId: String,
157         artifactId: String,
158         version: Version,
159         outputApiLocation: ApiLocation,
160     ) {
161         val mavenId = "$groupId:$artifactId:$version"
162         val (compiledSources, sourceSets) =
163             try {
164                 getFiles(runnerProject, mavenId)
165             } catch (e: TypedResolveException) {
166                 runnerProject.logger.info("Ignoring missing artifact $mavenId: $e")
167                 return
168             }
169 
170         if (outputApiLocation.publicApiFile.exists()) {
171             project.logger.lifecycle("Regenerating $mavenId")
172             val projectXml = File(temporaryDir, "$mavenId-project.xml")
173             ProjectXml.create(
174                 sourceSets,
175                 project.getAndroidJar().files,
176                 compiledSources,
177                 projectXml
178             )
179             generateApi(
180                 project.getMetalavaClasspath(),
181                 projectXml,
182                 sourceSets.flatMap { it.sourcePaths.files },
183                 outputApiLocation,
184                 ApiLintMode.Skip,
185                 generateRestrictToLibraryGroupAPIs,
186                 emptyList(),
187                 false,
188                 kotlinSourceLevel.get(),
189                 workerExecutor
190             )
191         } else {
192             logger.warn("No API file for $mavenId")
193         }
194     }
195 
196     /**
197      * For the given [mavenId], returns a pair with the source jar as the first element, and
198      * [SourceSetInputs] representing the unzipped sources as the second element.
199      */
200     private fun getFiles(
201         runnerProject: Project,
202         mavenId: String
203     ): Pair<File, List<SourceSetInputs>> {
204         val jars = getJars(runnerProject, mavenId)
205         val sourcesMavenId = "$mavenId:sources"
206         val compiledSources = getCompiledSources(runnerProject, sourcesMavenId)
207         val sources = getSources(runnerProject, sourcesMavenId, compiledSources)
208 
209         // TODO(b/330721660) parse META-INF/kotlin-project-structure-metadata.json for KMP projects
210         // Represent the project as a single source set.
211         return compiledSources to
212             listOf(
213                 SourceSetInputs(
214                     // Since there's just one source set, the name is arbitrary.
215                     sourceSetName = "main",
216                     // There are no other source sets to depend on.
217                     dependsOnSourceSets = emptyList(),
218                     sourcePaths = sources,
219                     dependencyClasspath = jars,
220                 )
221             )
222     }
223 
224     private fun getJars(runnerProject: Project, mavenId: String): FileCollection {
225         val configuration =
226             runnerProject.configurations.detachedConfiguration(
227                 runnerProject.dependencies.create(mavenId)
228             )
229         val resolvedConfiguration = configuration.resolvedConfiguration.resolvedArtifacts
230         val dependencyFiles = resolvedConfiguration.map { artifact -> artifact.file }
231 
232         val jars = dependencyFiles.filter { file -> file.name.endsWith(".jar") }
233         val aars = dependencyFiles.filter { file -> file.name.endsWith(".aar") }
234         val classesJars =
235             aars.map { aar ->
236                 val tree = project.zipTree(aar)
237                 val classesJar =
238                     tree
239                         .matching { filter: PatternFilterable -> filter.include("classes.jar") }
240                         .single()
241                 classesJar
242             }
243         val embeddedLibs = getEmbeddedLibs(runnerProject, mavenId)
244         val undeclaredJarDeps = getUndeclaredJarDeps(runnerProject, mavenId)
245         return runnerProject.files(jars + classesJars + embeddedLibs + undeclaredJarDeps)
246     }
247 
248     private fun getUndeclaredJarDeps(runnerProject: Project, mavenId: String): FileCollection {
249         if (mavenId.startsWith("androidx.wear:wear:")) {
250             return runnerProject.files("wear/wear_stubs/com.google.android.wearable-stubs.jar")
251         }
252         return runnerProject.files()
253     }
254 
255     /** Returns the source jar for the [mavenId]. */
256     private fun getCompiledSources(runnerProject: Project, mavenId: String): File {
257         val configuration =
258             runnerProject.configurations.detachedConfiguration(
259                 runnerProject.dependencies.create(mavenId)
260             )
261         configuration.isTransitive = false
262         return configuration.singleFile
263     }
264 
265     /** Returns a file collection containing the unzipped sources from [compiledSources]. */
266     private fun getSources(
267         runnerProject: Project,
268         mavenId: String,
269         compiledSources: File
270     ): FileCollection {
271         val sanitizedMavenId = mavenId.replace(":", "-")
272         @Suppress("DEPRECATION")
273         val unzippedDir = File("${runnerProject.buildDir.path}/sources-unzipped/$sanitizedMavenId")
274         runnerProject.copy { copySpec ->
275             copySpec.from(runnerProject.zipTree(compiledSources))
276             copySpec.into(unzippedDir)
277         }
278         return project.files(unzippedDir)
279     }
280 
281     private fun getEmbeddedLibs(runnerProject: Project, mavenId: String): Collection<File> {
282         val configuration =
283             runnerProject.configurations.detachedConfiguration(
284                 runnerProject.dependencies.create(mavenId)
285             )
286         configuration.isTransitive = false
287 
288         val sanitizedMavenId = mavenId.replace(":", "-")
289         @Suppress("DEPRECATION")
290         val unzippedDir = File("${runnerProject.buildDir.path}/aars-unzipped/$sanitizedMavenId")
291         runnerProject.copy { copySpec ->
292             copySpec.from(runnerProject.zipTree(configuration.singleFile))
293             copySpec.into(unzippedDir)
294         }
295         val libsDir = File(unzippedDir, "libs")
296         if (libsDir.exists()) {
297             return libsDir.listFiles()?.toList() ?: listOf()
298         }
299 
300         return listOf()
301     }
302 }
303