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