From d811f02f2b751757579f671250e14436a4f4b6d3 Mon Sep 17 00:00:00 2001 From: Jeff Gaston Date: Thu, 13 Jun 2019 18:22:36 -0400 Subject: [PATCH] Task to regenerate previous api files There are still some things left to do to make everything be correct, but this seems to mostly work anyway Bug: 134590344 Test: ./gradlew regenerateOldApis -Pforce # and visually inspect the differences to see that the latest metalava was used to generate a bunch of previous api txt files Test: ./gradlew checkApi Test: echo > appcompat/api/api_lint.ignore && ./gradlew :appcompat:checkApi # and see that it fails Change-Id: I0dac0125b17ff465dcb74bef52d448620c1332c5 --- .../kotlin/androidx/build/AndroidXPlugin.kt | 6 +- .../androidx/build/checkapi/CheckApi.kt | 20 ++- .../androidx/build/java/JavaCompileInputs.kt | 8 + .../build/metalava/GenerateApiTask.kt | 22 +-- .../androidx/build/metalava/MetalavaRunner.kt | 64 +++++-- .../androidx/build/metalava/MetalavaTasks.kt | 16 +- .../build/metalava/RegenerateOldApisTask.kt | 165 ++++++++++++++++++ 7 files changed, 263 insertions(+), 38 deletions(-) create mode 100644 buildSrc/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt diff --git a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt index 7a7d6b8afae36..2595865cfe7a7 100644 --- a/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt +++ b/buildSrc/src/main/kotlin/androidx/build/AndroidXPlugin.kt @@ -25,7 +25,7 @@ import androidx.build.SupportConfig.DEFAULT_MIN_SDK_VERSION import androidx.build.SupportConfig.INSTRUMENTATION_RUNNER import androidx.build.SupportConfig.TARGET_SDK_VERSION import androidx.build.checkapi.ApiType -import androidx.build.checkapi.getCurrentApiLocation +import androidx.build.checkapi.getApiLocation import androidx.build.checkapi.getRequiredCompatibilityApiFileFromDir import androidx.build.checkapi.hasApiFolder import androidx.build.dependencyTracker.AffectedModuleDetector @@ -668,7 +668,7 @@ private fun Project.createCheckResourceApiTask(): DefaultTask { return tasks.createWithConfig("checkResourceApi", CheckResourceApiTask::class.java) { newApiFile = getGenerateResourceApiFile() - oldApiFile = getCurrentApiLocation().resourceFile + oldApiFile = getApiLocation().resourceFile } } @@ -685,7 +685,7 @@ private fun Project.createUpdateResourceApiTask(): DefaultTask { newApiFile = getGenerateResourceApiFile() oldApiFile = getRequiredCompatibilityApiFileFromDir(File(projectDir, "api/"), version(), ApiType.RESOURCEAPI) - destApiFile = getCurrentApiLocation().resourceFile + destApiFile = getApiLocation().resourceFile } } diff --git a/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt b/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt index c75073d74a1f3..f08bd62880168 100644 --- a/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt +++ b/buildSrc/src/main/kotlin/androidx/build/checkapi/CheckApi.kt @@ -65,9 +65,11 @@ fun hasApiTasks(project: Project, extension: AndroidXExtension): Boolean { fun Project.getCurrentApiFile() = getApiFile(project.projectDir, project.version()) /** - * Same as getCurrentApiFile but also contains a restricted API file too + * Returns an ApiLocation with the given version */ -fun Project.getCurrentApiLocation() = ApiLocation.fromPublicApiFile(project.getCurrentApiFile()) +fun Project.getApiLocation(version: Version = project.version()): ApiLocation { + return ApiLocation.fromPublicApiFile(getApiFile(project.projectDir, version)) +} /** * Returns the API file containing the public API that this library promises to support @@ -95,8 +97,8 @@ fun Project.getRequiredCompatibilityApiLocation(): ApiLocation? { * @param version the API version, ex. 25.0.0-SNAPSHOT * @return the API file of this version */ -private fun getApiFile(rootDir: File, version: Version): File { - if (version.patch != 0 && (version.isAlpha() || version.isBeta())) { +fun getApiFile(rootDir: File, version: Version): File { + if (!isValidApiVersion(version)) { val suggestedVersion = Version("${version.major}.${version.minor}.${version.patch}-rc01") throw GradleException("Illegal version $version . It is not allowed to have a nonzero " + "patch number and be alpha or beta at the same time.\n" + @@ -111,6 +113,16 @@ private fun getApiFile(rootDir: File, version: Version): File { return File(apiDir, "${version.major}.${version.minor}.0$extra.txt") } +/** + * Whether it is allowed to generate an API of the given version + */ +fun isValidApiVersion(version: Version): Boolean { + if (version.patch != 0 && (version.isAlpha() || version.isBeta())) { + return false + } + return true +} + /** * Returns the api file that version is required to be compatible with. * If apiType is RESOURCEAPI, it will return the resource api file and if it is CLASSAPI, it will diff --git a/buildSrc/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt b/buildSrc/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt index ce3eb8db5fc15..236efc53dae5a 100644 --- a/buildSrc/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt +++ b/buildSrc/src/main/kotlin/androidx/build/java/JavaCompileInputs.kt @@ -57,6 +57,14 @@ data class JavaCompileInputs( fun fromSourceSet(sourceSet: SourceSet, project: Project): JavaCompileInputs { val sourcePaths: Collection = sourceSet.allSource.srcDirs val dependencyClasspath = sourceSet.compileClasspath + return fromSourcesAndDeps(sourcePaths, dependencyClasspath, project) + } + + fun fromSourcesAndDeps( + sourcePaths: Collection, + dependencyClasspath: FileCollection, + project: Project + ): JavaCompileInputs { val bootClasspath: Collection = androidJarFile(project).files return JavaCompileInputs(sourcePaths, dependencyClasspath, bootClasspath) } diff --git a/buildSrc/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt b/buildSrc/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt index 39598e3195a6b..6eb12cb85ef71 100644 --- a/buildSrc/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt +++ b/buildSrc/src/main/kotlin/androidx/build/metalava/GenerateApiTask.kt @@ -18,6 +18,7 @@ package androidx.build.metalava import androidx.build.checkapi.ApiLocation import androidx.build.checkapi.ApiViolationBaselines +import androidx.build.java.JavaCompileInputs import org.gradle.api.provider.Property import org.gradle.api.tasks.Input import org.gradle.api.tasks.InputFile @@ -60,22 +61,9 @@ abstract class GenerateApiTask : MetalavaTask() { check(bootClasspath.isNotEmpty()) { "Android boot classpath not set." } check(sourcePaths.isNotEmpty()) { "Source paths not set." } - project.generateApi( - bootClasspath, - dependencyClasspath, - sourcePaths, - apiLocation.get().publicApiFile, - GenerateApiMode.PublicApi(baselines.get().apiLintFile) - ) - - if (generateRestrictedAPIs) { - project.generateApi( - bootClasspath, - dependencyClasspath, - sourcePaths, - apiLocation.get().restrictedApiFile, - GenerateApiMode.RestrictedApi - ) - } + val inputs = JavaCompileInputs.fromSourcesAndDeps(sourcePaths, dependencyClasspath, + project) + project.generateApi(inputs, apiLocation.get(), apiLocation.get().publicApiFile.parentFile, + ApiLintMode.CheckBaseline(baselines.get().apiLintFile), generateRestrictedAPIs) } } diff --git a/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt b/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt index ab1575b7d66d8..931087364b2b5 100644 --- a/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt +++ b/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaRunner.kt @@ -16,6 +16,8 @@ package androidx.build.metalava +import androidx.build.checkapi.ApiLocation +import androidx.build.java.JavaCompileInputs import org.gradle.api.GradleException import org.gradle.api.Project import org.gradle.api.artifacts.Configuration @@ -36,9 +38,7 @@ fun Project.runMetalavaWithArgs(configuration: Configuration, args: List it.classpath = checkNotNull(configuration) { "Configuration not set." } it.main = "com.android.tools.metalava.Driver" it.args = listOf( - "--no-banner", - "--error", - "DeprecationMismatch" // Enforce deprecation mismatch + "--no-banner" ) + args } } @@ -91,19 +91,43 @@ val API_LINT_ARGS: List = listOf( ) sealed class GenerateApiMode { - class PublicApi(val apiLintBaseline: File) : GenerateApiMode() + object PublicApi : GenerateApiMode() object RestrictedApi : GenerateApiMode() } +sealed class ApiLintMode { + class CheckBaseline(val apiLintBaseline: File) : ApiLintMode() + object Skip : ApiLintMode() +} + +// Generates all of the specified api files +fun Project.generateApi( + files: JavaCompileInputs, + apiLocation: ApiLocation, + tempDir: File, + apiLintMode: ApiLintMode, + includeRestrictedApis: Boolean +) { + generateApi(files.bootClasspath, files.dependencyClasspath, files.sourcePaths, + apiLocation.publicApiFile, tempDir, GenerateApiMode.PublicApi, apiLintMode) + if (includeRestrictedApis) { + generateApi(files.bootClasspath, files.dependencyClasspath, files.sourcePaths, + apiLocation.restrictedApiFile, tempDir, GenerateApiMode.RestrictedApi, ApiLintMode.Skip) + } +} + +// Generates the specified api file fun Project.generateApi( bootClasspath: Collection, dependencyClasspath: FileCollection, sourcePaths: Collection, outputFile: File, - generateApiMode: GenerateApiMode + tempDir: File, + generateApiMode: GenerateApiMode, + apiLintMode: ApiLintMode ) { val tempOutputFile = if (generateApiMode is GenerateApiMode.RestrictedApi) { - File(outputFile.path + ".tmp") + File(tempDir, outputFile.name + ".tmp") } else { outputFile } @@ -124,18 +148,36 @@ fun Project.generateApi( when (generateApiMode) { is GenerateApiMode.PublicApi -> { - args += API_LINT_ARGS - if (generateApiMode.apiLintBaseline.exists()) { - args += listOf("--baseline", generateApiMode.apiLintBaseline.toString()) - } } is GenerateApiMode.RestrictedApi -> { args += listOf("--show-annotation", "androidx.annotation.RestrictTo") } } - val metalavaConfiguration = getMetalavaConfiguration() + when (apiLintMode) { + is ApiLintMode.CheckBaseline -> { + args += API_LINT_ARGS + if (apiLintMode.apiLintBaseline.exists()) { + args += listOf("--baseline", apiLintMode.apiLintBaseline.toString()) + } + args.addAll(listOf( + "--error", + "DeprecationMismatch" // Enforce deprecation mismatch + )) + } + is ApiLintMode.Skip -> { + args.addAll(listOf( + "--hide", + "DeprecationMismatch", + "--hide", + "UnhiddenSystemApi", + "--hide", + "ReferencesHidden" + )) + } + } + val metalavaConfiguration = getMetalavaConfiguration() runMetalavaWithArgs(metalavaConfiguration, args) if (generateApiMode is GenerateApiMode.RestrictedApi) { diff --git a/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt b/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt index 534bc882e9fe4..ef0da201ef730 100644 --- a/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt +++ b/buildSrc/src/main/kotlin/androidx/build/metalava/MetalavaTasks.kt @@ -20,7 +20,7 @@ import androidx.build.AndroidXPlugin.Companion.BUILD_ON_SERVER_TASK import androidx.build.AndroidXExtension import androidx.build.checkapi.ApiLocation import androidx.build.checkapi.ApiViolationBaselines -import androidx.build.checkapi.getCurrentApiLocation +import androidx.build.checkapi.getApiLocation import androidx.build.checkapi.getRequiredCompatibilityApiLocation import androidx.build.checkapi.hasApiFolder import androidx.build.checkapi.hasApiTasks @@ -96,7 +96,7 @@ object MetalavaTasks { val metalavaConfiguration = project.getMetalavaConfiguration() // the api files whose file names contain the version of the library - val libraryVersionApi = project.getCurrentApiLocation() + val libraryVersionApi = project.getApiLocation() // the api files whose file names contain "current.txt" val currentTxtApi = ApiLocation.fromPublicApiFile(File( libraryVersionApi.publicApiFile.parentFile, "current.txt")) @@ -173,7 +173,7 @@ object MetalavaTasks { } } - project.tasks.register("updateApi", UpdateApiTask::class.java) { task -> + val updateApi = project.tasks.register("updateApi", UpdateApiTask::class.java) { task -> task.group = "API" task.description = "Updates the checked in API files to match source code API" task.inputApiLocation.set(generateApi.flatMap { it.apiLocation }) @@ -182,6 +182,16 @@ object MetalavaTasks { task.dependsOn(generateApi) } + project.tasks.register("regenerateOldApis", RegenerateOldApisTask::class.java) { task -> + task.group = "API" + task.description = "Regenerates current and historic API .txt files using the " + + "corresponding prebuilt and the latest Metalava" + // Technically this doesn't need updateApi to happen first, but adding this dependency + // is a convenient way to make updateApi also happen when the user runs + // `./gradlew regenerateOldApis` + task.dependsOn(updateApi) + } + project.tasks.named("check").configure { it.dependsOn(checkApi) } diff --git a/buildSrc/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt b/buildSrc/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt new file mode 100644 index 0000000000000..118e3ea5f1213 --- /dev/null +++ b/buildSrc/src/main/kotlin/androidx/build/metalava/RegenerateOldApisTask.kt @@ -0,0 +1,165 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.build.metalava + +import androidx.build.SupportConfig +import androidx.build.Version +import androidx.build.docsDir +import androidx.build.checkapi.getApiLocation +import androidx.build.checkapi.isValidApiVersion +import androidx.build.java.JavaCompileInputs +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.FileCollection +import org.gradle.api.internal.artifacts.ivyservice.DefaultLenientConfiguration +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.util.PatternFilterable +import java.io.File + +/** Generate API signature text files using previously built .jar/.aar artifacts. */ +abstract class RegenerateOldApisTask : DefaultTask() { + var generateRestrictedAPIs = false + + @TaskAction + fun exec() { + val groupId = project.group.toString() + val artifactId = project.name + val internalPrebuiltsDir = + File(SupportConfig.getSupportRoot(project), "../../prebuilts/androidx/internal") + val projectPrebuiltsDir = + File(internalPrebuiltsDir, groupId.replace(".", "/") + "/" + artifactId) + + val versions = listVersions(projectPrebuiltsDir) + + for (version in versions) { + if (version != project.version) { + regenerate(project.rootProject, groupId, artifactId, version) + } + } + } + + // Returns the artifact versions that appear to exist in + fun listVersions(dir: File): List { + val pathNames: Array = dir.list() ?: arrayOf() + val files = pathNames.map({ name -> File(dir, name) }) + val subdirs = files.filter({ child -> child.isDirectory() }) + val versions = subdirs.map({ child -> child.name }) + return versions.sorted() + } + + fun regenerate( + runnerProject: Project, + groupId: String, + artifactId: String, + versionString: String + ) { + val mavenId = "$groupId:$artifactId:$versionString" + val version = Version(versionString) + if (!isValidApiVersion(version)) { + runnerProject.logger.info("Skipping illegal version $version from $mavenId") + return + } + project.logger.lifecycle("Regenerating $mavenId") + val inputs: JavaCompileInputs? + try { + inputs = getFiles(runnerProject, mavenId) + } catch (e: DefaultLenientConfiguration.ArtifactResolveException) { + runnerProject.logger.info("Ignoring missing artifact $mavenId: $e") + return + } + + val outputApiLocation = project.getApiLocation(version) + val tempDir = File(project.docsDir(), "release/${project.name}") + if (outputApiLocation.publicApiFile.exists()) { + val generateRestrictedAPIs = outputApiLocation.restrictedApiFile.exists() + project.generateApi( + inputs, outputApiLocation, tempDir, ApiLintMode.Skip, generateRestrictedAPIs) + } + } + + fun getFiles(runnerProject: Project, mavenId: String): JavaCompileInputs { + val jars = getJars(runnerProject, mavenId) + val sources = getSources(runnerProject, mavenId + ":sources") + + return JavaCompileInputs.fromSourcesAndDeps(sources, jars, runnerProject) + } + + fun getJars(runnerProject: Project, mavenId: String): FileCollection { + val configuration = runnerProject.configurations.detachedConfiguration( + runnerProject.dependencies.create("$mavenId") + ) + val resolvedConfiguration = configuration.resolvedConfiguration.resolvedArtifacts + val dependencyFiles = resolvedConfiguration.map({ artifact -> + artifact.file + }) + + val jars = dependencyFiles.filter({ file -> file.name.endsWith(".jar") }) + val aars = dependencyFiles.filter({ file -> file.name.endsWith(".aar") }) + val classesJars = aars.map({ aar -> + val tree = project.zipTree(aar) + val classesJar = tree.matching { filter: PatternFilterable -> + filter.include("classes.jar") + }.single() + classesJar + }) + val embeddedLibs = getEmbeddedLibs(runnerProject, mavenId) + val undeclaredJarDeps = getUndeclaredJarDeps(runnerProject, mavenId) + return runnerProject.files(jars + classesJars + embeddedLibs + undeclaredJarDeps) + } + + fun getUndeclaredJarDeps(runnerProject: Project, mavenId: String): FileCollection { + if (mavenId.startsWith("androidx.wear:wear:")) { + return runnerProject.files("wear/wear_stubs/com.google.android.wearable-stubs.jar") + } + return runnerProject.files() + } + + fun getSources(runnerProject: Project, mavenId: String): Collection { + val configuration = runnerProject.configurations.detachedConfiguration( + runnerProject.dependencies.create(mavenId) + ) + configuration.isTransitive = false + + val sanitizedMavenId = mavenId.replace(":", "-") + val unzippedDir = File("${runnerProject.buildDir.path}/sources-unzipped/$sanitizedMavenId") + runnerProject.copy({ copySpec -> + copySpec.from(runnerProject.zipTree(configuration.singleFile)) + copySpec.into(unzippedDir) + }) + return listOf(unzippedDir) + } + + fun getEmbeddedLibs(runnerProject: Project, mavenId: String): Collection { + val configuration = runnerProject.configurations.detachedConfiguration( + runnerProject.dependencies.create(mavenId) + ) + configuration.isTransitive = false + + val sanitizedMavenId = mavenId.replace(":", "-") + val unzippedDir = File("${runnerProject.buildDir.path}/aars-unzipped/$sanitizedMavenId") + runnerProject.copy({ copySpec -> + copySpec.from(runnerProject.zipTree(configuration.singleFile)) + copySpec.into(unzippedDir) + }) + val libsDir = File(unzippedDir, "libs") + if (libsDir.exists()) { + return libsDir.listFiles().toList() + } + + return listOf() + } +}