• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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