1 /*
<lambda>null2  * Copyright 2024 The Android Open Source Project
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 androidx.build.checkapi
18 
19 import androidx.build.getAndroidJar
20 import androidx.build.multiplatformExtension
21 import com.android.build.api.dsl.KotlinMultiplatformAndroidLibraryTarget
22 import com.android.build.api.variant.LibraryAndroidComponentsExtension
23 import com.android.build.api.variant.LibraryVariant
24 import org.gradle.api.Project
25 import org.gradle.api.attributes.Attribute
26 import org.gradle.api.file.ConfigurableFileCollection
27 import org.gradle.api.file.FileCollection
28 import org.gradle.api.provider.Provider
29 import org.gradle.api.tasks.SourceSet
30 import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation
31 import org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType
32 import org.jetbrains.kotlin.gradle.plugin.KotlinTarget
33 
34 /**
35  * [CompilationInputs] contains the information required to compile Java/Kotlin code. This can be
36  * helpful for creating Metalava and Kzip tasks with the same settings.
37  *
38  * There are two implementations: [StandardCompilationInputs] for non-multiplatform projects and
39  * [MultiplatformCompilationInputs] for multiplatform projects.
40  */
41 internal sealed interface CompilationInputs {
42     /** Source files to process */
43     val sourcePaths: FileCollection
44 
45     /** Dependencies (compiled classes) of [sourcePaths]. */
46     val dependencyClasspath: FileCollection
47 
48     /** Android's boot classpath. */
49     val bootClasspath: FileCollection
50 
51     companion object {
52         /** Constructs a [CompilationInputs] from a library and its variant */
53         fun fromLibraryVariant(variant: LibraryVariant, project: Project): CompilationInputs {
54             // The boot classpath is common to both multiplatform and standard configurations.
55             val bootClasspath =
56                 project.files(
57                     project.extensions
58                         .findByType(LibraryAndroidComponentsExtension::class.java)!!
59                         .sdkComponents
60                         .bootClasspath
61                 )
62 
63             // If this is a multiplatform project, set up inputs for the androidJvm compilation
64             val multiplatformExtension = project.multiplatformExtension
65             if (multiplatformExtension != null) {
66                 val androidJvmTarget =
67                     multiplatformExtension.targets
68                         .requirePlatform(KotlinPlatformType.androidJvm)
69                         .findCompilation(compilationName = variant.name)
70 
71                 return MultiplatformCompilationInputs.fromCompilation(
72                     project = project,
73                     compilationProvider = androidJvmTarget,
74                     bootClasspath = bootClasspath,
75                 )
76             }
77 
78             // Not a multiplatform project, set up standard inputs
79             val kotlinCollection = project.files(variant.sources.kotlin?.all)
80             val javaCollection = project.files(variant.sources.java?.all)
81             val sourceCollection = kotlinCollection + javaCollection
82 
83             return StandardCompilationInputs(
84                 sourcePaths = sourceCollection,
85                 dependencyClasspath = variant.compileClasspath,
86                 bootClasspath = bootClasspath
87             )
88         }
89 
90         /**
91          * Returns the CompilationInputs for the `jvm` target of a KMP project.
92          *
93          * @param project The project whose main jvm target inputs will be returned.
94          */
95         fun fromKmpJvmTarget(project: Project): CompilationInputs {
96             val kmpExtension =
97                 checkNotNull(project.multiplatformExtension) {
98                     """
99                 ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
100                 jvm source sets.
101                 """
102                         .trimIndent()
103                 }
104             val jvmTarget = kmpExtension.targets.requirePlatform(KotlinPlatformType.jvm)
105             val jvmCompilation =
106                 jvmTarget.findCompilation(compilationName = KotlinCompilation.MAIN_COMPILATION_NAME)
107 
108             return MultiplatformCompilationInputs.fromCompilation(
109                 project = project,
110                 compilationProvider = jvmCompilation,
111                 bootClasspath = project.getAndroidJar()
112             )
113         }
114 
115         /**
116          * Returns the CompilationInputs for the `android` target of a KMP project.
117          *
118          * @param project The project whose main android target inputs will be returned.
119          */
120         fun fromKmpAndroidTarget(project: Project): CompilationInputs {
121             val kmpExtension =
122                 checkNotNull(project.multiplatformExtension) {
123                     """
124                 ${project.path} needs to have Kotlin Multiplatform Plugin applied to obtain its
125                 android source sets.
126                 """
127                         .trimIndent()
128                 }
129             val target =
130                 kmpExtension.targets
131                     .withType(KotlinMultiplatformAndroidLibraryTarget::class.java)
132                     .single()
133             val compilation = target.findCompilation(KotlinCompilation.MAIN_COMPILATION_NAME)
134 
135             return MultiplatformCompilationInputs.fromCompilation(
136                 project = project,
137                 compilationProvider = compilation,
138                 bootClasspath = project.getAndroidJar()
139             )
140         }
141 
142         /** Constructs a [CompilationInputs] from a sourceset */
143         fun fromSourceSet(sourceSet: SourceSet, project: Project): CompilationInputs {
144             val sourcePaths: FileCollection =
145                 project.files(project.provider { sourceSet.allSource.srcDirs })
146             val dependencyClasspath = sourceSet.compileClasspath
147             return StandardCompilationInputs(
148                 sourcePaths = sourcePaths,
149                 dependencyClasspath = dependencyClasspath,
150                 bootClasspath = project.getAndroidJar()
151             )
152         }
153 
154         /**
155          * Returns the list of Files (might be directories) that are included in the compilation of
156          * this target.
157          *
158          * @param compilationName The name of the compilation. A target might have separate
159          *   compilations (e.g. main vs test for jvm or debug vs release for Android)
160          */
161         private fun KotlinTarget.findCompilation(
162             compilationName: String
163         ): Provider<KotlinCompilation<*>> {
164             return project.provider {
165                 val selectedCompilation =
166                     checkNotNull(compilations.findByName(compilationName)) {
167                         """
168                     Cannot find $compilationName compilation configuration of $name in
169                     ${project.parent}.
170                     Available compilations: ${compilations.joinToString(", ") { it.name }}
171                     """
172                             .trimIndent()
173                     }
174                 selectedCompilation
175             }
176         }
177 
178         /**
179          * Returns the [KotlinTarget] that targets the given platform type.
180          *
181          * This method will throw if there are no matching targets or there are more than 1 matching
182          * target.
183          */
184         private fun Collection<KotlinTarget>.requirePlatform(
185             expectedPlatformType: KotlinPlatformType
186         ): KotlinTarget {
187             return this.singleOrNull { it.platformType == expectedPlatformType }
188                 ?: error(
189                     """
190                 Expected 1 and only 1 kotlin target with $expectedPlatformType. Found $size.
191                 Matching compilation targets:
192                     ${joinToString(",") { it.name }}
193                 All compilation targets:
194                     ${this@requirePlatform.joinToString(",") { it.name }}
195                 """
196                         .trimIndent()
197                 )
198         }
199     }
200 }
201 
202 /** Compile inputs for a regular (non-multiplatform) project */
203 internal data class StandardCompilationInputs(
204     override val sourcePaths: FileCollection,
205     override val dependencyClasspath: FileCollection,
206     override val bootClasspath: FileCollection,
207 ) : CompilationInputs
208 
209 /** Compile inputs for a single source set from a multiplatform project. */
210 internal data class SourceSetInputs(
211     /** Name of the source set, e.g. "androidMain" */
212     val sourceSetName: String,
213     /** Names of other source sets that this one depends on */
214     val dependsOnSourceSets: List<String>,
215     /** Source files of this source set */
216     val sourcePaths: FileCollection,
217     /** Compile dependencies for this source set */
218     val dependencyClasspath: FileCollection,
219 )
220 
221 /** Inputs for a single compilation of a multiplatform project (just the android or jvm target) */
222 internal class MultiplatformCompilationInputs(
223     project: Project,
224     /**
225      * The [SourceSetInputs] for this project's source sets. This is a [Provider] because not all
226      * relationships between source sets will be loaded at configuration time.
227      */
228     val sourceSets: Provider<List<SourceSetInputs>>,
229     override val bootClasspath: FileCollection,
230 ) : CompilationInputs {
231     // Aggregate sources and classpath from all source sets
232     override val sourcePaths: ConfigurableFileCollection =
<lambda>null233         project.files(sourceSets.map { it.map { sourceSet -> sourceSet.sourcePaths } })
234     override val dependencyClasspath: ConfigurableFileCollection =
<lambda>null235         project.files(sourceSets.map { it.map { sourceSet -> sourceSet.dependencyClasspath } })
236 
237     /** Source files from the KMP common module of this project */
238     val commonModuleSourcePaths: FileCollection =
239         project.files(
<lambda>null240             sourceSets.map {
241                 it.filter { sourceSet -> sourceSet.dependsOnSourceSets.isEmpty() }
242                     .map { sourceSet -> sourceSet.sourcePaths }
243             }
244         )
245 
246     companion object {
247         /** Creates inputs based on one compilation of a multiplatform project. */
fromCompilationnull248         fun fromCompilation(
249             project: Project,
250             compilationProvider: Provider<KotlinCompilation<*>>,
251             bootClasspath: FileCollection,
252         ): MultiplatformCompilationInputs {
253             val compileDependencies =
254                 compilationProvider.map { compilation ->
255                     // Sometimes an Android source set has the jvm platform type instead of
256                     // androidJvm
257                     val platformType =
258                         if (compilation.defaultSourceSet.name == "androidMain") {
259                             KotlinPlatformType.androidJvm
260                         } else {
261                             compilation.platformType
262                         }
263 
264                     project.configurations
265                         .named(compilation.compileDependencyConfigurationName)
266                         .map { config ->
267                             // AGP adds files from many configurations to the
268                             // compileDependencyFiles,
269                             // so it needs to be filtered to avoid variant resolution errors.
270                             config.incoming
271                                 .artifactView {
272                                     val artifactType =
273                                         if (platformType == KotlinPlatformType.androidJvm) {
274                                             "android-classes"
275                                         } else {
276                                             "jar"
277                                         }
278                                     it.attributes.attribute(
279                                         Attribute.of("artifactType", String::class.java),
280                                         artifactType
281                                     )
282                                 }
283                                 .files
284                         }
285                 }
286             val sourceSets =
287                 compilationProvider.map { compilation ->
288                     compilation.allKotlinSourceSets.map { sourceSet ->
289                         SourceSetInputs(
290                             sourceSet.name,
291                             sourceSet.dependsOn.map { it.name },
292                             sourceSet.kotlin.sourceDirectories,
293                             project.files(compileDependencies),
294                         )
295                     }
296                 }
297             return MultiplatformCompilationInputs(
298                 project,
299                 sourceSets,
300                 bootClasspath,
301             )
302         }
303     }
304 }
305