aboutsummaryrefslogtreecommitdiff
path: root/gradle-plugin/src/main/kotlin/com/google/devtools/ksp/gradle/KspConfigurations.kt
blob: e43258941ba8f9acc93f3ed74f9958af08d3f232 (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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
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"
    }

    private val allowAllTargetConfiguration =
        project.findProperty("ksp.allow.all.target.configuration")?.let {
            it.toString().toBoolean()
        } ?: true

    // The "ksp" configuration, applied to every compilations.
    private val configurationForAll = project.configurations.create(PREFIX)

    private fun configurationNameOf(vararg parts: String): String {
        return parts.joinToString("") {
            it.replaceFirstChar { it.uppercase() }
        }.replaceFirstChar { it.lowercase() }
    }

    @OptIn(ExperimentalStdlibApi::class)
    private fun createConfiguration(
        name: String,
        readableSetName: String,
    ): Configuration {
        // maybeCreate to be future-proof, but we should never have a duplicate with current logic
        return project.configurations.maybeCreate(name).apply {
            description = "KSP dependencies for the '$readableSetName' source set."
            isCanBeResolved = false // we'll resolve the processor classpath config
            isCanBeConsumed = false
            isVisible = false
        }
    }

    private fun getAndroidConfigurationName(target: KotlinTarget, sourceSet: String): String {
        val isMain = sourceSet.endsWith("main", ignoreCase = true)
        val nameWithoutMain = when {
            isMain -> sourceSet.substring(0, sourceSet.length - 4)
            else -> sourceSet
        }
        // Note: on single-platform, target name is conveniently set to "".
        return configurationNameOf(PREFIX, target.name, nameWithoutMain)
    }

    private fun getKotlinConfigurationName(compilation: KotlinCompilation<*>, sourceSet: KotlinSourceSet): String {
        val isMain = compilation.name == KotlinCompilation.MAIN_COMPILATION_NAME
        val isDefault = sourceSet.name == compilation.defaultSourceSetName
        // Note: on single-platform, target name is conveniently set to "".
        val name = if (isMain && isDefault) {
            // For js(IR), js(LEGACY), the target "js" is created.
            //
            // When js(BOTH) is used, target "jsLegacy" and "jsIr" are created.
            // Both targets share the same source set. Therefore configurations other than main compilation
            // are shared. E.g., "kspJsTest".
            // For simplicity and consistency, let's not distinguish them.
            when (val targetName = compilation.target.name) {
                "jsLegacy", "jsIr" -> "js"
                else -> targetName
            }
        } else {
            sourceSet.name
        }
        return configurationNameOf(PREFIX, name)
    }

    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)

                var reported = false
                configurationForAll.dependencies.whenObjectAdded {
                    if (!reported) {
                        reported = true
                        val msg = "The 'ksp' configuration is deprecated in Kotlin Multiplatform projects. " +
                            "Please use target-specific configurations like 'kspJvm' instead."

                        if (allowAllTargetConfiguration) {
                            project.logger.warn(msg)
                        } else {
                            throw InvalidUserCodeException(msg)
                        }
                    }
                }
            }
        }
    }

    /**
     * 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.forEachAndroidSourceSet(target.project) { sourceSet ->
                createConfiguration(
                    name = getAndroidConfigurationName(target, sourceSet),
                    readableSetName = "$sourceSet (Android)"
                )
            }
        } else {
            target.compilations.configureEach { compilation ->
                compilation.kotlinSourceSets.forEach { sourceSet ->
                    createConfiguration(
                        name = getKotlinConfigurationName(compilation, sourceSet),
                        readableSetName = 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 results = mutableListOf<String>()
        compilation.kotlinSourceSets.mapTo(results) {
            getKotlinConfigurationName(compilation, it)
        }
        if (compilation.platformType == KotlinPlatformType.androidJvm) {
            compilation as KotlinJvmAndroidCompilation
            AndroidPluginIntegration.getCompilationSourceSets(compilation).mapTo(results) {
                getAndroidConfigurationName(compilation.target, it)
            }
        }

        // Include the `ksp` configuration, if it exists, for all compilations.
        if (allowAllTargetConfiguration) {
            results.add(configurationForAll.name)
        }

        return results.mapNotNull {
            compilation.target.project.configurations.findByName(it)
        }.toSet()
    }
}