aboutsummaryrefslogtreecommitdiff
path: root/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt
blob: 45b23d6a6e9bdbf54467dc71384d9960418a088b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
package com.google.devtools.ksp.gradle

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 {
        const val ROOT = "ksp"
    }

    // "ksp" configuration. In single-platform projects, it is applied to the "main" sourceSet.
    // In multi-platform projects, it is applied to the "main" sourceSet of all targets.
    private val rootMainConfiguration = project.configurations.create(ROOT)

    // Stores all saved configurations for quick access.
    private val kotlinConfigurations = mutableMapOf<KotlinSourceSet, Configuration>()
    private val androidConfigurations = mutableMapOf<String, Configuration>()

    @OptIn(ExperimentalStdlibApi::class)
    private fun <T: Any> saveConfiguration(
        owner: T,
        parent: Configuration?,
        name: String,
        cache: MutableMap<T, Configuration>
    ): Configuration {
        val configName = ROOT + name.replaceFirstChar { it.uppercase() }
        val existingConfig = project.configurations.findByName(configName)
        if (existingConfig != null && configName != ROOT) {
            error("Unexpected duplicate configuration ($configName).")
        }

        val config = existingConfig ?: project.configurations.create(configName)
        if (parent != null && parent.name != configName) {
            config.extendsFrom(parent)
        }
        cache[owner] = config
        return config
    }

    private fun saveKotlinConfiguration(owner: KotlinSourceSet, parent: Configuration?, name: String) =
        saveConfiguration(owner, parent, name, kotlinConfigurations)

    private fun saveAndroidConfiguration(owner: String, parent: Configuration?, name: String) =
        saveConfiguration(owner, parent, name, androidConfigurations)

    init {
        project.plugins.withType(KotlinBasePluginWrapper::class.java).configureEach {
            // 1.6.0: decorateKotlinProject(project.kotlinExtension)
            decorateKotlinProject(project.extensions.getByName("kotlin") as KotlinProjectExtension)
        }
    }

    private fun decorateKotlinProject(kotlin: KotlinProjectExtension) {
        when (kotlin) {
            is KotlinMultiplatformExtension -> kotlin.targets.configureEach(::decorateKotlinTarget)
            is KotlinSingleTargetExtension -> decorateKotlinTarget(kotlin.target)
        }
    }

    /**
     * Decorate the source sets belonging to [target].
     * The end goal is to have one KSP configuration per source set. Examples:
     * - in a kotlin-jvm project, we want "ksp" (applied to main set) and "kspTest" (applied to test set)
     * - in a kotlin-multiplatform project, we want "ksp<Target>" and "ksp<Target>Test" for each target.
     * This is done by reading [KotlinCompilation.kotlinSourceSets], which contains appropriately named sets.
     *
     * For Android, we prefer to use AndroidSourceSets from AGP rather than [KotlinSourceSet]s like all other
     * targets. There are very slight differences between the two - this could be re-evaluated in the future,
     * because Kotlin Plugin does already create [KotlinSourceSet]s out of AndroidSourceSets
     * ( https://kotlinlang.org/docs/mpp-configure-compilations.html#compilation-of-the-source-set-hierarchy ).
     */
    private fun decorateKotlinTarget(target: KotlinTarget) {
        if (target.platformType == KotlinPlatformType.androidJvm) {
            AndroidPluginIntegration.findSourceSets(target.project) { setName ->
                val isMain = setName.endsWith("main", ignoreCase = true)
                val nameWithoutMain = when {
                    isMain -> setName.substring(0, setName.length - 4)
                    else -> setName
                }
                val nameWithTargetPrefix = when {
                    target.name.isEmpty() -> nameWithoutMain
                    else -> target.name + nameWithoutMain.replaceFirstChar { it.uppercase() }
                }
                val parent = if (isMain) rootMainConfiguration else null
                saveAndroidConfiguration(setName, parent, nameWithTargetPrefix)
            }
        } else {
            // We could add target-specific configurations here (kspJvm, parent of kspJvmMain & kspJvmTest)
            // but we decided that kspJvm should actually mean kspJvmMain, which in turn is not created.
            target.compilations.configureEach { compilation ->
                val isMain = compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME
                compilation.kotlinSourceSets.forEach { sourceSet ->
                    val isDefault = sourceSet.name == compilation.defaultSourceSetName
                    decorateKotlinSourceSet(compilation, isMain, sourceSet, isDefault)
                }
            }
        }
    }

    private fun decorateKotlinSourceSet(
        compilation: KotlinCompilation<*>,
        isMainCompilation: Boolean,
        sourceSet: KotlinSourceSet,
        isDefaultSourceSet: Boolean
    ) {
        val parent = if (isMainCompilation) rootMainConfiguration else null
        if (isMainCompilation && isDefaultSourceSet) {
            // Use target name instead of sourceSet name, to avoid creating "kspMain" or "kspJvmMain".
            // Note: on single-platform, target name is conveniently set to "" so this resolves to "ksp".
            saveKotlinConfiguration(sourceSet, parent, compilation.target.name)
        } else {
            saveKotlinConfiguration(sourceSet, parent, 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
     * 2) all* can return sets belonging to other compilations. In this case the dependency should be tracked
     *    by Gradle at the task level, not by us through configurations.
     * 3) all* can return user-defined sets belonging to no compilation, like intermediate source sets defined
     *    to share code between targets. They do not currently have their own ksp configuration.
     */
    fun find(compilation: KotlinCompilation<*>): Set<Configuration> {
        val kotlinSourceSets = compilation.kotlinSourceSets
        val kotlinConfigurations = kotlinSourceSets.mapNotNull { kotlinConfigurations[it] }
        val androidConfigurations = if (compilation.platformType == KotlinPlatformType.androidJvm) {
            val androidSourceSets = AndroidPluginIntegration.getCompilationSourceSets(compilation as KotlinJvmAndroidCompilation)
            androidSourceSets.mapNotNull { androidConfigurations[it] }
        } else emptyList()
        return (kotlinConfigurations + androidConfigurations).toSet()
    }
}