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