summaryrefslogtreecommitdiff
path: root/plugins/kotlin/gradle/gradle-java/src/org/jetbrains/kotlin/idea/gradleJava/configuration/mpp/populateModuleDependenciesBySourceSetVisibilityGraph.kt
blob: c0e227fa5c951245269285f2fd8897aa88775655 (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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package org.jetbrains.kotlin.idea.gradleJava.configuration.mpp

import com.google.common.graph.Graph
import com.intellij.openapi.externalSystem.model.DataNode
import org.jetbrains.annotations.NonNls
import org.jetbrains.kotlin.idea.gradle.configuration.klib.KotlinNativeLibraryNameUtil
import org.jetbrains.kotlin.idea.gradle.configuration.kotlinAndroidSourceSets
import org.jetbrains.kotlin.idea.gradle.configuration.kotlinSourceSetData
import org.jetbrains.kotlin.idea.gradle.configuration.utils.createSourceSetVisibilityGraph
import org.jetbrains.kotlin.idea.gradle.configuration.utils.transitiveClosure
import org.jetbrains.kotlin.idea.gradleJava.configuration.KotlinMPPGradleProjectResolver
import org.jetbrains.kotlin.idea.gradleJava.configuration.KotlinMPPGradleProjectResolver.Companion.CompilationWithDependencies
import org.jetbrains.kotlin.idea.gradleJava.configuration.utils.fullName
import org.jetbrains.kotlin.idea.gradleJava.configuration.utils.getKotlinModuleId
import org.jetbrains.kotlin.idea.gradleTooling.*
import org.jetbrains.kotlin.idea.projectModel.KotlinCompilation
import org.jetbrains.kotlin.idea.projectModel.KotlinPlatform
import org.jetbrains.kotlin.idea.projectModel.KotlinSourceSet
import org.jetbrains.plugins.gradle.model.ExternalDependency
import org.jetbrains.plugins.gradle.model.data.GradleSourceSetData

internal fun KotlinMPPGradleProjectResolver.Companion.populateModuleDependenciesBySourceSetVisibilityGraph(
    context: KotlinMppPopulateModuleDependenciesContext
): Unit = with(context) {
    val sourceSetVisibilityGraph = createSourceSetVisibilityGraph(mppModel).transitiveClosure
    for (sourceSet in sourceSetVisibilityGraph.nodes()) {
        populateSourceSetInfos(context, sourceSetVisibilityGraph, sourceSet)
        if (shouldDelegateToOtherPlugin(sourceSet)) continue


        val visibleSourceSets = sourceSetVisibilityGraph.successors(sourceSet) - sourceSet
        val fromDataNode = getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx)?.cast<GradleSourceSetData>()
            ?: continue


        /* Add dependencies from current sourceSet to all visible source sets (dependsOn, test to production, ...)*/
        for (visibleSourceSet in visibleSourceSets) {
            val toDataNode = getSiblingKotlinModuleData(visibleSourceSet, gradleModule, ideModule, resolverCtx) ?: continue
            addDependency(fromDataNode, toDataNode, visibleSourceSet.isTestModule)
        }

        if (!processedModuleIds.add(getKotlinModuleId(gradleModule, sourceSet, resolverCtx))) continue
        val settings = dependencyPopulationSettings(mppModel, sourceSet)
        val directDependencies = getDependencies(sourceSet).toSet()
        val directIntransitiveDependencies = getIntransitiveDependencies(sourceSet).toSet()
        val dependenciesFromVisibleSourceSets = getDependenciesFromVisibleSourceSets(settings, visibleSourceSets)
        val dependenciesFromNativePropagation = getPropagatedNativeDependencies(settings, sourceSet)
        val dependenciesFromPlatformPropagation = getPropagatedPlatformDependencies(sourceSet)

        val dependencies = dependenciesPreprocessor(
            dependenciesFromNativePropagation + dependenciesFromPlatformPropagation
                    + dependenciesFromVisibleSourceSets + directDependencies + directIntransitiveDependencies
        )

        buildDependencies(resolverCtx, sourceSetMap, artifactsMap, fromDataNode, dependencies, ideProject)
    }
}

