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