blob: 31bba813bb32ba0bf4377166a0476e348af5e2af [file] [log] [blame]
/*
* 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.Version
import androidx.build.checkapi.ApiLocation
import androidx.build.getLibraryByName
import androidx.build.java.JavaCompileInputs
import androidx.build.logging.TERMINAL_RED
import androidx.build.logging.TERMINAL_RESET
import java.io.ByteArrayOutputStream
import java.io.File
import javax.inject.Inject
import org.gradle.api.Project
import org.gradle.api.file.FileCollection
import org.gradle.api.provider.ListProperty
import org.gradle.api.provider.Property
import org.gradle.api.provider.SetProperty
import org.gradle.process.ExecOperations
import org.gradle.workers.WorkAction
import org.gradle.workers.WorkParameters
import org.gradle.workers.WorkerExecutor
// MetalavaRunner stores common configuration for executing Metalava
fun runMetalavaWithArgs(
metalavaClasspath: FileCollection,
args: List<String>,
k2UastEnabled: Boolean,
workerExecutor: WorkerExecutor,
) {
val allArgs =
args +
listOf(
"--hide",
"HiddenSuperclass", // We allow having a hidden parent class
"--hide",
// Removing final from a method does not cause compatibility issues for AndroidX.
"RemovedFinalStrict",
"--error",
"UnresolvedImport",
"--delete-empty-removed-signatures",
// Metalava arguments to suppress compatibility checks for experimental API
// surfaces.
"--suppress-compatibility-meta-annotation",
"androidx.annotation.RequiresOptIn",
"--suppress-compatibility-meta-annotation",
"kotlin.RequiresOptIn",
)
val workQueue = workerExecutor.processIsolation()
workQueue.submit(MetalavaWorkAction::class.java) { parameters ->
parameters.args.set(allArgs)
parameters.metalavaClasspath.set(metalavaClasspath.files)
parameters.k2UastEnabled.set(k2UastEnabled)
}
}
interface MetalavaParams : WorkParameters {
val args: ListProperty<String>
val metalavaClasspath: SetProperty<File>
val k2UastEnabled: Property<Boolean>
}
abstract class MetalavaWorkAction @Inject constructor(private val execOperations: ExecOperations) :
WorkAction<MetalavaParams> {
override fun execute() {
val outputStream = ByteArrayOutputStream()
var successful = false
val k2UastArg =
listOfNotNull(
// Enable Android Lint infrastructure used by Metalava to use K2 UAST
// (also historically known as FIR) when running Metalava for this module.
"--Xuse-k2-uast".takeIf { parameters.k2UastEnabled.get() }
)
try {
execOperations.javaexec {
// Intellij core reflects into java.util.ResourceBundle
it.jvmArgs = listOf("--add-opens", "java.base/java.util=ALL-UNNAMED")
it.classpath(parameters.metalavaClasspath.get())
it.mainClass.set("com.android.tools.metalava.Driver")
it.args = parameters.args.get() + k2UastArg
it.setStandardOutput(outputStream)
it.setErrorOutput(outputStream)
}
successful = true
} finally {
if (!successful) {
System.err.println(outputStream.toString(Charsets.UTF_8))
}
}
}
}
fun Project.getMetalavaClasspath(): FileCollection {
val configuration =
configurations.findByName("metalava")
?: configurations.create("metalava") {
it.dependencies.add(dependencies.create(getLibraryByName("metalava")))
}
return project.files(configuration)
}
fun getApiLintArgs(targetsJavaConsumers: Boolean): List<String> {
val args =
mutableListOf(
"--api-lint",
"--hide",
listOf(
// The list of checks that are hidden as they are not useful in androidx
"Enum", // Enums are allowed to be use in androidx
"CallbackInterface", // With target Java 8, we have default methods
"ProtectedMember", // We allow using protected members in androidx
"ManagerLookup", // Managers in androidx are not the same as platform services
"ManagerConstructor",
"RethrowRemoteException", // This check is for calls into system_server
"PackageLayering", // This check is not relevant to androidx.* code.
"UserHandle", // This check is not relevant to androidx.* code.
"ParcelableList", // This check is only relevant to android platform that has
// managers.
// List of checks that have bugs, but should be enabled once fixed.
"StaticUtils", // b/135489083
"StartWithLower", // b/135710527
// The list of checks that are API lint warnings and are yet to be enabled
"SamShouldBeLast",
// We should only treat these as warnings
"IntentBuilderName",
"OnNameExpected",
"UserHandleName"
)
.joinToString(),
"--error",
listOf(
"AllUpper",
"GetterSetterNames",
"MinMaxConstant",
"TopLevelBuilder",
"BuilderSetStyle",
"MissingBuildMethod",
"SetterReturnsThis",
"OverlappingConstants",
"IllegalStateException",
"ListenerLast",
"ExecutorRegistration",
"StreamFiles",
"AbstractInner",
"NotCloseable",
"MethodNameTense",
"UseIcu",
"NoByteOrShort",
"CommonArgsFirst",
"GetterOnBuilder",
"CallbackMethodName",
"StaticFinalBuilder",
"MissingGetterMatchingBuilder",
"HiddenSuperclass",
"KotlinOperator"
)
.joinToString()
)
if (targetsJavaConsumers) {
args.addAll(listOf("--error", "MissingJvmstatic", "--error", "ArrayReturn"))
} else {
args.addAll(listOf("--hide", "MissingJvmstatic", "--hide", "ArrayReturn"))
}
return args
}
/** Returns the args needed to generate a version history JSON from the previous API files. */
internal fun getGenerateApiLevelsArgs(
apiFiles: List<File>,
currentVersion: Version,
outputLocation: File
): List<String> {
val versions = getVersionsForApiLevels(apiFiles) + currentVersion
val args =
listOf(
"--generate-api-version-history",
outputLocation.absolutePath,
"--api-version-names",
versions.joinToString(" ")
)
return if (apiFiles.isEmpty()) {
args
} else {
args + listOf("--api-version-signature-files", apiFiles.joinToString(":"))
}
}
sealed class GenerateApiMode {
object PublicApi : GenerateApiMode()
object AllRestrictedApis : GenerateApiMode()
object RestrictToLibraryGroupPrefixApis : GenerateApiMode()
}
sealed class ApiLintMode {
class CheckBaseline(val apiLintBaseline: File, val targetsJavaConsumers: Boolean) :
ApiLintMode()
object Skip : ApiLintMode()
}
/**
* Generates all of the specified api files, as well as a version history JSON for the public API.
*/
fun generateApi(
metalavaClasspath: FileCollection,
files: JavaCompileInputs,
apiLocation: ApiLocation,
apiLintMode: ApiLintMode,
includeRestrictToLibraryGroupApis: Boolean,
apiLevelsArgs: List<String>,
k2UastEnabled: Boolean,
workerExecutor: WorkerExecutor,
pathToManifest: String? = null,
) {
val generateApiConfigs: MutableList<Pair<GenerateApiMode, ApiLintMode>> =
mutableListOf(GenerateApiMode.PublicApi to apiLintMode)
@Suppress("LiftReturnOrAssignment")
if (includeRestrictToLibraryGroupApis) {
generateApiConfigs += GenerateApiMode.AllRestrictedApis to ApiLintMode.Skip
} else {
generateApiConfigs += GenerateApiMode.RestrictToLibraryGroupPrefixApis to ApiLintMode.Skip
}
generateApiConfigs.forEach { (generateApiMode, apiLintMode) ->
generateApi(
metalavaClasspath,
files.bootClasspath,
files.dependencyClasspath,
files.sourcePaths.files,
apiLocation,
generateApiMode,
apiLintMode,
apiLevelsArgs,
k2UastEnabled,
workerExecutor,
pathToManifest
)
}
}
/**
* Gets arguments for generating the specified api file (and a version history JSON if the
* [generateApiMode] is [GenerateApiMode.PublicApi].
*/
private fun generateApi(
metalavaClasspath: FileCollection,
bootClasspath: FileCollection,
dependencyClasspath: FileCollection,
sourcePaths: Collection<File>,
outputLocation: ApiLocation,
generateApiMode: GenerateApiMode,
apiLintMode: ApiLintMode,
apiLevelsArgs: List<String>,
k2UastEnabled: Boolean,
workerExecutor: WorkerExecutor,
pathToManifest: String? = null
) {
val args =
getGenerateApiArgs(
bootClasspath,
dependencyClasspath,
sourcePaths,
outputLocation,
generateApiMode,
apiLintMode,
apiLevelsArgs,
pathToManifest
)
runMetalavaWithArgs(metalavaClasspath, args, k2UastEnabled, workerExecutor)
}
/**
* Generates the specified api file, and a version history JSON if the [generateApiMode] is
* [GenerateApiMode.PublicApi].
*/
fun getGenerateApiArgs(
bootClasspath: FileCollection,
dependencyClasspath: FileCollection,
sourcePaths: Collection<File>,
outputLocation: ApiLocation?,
generateApiMode: GenerateApiMode,
apiLintMode: ApiLintMode,
apiLevelsArgs: List<String>,
pathToManifest: String? = null
): List<String> {
// generate public API txt
val args =
mutableListOf(
"--classpath",
(bootClasspath.files + dependencyClasspath.files).joinToString(File.pathSeparator),
"--source-path",
sourcePaths.filter { it.exists() }.joinToString(File.pathSeparator),
"--format=v4",
"--output-kotlin-nulls=yes",
"--warnings-as-errors"
)
pathToManifest?.let { args += listOf("--manifest", pathToManifest) }
if (outputLocation != null) {
when (generateApiMode) {
is GenerateApiMode.PublicApi -> {
args += listOf("--api", outputLocation.publicApiFile.toString())
args += listOf("--removed-api", outputLocation.removedApiFile.toString())
// Generate API levels just for the public API
args += apiLevelsArgs
}
is GenerateApiMode.AllRestrictedApis,
GenerateApiMode.RestrictToLibraryGroupPrefixApis -> {
args += listOf("--api", outputLocation.restrictedApiFile.toString())
}
}
}
when (generateApiMode) {
is GenerateApiMode.PublicApi -> {
args += listOf("--hide-annotation", "androidx.annotation.RestrictTo")
args += listOf("--show-unannotated")
}
is GenerateApiMode.AllRestrictedApis,
GenerateApiMode.RestrictToLibraryGroupPrefixApis -> {
// Despite being hidden we still track the following:
// * @RestrictTo(Scope.LIBRARY_GROUP_PREFIX): inter-library APIs
// * @PublishedApi: needs binary stability for inline methods
// * @RestrictTo(Scope.LIBRARY_GROUP): APIs between libraries in non-atomic groups
args +=
listOf(
// hide RestrictTo(LIBRARY), use --show-annotation for RestrictTo with
// specific arguments
"--hide-annotation",
"androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY)",
"--show-annotation",
"androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
"LIBRARY_GROUP_PREFIX)",
"--show-annotation",
"kotlin.PublishedApi",
"--show-unannotated"
)
if (generateApiMode is GenerateApiMode.AllRestrictedApis) {
args +=
listOf(
"--show-annotation",
"androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
"LIBRARY_GROUP)"
)
} else {
args +=
listOf(
"--hide-annotation",
"androidx.annotation.RestrictTo(androidx.annotation.RestrictTo.Scope." +
"LIBRARY_GROUP)"
)
}
}
}
when (apiLintMode) {
is ApiLintMode.CheckBaseline -> {
args += getApiLintArgs(apiLintMode.targetsJavaConsumers)
if (apiLintMode.apiLintBaseline.exists()) {
args += listOf("--baseline", apiLintMode.apiLintBaseline.toString())
}
args.addAll(
listOf(
"--error",
"DeprecationMismatch", // Enforce deprecation mismatch
"--error",
"ReferencesDeprecated",
"--error-message:api-lint",
"""
${TERMINAL_RED}Your change has API lint issues. Fix the code according to the messages above.$TERMINAL_RESET
If a check is broken, suppress it in code in Kotlin with @Suppress("id")/@get:Suppress("id")
and in Java with @SuppressWarnings("id") and file bug to
https://issuetracker.google.com/issues/new?component=739152&template=1344623
If you are doing a refactoring or suppression above does not work, use ./gradlew updateApiLintBaseline
"""
)
)
}
is ApiLintMode.Skip -> {
args.addAll(
listOf(
"--hide",
"DeprecationMismatch",
"--hide",
"UnhiddenSystemApi",
"--hide",
"ReferencesHidden",
"--hide",
"ReferencesDeprecated"
)
)
}
}
return args
}