private data class DependencyPopulationSettings(
    val forceNativeDependencyPropagation: Boolean,
    val excludeInheritedNativeDependencies: Boolean
)

private fun dependencyPopulationSettings(mppModel: KotlinMPPGradleModel, sourceSet: KotlinSourceSet): DependencyPopulationSettings {
    val forceNativeDependencyPropagation: Boolean
    val excludeInheritedNativeDependencies: Boolean
    if (mppModel.extraFeatures.isHMPPEnabled && sourceSet.actualPlatforms.singleOrNull() == KotlinPlatform.NATIVE) {
        forceNativeDependencyPropagation = mppModel.extraFeatures.isNativeDependencyPropagationEnabled
        excludeInheritedNativeDependencies = !forceNativeDependencyPropagation
    } else {
        forceNativeDependencyPropagation = false
        excludeInheritedNativeDependencies = false
    }
    return DependencyPopulationSettings(
        forceNativeDependencyPropagation = forceNativeDependencyPropagation,
        excludeInheritedNativeDependencies = excludeInheritedNativeDependencies
    )
}

private fun KotlinMppPopulateModuleDependenciesContext.getDependenciesFromVisibleSourceSets(
    settings: DependencyPopulationSettings,
    visibleSourceSets: Set<KotlinSourceSet>
): Set<KotlinDependency> {
    val inheritedDependencies = visibleSourceSets
        .flatMap { visibleSourceSet -> getRegularDependencies(visibleSourceSet) }

    return if (settings.excludeInheritedNativeDependencies) {
        inheritedDependencies.filter { !it.name.startsWith(KotlinNativeLibraryNameUtil.KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE) }
    } else {
        inheritedDependencies
    }.toSet()
}

private fun KotlinMppPopulateModuleDependenciesContext.getPropagatedNativeDependencies(
    settings: DependencyPopulationSettings,
    sourceSet: KotlinSourceSet
): Set<KotlinDependency> {
    if (!settings.forceNativeDependencyPropagation) {
        return emptySet()
    }

    return getPropagatedNativeDependencies(getCompilationsWithDependencies(sourceSet)).toSet()
}

/**
 * We can't really commonize native platform libraries yet.
 * But APIs for different targets may be very similar.
 * E.g. ios_arm64 and ios_x64 have almost identical platform libraries
 * We handle these special cases and resolve common sources for such
 * targets against libraries of one of them. E.g. common sources for
 * ios_x64 and ios_arm64 will be resolved against ios_arm64 libraries.
 * Currently such special casing is available for Apple platforms
 * (iOS, watchOS and tvOS) and native Android (ARM, X86).
 * TODO: Do we need to support user's interop libraries too?
 */
private fun getPropagatedNativeDependencies(compilations: List<CompilationWithDependencies>): List<ExternalDependency> {
    if (compilations.size <= 1) {
        return emptyList()
    }

    val copyFrom = when {
        compilations.all { it.isAppleCompilation } ->
            compilations.selectFirstAvailableTarget(
                "watchos_arm64", "watchos_arm32", "watchos_x86",
                "ios_arm64", "ios_arm32", "ios_x64",
                "tvos_arm64", "tvos_x64"
            )
        compilations.all { it.konanTarget?.startsWith("android") == true } ->
            compilations.selectFirstAvailableTarget(
                "android_arm64", "android_arm32", "android_x64", "android_x86"
            )
        else -> return emptyList()
    }

    return copyFrom.dependencyNames.mapNotNull { (name, dependency) ->
        when {
            !name.startsWith(KotlinNativeLibraryNameUtil.KOTLIN_NATIVE_LIBRARY_PREFIX_PLUS_SPACE) -> null  // Support only default platform libs for now.
            compilations.all { it.dependencyNames.containsKey(name) } -> dependency
            else -> null
        }
    }
}

