1 /* <lambda>null2 * Copyright (C) 2020 The Dagger Authors. 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 dagger.hilt.android.plugin 18 19 import com.android.build.api.transform.DirectoryInput 20 import com.android.build.api.transform.Format 21 import com.android.build.api.transform.JarInput 22 import com.android.build.api.transform.QualifiedContent 23 import com.android.build.api.transform.Status 24 import com.android.build.api.transform.Transform 25 import com.android.build.api.transform.TransformInput 26 import com.android.build.api.transform.TransformInvocation 27 import dagger.hilt.android.plugin.util.isClassFile 28 import java.io.File 29 30 /** 31 * Bytecode transformation to make @AndroidEntryPoint annotated classes extend the Hilt 32 * generated android classes, including the @HiltAndroidApp application class. 33 * 34 * A transform receives input as a collection [TransformInput], which is composed of [JarInput]s and 35 * [DirectoryInput]s. The resulting files must be placed in the 36 * [TransformInvocation.getOutputProvider]. The bytecode transformation can be done with any library 37 * (in our case Javaassit). The [QualifiedContent.Scope] defined in a transform defines the input 38 * the transform will receive and if it can be applied to only the Android application projects or 39 * Android libraries too. 40 * 41 * See: [TransformPublic Docs](https://google.github.io/android-gradle-dsl/javadoc/current/com/android/build/api/transform/Transform.html) 42 */ 43 class AndroidEntryPointTransform : Transform() { 44 // The name of the transform. This name appears as a gradle task. 45 override fun getName() = "AndroidEntryPointTransform" 46 47 // The type of input this transform will handle. 48 override fun getInputTypes() = setOf(QualifiedContent.DefaultContentType.CLASSES) 49 50 override fun isIncremental() = true 51 52 // The project scope this transform is applied to. 53 override fun getScopes() = mutableSetOf(QualifiedContent.Scope.PROJECT) 54 55 /** 56 * Performs the transformation of the bytecode. 57 * 58 * The inputs will be available in the [TransformInvocation] along with referenced inputs that 59 * should not be transformed. The inputs received along with the referenced inputs depend on the 60 * scope of the transform. 61 * 62 * The invocation will also indicate if an incremental transform has to be applied or not. Even 63 * though a transform might return true in its [isIncremental] function, the invocation might 64 * return false in [TransformInvocation.isIncremental], therefore both cases must be handled. 65 */ 66 override fun transform(invocation: TransformInvocation) { 67 if (!invocation.isIncremental) { 68 // Remove any lingering files on a non-incremental invocation since everything has to be 69 // transformed. 70 invocation.outputProvider.deleteAll() 71 } 72 73 invocation.inputs.forEach { transformInput -> 74 transformInput.jarInputs.forEach { jarInput -> 75 val outputJar = 76 invocation.outputProvider.getContentLocation( 77 jarInput.name, 78 jarInput.contentTypes, 79 jarInput.scopes, 80 Format.JAR 81 ) 82 if (invocation.isIncremental) { 83 when (jarInput.status) { 84 Status.ADDED, Status.CHANGED -> copyJar(jarInput.file, outputJar) 85 Status.REMOVED -> outputJar.delete() 86 Status.NOTCHANGED -> { 87 // No need to transform. 88 } 89 else -> { 90 error("Unknown status: ${jarInput.status}") 91 } 92 } 93 } else { 94 copyJar(jarInput.file, outputJar) 95 } 96 } 97 transformInput.directoryInputs.forEach { directoryInput -> 98 val outputDir = invocation.outputProvider.getContentLocation( 99 directoryInput.name, 100 directoryInput.contentTypes, 101 directoryInput.scopes, 102 Format.DIRECTORY 103 ) 104 val classTransformer = 105 createHiltClassTransformer(invocation.inputs, invocation.referencedInputs, outputDir) 106 if (invocation.isIncremental) { 107 directoryInput.changedFiles.forEach { (file, status) -> 108 val outputFile = toOutputFile(outputDir, directoryInput.file, file) 109 when (status) { 110 Status.ADDED, Status.CHANGED -> 111 transformFile(file, outputFile.parentFile, classTransformer) 112 Status.REMOVED -> outputFile.delete() 113 Status.NOTCHANGED -> { 114 // No need to transform. 115 } 116 else -> { 117 error("Unknown status: $status") 118 } 119 } 120 } 121 } else { 122 directoryInput.file.walkTopDown().forEach { file -> 123 val outputFile = toOutputFile(outputDir, directoryInput.file, file) 124 transformFile(file, outputFile.parentFile, classTransformer) 125 } 126 } 127 } 128 } 129 } 130 131 // Create a transformer given an invocation inputs. Note that since this is a PROJECT scoped 132 // transform the actual transformation is only done on project files and not its dependencies. 133 private fun createHiltClassTransformer( 134 inputs: Collection<TransformInput>, 135 referencedInputs: Collection<TransformInput>, 136 outputDir: File 137 ): AndroidEntryPointClassTransformer { 138 val classFiles = (inputs + referencedInputs).flatMap { input -> 139 (input.directoryInputs + input.jarInputs).map { it.file } 140 } 141 return AndroidEntryPointClassTransformer( 142 taskName = name, 143 allInputs = classFiles, 144 sourceRootOutputDir = outputDir, 145 copyNonTransformed = true 146 ) 147 } 148 149 // Transform a single file. If the file is not a class file it is just copied to the output dir. 150 private fun transformFile( 151 inputFile: File, 152 outputDir: File, 153 transformer: AndroidEntryPointClassTransformer 154 ) { 155 if (inputFile.isClassFile()) { 156 transformer.transformFile(inputFile) 157 } else if (inputFile.isFile) { 158 // Copy all non .class files to the output. 159 outputDir.mkdirs() 160 val outputFile = File(outputDir, inputFile.name) 161 inputFile.copyTo(target = outputFile, overwrite = true) 162 } 163 } 164 165 // We are only interested in project compiled classes but we have to copy received jars to the 166 // output. 167 private fun copyJar(inputJar: File, outputJar: File) { 168 outputJar.parentFile?.mkdirs() 169 inputJar.copyTo(target = outputJar, overwrite = true) 170 } 171 172 private fun toOutputFile(outputDir: File, inputDir: File, inputFile: File) = 173 File(outputDir, inputFile.relativeTo(inputDir).path) 174 } 175