diff --git a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt index e1367280f85..72ea3724dca 100644 --- a/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt +++ b/libraries/tools/kotlin-gradle-plugin-integration-tests/src/test/kotlin/org/jetbrains/kotlin/gradle/HierarchicalMppIT.kt @@ -427,8 +427,7 @@ class HierarchicalMppIT : BaseGradleIT() { shouldInclude: Iterable> = emptyList(), shouldNotInclude: Iterable> = emptyList() ) { - val taskOutput = getOutputForTask(taskPath.removePrefix(":")) - val compilerArgsLine = taskOutput.lines().single { "Kotlin compiler args:" in it } + val compilerArgsLine = output.lines().single { "$taskPath Kotlin compiler args:" in it } val classpathItems = compilerArgsLine.substringAfter("-classpath").substringBefore(" -").split(File.pathSeparator) val actualClasspath = classpathItems.joinToString("\n") diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GradleProjectModuleBuilder.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GradleProjectModuleBuilder.kt index 4e704db86db..514a95d8836 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GradleProjectModuleBuilder.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GradleProjectModuleBuilder.kt @@ -8,6 +8,7 @@ package org.jetbrains.kotlin.gradle.plugin.mpp import org.gradle.api.Named import org.gradle.api.Project import org.gradle.api.artifacts.Dependency +import org.gradle.api.artifacts.ModuleDependency import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.attributes.Attribute import org.gradle.api.attributes.AttributeContainer @@ -16,44 +17,53 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinSingleTargetExtension import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.plugin.KotlinCompilation -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.currentBuildId -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.matchesModule -import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.representsProject +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet +import org.jetbrains.kotlin.gradle.plugin.KotlinTarget +import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.* +import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope import org.jetbrains.kotlin.gradle.plugin.sources.getVisibleSourceSetsFromAssociateCompilations import org.jetbrains.kotlin.project.model.* class ProjectStructureMetadataModuleBuilder { - private val modulesCache = mutableMapOf() + private val modulesCache = mutableMapOf() private fun buildModuleFromProjectStructureMetadata( - moduleIdentifier: ModuleIdentifier, + moduleIdentifier: KotlinModuleIdentifier, metadata: KotlinProjectStructureMetadata ): KotlinModule = BasicKotlinModule(moduleIdentifier).apply { metadata.sourceSetNamesByVariantName.keys.forEach { variantName -> fragments.add(BasicKotlinModuleVariant(this@apply, variantName)) } + fun fragment(sourceSetName: String): BasicKotlinModuleFragment { + if (fragments.none { it.fragmentName == sourceSetName }) + fragments.add(BasicKotlinModuleFragment(this@apply, sourceSetName)) + return fragmentByName(sourceSetName) + } metadata.sourceSetNamesByVariantName.forEach { (variantName, sourceSets) -> val variant = fragmentByName(variantName) sourceSets.forEach { sourceSetName -> - if (fragments.none { it.fragmentName == sourceSetName }) - fragments.add(BasicKotlinModuleFragment(this@apply, sourceSetName)) - val fragment = fragmentByName(sourceSetName) - variant.directRefinesDependencies.add(fragment) + variant.directRefinesDependencies.add(fragment(sourceSetName)) } } - metadata.sourceSetsDependsOnRelation.forEach { (depending, dependencies) -> - val dependingFragment = fragmentByName(depending) + metadata.sourceSetModuleDependencies.forEach { (sourceSetName, dependencies) -> + val fragment = fragment(sourceSetName) dependencies.forEach { dependency -> - if (fragments.none { it.fragmentName == dependency }) - fragments.add(BasicKotlinModuleFragment(this@apply, dependency)) - val fragment = fragmentByName(dependency) - dependingFragment.directRefinesDependencies.add(fragment) + fragment.declaredModuleDependencies.add( + KotlinModuleDependency(MavenModuleIdentifier(dependency.groupId.orEmpty(), dependency.moduleId, null /* TODO */)) + ) + } + } + + metadata.sourceSetsDependsOnRelation.forEach { (depending, dependencies) -> + val dependingFragment = fragment(depending) + dependencies.forEach { dependency -> + dependingFragment.directRefinesDependencies.add(fragment(dependency)) } } } - fun getModule(moduleIdentifier: ModuleIdentifier, projectStructureMetadata: KotlinProjectStructureMetadata): KotlinModule { + fun getModule(moduleIdentifier: KotlinModuleIdentifier, projectStructureMetadata: KotlinProjectStructureMetadata): KotlinModule { return modulesCache.getOrPut(moduleIdentifier) { buildModuleFromProjectStructureMetadata( moduleIdentifier, @@ -63,34 +73,81 @@ class ProjectStructureMetadataModuleBuilder { } } +private fun detectModules(targets: Iterable, sourceSets: Iterable): Map>> { + // DSU-like approach: all compilations and source sets that are reachable via dependsOn edges are considered a single module + + val compilations = targets.flatMap { it.compilations } + + val dsu = mutableMapOf().apply { + compilations.forEach { put(it, it) } + sourceSets.forEach { put(it, it) } + } + + fun get(item: Any): Any = + dsu.getValue(item).let { leader -> if (leader === item) leader else get(leader).also { dsu[item] = it } } + + fun union(item: Any, other: Any) = dsu.put(get(item), get(other)) + + sourceSets.forEach { sourceSet -> + sourceSet.dependsOn.forEach { other -> union(sourceSet, other) } + } + compilations.forEach { compilation -> + compilation.kotlinSourceSets.forEach { union(compilation, it) } + } + val uniqueCompilationNamesCounter = mutableMapOf, Int>() + + fun moduleName(compilations: Iterable>): String { + val names = compilations.map { it.name }.toSortedSet() + val uniqueNumber = uniqueCompilationNamesCounter.put(names, uniqueCompilationNamesCounter[names]?.plus(1) ?: 0) + return names.joinToString("-") + (uniqueNumber?.let { "-$it" } ?: "") + } + + return compilations.groupBy { get(it) }.values.associateBy { moduleName(it) } +} + @Suppress("unused") class GradleProjectModuleBuilder(private val addInferredSourceSetVisibilityAsExplicit: Boolean) { - fun buildModuleFromProject(project: Project): KotlinModule? { + fun buildModulesFromProject(project: Project): List { val extension = project.multiplatformExtensionOrNull ?: project.kotlinExtension - ?: return null + ?: return emptyList() val targets = when (extension) { - is KotlinMultiplatformExtension -> extension.targets.filter { it.name != "metadata" } + is KotlinMultiplatformExtension -> extension.targets.filter { it.name != KotlinMultiplatformPlugin.METADATA_TARGET_NAME } is KotlinSingleTargetExtension -> listOf(extension.target) - else -> return null + else -> return emptyList() } - return BasicKotlinModule(LocalModuleIdentifier(project.currentBuildId().name, project.path)).apply { - val variantToCompilation = mutableMapOf>() + val moduleCompilationCluster = detectModules(targets, extension.sourceSets) - targets.forEach { target -> - val publishedVariantsByCompilation = (target as? AbstractKotlinTarget)?.kotlinComponents.orEmpty() - .flatMap { component -> (component as? KotlinVariant)?.usages.orEmpty() } - .groupBy { it.compilation } + val publishedVariantsByCompilation = targets.flatMap { target -> + (target as? AbstractKotlinTarget)?.kotlinComponents.orEmpty() + .flatMap { component -> (component as? KotlinVariant)?.usages.orEmpty() } + }.groupBy { it.compilation } - target.compilations.forEach { compilation -> - val names = - publishedVariantsByCompilation[compilation].orEmpty() - .filter { it.includeIntoProjectStructureMetadata }.map { it.name } + val moduleByFragment = mutableMapOf() - names.forEach { variantName -> + val result = moduleCompilationCluster.entries.map { (classifier, compilationsToInclude) -> + val sourceSetsToInclude = compilationsToInclude.flatMapTo(mutableSetOf()) { it.allKotlinSourceSets } + + val moduleIdentifier = LocalModuleIdentifier( + project.currentBuildId().name, + project.path, + classifier.takeIf { it != KotlinCompilation.MAIN_COMPILATION_NAME } + ) + + BasicKotlinModule(moduleIdentifier).apply { + val variantToCompilation = mutableMapOf>() + + compilationsToInclude.forEach { compilation -> + // A compilation may be exposed as more than one variant, so we collect all of its names + val variantNames = + publishedVariantsByCompilation[compilation]?.filter { it.includeIntoProjectStructureMetadata }?.map { it.name } + ?: listOf(compilation.defaultSourceSetName) + + variantNames.forEach { variantName -> val variant = BasicKotlinModuleVariant(this@apply, variantName) + moduleByFragment[variant] = this@apply variantToCompilation[variant] = compilation fragments.add(variant) @@ -104,50 +161,53 @@ class GradleProjectModuleBuilder(private val addInferredSourceSetVisibilityAsExp variant.isExported = compilation.isMain() } } - } - extension.sourceSets.forEach { sourceSet -> - val existingVariant = fragments.filterIsInstance().find { it.fragmentName == sourceSet.name } - val fragment = existingVariant ?: BasicKotlinModuleFragment(this@apply, sourceSet.name).also { fragments.add(it) } - fragment.kotlinSourceRoots = sourceSet.kotlin.sourceDirectories.toList() + // Once all fragments are created, add dependencies between them + sourceSetsToInclude.forEach { sourceSet -> + val existingVariant = fragments.filterIsInstance().find { it.fragmentName == sourceSet.name } + val fragment = existingVariant ?: BasicKotlinModuleFragment(this@apply, sourceSet.name).also { fragments.add(it) } + moduleByFragment[fragment] = this@apply + fragment.kotlinSourceRoots = sourceSet.kotlin.sourceDirectories.toList() - // FIXME: Kotlin/Native implementation-effective-api dependencies are missing here. Introduce dependency scopes - project.configurations.getByName(sourceSet.apiConfigurationName).allDependencies.forEach { - val moduleDependency = it.toModuleDependency(project) - fragment.declaredModuleDependencies.add(moduleDependency) - } - } - - // Once all fragments are created, add dependencies between them - - fragments.forEach { fragment -> - val sourceSet = extension.sourceSets.findByName(fragment.fragmentName) - ?: variantToCompilation.getValue(fragment).defaultSourceSet - sourceSet.dependsOn.forEach { dependency -> - val dependencyFragment = fragmentByName(dependency.name) - fragment.directRefinesDependencies.add(dependencyFragment) - } - } - targets.forEach { target -> - target.compilations.forEach { compilation -> - val fragment = fragmentByName(compilation.defaultSourceSetName) - compilation.associateWith.forEach { associate -> - val dependencyFragment = fragmentByName(associate.defaultSourceSetName) - fragment.declaredContainingModuleFragmentDependencies.add(dependencyFragment) + // FIXME: Kotlin/Native implementation-effective-api dependencies are missing here. Introduce dependency scopes + requestedDependencies(project, sourceSet, listOf(KotlinDependencyScope.API_SCOPE)).forEach { + val moduleDependency = it.toModuleDependency(project) + fragment.declaredModuleDependencies.add(moduleDependency) } } - } - if (addInferredSourceSetVisibilityAsExplicit) { - extension.sourceSets.forEach { sourceSet -> - val fragment = fragmentByName(sourceSet.name) - getVisibleSourceSetsFromAssociateCompilations(project, sourceSet).forEach { dependency -> + fragments.forEach { fragment -> + val sourceSet = extension.sourceSets.findByName(fragment.fragmentName) + ?: variantToCompilation.getValue(fragment).defaultSourceSet + sourceSet.dependsOn.forEach { dependency -> val dependencyFragment = fragmentByName(dependency.name) - fragment.declaredContainingModuleFragmentDependencies.add(dependencyFragment) + fragment.directRefinesDependencies.add(dependencyFragment) } } } } - } + fun fragmentByName(name: String) = + result.asSequence().flatMap { it.fragments.asSequence() }.first { it.fragmentName == name } + + targets.flatMap { it.compilations }.forEach { compilation -> + val variant = fragmentByName(compilation.defaultSourceSetName) + compilation.associateWith.forEach { associate -> + val associateVariant = fragmentByName(associate.defaultSourceSetName) + variant.declaredModuleDependencies.add(KotlinModuleDependency(associateVariant.containingModule.moduleIdentifier)) + } + } + + if (addInferredSourceSetVisibilityAsExplicit) { + project.kotlinExtension.sourceSets.forEach { sourceSet -> + val fragment = fragmentByName(sourceSet.name) + getVisibleSourceSetsFromAssociateCompilations(project, sourceSet).forEach { dependency -> + val dependencyFragment = fragmentByName(dependency.name) + fragment.declaredModuleDependencies.add(KotlinModuleDependency(dependencyFragment.containingModule.moduleIdentifier)) + } + } + } + + return result + } private fun attributeString(container: AttributeContainer, attributeKey: Attribute): String { val value = container.getAttribute(attributeKey) @@ -158,13 +218,35 @@ class GradleProjectModuleBuilder(private val addInferredSourceSetVisibilityAsExp } } + +private fun attributeString(container: AttributeContainer, attributeKey: Attribute): String { + val value = container.getAttribute(attributeKey) + return when (value) { + is Named -> value.name + else -> value.toString() + } +} + internal fun Dependency.toModuleDependency( project: Project -) = when (val dependency = this) { - is ProjectDependency -> - ModuleDependency(LocalModuleIdentifier(project.currentBuildId().name, dependency.dependencyProject.path)) - else -> - ModuleDependency(MavenModuleIdentifier(dependency.group.orEmpty(), dependency.name)) +): KotlinModuleDependency { + return KotlinModuleDependency( + when (this) { + is ProjectDependency -> + LocalModuleIdentifier( + project.currentBuildId().name, + dependencyProject.path, + moduleClassifiersFromCapabilities(requestedCapabilities).single() // FIXME multiple capabilities + ) + is ModuleDependency -> + MavenModuleIdentifier( + group.orEmpty(), + name, + moduleClassifiersFromCapabilities(requestedCapabilities).single() // FIXME multiple capabilities + ) + else -> MavenModuleIdentifier(group.orEmpty(), name, null) + } + ) } private fun BasicKotlinModule.fragmentByName(name: String) = @@ -194,7 +276,10 @@ class GradleModuleVariantResolver(val project: Project) : ModuleVariantResolver .flatMap { it.usages } .firstOrNull { it.name == requestingVariant.fragmentName } ?.compilation - ?: return VariantResolution.NotRequested(requestingVariant, dependencyModule) + ?: targets.asSequence().flatMap { it.compilations.asSequence() }.single { + it.defaultSourceSetName == requestingVariant.fragmentName + } // TODO: generalize the mapping PM2.0 <-> MPP + ?: return VariantResolution.Unknown(requestingVariant, dependencyModule) val compileClasspath = project.configurations.getByName(compilation.compileDependencyConfigurationName) diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GranularMetadataTransformation.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GranularMetadataTransformation.kt index 3d6091b75fb..a63cdf24ae4 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GranularMetadataTransformation.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/GranularMetadataTransformation.kt @@ -18,6 +18,8 @@ import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope import org.jetbrains.kotlin.gradle.targets.metadata.ALL_COMPILE_METADATA_CONFIGURATION_NAME +import org.jetbrains.kotlin.gradle.targets.metadata.ALL_RUNTIME_METADATA_CONFIGURATION_NAME +import org.jetbrains.kotlin.gradle.targets.metadata.dependsOnClosureWithInterCompilationDependencies import java.io.File import java.io.InputStream import java.util.* @@ -85,7 +87,6 @@ internal class GranularMetadataTransformation( /** A list of scopes that the dependencies from [kotlinSourceSet] are treated as requested dependencies. */ private val sourceSetRequestedScopes: List, /** A configuration that holds the dependencies of the appropriate scope for all Kotlin source sets in the project */ - private val allSourceSetsConfiguration: Configuration, private val parentTransformations: Lazy> ) { val metadataDependencyResolutions: Iterable by lazy { doTransform() } @@ -98,40 +99,14 @@ internal class GranularMetadataTransformation( ) private val requestedDependencies: Iterable by lazy { - fun collectScopedDependenciesFromSourceSet(sourceSet: KotlinSourceSet): Set = - sourceSetRequestedScopes.flatMapTo(mutableSetOf()) { scope -> - project.sourceSetDependencyConfigurationByScope(sourceSet, scope).incoming.dependencies - } - - val ownDependencies = collectScopedDependenciesFromSourceSet(kotlinSourceSet) - val parentDependencies = parentTransformations.value.flatMapTo(mutableSetOf()) { it.requestedDependencies } - - ownDependencies + parentDependencies + requestedDependencies(project, kotlinSourceSet, sourceSetRequestedScopes) } + private val allSourceSetsConfiguration: Configuration = + commonMetadataDependenciesConfigurationForScopes(project, sourceSetRequestedScopes) + private val configurationToResolve: Configuration by lazy { - /** If [kotlinSourceSet] is not a published source set, its dependencies are not included in [allSourceSetsConfiguration]. - * In that case, to resolve the dependencies of the source set in a way that is consistent with the published source sets, - * we need to create a new configuration with the dependencies from both [allSourceSetsConfiguration] and the - * input configuration(s) of the source set. */ - var modifiedConfiguration: Configuration? = null - - val originalDependencies = allSourceSetsConfiguration.allDependencies - - requestedDependencies.forEach { dependency -> - if (dependency !in originalDependencies) { - modifiedConfiguration = modifiedConfiguration ?: project.configurations.detachedConfiguration().apply { - fun copyAttribute(key: Attribute) { - attributes.attribute(key, allSourceSetsConfiguration.attributes.getAttribute(key)!!) - } - allSourceSetsConfiguration.attributes.keySet().forEach { copyAttribute(it) } - allSourceSetsConfiguration.extendsFrom.forEach { extendsFrom(it) } - } - modifiedConfiguration!!.dependencies.add(dependency) - } - } - - modifiedConfiguration ?: allSourceSetsConfiguration + resolvableMetadataConfiguration(project, allSourceSetsConfiguration, requestedDependencies) } private fun doTransform(): Iterable { @@ -229,7 +204,7 @@ internal class GranularMetadataTransformation( project, module, configurationToResolve, - resolveViaAvailableAt = false // we will process this dependency later in the queue + resolveViaAvailableAt = false // we will process the available-at module as a dependency later in the queue ) val resolvedToProject: Project? = (mppDependencyMetadataExtractor as? ProjectMppDependencyMetadataExtractor)?.dependencyProject @@ -301,6 +276,75 @@ internal class GranularMetadataTransformation( } } +internal fun resolvableMetadataConfiguration( + project: Project, + sourceSets: Iterable, + scopes: Iterable +) = resolvableMetadataConfiguration( + project, + commonMetadataDependenciesConfigurationForScopes(project, scopes), + sourceSets.flatMapTo(mutableListOf()) { requestedDependencies(project, it, scopes) } +) + +/** If a source set is not a published source set, its dependencies are not included in [allSourceSetsConfiguration]. + * In that case, to resolve the dependencies of the source set in a way that is consistent with the published source sets, + * we need to create a new configuration with the dependencies from both [allSourceSetsConfiguration] and the + * other [requestedDependencies] */ +// TODO: optimize by caching the resulting configurations? +internal fun resolvableMetadataConfiguration( + project: Project, + allSourceSetsConfiguration: Configuration, + requestedDependencies: Iterable +): Configuration { + var modifiedConfiguration: Configuration? = null + + val originalDependencies = allSourceSetsConfiguration.allDependencies + + requestedDependencies.forEach { dependency -> + if (dependency !in originalDependencies) { + modifiedConfiguration = modifiedConfiguration ?: project.configurations.detachedConfiguration().apply { + fun copyAttribute(key: Attribute) { + attributes.attribute(key, allSourceSetsConfiguration.attributes.getAttribute(key)!!) + } + allSourceSetsConfiguration.attributes.keySet().forEach { copyAttribute(it) } + allSourceSetsConfiguration.extendsFrom.forEach { extendsFrom(it) } + dependencies.addAll(originalDependencies) // TODO: check if this line is redundant + } + modifiedConfiguration!!.dependencies.add(dependency) + } + } + return modifiedConfiguration ?: allSourceSetsConfiguration +} + +/** The configuration that contains the dependencies of the corresponding scopes (and maybe others) + * from all published source sets. */ +internal fun commonMetadataDependenciesConfigurationForScopes( + project: Project, + scopes: Iterable +): Configuration { + // TODO: what if 'runtimeOnly' is combined with 'compileOnly'? prohibit this or merge the two? we never do that now, though + val configurationName = if (KotlinDependencyScope.RUNTIME_ONLY_SCOPE in scopes) + ALL_RUNTIME_METADATA_CONFIGURATION_NAME + else + ALL_COMPILE_METADATA_CONFIGURATION_NAME + return project.configurations.getByName(configurationName) +} + +internal fun requestedDependencies( + project: Project, + sourceSet: KotlinSourceSet, + requestedScopes: Iterable +): Iterable { + fun collectScopedDependenciesFromSourceSet(sourceSet: KotlinSourceSet): Set = + requestedScopes.flatMapTo(mutableSetOf()) { scope -> + project.sourceSetDependencyConfigurationByScope(sourceSet, scope).incoming.dependencies + } + + val otherContributingSourceSets = dependsOnClosureWithInterCompilationDependencies(project, sourceSet) + return listOf(sourceSet, *otherContributingSourceSets.toTypedArray()).flatMap(::collectScopedDependenciesFromSourceSet) +} + + private abstract class MppDependencyMetadataExtractor(val project: Project, val dependency: ResolvedComponentResult) { abstract fun getProjectStructureMetadata(): KotlinProjectStructureMetadata? @@ -343,7 +387,7 @@ private class IncludedBuildMetadataExtractor( init { val id = dependency.id require(id is ProjectComponentIdentifier) { "dependency should resolve to a project" } - require(!(id as ProjectComponentIdentifier).build.isCurrentBuild) { "should be a project from an included build" } + require(!id.build.isCurrentBuild) { "should be a project from an included build" } this.id = id } @@ -493,7 +537,6 @@ internal fun getProjectStructureMetadata( module: ResolvedComponentResult, configuration: Configuration ): KotlinProjectStructureMetadata? { - // FIXME test dependencies won't work val extractor = getMetadataExtractor(project, module, configuration, resolveViaAvailableAt = true) return extractor?.getProjectStructureMetadata() } diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt index 56bab015773..f4ec41648b2 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/KotlinProjectStructureMetadata.kt @@ -17,7 +17,9 @@ import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope import org.jetbrains.kotlin.gradle.plugin.sources.withAllDependsOnSourceSets import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope +import org.jetbrains.kotlin.gradle.targets.metadata.dependsOnClosureWithInterCompilationDependencies import org.jetbrains.kotlin.gradle.targets.metadata.getPublishedPlatformCompilations +import org.jetbrains.kotlin.gradle.targets.metadata.isSharedNativeSourceSet import org.w3c.dom.Document import org.w3c.dom.Element import org.w3c.dom.Node @@ -114,14 +116,19 @@ internal fun buildKotlinProjectStructureMetadata(project: Project): KotlinProjec * published as API dependencies of the metadata module to get into the resolution result, see * [KotlinMetadataTargetConfigurator.exportDependenciesForPublishing]. */ - val isNativeSharedSourceSet = sourceSetsWithMetadataCompilations[sourceSet] is KotlinSharedNativeCompilation - val sourceSetExportedDependencies = when { - isNativeSharedSourceSet -> sourceSet.withAllDependsOnSourceSets().flatMap { hierarchySourceSet -> - listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE).flatMap { scope -> - project.sourceSetDependencyConfigurationByScope(hierarchySourceSet, scope).allDependencies.toList() - } - }.distinct() - else -> project.configurations.getByName(sourceSet.apiConfigurationName).allDependencies + val isNativeSharedSourceSet = isSharedNativeSourceSet(project, sourceSet) + val scopes = listOfNotNull( + KotlinDependencyScope.API_SCOPE, + KotlinDependencyScope.IMPLEMENTATION_SCOPE.takeIf { isNativeSharedSourceSet } + ) + val sourceSetsToIncludeDependencies = + if (isNativeSharedSourceSet) + dependsOnClosureWithInterCompilationDependencies(project, sourceSet).plus(sourceSet) + else listOf(sourceSet) + val sourceSetExportedDependencies = scopes.flatMap { scope -> + sourceSetsToIncludeDependencies.flatMap { hierarchySourceSet -> + project.sourceSetDependencyConfigurationByScope(hierarchySourceSet, scope).allDependencies.toList() + } } sourceSet.name to sourceSetExportedDependencies.map { ModuleIds.fromDependency(it) }.toSet() }, diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/TransformKotlinGranularMetadata.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/TransformKotlinGranularMetadata.kt index 14dff6f1574..fab40dcfb95 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/TransformKotlinGranularMetadata.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/TransformKotlinGranularMetadata.kt @@ -15,6 +15,7 @@ import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope.* import org.jetbrains.kotlin.gradle.plugin.sources.withAllDependsOnSourceSets import org.jetbrains.kotlin.gradle.targets.metadata.ALL_COMPILE_METADATA_CONFIGURATION_NAME import org.jetbrains.kotlin.gradle.targets.metadata.KotlinMetadataTargetConfigurator +import org.jetbrains.kotlin.gradle.targets.metadata.dependsOnClosureWithInterCompilationDependencies import org.jetbrains.kotlin.gradle.utils.getValue import java.io.File import javax.inject.Inject @@ -77,9 +78,8 @@ open class TransformKotlinGranularMetadata project, kotlinSourceSet, listOf(API_SCOPE, IMPLEMENTATION_SCOPE, COMPILE_ONLY_SCOPE), - project.configurations.getByName(ALL_COMPILE_METADATA_CONFIGURATION_NAME), lazy { - KotlinMetadataTargetConfigurator.dependsOnWithInterCompilationDependencies(project, kotlinSourceSet).map { + dependsOnClosureWithInterCompilationDependencies(project, kotlinSourceSet).map { project.tasks.withType(TransformKotlinGranularMetadata::class.java) .getByName(KotlinMetadataTargetConfigurator.transformGranularMetadataTaskName(it.name)) .transformation @@ -97,7 +97,7 @@ open class TransformKotlinGranularMetadata private val extractableFilesByResolution: Map get() = metadataDependencyResolutions .filterIsInstance() - .associate { it to it.getExtractableMetadataFiles(outputsDir) } + .associateWith { it.getExtractableMetadataFiles(outputsDir) } @get:Internal internal val filesByResolution: Map diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt index 74a98129e2b..a09735864b9 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/kotlinCompilations.kt @@ -338,6 +338,10 @@ internal object CompilationSourceSetUtil { return ext.get(EXT_NAME) as Property } + // FIXME: the results include the compilations of the metadata target; the callers should care about filtering them out + // if they need only the platform compilations + // TODO: create a separate util function: `platformCompilationsBySourceSets` + // TODO: visit all call sites, check if they handle the metadata compilations correctly fun compilationsBySourceSets(project: Project): CompilationsBySourceSet { val compilationNamesBySourceSetName = getOrCreateProperty(project) { var shouldFinalizeValue = false diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyGraphResolver.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyGraphResolver.kt new file mode 100644 index 00000000000..e99289ae2f5 --- /dev/null +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyGraphResolver.kt @@ -0,0 +1,67 @@ +/* + * Copyright 2010-2021 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.gradle.plugin.mpp.pm20 + +import org.gradle.api.Project +import org.gradle.api.artifacts.Configuration +import org.gradle.api.artifacts.result.ResolvedComponentResult +import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.gradle.plugin.mpp.resolvableMetadataConfiguration +import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope +import org.jetbrains.kotlin.project.model.* + +class GradleKotlinDependencyGraphResolver( + private val project: Project, + private val moduleResolver: ModuleDependencyResolver, + private val fragmentsResolver: ModuleFragmentsResolver +) : KotlinDependencyGraphResolver { + + private val configurationToResolve: Configuration + get() = resolvableMetadataConfiguration( + project, + project.kotlinExtension.sourceSets, // take dependencies from all source sets; TODO introduce consistency scopes? + KotlinDependencyScope.compileScopes + ) + + override fun resolveDependencyGraph(requestingModule: KotlinModule): DependencyGraphResolution { + if (!requestingModule.representsProject(project)) + return DependencyGraphResolution.Unknown(requestingModule) + + val excludeLegacyMetadataModulesFromResult = mutableSetOf() + + val nodeByModuleId = mutableMapOf() + + fun nodeFromComponent(component: ResolvedComponentResult, isRoot: Boolean /*refactor*/): DependencyGraphNode { + val id = component.toModuleIdentifier() + return nodeByModuleId.getOrPut(id) { + val module = if (isRoot) + requestingModule + else moduleResolver.resolveDependency(component.toModuleDependency()) + .takeIf { component !in excludeLegacyMetadataModulesFromResult } + ?: buildStubModule(component, component.variants.singleOrNull()?.displayName ?: "default") + + val resolvedComponentDependencies = component.dependencies + .filterIsInstance() + .flatMap { dependency -> dependency.requested.toModuleIdentifiers().map { id -> id to dependency.selected } } + .toMap() + + val fragmentDependencies = module.fragments.associateWith { it.declaredModuleDependencies } + + val nodeDependenciesMap = fragmentDependencies.mapValues { (_, deps) -> + deps.mapNotNull { resolvedComponentDependencies[it.moduleIdentifier] }.map { nodeFromComponent(it, isRoot = false) } + } + + DependencyGraphNode(module, nodeDependenciesMap) + } + } + + return DependencyGraphResolution.DependencyGraph( + requestingModule, + nodeFromComponent(configurationToResolve.incoming.resolutionResult.root, isRoot = true) + ) + } +} \ No newline at end of file diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyResolution.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyResolution.kt index d94daf21130..b471a32bdd7 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyResolution.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/mpp/pm20/DependencyResolution.kt @@ -7,16 +7,17 @@ package org.jetbrains.kotlin.gradle.plugin.mpp.pm20 import org.gradle.api.Project import org.gradle.api.artifacts.Configuration -import org.gradle.api.artifacts.component.ComponentIdentifier -import org.gradle.api.artifacts.component.ModuleComponentIdentifier -import org.gradle.api.artifacts.component.ProjectComponentIdentifier +import org.gradle.api.artifacts.component.* import org.gradle.api.artifacts.result.ResolvedComponentResult import org.gradle.api.artifacts.result.ResolvedDependencyResult +import org.gradle.api.capabilities.Capability import org.jetbrains.kotlin.gradle.dsl.kotlinExtension +import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet import org.jetbrains.kotlin.gradle.plugin.mpp.ProjectStructureMetadataModuleBuilder import org.jetbrains.kotlin.gradle.plugin.mpp.GradleProjectModuleBuilder import org.jetbrains.kotlin.gradle.plugin.mpp.getProjectStructureMetadata -import org.jetbrains.kotlin.gradle.targets.metadata.ALL_COMPILE_METADATA_CONFIGURATION_NAME +import org.jetbrains.kotlin.gradle.plugin.mpp.resolvableMetadataConfiguration +import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope import org.jetbrains.kotlin.project.model.* import java.util.ArrayDeque @@ -26,21 +27,29 @@ class GradleModuleDependencyResolver( private val projectModuleBuilder: GradleProjectModuleBuilder ) : ModuleDependencyResolver { - // FIXME this wont work for tests, as their dependencies are currently not in the all compile dependencies configuration private val configurationToResolve: Configuration - get() = project.configurations.getByName(ALL_COMPILE_METADATA_CONFIGURATION_NAME) + get() = resolvableMetadataConfiguration( + project, + project.kotlinExtension.sourceSets, // take dependencies from all source sets; TODO introduce consistency scopes? + listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE, KotlinDependencyScope.COMPILE_ONLY_SCOPE) + ) - override fun resolveDependency(moduleDependency: ModuleDependency): KotlinModule? { + override fun resolveDependency(moduleDependency: KotlinModuleDependency): KotlinModule? { val allComponents = configurationToResolve.incoming.resolutionResult.allComponents - // TODO: optimize O(n) search, store the resolved components with dependency keys + // TODO: optimize O(n) search, store the resolved components with dependency keys? val component = allComponents.find { it.id.matchesModuleDependency(moduleDependency) } val id = component?.id + + //FIXME multiple? + val classifier = moduleClassifiersFromCapabilities(component?.variants?.flatMap { it.capabilities }.orEmpty()).single() + return when { id is ProjectComponentIdentifier && id.build.isCurrentBuild -> - projectModuleBuilder.buildModuleFromProject(project.project(id.projectPath)) + projectModuleBuilder.buildModulesFromProject(project.project(id.projectPath)) + .find { it.moduleIdentifier.moduleClassifier == classifier } id is ModuleComponentIdentifier -> { val metadata = getProjectStructureMetadata(project, component, configurationToResolve) ?: return null - projectStructureMetadataModuleBuilder.getModule(id.toModuleIdentifier(), metadata) + projectStructureMetadataModuleBuilder.getModule(id.toModuleIdentifier(classifier), metadata) } else -> null } @@ -52,7 +61,7 @@ class GradleProjectDependencyDiscovery( private val variantDependencyDiscovery: VariantDependencyDiscovery, private val fragmentDependenciesDiscovery: FragmentDependenciesDiscovery ) : DependencyDiscovery { - override fun discoverDependencies(fragment: KotlinModuleFragment): Iterable { + override fun discoverDependencies(fragment: KotlinModuleFragment): Iterable { require(fragment.containingModule.representsProject(project)) return when (fragment) { is KotlinModuleVariant -> variantDependencyDiscovery.discoverDependencies(fragment) @@ -63,63 +72,88 @@ class GradleProjectDependencyDiscovery( class FragmentDependenciesDiscovery( private val project: Project, - private val expansion: InternalDependencyExpansion, private val moduleResolver: ModuleDependencyResolver, - private val fragmentsResolver: KotlinModuleFragmentsResolver + private val fragmentsResolver: ModuleFragmentsResolver ) : DependencyDiscovery { - override fun discoverDependencies(fragment: KotlinModuleFragment): Iterable { + private val dependencyScopes = listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE) + + private fun KotlinModuleFragment.toSourceSet(): KotlinSourceSet = project.kotlinExtension.sourceSets.getByName(fragmentName) + + // TODO: use the dependency graph resolution implementation? + override fun discoverDependencies(fragment: KotlinModuleFragment): Iterable { require(fragment.containingModule.representsProject(project)) - val requested = expansion.expandInternalFragmentDependencies(fragment).visibleFragments().plus(fragment.refinesClosure) - .flatMap { it.declaredModuleDependencies } - .toSet() + val fragmentsWhoseDependenciesAreVisible = fragment.refinesClosure - // FIXME: test dependencies won't work here - val allComponents = - project.configurations.getByName(ALL_COMPILE_METADATA_CONFIGURATION_NAME).incoming.resolutionResult.allComponents + // FIXME use dependencyScopes, not just sets of declared module dependencies + val requestedDependencies = fragmentsWhoseDependenciesAreVisible.flatMap { it.declaredModuleDependencies }.toSet() - val componentByModuleDependency = allComponents.associateBy { it.toModuleDependency() } + val configurationToResolve = resolvableMetadataConfiguration( + project, + fragmentsWhoseDependenciesAreVisible.map { it.toSourceSet() }, + KotlinDependencyScope.compileScopes + ) + val allComponents = configurationToResolve.incoming.resolutionResult.allComponents + + val componentsByRequestedDependency = + allComponents.flatMap { component -> component.dependents.map { it to component } }.toMap() + .mapKeys { it.key.requested.toModuleDependency() } val resolvedComponentQueue = ArrayDeque().apply { - addAll(componentByModuleDependency.filterKeys { it in requested }.values) + addAll(componentsByRequestedDependency.filterKeys { it in requestedDependencies }.values) } val visited = mutableSetOf() + val excludeLegacyMetadataModulesFromResult = mutableSetOf() while (resolvedComponentQueue.isNotEmpty()) { val component = resolvedComponentQueue.removeFirst() visited.add(component) val module = moduleResolver.resolveDependency(component.toModuleDependency()) + .takeIf { component !in excludeLegacyMetadataModulesFromResult } ?: buildStubModule(component, component.variants.singleOrNull()?.displayName ?: "default") - val newTransitiveDeps = when (val fragmentsResolution = fragmentsResolver.getChosenFragments(fragment, module)) { + val transitiveDepsToVisit = when (val fragmentsResolution = fragmentsResolver.getChosenFragments(fragment, module)) { is FragmentResolution.ChosenFragments -> fragmentsResolution.visibleFragments.flatMap { it.declaredModuleDependencies }.mapNotNull { dependency -> - componentByModuleDependency[dependency].takeIf { component -> component !in visited } + componentsByRequestedDependency[dependency] } else -> emptyList() } + val newTransitiveDeps = transitiveDepsToVisit.filterTo(mutableListOf()) { it !in visited } + + // With legacy publication scheme, the root MPP module may have a single 'dependency' (available-at) to the metadata module + val isMetadataModulePublishedSeparately = + getProjectStructureMetadata(project, component, configurationToResolve) + ?.let { !it.isPublishedAsRoot } + ?: false + val singleDependencyComponentOrNull = (component.dependencies.singleOrNull() as? ResolvedDependencyResult)?.selected + if (isMetadataModulePublishedSeparately && singleDependencyComponentOrNull != null) { + newTransitiveDeps.add(singleDependencyComponentOrNull) + excludeLegacyMetadataModulesFromResult.add(singleDependencyComponentOrNull) + } + resolvedComponentQueue.addAll(newTransitiveDeps) } - return visited.map { it.toModuleDependency() } + return (visited - excludeLegacyMetadataModulesFromResult).map { it.toModuleDependency() } } +} - // refactor extract to a separate class - // TODO think about multi-variant stub modules for non-Kotlin modules which got more than one chosen variant - private fun buildStubModule(resolvedComponentResult: ResolvedComponentResult, singleVariantName: String): KotlinModule { - val moduleDependency = resolvedComponentResult.toModuleDependency() - return BasicKotlinModule(moduleDependency.moduleIdentifier).apply { - BasicKotlinModuleVariant(this@apply, singleVariantName).apply { - fragments.add(this) - this.declaredModuleDependencies.addAll( - resolvedComponentResult.dependencies - .filterIsInstance() - .map { it.selected.toModuleDependency() } - ) - } +// refactor extract to a separate class/interface +// TODO think about multi-variant stub modules for non-Kotlin modules which got more than one chosen variant +internal fun buildStubModule(resolvedComponentResult: ResolvedComponentResult, singleVariantName: String): KotlinModule { + val moduleDependency = resolvedComponentResult.toModuleDependency() + return BasicKotlinModule(moduleDependency.moduleIdentifier).apply { + BasicKotlinModuleVariant(this@apply, singleVariantName).apply { + fragments.add(this) + this.declaredModuleDependencies.addAll( + resolvedComponentResult.dependencies + .filterIsInstance() + .map { it.selected.toModuleDependency() } + ) } } } @@ -142,24 +176,28 @@ class VariantDependencyDiscovery( return configuration.incoming.resolutionResult.allComponents } - override fun discoverDependencies(fragment: KotlinModuleFragment): Iterable { + override fun discoverDependencies(fragment: KotlinModuleFragment): Iterable { require(fragment is KotlinModuleVariant) require(fragment.containingModule.representsProject(project)) val components = resolvedComponentResults(fragment) - return components.mapNotNull { component -> - val moduleIdentifier = when (val id = component.id) { - is ProjectComponentIdentifier -> LocalModuleIdentifier(id.build.name, id.projectPath) - is ModuleComponentIdentifier -> id.toModuleIdentifier() - else -> return@mapNotNull null // TODO check that no other options are possible, throw errors? + return components.flatMap { component -> + val classifiers = moduleClassifiersFromCapabilities(component.variants.flatMap { it.capabilities }) + when (val id = component.id) { + is ProjectComponentIdentifier -> { + classifiers.map { LocalModuleIdentifier(id.build.name, id.projectPath, it) } + } + is ModuleComponentIdentifier -> { + classifiers.map { id.toModuleIdentifier(it) } + } + else -> return@flatMap emptyList() // TODO check that no other options are possible, throw errors? } - ModuleDependency(moduleIdentifier) - } + }.map(::KotlinModuleDependency) } } -private fun ModuleComponentIdentifier.toModuleIdentifier(): MavenModuleIdentifier = - MavenModuleIdentifier(moduleIdentifier.group, moduleIdentifier.name) +private fun ModuleComponentIdentifier.toModuleIdentifier(classifier: String? = null): MavenModuleIdentifier = + MavenModuleIdentifier(moduleIdentifier.group, moduleIdentifier.name, classifier) internal fun ComponentIdentifier.matchesModule(module: KotlinModule): Boolean { return when (val moduleId = module.moduleIdentifier) { @@ -175,15 +213,37 @@ internal fun ComponentIdentifier.matchesModule(module: KotlinModule): Boolean { } } -internal fun ResolvedComponentResult.toModuleDependency(): ModuleDependency = ModuleDependency( - when (val id = id) { - is ProjectComponentIdentifier -> LocalModuleIdentifier(id.build.name, id.projectPath) +internal fun ResolvedComponentResult.toModuleIdentifier(): KotlinModuleIdentifier { + val capabilities = variants.flatMap { it.capabilities } // expected to be disjoint + val moduleClassifier = moduleClassifiersFromCapabilities(capabilities).single() // FIXME handle multiple capabilities + return when (val id = id) { + is ProjectComponentIdentifier -> LocalModuleIdentifier(id.build.name, id.projectPath, moduleClassifier) is ModuleComponentIdentifier -> id.toModuleIdentifier() - else -> MavenModuleIdentifier(moduleVersion?.group.orEmpty(), moduleVersion?.name.orEmpty()) + else -> MavenModuleIdentifier(moduleVersion?.group.orEmpty(), moduleVersion?.name.orEmpty(), moduleClassifier) } -) +} -internal fun ComponentIdentifier.matchesModuleDependency(moduleDependency: ModuleDependency) = +internal fun moduleClassifiersFromCapabilities(capabilities: Iterable): Iterable { + val classifierCapabilities = capabilities.filter { it.name.contains("..") } + return if (classifierCapabilities.none()) listOf(null) else classifierCapabilities.map { it.name.substringAfterLast("..") /*FIXME invent a more stable scheme*/ } +} + +internal fun ComponentSelector.toModuleIdentifiers(): Iterable { + val moduleClassifiers = moduleClassifiersFromCapabilities(requestedCapabilities) + return when (this) { + is ProjectComponentSelector -> moduleClassifiers.map { LocalModuleIdentifier(buildName, projectPath, it) } + is ModuleComponentSelector -> moduleClassifiers.map { MavenModuleIdentifier(moduleIdentifier.group, moduleIdentifier.name, it) } + else -> error("unexpected component selector") + } +} + +internal fun ResolvedComponentResult.toModuleDependency(): KotlinModuleDependency = KotlinModuleDependency(toModuleIdentifier()) +internal fun ComponentSelector.toModuleDependency(): KotlinModuleDependency { + val moduleId = toModuleIdentifiers().single() // FIXME handle multiple + return KotlinModuleDependency(moduleId) +} + +internal fun ComponentIdentifier.matchesModuleDependency(moduleDependency: KotlinModuleDependency) = when (val id = moduleDependency.moduleIdentifier) { is LocalModuleIdentifier -> { val projectId = this as? ProjectComponentIdentifier diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinDependencyScope.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinDependencyScope.kt index 6feb965ecd0..3d8cb0e3089 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinDependencyScope.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/plugin/sources/KotlinDependencyScope.kt @@ -19,7 +19,11 @@ internal enum class KotlinDependencyScope(val scopeName: String) { API_SCOPE(API), IMPLEMENTATION_SCOPE(IMPLEMENTATION), COMPILE_ONLY_SCOPE(COMPILE_ONLY), - RUNTIME_ONLY_SCOPE(RUNTIME_ONLY) + RUNTIME_ONLY_SCOPE(RUNTIME_ONLY); + + companion object { + val compileScopes = listOf(KotlinDependencyScope.API_SCOPE, KotlinDependencyScope.IMPLEMENTATION_SCOPE, KotlinDependencyScope.COMPILE_ONLY_SCOPE) + } } internal fun Project.sourceSetDependencyConfigurationByScope(sourceSet: KotlinSourceSet, scope: KotlinDependencyScope): Configuration = diff --git a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt index 047275d680a..444f3b4e294 100644 --- a/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt +++ b/libraries/tools/kotlin-gradle-plugin/src/main/kotlin/org/jetbrains/kotlin/gradle/targets/metadata/KotlinMetadataTargetConfigurator.kt @@ -18,6 +18,7 @@ import org.gradle.api.tasks.bundling.Zip import org.jetbrains.kotlin.gradle.dsl.KotlinCommonOptions import org.jetbrains.kotlin.gradle.dsl.kotlinExtension import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension +import org.jetbrains.kotlin.gradle.dsl.multiplatformExtensionOrNull import org.jetbrains.kotlin.gradle.plugin.* import org.jetbrains.kotlin.gradle.plugin.mpp.* import org.jetbrains.kotlin.gradle.plugin.mpp.CompilationSourceSetUtil.compilationsBySourceSets @@ -53,11 +54,6 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) : internal fun transformGranularMetadataTaskName(sourceSetName: String) = lowerCamelCaseName("transform", sourceSetName, "DependenciesMetadata") - - internal fun dependsOnWithInterCompilationDependencies(project: Project, sourceSet: KotlinSourceSet): Set = - sourceSet.dependsOn.toMutableSet().apply { - addAll(getVisibleSourceSetsFromAssociateCompilations(project, sourceSet)) - } } override fun configureTarget(target: KotlinMetadataTarget) { @@ -292,11 +288,10 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) : val compilationName = sourceSet.name - val platformCompilations = compilationsBySourceSets(project).getValue(sourceSet) + val platformCompilations = compilationsBySourceSets(project) + .getValue(sourceSet).filter { it.target.name != KotlinMultiplatformPlugin.METADATA_TARGET_NAME } - val isNativeSourceSet = platformCompilations.all { compilation -> - compilation.target is KotlinNativeTarget || compilation is KotlinSharedNativeCompilation - } + val isNativeSourceSet = isSharedNativeSourceSet(project, sourceSet) val compilationFactory: KotlinCompilationFactory> = when { isNativeSourceSet -> KotlinSharedNativeCompilationFactory( @@ -362,20 +357,12 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) : isSourceSetPublished: Boolean ) { KotlinDependencyScope.values().forEach { scope -> - val allMetadataConfiguration = project.configurations.getByName( - when (scope) { - KotlinDependencyScope.RUNTIME_ONLY_SCOPE -> ALL_RUNTIME_METADATA_CONFIGURATION_NAME - else -> ALL_COMPILE_METADATA_CONFIGURATION_NAME - } - ) - val granularMetadataTransformation = GranularMetadataTransformation( project, sourceSet, listOf(scope), - allMetadataConfiguration, lazy { - dependsOnWithInterCompilationDependencies(project, sourceSet).filterIsInstance() + dependsOnClosureWithInterCompilationDependencies(project, sourceSet).filterIsInstance() .map { checkNotNull(it.dependencyTransformations[scope]) } } ) @@ -565,6 +552,20 @@ class KotlinMetadataTargetConfigurator(kotlinPluginVersion: String) : } } +internal fun isSharedNativeSourceSet(project: Project, sourceSet: KotlinSourceSet): Boolean { + val compilations = CompilationSourceSetUtil.compilationsBySourceSets(project)[sourceSet].orEmpty() + return compilations.isNotEmpty() && compilations.all { + it.platformType == KotlinPlatformType.common || it.platformType == KotlinPlatformType.native + } +} + +internal fun dependsOnClosureWithInterCompilationDependencies(project: Project, sourceSet: KotlinSourceSet): Set = + sourceSet.getSourceSetHierarchy().toMutableSet().apply { + /** exclude self from the results of [getSourceSetHierarchy] */ + remove(sourceSet) + addAll(getVisibleSourceSetsFromAssociateCompilations(project, sourceSet)) + } + /** * @return All common source sets that can potentially be published. Right now, not all combinations of platforms actually * support metadata compilation (see [KotlinMetadataTargetConfigurator.isMetadataCompilationSupported]. diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/DependencyResolution.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/DependencyResolution.kt index 3695caa7e7e..7b43bec5ab6 100644 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/DependencyResolution.kt +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/DependencyResolution.kt @@ -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 -} \ No newline at end of file + fun discoverDependencies(fragment: KotlinModuleFragment): Iterable +} + +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> +) \ 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 deleted file mode 100644 index a0f5d64db93..00000000000 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/InternalDependencyExpansion.kt +++ /dev/null @@ -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 -) { - sealed class ExpansionOutcome(val variantMatchingResults: Iterable) { - class VisibleFragments( - val fragments: Iterable, - variantMatchingResults: Iterable - ) : ExpansionOutcome( - variantMatchingResults - ) - - class Failure(variantMatchingResults: Iterable) : ExpansionOutcome(variantMatchingResults) - } - - class Entry( - val dependingFragment: KotlinModuleFragment, - val dependencyFragment: KotlinModuleFragment, - val outcome: ExpansionOutcome - ) -} - -fun InternalDependencyExpansionResult.visibleFragments(): List = - entries.flatMap { entry -> entry.outcome.visibleFragments() } - -fun InternalDependencyExpansionResult.ExpansionOutcome.visibleFragments(): Iterable = when (this) { - is InternalDependencyExpansionResult.ExpansionOutcome.VisibleFragments -> fragments - else -> emptyList() -} - -class DefaultInternalDependencyExpansion( - private val variantResolver: ContainingModuleVariantResolver -) : InternalDependencyExpansion { - interface ContainingModuleVariantResolver { - fun getChosenVariant( - dependingVariant: KotlinModuleVariant, - candidateVariants: Iterable - ): VariantResolution - } - - override fun expandInternalFragmentDependencies(consumingFragment: KotlinModuleFragment): InternalDependencyExpansionResult { - val visited = mutableSetOf(consumingFragment) - val answer = mutableListOf() - - /** - * 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().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 - ): VariantResolution { - val result = candidateVariants.filter { it in dependingVariant.declaredContainingModuleFragmentDependencies } - return VariantResolution.fromMatchingVariants(dependingVariant, dependingVariant.containingModule, result) - } -} \ No newline at end of file diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt index aba533eb4a4..18085e2dd13 100644 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModule.kt @@ -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 val variants: Iterable get() = fragments.filterIsInstance() + + // TODO: isSynthetic? } class BasicKotlinModule( - override val moduleIdentifier: ModuleIdentifier + override val moduleIdentifier: KotlinModuleIdentifier ) : KotlinModule { override val fragments = mutableListOf() diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/ModuleDependency.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleDependency.kt similarity index 86% rename from libraries/tools/kotlin-project-model/src/main/kotlin/ModuleDependency.kt rename to libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleDependency.kt index e96d46089ec..f2be292f030 100644 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/ModuleDependency.kt +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleDependency.kt @@ -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" } diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt index dbfa7d7c9d4..254023a78b6 100644 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragment.kt @@ -14,10 +14,8 @@ interface KotlinModuleFragment { val fragmentName: String val directRefinesDependencies: Iterable - val declaredContainingModuleFragmentDependencies: Iterable - // TODO: scopes - val declaredModuleDependencies: Iterable + val declaredModuleDependencies: Iterable val kotlinSourceRoots: Iterable } @@ -54,8 +52,7 @@ open class BasicKotlinModuleFragment( override val directRefinesDependencies: MutableSet = mutableSetOf() - override val declaredContainingModuleFragmentDependencies: MutableSet = mutableSetOf() - override val declaredModuleDependencies: MutableSet = mutableSetOf() + override val declaredModuleDependencies: MutableSet = mutableSetOf() override var kotlinSourceRoots: Iterable = emptyList() override fun toString(): String = "fragment $fragmentName" diff --git a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragmentsResolver.kt b/libraries/tools/kotlin-project-model/src/main/kotlin/ModuleFragmentsResolver.kt similarity index 82% rename from libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragmentsResolver.kt rename to libraries/tools/kotlin-project-model/src/main/kotlin/ModuleFragmentsResolver.kt index b489c6b798f..4c6979bf8cc 100644 --- a/libraries/tools/kotlin-project-model/src/main/kotlin/KotlinModuleFragmentsResolver.kt +++ b/libraries/tools/kotlin-project-model/src/main/kotlin/ModuleFragmentsResolver.kt @@ -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 diff --git a/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultKotlinModuleFragmentsResolverTest.kt b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultModuleFragmentsResolverTest.kt similarity index 94% rename from libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultKotlinModuleFragmentsResolverTest.kt rename to libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultModuleFragmentsResolverTest.kt index f1b1d138d16..8070a486fe4 100644 --- a/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultKotlinModuleFragmentsResolverTest.kt +++ b/libraries/tools/kotlin-project-model/src/test/kotlin/org/jetbrains/kotlin/project/model/DefaultModuleFragmentsResolverTest.kt @@ -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() {