Resolve dependencies as graph, support multiple modules in a project
This commit is contained in:
committed by
TeamCityServer
parent
f1fb438d00
commit
d66f95b80e
@@ -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>()
|
||||
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
+9
-4
@@ -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
|
||||
+2
-2
@@ -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() {
|
||||
Reference in New Issue
Block a user