blob: d6a7090b52a2ea916d98bfb887ae0b7482b3cdb3 [file] [log] [blame]
/*
* Copyright (C) 2018 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 com.android.build.gradle.tasks
import com.android.build.api.artifact.ArtifactType
import com.android.build.api.artifact.BuildableArtifact
import com.google.common.base.Joiner
import com.google.common.cache.CacheBuilder
import com.google.common.cache.CacheLoader
import com.google.common.cache.LoadingCache
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.UncheckedExecutionException
import org.gradle.api.file.Directory
import org.gradle.api.file.FileSystemLocation
import org.gradle.api.file.RegularFile
import org.gradle.api.provider.Provider
import org.gradle.api.tasks.InputFiles
import org.gradle.api.tasks.Optional
import org.gradle.api.tasks.OutputDirectory
import org.gradle.api.tasks.OutputFile
import java.lang.reflect.AnnotatedElement
import java.lang.reflect.Method
import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass
import kotlin.reflect.KMutableProperty1
import kotlin.reflect.KProperty1
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.superclasses
/**
* Populate a cache of introspection information for tasks. This information will be used when tasks
* are allocated and configured to store expected output and information about tasks inputs.
*
* The cache is a static field on the class as it never needs to be garbage collected. If the task
* changes, that means a new plugin has been deliverered and Gradle guarantees a new class loader
* will be used ensure the cache is always up to date.
*
* Cached information is the input and output injection points obtained through reflection.
*/
class TaskInjectionPointsCache {
/**
* Defines the kind of injection target we support.
* So far, only two kind of injection target are supported, input for all input related fields
* and output for all output related fields.
*/
private enum class InjectionKind {
INPUT {
override fun createInjectionPoint(
annotatedElement: Method,
property: KMutableProperty1<Any, Provider<FileSystemLocation>?>,
id: ArtifactType,
injectionPoints: InjectionPointsBuilder
) {
injectionPoints.addInput(InputInjectionPoint(property,
id,
annotatedElement.getDeclaredAnnotation(Current::class.java) == null,
annotatedElement.getDeclaredAnnotation(Optional::class.java) != null))
}
override fun checkFunction(annotatedElement: Method, annotation: Any, expectedType: KClass<*>) {
if (annotatedElement.returnType.typeName != BuildableArtifact::class.qualifiedName) {
throw RuntimeException("Annotated method does not return a BuildableArtifact")
}
}
},
OUTPUT {
private val annotationToOutputNameModels = mapOf(
Initial::class to Initial::out,
Replace::class to Replace::out)
override fun createInjectionPoint(
annotatedElement: Method,
property: KMutableProperty1<Any, Provider<FileSystemLocation>?>,
id: ArtifactType,
injectionPoints: InjectionPointsBuilder
) {
for ((annotationType, outputNameProvider) in annotationToOutputNameModels) {
val annotation = annotatedElement.getAnnotation(annotationType.java)
if (annotation != null) {
injectionPoints.addOutput(OutputInjectionPoint(property, id,
(outputNameProvider as KProperty1<Annotation, String>).get(annotation)))
return
}
}
throw RuntimeException(
"Task: ${annotatedElement.declaringClass.name}\n\t" +
"Method: ${annotatedElement.toGenericString()}\n\t" +
"must be annotated by one of the following annotations : \n\t" +
Joiner.on(',').join(annotationToOutputNameModels.keys))
}
override fun checkFunction(annotatedElement: Method, annotation: Any, expectedType: KClass<*>) {
val returnTypeName = annotatedElement.returnType.typeName
if (returnTypeName != Provider::class.qualifiedName) {
throw RuntimeException(
"Task: ${annotatedElement.declaringClass.name}\n\t" +
"Method: ${annotatedElement.toGenericString()}\n\t" +
"annotated with $annotation is expected to return a Provider<${expectedType.simpleName}> but instead returns $returnTypeName")
}
val genericReturnType = annotatedElement.genericReturnType
if (genericReturnType is ParameterizedType) {
val typeParameters = genericReturnType.actualTypeArguments
if (typeParameters.size != 1) {
throw RuntimeException("No parameterized type for Provider<> specified")
}
if (typeParameters[0].typeName != expectedType.qualifiedName) {
throw RuntimeException(
"Task: ${annotatedElement.declaringClass.name}\n\t" +
"Method: ${annotatedElement.toGenericString()}\n\t" +
"annotated with $annotation is expected to return a Provider<${expectedType.simpleName}>\n\t" +
"but instead returns Provider<${typeParameters[0].typeName}>")
}
}
}
};
/**
* check that the provided method for injecting the buildable artifact value is suitable
* for injection.
*/
abstract fun checkFunction(annotatedElement: Method, annotation: Any, expectedType: KClass<*>)
/**
* Creates the injection point metadata on the provided method. This metadata will be used
* later to allocate the buildable artifacts and finally inject them into the task instance.
*/
abstract fun createInjectionPoint(
annotatedElement: Method,
property: KMutableProperty1<Any, Provider<FileSystemLocation>?>,
id: ArtifactType,
injectionPoints: InjectionPointsBuilder)
}
companion object {
/**
* Data structure declaring possible signatures of injection points on Task implementation.
*
* An injection point has a kind (input for consuming and output for producing) a type
* which is the field type as defined on the task class (although it really is a Provider
* of that type) and a fileType which is the file type associated with the expected
* artifact (a file or directory).
*/
private data class InjectionPointModel(
val kind: InjectionKind,
val injectionPointType: KClass<*>,
val fileType: ArtifactType.Kind)
/**
* Helper object to populate the cache with information issued from reflection.
*
* it will go over all methods annotated for injection and will create injection point
* metadata.
*/
private val introspector = object : CacheLoader<Class<*>, InjectionPoints>() {
/**
* Injection Points model.
*/
private val injectionModels = mapOf(
OutputFile::class to InjectionPointModel(
InjectionKind.OUTPUT,
RegularFile::class,
ArtifactType.Kind.FILE
),
OutputDirectory::class to InjectionPointModel(
InjectionKind.OUTPUT,
Directory::class,
ArtifactType.Kind.DIRECTORY
),
InputFiles::class to InjectionPointModel(
InjectionKind.INPUT,
Directory::class,
ArtifactType.Kind.DIRECTORY
)
)
private fun <R> findProperty(kclass: KClass<*>, propertyName: String): KMutableProperty1<Any, R?>? {
val prop = kclass.memberProperties.find { it.name == propertyName }
if (prop!=null) {
return prop as KMutableProperty1<Any, R?>
}
kclass.superclasses.forEach {
val superTypeProp = findProperty<R>(it, propertyName)
if (superTypeProp != null) return superTypeProp
}
return null
}
/**
* Find the annotation that is itself annotated with @ProviderID and extract the ArtifactType
*/
private fun findID(annotatedElement: AnnotatedElement): ArtifactType? {
annotatedElement.annotations.forEach{
val idProvider = it.annotationClass.findAnnotation<IDProvider>()
if (idProvider != null) {
it.annotationClass.java.methods[0].invoke(it)
val idProviderMethod = it.annotationClass.java.methods.find { method ->
method.name == idProvider.fieldName
}
if (idProviderMethod != null) {
val id = idProviderMethod.invoke(it)
if (id is ArtifactType) {
return id
} else {
throw RuntimeException(
"$it is annotated with @IdProvider," +
" and the target method is $idProviderMethod, " +
"yet $id is not an instance of ArtifactType")
}
} else {
throw RuntimeException("Cannot find property ${idProvider.fieldName} " +
"on annotation type ${it.annotationClass}")
}
}
}
return null
}
override fun load(type: Class<*>?): InjectionPoints {
if (type == null) {
throw RuntimeException("Cannot pass null as a task type to cache.")
}
val injectionPoints = InjectionPointsBuilder()
type.methods
.filter { !it.declaringClass.name.startsWith("org.gradle.api") }
.forEach {
for ((annotationType, injectionPointModel) in injectionModels) {
val outputAnnotation = it.getAnnotation(annotationType.java)
if (outputAnnotation != null) {
val requestedId = findID(it)
if (requestedId != null) {
val propertyDefinition = findProperty<Provider<FileSystemLocation>>(
type.kotlin,
it.name.substring(3).decapitalize()
) ?: throw RuntimeException("Cannot find property for ${it.name}")
if (requestedId.kind() != injectionPointModel.fileType) {
throw RuntimeException(
"Task: ${it.declaringClass.name}\n\t" +
"Method: ${it.toGenericString()}\n\t" +
"annotated with $outputAnnotation expecting a ${injectionPointModel.fileType} \n\t" +
"but its ArtifactID \"${requestedId.name()} is set to be a ${requestedId.kind()}"
)
}
injectionPointModel.kind.checkFunction(it, outputAnnotation, injectionPointModel.injectionPointType)
injectionPointModel.kind.createInjectionPoint(it, propertyDefinition, requestedId, injectionPoints)
}
}
}
}
return injectionPoints.build()
}
}
// cache instance, one per class, never garbage collected.
private val cache : LoadingCache<Class<*>, InjectionPoints>
= CacheBuilder.newBuilder().build(introspector)
/**
* Retrieve or populate a cache entry providing its key (the task configuration class).
*/
fun getInjectionPoints(type: Class<*>): InjectionPoints {
try {
return cache.get(type)
} catch(e: UncheckedExecutionException) {
throw e.cause ?: e
}
}
}
/**
* Metadata about a declared output injection point on a task implementation.
*/
data class OutputInjectionPoint(
// is this making sense that it is an optional Provider like for input ?
val injectionPoint: KMutableProperty1<Any, Provider<FileSystemLocation>?>,
var id: ArtifactType,
val out: String)
/**
* Metadata about a declared input injection point on a task implementation.
*/
data class InputInjectionPoint(
val injectionPoint: KMutableProperty1<Any, Provider<FileSystemLocation>?>,
val id: ArtifactType,
val isFinalVersion: Boolean,
val isOptional: Boolean
)
private class InjectionPointsBuilder {
val outputs = mutableListOf<OutputInjectionPoint>()
val inputs = mutableListOf<InputInjectionPoint>()
fun addOutput(injectionPoint: OutputInjectionPoint) {
outputs.add(injectionPoint)
}
fun addInput(injectionPoint: InputInjectionPoint) {
inputs.add(injectionPoint)
}
fun build() = InjectionPoints(ImmutableList.copyOf(inputs),
ImmutableList.copyOf(outputs))
}
/**
* Collection of input and output injection points information for a task.
*/
class InjectionPoints(
val inputs: Collection<InputInjectionPoint>,
val outputs: Collection<OutputInjectionPoint>)
}