From 66b8a444c26f8bedb40de70c87c6966c9921f2a7 Mon Sep 17 00:00:00 2001 From: Sergey Igushkin Date: Tue, 17 Nov 2020 00:17:27 +0300 Subject: [PATCH] Add a basic markdown doc explaining dependency resolution. --- .../docs/dependency-resolution.md | 84 +++++++++++++++++++ .../kotlin/InternalDependencyExpansion.kt | 5 ++ 2 files changed, 89 insertions(+) create mode 100644 libraries/tools/kotlin-project-model/docs/dependency-resolution.md diff --git a/libraries/tools/kotlin-project-model/docs/dependency-resolution.md b/libraries/tools/kotlin-project-model/docs/dependency-resolution.md new file mode 100644 index 00000000000..31e48f19b17 --- /dev/null +++ b/libraries/tools/kotlin-project-model/docs/dependency-resolution.md @@ -0,0 +1,84 @@ +# Dependency Resolution in the Project Model + +Dependency resolution is not a single task, it is a complex pipeline where the requested dependencies, considered an input, are transformed +to mutliple possible kinds of results. To produce one kind of the results, another sort of dependency resolution result may be needed. +Therefore, one may think of the process of resolving dependencies as of a pipeline that may produce results of different shapes when +requested. + +Some use cases only require a dependency resolution result of one kind (for example, 'get all other fragments that a given +fragment can see'), and for them, it might be convenient to have a single facade returning just that kind of result. + +This document describes all the granular tasks that one might put before dependency resolution implementations that work with the Kotlin +Project Model, without attaching them to specific use cases. + +## Internal dependency expansion + +> We may remove this section if it turns out that we are going to use modules for test-on-production dependencies and the like + +We allow declaring only some of the fragment dependencies between fragments inside a single module, and the fragment dependencies for the +depending fragment's refines-children are inferred automatically. Also, declaring such a dependency implies that all refines-parents of the +dependency fragment should be visible, too, as well as everything it sees via its own expanded module fragment dependencies (as well as +transitive expansion results of the first-level expansion results, and so on). + +The interface for this is `InternalDependencyExpansion` with a function that takes a `fragment` and returns all other fragments of the same +module that the fragment should see. For ease of explanation and diagnostics, the results are grouped by the actual declared fragment +dependency that led to each particular fragment having been added to the result. + +Example: + +``` +commonMain commonTestFixtures < - - - - -commonTest +| | | +jvmAndJsMain < - - - - jvmAndJsTestFixtures jvmAndJsTest +| | | +jvmMain jvmTestFixtures jvmTest +``` + +Here, dashed arrows denote declared fragment dependencies. When we ask for fragments that `jvmAndJsTest` sees, we get: +`commonTestFixtures` and `jvmAndJsTestFixtures` because `commonTest` depends on `commonTestFixtures, and then also `commonMain` and +`jvmAndJsMain` because `jvmAndJsTestFixtures` depends on `jvmAndJsMain`. + +Currently, the `DefaultInternalDependencyExpansion` implementation takes a function that matches variants of the module as if they take +part in variant-aware dependency resolution. This function may actually perform matching of the variants or may rely on additional +information (until attributes matching is implemented, it may search for explicit dependencies added between the variants). + +## Dependency discovery + +Given that dependency resolution may bring transtive dependencies, it is not enough to know what *declared dependencies* a fragment has to +be able to properly inspect the resolution results. You With just those, you won't be able to ask +'what fragments does this fragment sees from module `foo`?' if the module `foo` is only a transitive dependency. + +This defines a task of *discovering module dependencies*, that is, finding which modules a fragment actually depends on, including the +modules brought in as transitive dependencies. + +With the granular modules of the Kotlin Project Model, this not a trivial task. A module may reference another module as a dependency onl in +some of its fragments. Therefore, to decide whether a particular module `m` should be transitively included in the resolution results for +one of our fragments `f` we have to first find out whether `f` sees any other fragment that declares a dependency on `m`. This is another +dependency resolution task that is covered below. + +## Module resolution + +Given a module dependency, we should be able to build a Kotlin module for that dependency, as we will have to decompose the dependency on +the whole module to dependencies on its granular parts. These granular parts are the variants and fragments. So it is reasonable to build +a Kotlin module for a resolved dependency and reason about it in the same terms as we use for locally built modules. + +This task in dependency resolution takes a module dependency and returns a Kotlin module. This may in fact be a Kotlin module that is built +locally (for dependencies that point to it directly). + +## Variant resolution + +This is a simple task that is very much like Gradle's variant aware dependency resolution: given a variant (not just a fragment) of a +consuming module and another dependency module, we should tell which of the dependency module's variants is the best match. + +When we look at a consumer's variant, it is important to be able to pick a dependency's variant (and not as set of fragments), because +variants produce final binaries, and for the consumer to produce a final binary from its variant, it needs a guarantee that the producer +also could generate a complete binary (with all `expect`s covered by `actual`s). The confirmation of this is exactly the producer's variant. + +## Fragment resolution + +This is the task of dependency resolution that we ultimately need to find out which declarations a module fragment sees. Namely, given a +module dependency, we have to determine which of the dependency's fragments the consumer's fragment may see. + +To do that, we have to perform variant resolution for each of the variants that the depending variant participates in, and then intersect +the refines-closures of the resolved variants, this will be the safe set of fragments whose declarations we may use because they are +available in any variant that we will compile the fragment for. \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt index 9763af1614a..a0f5d64db93 100644 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt @@ -119,6 +119,11 @@ class DefaultInternalDependencyExpansion( } } +/** + * 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,