blob: 9bb5160b6235297b821af414e2493564efd177ba [file] [log] [blame]
/*
* Copyright (C) 2020 The Dagger Authors.
*
* 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 dagger.hilt.android.plugin
import com.android.build.api.transform.DirectoryInput
import com.android.build.api.transform.Format
import com.android.build.api.transform.JarInput
import com.android.build.api.transform.QualifiedContent
import com.android.build.api.transform.Status
import com.android.build.api.transform.Transform
import com.android.build.api.transform.TransformInput
import com.android.build.api.transform.TransformInvocation
import dagger.hilt.android.plugin.util.isClassFile
import java.io.File
/**
* Bytecode transformation to make @AndroidEntryPoint annotated classes extend the Hilt
* generated android classes, including the @HiltAndroidApp application class.
*
* A transform receives input as a collection [TransformInput], which is composed of [JarInput]s and
* [DirectoryInput]s. The resulting files must be placed in the
* [TransformInvocation.getOutputProvider]. The bytecode transformation can be done with any library
* (in our case Javaassit). The [QualifiedContent.Scope] defined in a transform defines the input
* the transform will receive and if it can be applied to only the Android application projects or
* Android libraries too.
*
* See: [TransformPublic Docs](https://google.github.io/android-gradle-dsl/javadoc/current/com/android/build/api/transform/Transform.html)
*/
class AndroidEntryPointTransform : Transform() {
// The name of the transform. This name appears as a gradle task.
override fun getName() = "AndroidEntryPointTransform"
// The type of input this transform will handle.
override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES)
override fun isIncremental() = true
// The project scope this transform is applied to.
override fun getScopes() = mutableSetOf(QualifiedContent.Scope.PROJECT)
/**
* Performs the transformation of the bytecode.
*
* The inputs will be available in the [TransformInvocation] along with referenced inputs that
* should not be transformed. The inputs received along with the referenced inputs depend on the
* scope of the transform.
*
* The invocation will also indicate if an incremental transform has to be applied or not. Even
* though a transform might return true in its [isIncremental] function, the invocation might
* return false in [TransformInvocation.isIncremental], therefore both cases must be handled.
*/
override fun transform(invocation: TransformInvocation) {
if (!invocation.isIncremental) {
// Remove any lingering files on a non-incremental invocation since everything has to be
// transformed.
invocation.outputProvider.deleteAll()
}
invocation.inputs.forEach { transformInput ->
transformInput.jarInputs.forEach { jarInput ->
val outputJar =
invocation.outputProvider.getContentLocation(
jarInput.name,
jarInput.contentTypes,
jarInput.scopes,
Format.JAR
)
if (invocation.isIncremental) {
when (jarInput.status) {
Status.ADDED, Status.CHANGED -> copyJar(jarInput.file, outputJar)
Status.REMOVED -> outputJar.delete()
Status.NOTCHANGED -> {
// No need to transform.
}
else -> {
error("Unknown status: ${jarInput.status}")
}
}
} else {
copyJar(jarInput.file, outputJar)
}
}
transformInput.directoryInputs.forEach { directoryInput ->
val outputDir = invocation.outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes,
directoryInput.scopes,
Format.DIRECTORY
)
val classTransformer =
createHiltClassTransformer(invocation.inputs, invocation.referencedInputs, outputDir)
if (invocation.isIncremental) {
directoryInput.changedFiles.forEach { (file, status) ->
val outputFile = toOutputFile(outputDir, directoryInput.file, file)
when (status) {
Status.ADDED, Status.CHANGED ->
transformFile(file, outputFile.parentFile, classTransformer)
Status.REMOVED -> outputFile.delete()
Status.NOTCHANGED -> {
// No need to transform.
}
else -> {
error("Unknown status: $status")
}
}
}
} else {
directoryInput.file.walkTopDown().forEach { file ->
val outputFile = toOutputFile(outputDir, directoryInput.file, file)
transformFile(file, outputFile.parentFile, classTransformer)
}
}
}
}
}
// Create a transformer given an invocation inputs. Note that since this is a PROJECT scoped
// transform the actual transformation is only done on project files and not its dependencies.
private fun createHiltClassTransformer(
inputs: Collection<TransformInput>,
referencedInputs: Collection<TransformInput>,
outputDir: File
): AndroidEntryPointClassTransformer {
val classFiles = (inputs + referencedInputs).flatMap { input ->
(input.directoryInputs + input.jarInputs).map { it.file }
}
return AndroidEntryPointClassTransformer(
taskName = name,
allInputs = classFiles,
sourceRootOutputDir = outputDir,
copyNonTransformed = true
)
}
// Transform a single file. If the file is not a class file it is just copied to the output dir.
private fun transformFile(
inputFile: File,
outputDir: File,
transformer: AndroidEntryPointClassTransformer
) {
if (inputFile.isClassFile()) {
transformer.transformFile(inputFile)
} else if (inputFile.isFile) {
// Copy all non .class files to the output.
outputDir.mkdirs()
val outputFile = File(outputDir, inputFile.name)
inputFile.copyTo(target = outputFile, overwrite = true)
}
}
// We are only interested in project compiled classes but we have to copy received jars to the
// output.
private fun copyJar(inputJar: File, outputJar: File) {
outputJar.parentFile?.mkdirs()
inputJar.copyTo(target = outputJar, overwrite = true)
}
private fun toOutputFile(outputDir: File, inputDir: File, inputFile: File) =
File(outputDir, inputFile.relativeTo(inputDir).path)
}