• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download

<lambda>null1 package com.google.devtools.ksp.gradle
2 
3 import org.gradle.api.InvalidUserCodeException
4 import org.gradle.api.Project
5 import org.gradle.api.artifacts.Configuration
6 import org.jetbrains.kotlin.gradle.dsl.*
7 import org.jetbrains.kotlin.gradle.plugin.*
8 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinCommonCompilation
9 import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
10 
11 /**
12  * Creates and retrieves ksp-related configurations.
13  */
14 class KspConfigurations(private val project: Project) {
15     companion object {
16         private const val PREFIX = "ksp"
17     }
18 
19     private val allowAllTargetConfiguration =
20         project.findProperty("ksp.allow.all.target.configuration")?.let {
21             it.toString().toBoolean()
22         } ?: true
23 
24     // The "ksp" configuration, applied to every compilations.
25     private val configurationForAll = project.configurations.create(PREFIX)
26 
27     private fun configurationNameOf(vararg parts: String): String {
28         return parts.joinToString("") {
29             it.replaceFirstChar { it.uppercase() }
30         }.replaceFirstChar { it.lowercase() }
31     }
32 
33     @OptIn(ExperimentalStdlibApi::class)
34     private fun createConfiguration(
35         name: String,
36         readableSetName: String,
37     ): Configuration {
38         // maybeCreate to be future-proof, but we should never have a duplicate with current logic
39         return project.configurations.maybeCreate(name).apply {
40             description = "KSP dependencies for the '$readableSetName' source set."
41             isCanBeResolved = false // we'll resolve the processor classpath config
42             isCanBeConsumed = false
43             isVisible = false
44         }
45     }
46 
47     private fun getAndroidConfigurationName(target: KotlinTarget, sourceSet: String): String {
48         val isMain = sourceSet.endsWith("main", ignoreCase = true)
49         val nameWithoutMain = when {
50             isMain -> sourceSet.substring(0, sourceSet.length - 4)
51             else -> sourceSet
52         }
53         // Note: on single-platform, target name is conveniently set to "".
54         return configurationNameOf(PREFIX, target.name, nameWithoutMain)
55     }
56 
57     private fun getKotlinConfigurationName(compilation: KotlinCompilation<*>, sourceSet: KotlinSourceSet): String {
58         val isMain = compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME
59         val isDefault = sourceSet.name == compilation.defaultSourceSetName && compilation !is KotlinCommonCompilation
60         // Note: on single-platform, target name is conveniently set to "".
61         val name = if (isMain && isDefault) {
62             // For js(IR), js(LEGACY), the target "js" is created.
63             //
64             // When js(BOTH) is used, target "jsLegacy" and "jsIr" are created.
65             // Both targets share the same source set. Therefore configurations other than main compilation
66             // are shared. E.g., "kspJsTest".
67             // For simplicity and consistency, let's not distinguish them.
68             when (val targetName = compilation.target.name) {
69                 "jsLegacy", "jsIr" -> "js"
70                 else -> targetName
71             }
72         } else if (compilation is KotlinCommonCompilation) {
73             sourceSet.name + compilation.target.name.capitalize()
74         } else {
75             sourceSet.name
76         }
77         return configurationNameOf(PREFIX, name)
78     }
79 
80     init {
81         project.plugins.withType(KotlinBasePluginWrapper::class.java).configureEach {
82             // 1.6.0: decorateKotlinProject(project.kotlinExtension)?
83             decorateKotlinProject(project.extensions.getByName("kotlin") as KotlinProjectExtension, project)
84         }
85     }
86 
87     private fun decorateKotlinProject(kotlin: KotlinProjectExtension, project: Project) {
88         when (kotlin) {
89             is KotlinSingleTargetExtension<*> -> decorateKotlinTarget(kotlin.target)
90             is KotlinMultiplatformExtension -> {
91                 kotlin.targets.configureEach(::decorateKotlinTarget)
92 
93                 var reported = false
94                 configurationForAll.dependencies.whenObjectAdded {
95                     if (!reported) {
96                         reported = true
97                         val msg = "The 'ksp' configuration is deprecated in Kotlin Multiplatform projects. " +
98                             "Please use target-specific configurations like 'kspJvm' instead."
99 
100                         if (allowAllTargetConfiguration) {
101                             project.logger.warn(msg)
102                         } else {
103                             throw InvalidUserCodeException(msg)
104                         }
105                     }
106                 }
107             }
108         }
109     }
110 
111     /**
112      * Decorate the [KotlinSourceSet]s belonging to [target] to create one KSP configuration per source set,
113      * named ksp<SourceSet>. The only exception is the main source set, for which we avoid using the
114      * "main" suffix (so what would be "kspJvmMain" becomes "kspJvm").
115      *
116      * For Android, we prefer to use AndroidSourceSets from AGP rather than [KotlinSourceSet]s.
117      * Even though the Kotlin Plugin does create [KotlinSourceSet]s out of AndroidSourceSets
118      * ( https://kotlinlang.org/docs/mpp-configure-compilations.html#compilation-of-the-source-set-hierarchy ),
119      * there are slight differences between the two - Kotlin creates some extra sets with unexpected word ordering,
120      * and things get worse when you add product flavors. So, we use AGP sets as the source of truth.
121      */
122     private fun decorateKotlinTarget(target: KotlinTarget) {
123         if (target.platformType == KotlinPlatformType.androidJvm) {
124             AndroidPluginIntegration.forEachAndroidSourceSet(target.project) { sourceSet ->
125                 createConfiguration(
126                     name = getAndroidConfigurationName(target, sourceSet),
127                     readableSetName = "$sourceSet (Android)"
128                 )
129             }
130         } else {
131             target.compilations.configureEach { compilation ->
132                 compilation.kotlinSourceSets.forEach { sourceSet ->
133                     createConfiguration(
134                         name = getKotlinConfigurationName(compilation, sourceSet),
135                         readableSetName = sourceSet.name
136                     )
137                 }
138             }
139         }
140     }
141 
142     /**
143      * Returns the user-facing configurations involved in the given compilation.
144      * We use [KotlinCompilation.kotlinSourceSets], not [KotlinCompilation.allKotlinSourceSets] for a few reasons:
145      * 1) consistency with how we created the configurations. For example, all* can return user-defined sets
146      *    that don't belong to any compilation, like user-defined intermediate source sets (e.g. iosMain).
147      *    These do not currently have their own ksp configuration.
148      * 2) all* can return sets belonging to other [KotlinCompilation]s
149      *
150      * See test: SourceSetConfigurationsTest.configurationsForMultiplatformApp_doesNotCrossCompilationBoundaries
151      */
152     fun find(compilation: KotlinCompilation<*>): Set<Configuration> {
153         val results = mutableListOf<String>()
154         if (compilation is KotlinCommonCompilation) {
155             results.add(getKotlinConfigurationName(compilation, compilation.defaultSourceSet))
156         }
157         compilation.kotlinSourceSets.mapTo(results) {
158             getKotlinConfigurationName(compilation, it)
159         }
160         if (compilation.platformType == KotlinPlatformType.androidJvm) {
161             compilation as KotlinJvmAndroidCompilation
162             AndroidPluginIntegration.getCompilationSourceSets(compilation).mapTo(results) {
163                 getAndroidConfigurationName(compilation.target, it)
164             }
165         }
166 
167         // Include the `ksp` configuration, if it exists, for all compilations.
168         if (allowAllTargetConfiguration) {
169             results.add(configurationForAll.name)
170         }
171 
172         return results.mapNotNull {
173             compilation.target.project.configurations.findByName(it)
174         }.toSet()
175     }
176 }
177