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