blob: 5423a5349b146faa2509ce8568a831de76ccd612 [file] [log] [blame]
package com.google.devtools.ksp.gradle
import org.gradle.api.InvalidUserCodeException
import org.gradle.api.Project
import org.gradle.api.artifacts.Configuration
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJvmAndroidCompilation
/**
* Creates and retrieves ksp-related configurations.
*/
class KspConfigurations(private val project: Project) {
companion object {
private const val PREFIX = "ksp"
}
// Store all ksp configurations for quick retrieval.
private val kotlinConfigurations = mutableMapOf<KotlinSourceSet, Configuration>()
private val androidConfigurations = mutableMapOf<String, Configuration>()
@OptIn(ExperimentalStdlibApi::class)
private fun <T : Any> saveConfiguration(
ownerSet: T,
ownerName: String,
name: String,
cache: MutableMap<T, Configuration>
): Configuration {
val configName = PREFIX + name.replaceFirstChar { it.uppercase() }
// maybeCreate to be future-proof, but we should never have a duplicate with current logic
val config = project.configurations.maybeCreate(configName).apply {
description = "KSP dependencies for the '$ownerName' source set."
isCanBeResolved = false // we'll resolve the processor classpath config
isCanBeConsumed = false
isVisible = false
}
cache[ownerSet] = config
return config
}
private fun saveKotlinConfiguration(owner: KotlinSourceSet, name: String) =
saveConfiguration(owner, owner.name, name, kotlinConfigurations)
private fun saveAndroidConfiguration(key: String, name: String) =
saveConfiguration(key, "$key (Android)", name, androidConfigurations)
init {
project.plugins.withType(KotlinBasePluginWrapper::class.java).configureEach {
// 1.6.0: decorateKotlinProject(project.kotlinExtension)?
decorateKotlinProject(project.extensions.getByName("kotlin") as KotlinProjectExtension, project)
}
}
private fun decorateKotlinProject(kotlin: KotlinProjectExtension, project: Project) {
when (kotlin) {
is KotlinSingleTargetExtension -> decorateKotlinTarget(kotlin.target)
is KotlinMultiplatformExtension -> {
kotlin.targets.configureEach(::decorateKotlinTarget)
// Adding multiplatform configuration removed support for the root ksp configuration.
// Try to make this breaking change less breaking by adding a clear error.
project.configurations.create("ksp").dependencies.all {
throw InvalidUserCodeException(
"The 'ksp' configuration cannot be used in Kotlin Multiplatform projects. " +
"Please use target-specific configurations like 'kspJvm' instead."
)
}
}
}
}
/**
* Decorate the [KotlinSourceSet]s belonging to [target] to create one KSP configuration per source set,
* named ksp<SourceSet>. The only exception is the main source set, for which we avoid using the
* "main" suffix (so what would be "kspJvmMain" becomes "kspJvm").
*
* For Android, we prefer to use AndroidSourceSets from AGP rather than [KotlinSourceSet]s.
* Even though the Kotlin Plugin does create [KotlinSourceSet]s out of AndroidSourceSets
* ( https://kotlinlang.org/docs/mpp-configure-compilations.html#compilation-of-the-source-set-hierarchy ),
* there are slight differences between the two - Kotlin creates some extra sets with unexpected word ordering,
* and things get worse when you add product flavors. So, we use AGP sets as the source of truth.
*/
private fun decorateKotlinTarget(target: KotlinTarget) {
if (target.platformType == KotlinPlatformType.androidJvm) {
AndroidPluginIntegration.findSourceSets(target.project) { sourceSet ->
val isMain = sourceSet.endsWith("main", ignoreCase = true)
val nameWithoutMain = when {
isMain -> sourceSet.substring(0, sourceSet.length - 4)
else -> sourceSet
}
val nameWithTargetPrefix = when {
target.name.isEmpty() -> nameWithoutMain
else -> target.name + nameWithoutMain.replaceFirstChar { it.uppercase() }
}
saveAndroidConfiguration(sourceSet, nameWithTargetPrefix)
}
} else {
target.compilations.configureEach { compilation ->
val isMain = compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME
compilation.kotlinSourceSets.forEach { sourceSet ->
val isDefault = sourceSet.name == compilation.defaultSourceSetName
// Note: on single-platform, target name is conveniently set to "" so this resolves to "ksp".
val name = if (isMain && isDefault) target.name else sourceSet.name
saveKotlinConfiguration(sourceSet, name)
}
}
}
}
/**
* Returns the user-facing configurations involved in the given compilation.
* We use [KotlinCompilation.kotlinSourceSets], not [KotlinCompilation.allKotlinSourceSets] for a few reasons:
* 1) consistency with how we created the configurations. For example, all* can return user-defined sets
* that don't belong to any compilation, like user-defined intermediate source sets (e.g. iosMain).
* These do not currently have their own ksp configuration.
* 2) all* can return sets belonging to other [KotlinCompilation]s
*
* See test: SourceSetConfigurationsTest.configurationsForMultiplatformApp_doesNotCrossCompilationBoundaries
*/
fun find(compilation: KotlinCompilation<*>): Set<Configuration> {
val kotlinConfigurations = compilation.kotlinSourceSets.mapNotNull { kotlinConfigurations[it] }
val androidConfigurations = if (compilation.platformType == KotlinPlatformType.androidJvm) {
compilation as KotlinJvmAndroidCompilation
val androidSourceSets = AndroidPluginIntegration.getCompilationSourceSets(compilation)
androidSourceSets.mapNotNull { androidConfigurations[it] }
} else emptyList()
return (kotlinConfigurations + androidConfigurations).toSet()
}
}