Resolve dependencies as graph, support multiple modules in a project

This commit is contained in:
Sergey Igushkin
2021-01-18 13:27:25 +03:00
committed by TeamCityServer
parent f1fb438d00
commit d66f95b80e
17 changed files with 516 additions and 350 deletions
@@ -9,7 +9,7 @@ package org.jetbrains.kotlin.project.model
// TODO think about state management: unresolved -> (known dependency graph?) ... -> completely resolved
// it seems to be important to learn whether or not the model is final
interface ModuleDependencyResolver {
fun resolveDependency(moduleDependency: ModuleDependency): KotlinModule?
fun resolveDependency(moduleDependency: KotlinModuleDependency): KotlinModule?
}
// TODO merge with ModuleDependencyResolver?
@@ -17,5 +17,21 @@ interface ModuleDependencyResolver {
interface DependencyDiscovery {
// TODO return dependency graph rather than just iterable?
// TODO make this a partial function, too
fun discoverDependencies(fragment: KotlinModuleFragment): Iterable<ModuleDependency>
}
fun discoverDependencies(fragment: KotlinModuleFragment): Iterable<KotlinModuleDependency>
}
interface KotlinDependencyGraphResolver {
// TODO add explicit dependency consistency scopes if we decide to keep non-production code in the same variant
fun resolveDependencyGraph(requestingModule: KotlinModule): DependencyGraphResolution
}
sealed class DependencyGraphResolution(val requestingModule: KotlinModule) {
class Unknown(requestingModule: KotlinModule) : DependencyGraphResolution(requestingModule)
class DependencyGraph(requestingModule: KotlinModule, val root: DependencyGraphNode): DependencyGraphResolution(requestingModule)
}
// TODO: should this be a single graph for all dependency scopes as well, not just for all fragments?
class DependencyGraphNode(
val module: KotlinModule,
val dependenciesByFragment: Map<KotlinModuleFragment, Iterable<DependencyGraphNode>>
)
@@ -1,135 +0,0 @@
/*
* Copyright 2010-2020 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package org.jetbrains.kotlin.project.model
import org.jetbrains.kotlin.project.model.utils.variantsContainingFragment
import java.util.ArrayDeque
interface InternalDependencyExpansion {
fun expandInternalFragmentDependencies(consumingFragment: KotlinModuleFragment): InternalDependencyExpansionResult
}
class InternalDependencyExpansionResult(
val entries: Iterable<Entry>
) {
sealed class ExpansionOutcome(val variantMatchingResults: Iterable<VariantResolution>) {
class VisibleFragments(
val fragments: Iterable<KotlinModuleFragment>,
variantMatchingResults: Iterable<VariantResolution>
) : ExpansionOutcome(
variantMatchingResults
)
class Failure(variantMatchingResults: Iterable<VariantResolution>) : ExpansionOutcome(variantMatchingResults)
}
class Entry(
val dependingFragment: KotlinModuleFragment,
val dependencyFragment: KotlinModuleFragment,
val outcome: ExpansionOutcome
)
}
fun InternalDependencyExpansionResult.visibleFragments(): List<KotlinModuleFragment> =
entries.flatMap { entry -> entry.outcome.visibleFragments() }
fun InternalDependencyExpansionResult.ExpansionOutcome.visibleFragments(): Iterable<KotlinModuleFragment> = when (this) {
is InternalDependencyExpansionResult.ExpansionOutcome.VisibleFragments -> fragments
else -> emptyList()
}
class DefaultInternalDependencyExpansion(
private val variantResolver: ContainingModuleVariantResolver
) : InternalDependencyExpansion {
interface ContainingModuleVariantResolver {
fun getChosenVariant(
dependingVariant: KotlinModuleVariant,
candidateVariants: Iterable<KotlinModuleVariant>
): VariantResolution
}
override fun expandInternalFragmentDependencies(consumingFragment: KotlinModuleFragment): InternalDependencyExpansionResult {
val visited = mutableSetOf(consumingFragment)
val answer = mutableListOf<InternalDependencyExpansionResult.Entry>()
/**
* For each fragment **t** that we consider a source of declared dependencies that we should expand for [consumingFragment]:
* - also consider the refines-closure of **t** as sources of declared dependencies
* - expand the declared dependencies of **t** to a set of fragments **F**
* - add the expansion result to the answer
* - also consider all fragments in **F** as sources of declared dependencies
*/
val declaredDependencySourceQueue = ArrayDeque<KotlinModuleFragment>().apply { add(consumingFragment) }
while (declaredDependencySourceQueue.isNotEmpty()) {
val declaredDependencySource = declaredDependencySourceQueue.removeFirst()
declaredDependencySourceQueue.addAll(declaredDependencySource.refinesClosure.filter(visited::add))
val declaredDependenciesToExpand = declaredDependencySource.declaredContainingModuleFragmentDependencies
declaredDependenciesToExpand.forEach { declaredDependency ->
val answerEntry = expandSingleDeclaredDependency(consumingFragment, declaredDependencySource, declaredDependency)
answer += answerEntry
declaredDependencySourceQueue.addAll(answerEntry.outcome.visibleFragments())
}
}
return InternalDependencyExpansionResult(answer)
}
private fun expandSingleDeclaredDependency(
consumingFragment: KotlinModuleFragment,
declaredDependencySource: KotlinModuleFragment,
declaredDependency: KotlinModuleFragment
): InternalDependencyExpansionResult.Entry {
/**
* Go over the variants containing the fragment that we infer the expansion for (note: it's the original consuming fragment, which
* may not necessarily be the same as the fragment holding the declared dependency if the latter comes from the closure).
*
* For each such containing variant v_i, let w_i be the variant containing the declared dependency fragment. Failure to choose
* prevents us from inferring the dependency expansion and is reported as a failure to expand the dependency.
*
* Then intersect the refined fragments of each w_i and use the intersection as the result.
*/
val (consumingVariants, producingVariants) =
listOf(consumingFragment, declaredDependency).map { it.containingModule.variantsContainingFragment(it).toSet() }
val chosenVariants = consumingVariants.map { consumingVariant ->
if (consumingVariant in producingVariants)
VariantResolution.VariantMatch(consumingVariant, consumingVariant.containingModule, consumingVariant)
else
variantResolver.getChosenVariant(consumingVariant, producingVariants)
}
val mismatchedConsumingVariants = chosenVariants.filter { it !is VariantResolution.VariantMatch }
val outcome =
if (mismatchedConsumingVariants.isNotEmpty())
InternalDependencyExpansionResult.ExpansionOutcome.Failure(chosenVariants)
else
InternalDependencyExpansionResult.ExpansionOutcome.VisibleFragments(
chosenVariants
.map { (it as VariantResolution.VariantMatch).chosenVariant.refinesClosure }
.reduce { acc, it -> acc.intersect(it) },
chosenVariants
)
return InternalDependencyExpansionResult.Entry(declaredDependencySource, declaredDependency, outcome)
}
}
/**
* This implementation matches the variants of the module by checking
* their [KotlinModuleFragment.declaredContainingModuleFragmentDependencies], similar to how associate compilations work in MPP.
* If more than one such dependency is declared, the result is deemed ambiguous.
*/
class AssociateVariants : DefaultInternalDependencyExpansion.ContainingModuleVariantResolver {
override fun getChosenVariant(
dependingVariant: KotlinModuleVariant,
candidateVariants: Iterable<KotlinModuleVariant>
): VariantResolution {
val result = candidateVariants.filter { it in dependingVariant.declaredContainingModuleFragmentDependencies }
return VariantResolution.fromMatchingVariants(dependingVariant, dependingVariant.containingModule, result)
}
}
@@ -6,34 +6,47 @@
package org.jetbrains.kotlin.project.model
// TODO sealed with an abstract subclass? this will make exhaustive checks work
open class ModuleIdentifier
open class KotlinModuleIdentifier(open val moduleClassifier: String?)
// TODO consider id: Any, to allow IDs with custom equality?
data class LocalModuleIdentifier(val buildId: String, val projectId: String) : ModuleIdentifier() {
data class LocalModuleIdentifier(
val buildId: String,
val projectId: String,
override val moduleClassifier: String?
) : KotlinModuleIdentifier(moduleClassifier) {
companion object {
private const val SINGLE_BUILD_ID = ":"
}
override fun toString(): String = "project '$projectId'" + buildId.takeIf { it != SINGLE_BUILD_ID }?.let { "(build '$it')" }.orEmpty()
override fun toString(): String =
"project '$projectId'" +
moduleClassifier?.let { " / $it" }.orEmpty() +
buildId.takeIf { it != SINGLE_BUILD_ID }?.let { "(build '$it')" }.orEmpty()
}
data class MavenModuleIdentifier(val group: String, val name: String) : ModuleIdentifier() {
override fun toString(): String = "$group:$name"
data class MavenModuleIdentifier(
val group: String,
val name: String,
override val moduleClassifier: String?
) : KotlinModuleIdentifier(moduleClassifier) {
override fun toString(): String = "$group:$name" + moduleClassifier?.let { " / $it" }.orEmpty()
}
// TODO Gradle allows having multiple capabilities in a published module, we need to figure out how we can include them in the module IDs
interface KotlinModule {
val moduleIdentifier: ModuleIdentifier
val moduleIdentifier: KotlinModuleIdentifier
val fragments: Iterable<KotlinModuleFragment>
val variants: Iterable<KotlinModuleVariant>
get() = fragments.filterIsInstance<KotlinModuleVariant>()
// TODO: isSynthetic?
}
class BasicKotlinModule(
override val moduleIdentifier: ModuleIdentifier
override val moduleIdentifier: KotlinModuleIdentifier
) : KotlinModule {
override val fragments = mutableListOf<BasicKotlinModuleFragment>()
@@ -5,7 +5,7 @@
package org.jetbrains.kotlin.project.model
data class ModuleDependency(val moduleIdentifier: ModuleIdentifier) {
data class KotlinModuleDependency(val moduleIdentifier: KotlinModuleIdentifier) {
override fun toString(): String = "dependency $moduleIdentifier"
}
@@ -14,10 +14,8 @@ interface KotlinModuleFragment {
val fragmentName: String
val directRefinesDependencies: Iterable<KotlinModuleFragment>
val declaredContainingModuleFragmentDependencies: Iterable<KotlinModuleFragment>
// TODO: scopes
val declaredModuleDependencies: Iterable<ModuleDependency>
val declaredModuleDependencies: Iterable<KotlinModuleDependency>
val kotlinSourceRoots: Iterable<File>
}
@@ -54,8 +52,7 @@ open class BasicKotlinModuleFragment(
override val directRefinesDependencies: MutableSet<BasicKotlinModuleFragment> = mutableSetOf()
override val declaredContainingModuleFragmentDependencies: MutableSet<BasicKotlinModuleFragment> = mutableSetOf()
override val declaredModuleDependencies: MutableSet<ModuleDependency> = mutableSetOf()
override val declaredModuleDependencies: MutableSet<KotlinModuleDependency> = mutableSetOf()
override var kotlinSourceRoots: Iterable<File> = emptyList()
override fun toString(): String = "fragment $fragmentName"
@@ -7,7 +7,7 @@ package org.jetbrains.kotlin.project.model
import org.jetbrains.kotlin.project.model.utils.variantsContainingFragment
interface KotlinModuleFragmentsResolver {
interface ModuleFragmentsResolver {
fun getChosenFragments(
requestingFragment: KotlinModuleFragment,
dependencyModule: KotlinModule
@@ -30,18 +30,23 @@ sealed class FragmentResolution(val requestingFragment: KotlinModuleFragment, va
FragmentResolution(requestingFragment, dependencyModule)
}
class DefaultKotlinModuleFragmentsResolver(
class DefaultModuleFragmentsResolver(
private val variantResolver: ModuleVariantResolver
) : KotlinModuleFragmentsResolver {
) : ModuleFragmentsResolver {
override fun getChosenFragments(
requestingFragment: KotlinModuleFragment,
dependencyModule: KotlinModule
): FragmentResolution.ChosenFragments {
): FragmentResolution {
val dependingModule = requestingFragment.containingModule
val containingVariants = dependingModule.variantsContainingFragment(requestingFragment)
val chosenVariants = containingVariants.map { variantResolver.getChosenVariant(it, dependencyModule) }
// TODO: extend this to more cases with non-matching variants, revisit the behavior when no matching variant is found once we fix
// local publishing of libraries with missing host-specific parts (it breaks transitive dependencies now)
if (chosenVariants.none { it is VariantResolution.VariantMatch })
return FragmentResolution.NotRequested(requestingFragment, dependencyModule)
val chosenFragments = chosenVariants.map { variantResolution ->
when (variantResolution) {
is VariantResolution.VariantMatch -> variantResolution.chosenVariant.refinesClosure
@@ -9,11 +9,11 @@ import org.junit.Assume.assumeTrue
import org.junit.Test
import kotlin.test.assertEquals
class DefaultKotlinModuleFragmentsResolverTest {
class DefaultModuleFragmentsResolverTest {
val moduleFoo = simpleModule("foo")
val moduleBar = simpleModule("bar")
val fragmentResolver = DefaultKotlinModuleFragmentsResolver(MatchVariantsByExactAttributes())
val fragmentResolver = DefaultModuleFragmentsResolver(MatchVariantsByExactAttributes())
@Test
fun testFragmentVisibility() {