<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