private val CompilationWithDependencies.isAppleCompilation: Boolean
    get() = konanTarget?.let {
        it.startsWith("ios") || it.startsWith("watchos") || it.startsWith("tvos")
    } ?: false

private fun Iterable<CompilationWithDependencies>.selectFirstAvailableTarget(
    @NonNls vararg targetsByPriority: String
): CompilationWithDependencies {
    for (target in targetsByPriority) {
        val result = firstOrNull { it.konanTarget == target }
        if (result != null) {
            return result
        }
    }
    return first()
}


private fun KotlinMppPopulateModuleDependenciesContext.getPropagatedPlatformDependencies(
    sourceSet: KotlinSourceSet
): Set<KotlinDependency> {
    if (!mppModel.extraFeatures.isHMPPEnabled) {
        return emptySet()
    }

    return getPropagatedPlatformDependencies(mppModel, sourceSet)
}


/**
 * Source sets sharing code between JVM and Android are the only intermediate source sets that
 * can effectively consume a dependency's platform artifact.
 * When a library only offers a JVM variant, then Android and JVM consume this variant of the library.
 *
 * This will be replaced later on by [KT-43450](https://youtrack.jetbrains.com/issue/KT-43450)
 *
 * @return all dependencies being present across given JVM and Android compilations that this [sourceSet] can also participates in.
 */
private fun getPropagatedPlatformDependencies(
    mppModel: KotlinMPPGradleModel,
    sourceSet: KotlinSourceSet,
): Set<ExternalDependency> {
    if (
        sourceSet.actualPlatforms.platforms.sorted() == listOf(KotlinPlatform.JVM, KotlinPlatform.ANDROID).sorted()
    ) {
        return mppModel.targets
            .filter { target -> target.platform == KotlinPlatform.JVM || target.platform == KotlinPlatform.ANDROID }
            .flatMap { target -> target.compilations }
            .filter { compilation -> compilation.dependsOnSourceSet(mppModel, sourceSet) }
            .map { targetCompilations -> targetCompilations.dependencies.mapNotNull(mppModel.dependencyMap::get).toSet() }
            .reduceOrNull { acc, dependencies -> acc.intersect(dependencies) }.orEmpty()
    }

    return emptySet()
}


// TODO: Move this maybe to another semantic part of KotlinMPPGradleProjectResolver?
private fun KotlinMPPGradleProjectResolver.Companion.populateSourceSetInfos(
    context: KotlinMppPopulateModuleDependenciesContext,
    closedSourceSetGraph: Graph<KotlinSourceSet>,
    sourceSet: KotlinSourceSet
) = with(context) {
    val isAndroid = shouldDelegateToOtherPlugin(sourceSet)
    val fromDataNode = if (isAndroid) ideModule
    else getSiblingKotlinModuleData(sourceSet, gradleModule, ideModule, resolverCtx) ?: return

    val dependeeSourceSets = closedSourceSetGraph.successors(sourceSet)
    val sourceSetInfos = if (isAndroid) {
        ideModule.kotlinAndroidSourceSets?.filter {
            (it.kotlinModule as? KotlinCompilation)?.declaredSourceSets?.contains(sourceSet) ?: false
        } ?: emptyList()
    } else {
        listOfNotNull(fromDataNode.kotlinSourceSetData?.sourceSetInfo)
    }
    for (sourceSetInfo in sourceSetInfos) {
        if (sourceSetInfo.kotlinModule is KotlinCompilation) {
            val selfName = sourceSetInfo.kotlinModule.fullName()
            sourceSetInfo.addSourceSets(dependeeSourceSets, selfName, gradleModule, resolverCtx)
        }
    }
}

inline fun <reified T : Any> DataNode<*>.cast(): DataNode<T> {
    if (data !is T) {
        throw ClassCastException("DataNode<${data.javaClass.canonicalName}> cannot be cast to DataNode<${T::class.java.canonicalName}>")
    }
    @Suppress("UNCHECKED_CAST")
    return this as DataNode<T>
}