Model mapping experiment: map source sets to KPM fragments

TODO: keep type compatibility,
      inject the behavior in DefaultKotlinSourceSet
This commit is contained in:
Sergey Igushkin
2021-11-11 12:30:10 +04:00
committed by Space
parent 8274709621
commit a998e2d291
14 changed files with 249 additions and 26 deletions
@@ -16,6 +16,8 @@ import org.jetbrains.kotlin.gradle.plugin.*
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinAndroidTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinJsSingleTargetPreset
import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinWithJavaTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinGradleModule
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinGradleModuleFactory
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20ProjectExtension
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.targets.js.calculateJsCompilerType
@@ -115,6 +117,9 @@ open class KotlinProjectExtension @Inject constructor(project: Project) : Kotlin
internal set(value) {
DslObject(this).extensions.add("sourceSets", value)
}
internal val kpmModules: NamedDomainObjectContainer<KotlinGradleModule> =
project.objects.domainObjectContainer(KotlinGradleModule::class.java, KotlinGradleModuleFactory(project))
}
abstract class KotlinSingleTargetExtension(project: Project) : KotlinProjectExtension(project) {
@@ -73,8 +73,12 @@ private fun KotlinTarget.excludeStdlibAndKotlinTestCommonFromPlatformCompilation
compilations.all {
listOfNotNull(
it.compileDependencyConfigurationName,
it.defaultSourceSet.apiMetadataConfigurationName,
it.defaultSourceSet.implementationMetadataConfigurationName,
if (!PropertiesProvider(project).experimentalKpmModelMapping)
it.defaultSourceSet.apiMetadataConfigurationName
else null,
if (!PropertiesProvider(project).experimentalKpmModelMapping)
it.defaultSourceSet.implementationMetadataConfigurationName
else null,
(it as? KotlinCompilationToRunnableFiles<*>)?.runtimeDependencyConfigurationName,
// Additional configurations for (old) jvmWithJava-preset. Remove it when we drop it completely
@@ -35,6 +35,7 @@ import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinUsages
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20GradlePlugin
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.KotlinPm20ProjectExtension
import org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSetFactory
import org.jetbrains.kotlin.gradle.plugin.sources.kpm.FragmentMappedKotlinSourceSetFactory
import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService
import org.jetbrains.kotlin.gradle.report.BuildMetricsReporterService
import org.jetbrains.kotlin.gradle.report.HttpReportService
@@ -68,7 +69,9 @@ abstract class KotlinBasePluginWrapper : Plugin<Project> {
open val projectExtensionClass: KClass<out KotlinTopLevelExtension> get() = KotlinProjectExtension::class
internal open fun kotlinSourceSetFactory(project: Project): NamedDomainObjectFactory<KotlinSourceSet> =
DefaultKotlinSourceSetFactory(project)
if (PropertiesProvider(project).experimentalKpmModelMapping)
FragmentMappedKotlinSourceSetFactory(project)
else DefaultKotlinSourceSetFactory(project)
override fun apply(project: Project) {
val kotlinPluginVersion = project.getKotlinPluginVersion()
@@ -220,6 +220,9 @@ internal class PropertiesProvider private constructor(private val project: Proje
val wasmStabilityNoWarn: Boolean
get() = booleanProperty("kotlin.wasm.stability.nowarn") ?: false
val experimentalKpmModelMapping: Boolean
get() = booleanProperty("kotlin.kpm.experimentalModelMapping") ?: false
val ignoreDisabledNativeTargets: Boolean?
get() = booleanProperty(DisabledNativeTargetsReporter.DISABLE_WARNING_PROPERTY_NAME)
@@ -14,8 +14,10 @@ import org.gradle.api.plugins.JavaBasePlugin
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension
import org.jetbrains.kotlin.gradle.dsl.pm20Extension
import org.jetbrains.kotlin.gradle.dsl.topLevelExtensionOrNull
import org.jetbrains.kotlin.gradle.internal.customizeKotlinDependencies
import org.jetbrains.kotlin.gradle.utils.checkGradleCompatibility
import org.jetbrains.kotlin.project.model.KotlinModuleIdentifier
@@ -117,6 +119,13 @@ abstract class KotlinPm20GradlePlugin @Inject constructor(
fun rootPublicationComponentName(module: KotlinGradleModule) =
module.disambiguateName("root")
internal val Project.kpmModules: NamedDomainObjectContainer<KotlinGradleModule>
get() = when (val ext = project.topLevelExtensionOrNull) {
is KotlinPm20ProjectExtension -> ext.modules
is KotlinProjectExtension -> ext.kpmModules
else -> error("can't find Kotlin KPM modules in $project")
}
open class KotlinPm20ProjectExtension(project: Project) : KotlinTopLevelExtension(project) {
val modules: NamedDomainObjectContainer<KotlinGradleModule> =
project.objects.domainObjectContainer(
@@ -35,7 +35,7 @@ open class KotlinGradleModuleFactory(private val project: Project) : NamedDomain
.matching { it.fragmentName == KotlinGradleFragment.COMMON_FRAGMENT_NAME }
.configureEach { commonFragment ->
commonFragment.dependencies {
api(module.project.pm20Extension.modules.getByName(KotlinGradleModule.MAIN_MODULE_NAME))
api(module.project.kpmModules.getByName(KotlinGradleModule.MAIN_MODULE_NAME))
}
}
}
@@ -52,6 +52,9 @@ private class DefaultKotlinFragmentNameDisambiguation(
internal fun KotlinModuleFragment.disambiguateName(simpleName: String) =
KotlinModuleFragment.disambiguateName(containingModule, fragmentName, simpleName)
internal val KotlinModuleFragment.unambiguousNameInProject
get() = disambiguateName("")
internal fun KotlinModuleFragment.Companion.disambiguateName(module: KotlinModule, fragmentName: String, simpleName: String) =
lowerCamelCaseName(fragmentName, module.moduleIdentifier.moduleClassifier ?: KotlinGradleModule.MAIN_MODULE_NAME, simpleName)
@@ -249,7 +249,7 @@ internal fun KotlinSourceSet.disambiguateName(simpleName: String): String {
return lowerCamelCaseName(*nameParts.toTypedArray())
}
private fun createDefaultSourceDirectorySet(project: Project, name: String?): SourceDirectorySet =
internal fun createDefaultSourceDirectorySet(project: Project, name: String?): SourceDirectorySet =
project.objects.sourceDirectorySet(name!!, name)
/**
@@ -0,0 +1,111 @@
/*
* 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.sources.kpm
import groovy.lang.Closure
import org.gradle.api.InvalidUserDataException
import org.gradle.api.Project
import org.gradle.api.file.SourceDirectorySet
import org.jetbrains.kotlin.build.DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinDependencyHandler
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
import org.jetbrains.kotlin.gradle.plugin.LanguageSettingsBuilder
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.*
import org.jetbrains.kotlin.gradle.plugin.mpp.pm20.util.unambiguousNameInProject
import org.jetbrains.kotlin.gradle.plugin.sources.createDefaultSourceDirectorySet
class FragmentMappedKotlinSourceSet(
private val project: Project,
internal val underlyingFragment: KotlinGradleFragment
) : KotlinSourceSet {
val displayName: String
get() = underlyingFragment.unambiguousNameInProject
override val apiConfigurationName: String get() = underlyingFragment.apiConfigurationName
override val implementationConfigurationName: String get() = underlyingFragment.implementationConfigurationName
override val compileOnlyConfigurationName: String get() = underlyingFragment.compileOnlyConfigurationName
override val runtimeOnlyConfigurationName: String get() = underlyingFragment.runtimeOnlyConfigurationName
// FIXME deprecate the property in the supertype?
override val apiMetadataConfigurationName: String
get() = error("should not be used with KPM mapped model")
override val implementationMetadataConfigurationName: String
get() = error("should not be used with KPM mapped model")
override val compileOnlyMetadataConfigurationName: String
get() = error("should not be used with KPM mapped model")
override val runtimeOnlyMetadataConfigurationName: String
get() = error("should not be used with KPM mapped model")
override val kotlin: SourceDirectorySet = underlyingFragment.kotlinSourceRoots
override val languageSettings: LanguageSettingsBuilder = underlyingFragment.languageSettings
// FIXME: not supported yet
override val resources: SourceDirectorySet = createDefaultSourceDirectorySet(project, "$name resources")
override fun kotlin(configureClosure: Closure<Any?>): SourceDirectorySet =
kotlin.apply { project.configure(this, configureClosure) }
override fun languageSettings(configureClosure: Closure<Any?>): LanguageSettingsBuilder = languageSettings.apply {
project.configure(this, configureClosure)
}
override fun languageSettings(configure: LanguageSettingsBuilder.() -> Unit): LanguageSettingsBuilder =
languageSettings.apply { configure(this) }
override fun getName(): String = displayName
override fun dependencies(configure: KotlinDependencyHandler.() -> Unit): Unit =
underlyingFragment.dependencies(configure)
override fun dependencies(configureClosure: Closure<Any?>) =
underlyingFragment.dependencies(configureClosure)
override fun dependsOn(other: KotlinSourceSet) {
if (other !is FragmentMappedKotlinSourceSet) {
throw InvalidUserDataException("Could set up dependsOn relationship with an unknown source set $other")
}
val otherFragment = other.underlyingFragment
underlyingFragment.refines(otherFragment)
}
override val dependsOn: Set<KotlinSourceSet>
get() = project.kotlinExtension.sourceSets.filter {
it is FragmentMappedKotlinSourceSet && it.underlyingFragment in underlyingFragment.directRefinesDependencies
}.toSet()
override fun toString(): String = "source set $name"
override val requiresVisibilityOf: Set<KotlinSourceSet>
get() = emptySet()
override fun requiresVisibilityOf(other: KotlinSourceSet) {
throw UnsupportedOperationException("requiresVisibilityOf is not supported for the mapped model")
}
// region Ported with copy & paste
// FIXME move to fragment?
private val explicitlyAddedCustomSourceFilesExtensions = ArrayList<String>()
override val customSourceFilesExtensions: Iterable<String>
get() = Iterable {
val fromExplicitFilters = kotlin.filter.includes.mapNotNull { pattern ->
pattern.substringAfterLast('.')
}
val merged = (fromExplicitFilters + explicitlyAddedCustomSourceFilesExtensions).filterNot { extension ->
DEFAULT_KOTLIN_SOURCE_FILES_EXTENSIONS.any { extension.equals(it, ignoreCase = true) }
|| extension.any { it == '\\' || it == '/' }
}.distinct()
merged.iterator()
}
override fun addCustomSourceFilesExtensions(extensions: List<String>) {
explicitlyAddedCustomSourceFilesExtensions.addAll(extensions)
}
//endregion
}
@@ -0,0 +1,25 @@
/*
* 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.sources.kpm
import org.gradle.api.NamedDomainObjectFactory
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension
import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSet
class FragmentMappedKotlinSourceSetFactory constructor(private val project: Project) :
NamedDomainObjectFactory<KotlinSourceSet> {
override fun create(name: String): FragmentMappedKotlinSourceSet {
val locator = SourceSetMappedFragmentLocator.get(project)
val location = locator.locateFragmentForSourceSet(project, name) ?: error("Couldn't map the source set name $name to KPM fragment")
val (moduleName, fragmentName) = location
val modules = project.multiplatformExtension.kpmModules
val fragment = modules.maybeCreate(moduleName).fragments.maybeCreate(fragmentName)
/** TODO setup JS-specific attributes similar to [org.jetbrains.kotlin.gradle.plugin.sources.DefaultKotlinSourceSetFactory]*/
return FragmentMappedKotlinSourceSet(project, fragment)
}
}
@@ -0,0 +1,59 @@
/*
* 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.sources.kpm
import org.gradle.api.Project
import org.jetbrains.kotlin.gradle.dsl.*
import org.jetbrains.kotlin.gradle.dsl.pm20Extension
import org.jetbrains.kotlin.gradle.dsl.topLevelExtensionOrNull
interface SourceSetMappedFragmentLocator {
data class FragmentLocation(val moduleName: String, val fragmentName: String) {
init {
require(moduleName.isNotEmpty()) { "module name should not be empty, got empty one and fragment name $fragmentName" }
require(fragmentName.isNotEmpty()) { "fragment name should not be empty, got empty one and module name $moduleName" }
}
}
fun locateFragmentForSourceSet(project: Project, sourceSetName: String): FragmentLocation?
companion object {
fun get(project: Project): SourceSetMappedFragmentLocator = when (project.topLevelExtensionOrNull) {
is KotlinMultiplatformExtension -> MultiplatformSourceSetMappedFragmentLocator()
is KotlinSingleTargetExtension -> TODO()
else -> error("couldn't provide model mapping utilities for project $project")
}
}
}
private class MultiplatformSourceSetMappedFragmentLocator : SourceSetMappedFragmentLocator {
override fun locateFragmentForSourceSet(project: Project, sourceSetName: String): SourceSetMappedFragmentLocator.FragmentLocation? {
val camelCaseParts = sourceSetName.camelCaseParts()
if (camelCaseParts.size < 2) {
return null
}
val candidateModuleNames = (1..camelCaseParts.size).asSequence().map { camelCaseParts.takeLast(it).joinToString("").decapitalize() }
val moduleName =
candidateModuleNames.firstOrNull { project.kotlinExtension.kpmModules.findByName(it) != null } ?: candidateModuleNames.first()
val fragmentName = sourceSetName.dropLast(moduleName.length)
return SourceSetMappedFragmentLocator.FragmentLocation(moduleName, fragmentName)
}
private fun String.camelCaseParts(): List<String> {
val capitalizedMatches = Regex("[A-Z][^A-Z]*").findAll(this).toList()
val firstNonCapitalizedWord = capitalizedMatches.firstOrNull()?.let { substring(it.range.first) } ?: this
return listOf(firstNonCapitalizedWord) + capitalizedMatches.map { it.value }
}
}
private class SingleTargetSourceSetMappedFragmentLocator : SourceSetMappedFragmentLocator {
override fun locateFragmentForSourceSet(project: Project, sourceSetName: String): SourceSetMappedFragmentLocator.FragmentLocation {
return SourceSetMappedFragmentLocator.FragmentLocation(sourceSetName, SINGLE_PLATFORM_FRAGMENT_NAME)
}
}
internal const val SINGLE_PLATFORM_FRAGMENT_NAME = "common"
@@ -358,40 +358,41 @@ class KotlinMetadataTargetConfigurator :
}
)
(sourceSet as DefaultKotlinSourceSet).dependencyTransformations[scope] = granularMetadataTransformation
val sourceSetMetadataConfigurationByScope = project.sourceSetMetadataConfigurationByScope(sourceSet, scope)
granularMetadataTransformation.applyToConfiguration(sourceSetMetadataConfigurationByScope)
if (sourceSet is DefaultKotlinSourceSet)
sourceSet.dependencyTransformations[scope] = granularMetadataTransformation
val sourceSetDependencyConfigurationByScope = project.sourceSetDependencyConfigurationByScope(sourceSet, scope)
// All source set dependencies except for compileOnly agree in versions with all other published runtime dependencies:
if (scope != KotlinDependencyScope.COMPILE_ONLY_SCOPE) {
if (isSourceSetPublished) {
if (isSourceSetPublished) {
if (scope != KotlinDependencyScope.COMPILE_ONLY_SCOPE) {
project.addExtendsFromRelation(
ALL_RUNTIME_METADATA_CONFIGURATION_NAME,
sourceSetDependencyConfigurationByScope.name
)
}
project.addExtendsFromRelation(
sourceSetMetadataConfigurationByScope.name,
ALL_RUNTIME_METADATA_CONFIGURATION_NAME
)
}
// All source set dependencies except for runtimeOnly agree in versions with all other published compile dependencies:
if (scope != KotlinDependencyScope.RUNTIME_ONLY_SCOPE) {
if (isSourceSetPublished) {
if (scope != KotlinDependencyScope.RUNTIME_ONLY_SCOPE) {
project.addExtendsFromRelation(
ALL_COMPILE_METADATA_CONFIGURATION_NAME,
sourceSetDependencyConfigurationByScope.name
)
}
project.addExtendsFromRelation(
sourceSetMetadataConfigurationByScope.name,
ALL_COMPILE_METADATA_CONFIGURATION_NAME
)
}
if (!PropertiesProvider(project).experimentalKpmModelMapping) {
val sourceSetMetadataConfigurationByScope = project.sourceSetMetadataConfigurationByScope(sourceSet, scope)
granularMetadataTransformation.applyToConfiguration(sourceSetMetadataConfigurationByScope)
if (scope != KotlinDependencyScope.COMPILE_ONLY_SCOPE) {
project.addExtendsFromRelation(
sourceSetMetadataConfigurationByScope.name,
ALL_COMPILE_METADATA_CONFIGURATION_NAME
)
}
if (scope != KotlinDependencyScope.RUNTIME_ONLY_SCOPE) {
project.addExtendsFromRelation(
sourceSetMetadataConfigurationByScope.name,
ALL_COMPILE_METADATA_CONFIGURATION_NAME
)
}
}
}
}