| /* |
| * 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.gradle.plugin.mpp |
| |
| import groovy.lang.Closure |
| import org.gradle.api.Plugin |
| import org.gradle.api.Project |
| import org.gradle.api.attributes.Attribute |
| import org.gradle.api.attributes.AttributeContainer |
| import org.gradle.api.internal.FeaturePreviews |
| import org.gradle.api.internal.plugins.DslObject |
| import org.gradle.api.plugins.JavaBasePlugin |
| import org.gradle.api.provider.Provider |
| import org.gradle.api.publish.PublicationContainer |
| import org.gradle.api.publish.PublishingExtension |
| import org.gradle.api.publish.maven.MavenPom |
| import org.gradle.api.publish.maven.MavenPublication |
| import org.gradle.api.publish.maven.internal.publication.MavenPublicationInternal |
| import org.gradle.api.tasks.SourceTask |
| import org.gradle.api.tasks.TaskProvider |
| import org.gradle.jvm.tasks.Jar |
| import org.gradle.util.ConfigureUtil |
| import org.gradle.util.GradleVersion |
| import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension |
| import org.jetbrains.kotlin.gradle.dsl.configureOrCreate |
| import org.jetbrains.kotlin.gradle.dsl.kotlinExtension |
| import org.jetbrains.kotlin.gradle.dsl.multiplatformExtension |
| import org.jetbrains.kotlin.gradle.internal.customizeKotlinDependencies |
| import org.jetbrains.kotlin.gradle.plugin.* |
| import org.jetbrains.kotlin.gradle.plugin.mpp.KotlinMultiplatformPlugin.Companion.sourceSetFreeCompilerArgsPropertyName |
| import org.jetbrains.kotlin.gradle.plugin.sources.* |
| import org.jetbrains.kotlin.gradle.plugin.sources.DefaultLanguageSettingsBuilder |
| import org.jetbrains.kotlin.gradle.plugin.sources.KotlinDependencyScope |
| import org.jetbrains.kotlin.gradle.plugin.sources.sourceSetDependencyConfigurationByScope |
| import org.jetbrains.kotlin.gradle.plugin.statistics.KotlinBuildStatsService |
| import org.jetbrains.kotlin.gradle.scripting.internal.ScriptingGradleSubplugin |
| import org.jetbrains.kotlin.gradle.targets.js.ir.KotlinJsIrTargetPreset |
| import org.jetbrains.kotlin.gradle.targets.metadata.isKotlinGranularMetadataEnabled |
| import org.jetbrains.kotlin.gradle.tasks.locateTask |
| import org.jetbrains.kotlin.gradle.tasks.registerTask |
| import org.jetbrains.kotlin.gradle.utils.* |
| import org.jetbrains.kotlin.konan.target.HostManager |
| import org.jetbrains.kotlin.konan.target.KonanTarget.* |
| import org.jetbrains.kotlin.konan.target.presetName |
| import org.jetbrains.kotlin.statistics.metrics.StringMetrics |
| |
| class KotlinMultiplatformPlugin( |
| private val kotlinPluginVersion: String, |
| private val featurePreviews: FeaturePreviews // TODO get rid of this internal API usage once we don't need it |
| ) : Plugin<Project> { |
| |
| private class TargetFromPresetExtension(val targetsContainer: KotlinTargetsContainerWithPresets) { |
| fun <T : KotlinTarget> fromPreset(preset: KotlinTargetPreset<T>, name: String, configureClosure: Closure<*>): T = |
| fromPreset(preset, name) { ConfigureUtil.configure(configureClosure, this) } |
| |
| @JvmOverloads |
| fun <T : KotlinTarget> fromPreset(preset: KotlinTargetPreset<T>, name: String, configureAction: T.() -> Unit = { }): T = |
| targetsContainer.configureOrCreate(name, preset, configureAction) |
| } |
| |
| override fun apply(project: Project) { |
| checkGradleCompatibility("the Kotlin Multiplatform plugin", GradleVersion.version("6.0")) |
| |
| project.plugins.apply(JavaBasePlugin::class.java) |
| |
| if (PropertiesProvider(project).mppStabilityNoWarn != true) { |
| SingleWarningPerBuild.show( |
| project, |
| "Kotlin Multiplatform Projects are an Alpha feature. " + |
| "See: https://kotlinlang.org/docs/reference/evolution/components-stability.html. " + |
| "To hide this message, add '$STABILITY_NOWARN_FLAG=true' to the Gradle properties.\n" |
| ) |
| } |
| |
| val targetsContainer = project.container(KotlinTarget::class.java) |
| val kotlinMultiplatformExtension = project.extensions.getByType(KotlinMultiplatformExtension::class.java) |
| val targetsFromPreset = TargetFromPresetExtension(kotlinMultiplatformExtension) |
| |
| kotlinMultiplatformExtension.apply { |
| DslObject(targetsContainer).addConvention("fromPreset", targetsFromPreset) |
| |
| targets = targetsContainer |
| addExtension("targets", targets) |
| |
| presets = project.container(KotlinTargetPreset::class.java) |
| addExtension("presets", presets) |
| |
| defaultJsCompilerType = PropertiesProvider(project).jsCompiler |
| } |
| |
| setupDefaultPresets(project) |
| customizeKotlinDependencies(project) |
| configureSourceSets(project) |
| |
| // set up metadata publishing |
| targetsFromPreset.fromPreset( |
| KotlinMetadataTargetPreset(project, kotlinPluginVersion), |
| METADATA_TARGET_NAME |
| ) |
| configurePublishingWithMavenPublish(project) |
| |
| targetsContainer.withType(AbstractKotlinTarget::class.java).all { applyUserDefinedAttributes(it) } |
| |
| // propagate compiler plugin options to the source set language settings |
| setupAdditionalCompilerArguments(project) |
| project.setupGeneralKotlinExtensionParameters() |
| |
| project.pluginManager.apply(ScriptingGradleSubplugin::class.java) |
| |
| exportProjectStructureMetadataForOtherBuilds(project) |
| |
| SingleActionPerBuild.run(project.rootProject, "cleanup-processed-metadata") { |
| project.gradle.buildFinished { |
| SourceSetMetadataStorageForIde.cleanupStaleEntries(project) |
| } |
| } |
| } |
| |
| private fun exportProjectStructureMetadataForOtherBuilds( |
| project: Project |
| ) { |
| GlobalProjectStructureMetadataStorage.registerProjectStructureMetadata(project) { |
| checkNotNull(buildKotlinProjectStructureMetadata(project)) |
| } |
| } |
| |
| private fun setupAdditionalCompilerArguments(project: Project) { |
| // common source sets use the compiler options from the metadata compilation: |
| val metadataCompilation = |
| project.multiplatformExtension.metadata().compilations.getByName(KotlinCompilation.MAIN_COMPILATION_NAME) |
| |
| val primaryCompilationsBySourceSet by lazy { // don't evaluate eagerly: Android targets are not created at this point |
| val allCompilationsForSourceSets = CompilationSourceSetUtil.compilationsBySourceSets(project).mapValues { (_, compilations) -> |
| compilations.filter { it.target.platformType != KotlinPlatformType.common } |
| } |
| |
| allCompilationsForSourceSets.mapValues { (_, compilations) -> // choose one primary compilation |
| when (compilations.size) { |
| 0 -> metadataCompilation |
| 1 -> compilations.single() |
| else -> { |
| val sourceSetTargets = compilations.map { it.target }.distinct() |
| when (sourceSetTargets.size) { |
| 1 -> sourceSetTargets.single().compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME) |
| ?: // use any of the compilations for now, looks OK for Android TODO maybe reconsider |
| compilations.first() |
| else -> metadataCompilation |
| } |
| } |
| } |
| } |
| } |
| |
| project.kotlinExtension.sourceSets.all { sourceSet -> |
| (sourceSet.languageSettings as? DefaultLanguageSettingsBuilder)?.run { |
| compilerPluginOptionsTask = lazy { |
| val associatedCompilation = primaryCompilationsBySourceSet[sourceSet] ?: metadataCompilation |
| project.tasks.getByName(associatedCompilation.compileKotlinTaskName) as SourceTask |
| } |
| } |
| } |
| } |
| |
| fun setupDefaultPresets(project: Project) { |
| with(project.multiplatformExtension.presets) { |
| add(KotlinJvmTargetPreset(project, kotlinPluginVersion)) |
| add(KotlinJsTargetPreset(project, kotlinPluginVersion).apply { irPreset = null }) |
| add(KotlinJsIrTargetPreset(project, kotlinPluginVersion).apply { mixedMode = false }) |
| add( |
| KotlinJsTargetPreset( |
| project, |
| kotlinPluginVersion |
| ).apply { |
| irPreset = KotlinJsIrTargetPreset(project, kotlinPluginVersion) |
| .apply { mixedMode = true } |
| } |
| ) |
| add(KotlinAndroidTargetPreset(project, kotlinPluginVersion)) |
| add(KotlinJvmWithJavaTargetPreset(project, kotlinPluginVersion)) |
| |
| // Note: modifying these sets should also be reflected in the DSL code generator, see 'presetEntries.kt' |
| val nativeTargetsWithHostTests = setOf(LINUX_X64, MACOS_X64, MINGW_X64) |
| val nativeTargetsWithSimulatorTests = setOf(IOS_X64, WATCHOS_X86, WATCHOS_X64, TVOS_X64) |
| |
| HostManager().targets |
| .forEach { (_, konanTarget) -> |
| val targetToAdd = when (konanTarget) { |
| in nativeTargetsWithHostTests -> |
| KotlinNativeTargetWithHostTestsPreset(konanTarget.presetName, project, konanTarget, kotlinPluginVersion) |
| in nativeTargetsWithSimulatorTests -> |
| KotlinNativeTargetWithSimulatorTestsPreset(konanTarget.presetName, project, konanTarget, kotlinPluginVersion) |
| else -> KotlinNativeTargetPreset(konanTarget.presetName, project, konanTarget, kotlinPluginVersion) |
| } |
| |
| add(targetToAdd) |
| } |
| } |
| } |
| |
| private fun configurePublishingWithMavenPublish(project: Project) = project.pluginManager.withPlugin("maven-publish") { _ -> |
| |
| val targets = project.multiplatformExtension.targets |
| val metadataTarget = project.multiplatformExtension.metadata() |
| val kotlinSoftwareComponent = project.multiplatformExtension.rootSoftwareComponent |
| |
| project.extensions.configure(PublishingExtension::class.java) { publishing -> |
| |
| // The root publication that references the platform specific publications as its variants: |
| publishing.publications.create("kotlinMultiplatform", MavenPublication::class.java).apply { |
| from(kotlinSoftwareComponent) |
| (this as MavenPublicationInternal).publishWithOriginalFileName() |
| kotlinSoftwareComponent.publicationDelegate = this@apply |
| |
| metadataTarget.kotlinComponents.filterIsInstance<KotlinTargetComponentWithPublication>() |
| .single().publicationDelegate = this@apply |
| |
| project.whenEvaluated { |
| if (!metadataTarget.publishable) return@whenEvaluated |
| metadataTarget.kotlinComponents |
| .flatMap { component -> component.sourcesArtifacts } |
| .forEach { sourcesArtifact -> artifact(sourcesArtifact) } |
| } |
| } |
| |
| // Enforce the order of creating the publications, since the metadata publication is used in the other publications: |
| metadataTarget.createMavenPublications(publishing.publications) |
| targets |
| .withType(AbstractKotlinTarget::class.java).matching { it.publishable && it.name != METADATA_TARGET_NAME } |
| .all { |
| if (it is KotlinAndroidTarget || it is KotlinMetadataTarget) |
| // Android targets have their variants created in afterEvaluate; TODO handle this better? |
| // Kotlin Metadata targets rely on complete source sets hierearchy and cannot be inspected for publication earlier |
| project.whenEvaluated { it.createMavenPublications(publishing.publications) } |
| else |
| it.createMavenPublications(publishing.publications) |
| } |
| } |
| |
| project.components.add(kotlinSoftwareComponent) |
| } |
| |
| private fun rewritePom( |
| pom: MavenPom, |
| pomRewriter: PomDependenciesRewriter, |
| shouldRewritePomDependencies: Provider<Boolean>, |
| includeOnlySpecifiedDependencies: Provider<Set<ModuleCoordinates>>? |
| ) { |
| pom.withXml { xml -> |
| if (shouldRewritePomDependencies.get()) |
| pomRewriter.rewritePomMppDependenciesToActualTargetModules(xml, includeOnlySpecifiedDependencies) |
| } |
| } |
| |
| private fun AbstractKotlinTarget.createMavenPublications(publications: PublicationContainer) { |
| components |
| .map { gradleComponent -> gradleComponent to kotlinComponents.single { it.name == gradleComponent.name } } |
| .filter { (_, kotlinComponent) -> kotlinComponent.publishable } |
| .forEach { (gradleComponent, kotlinComponent) -> |
| val componentPublication = publications.create(kotlinComponent.name, MavenPublication::class.java).apply { |
| // do this in whenEvaluated since older Gradle versions seem to check the files in the variant eagerly: |
| project.whenEvaluated { |
| from(gradleComponent) |
| kotlinComponent.sourcesArtifacts.forEach { sourceArtifact -> |
| artifact(sourceArtifact) |
| } |
| } |
| (this as MavenPublicationInternal).publishWithOriginalFileName() |
| artifactId = kotlinComponent.defaultArtifactId |
| |
| val pomRewriter = PomDependenciesRewriter(project, kotlinComponent) |
| val shouldRewritePomDependencies = |
| project.provider { PropertiesProvider(project).keepMppDependenciesIntactInPoms != true } |
| |
| rewritePom( |
| pom, |
| pomRewriter, |
| shouldRewritePomDependencies, |
| dependenciesForPomRewriting(this@createMavenPublications) |
| ) |
| } |
| |
| (kotlinComponent as? KotlinTargetComponentWithPublication)?.publicationDelegate = componentPublication |
| publicationConfigureActions.all { it.execute(componentPublication) } |
| } |
| } |
| |
| /** |
| * The metadata targets need their POMs to only include the dependencies from the commonMain API configuration. |
| * The actual apiElements configurations of metadata targets now contain dependencies from all source sets, but, as the consumers who |
| * can't read Gradle module metadata won't resolve a dependency on an MPP to the granular metadata variant and won't then choose the |
| * right dependencies for each source set, we put only the dependencies of the legacy common variant into the POM, i.e. commonMain API. |
| */ |
| private fun dependenciesForPomRewriting(target: AbstractKotlinTarget): Provider<Set<ModuleCoordinates>>? = |
| if (target !is KotlinMetadataTarget || !target.project.isKotlinGranularMetadataEnabled) |
| null |
| else { |
| val commonMain = target.project.kotlinExtension.sourceSets.findByName(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) |
| if (commonMain == null) |
| null |
| else |
| target.project.provider { |
| val project = target.project |
| |
| // Only the commonMain API dependencies can be published for consumers who can't read Gradle project metadata |
| val commonMainApi = project.sourceSetDependencyConfigurationByScope(commonMain, KotlinDependencyScope.API_SCOPE) |
| val commonMainDependencies = commonMainApi.allDependencies |
| commonMainDependencies.map { ModuleCoordinates(it.group, it.name, it.version) }.toSet() |
| } |
| } |
| |
| private fun configureSourceSets(project: Project) = with(project.multiplatformExtension) { |
| val production = sourceSets.create(KotlinSourceSet.COMMON_MAIN_SOURCE_SET_NAME) |
| val test = sourceSets.create(KotlinSourceSet.COMMON_TEST_SOURCE_SET_NAME) |
| |
| targets.all { target -> |
| target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)?.let { mainCompilation -> |
| mainCompilation.defaultSourceSet.takeIf { it != production }?.dependsOn(production) |
| } |
| |
| target.compilations.findByName(KotlinCompilation.TEST_COMPILATION_NAME)?.let { testCompilation -> |
| testCompilation.defaultSourceSet.takeIf { it != test }?.dependsOn(test) |
| } |
| |
| val targetName = if (target is KotlinNativeTarget) |
| target.konanTarget.name |
| else |
| target.platformType.name |
| KotlinBuildStatsService.getInstance()?.report(StringMetrics.MPP_PLATFORMS, targetName) |
| } |
| |
| UnusedSourceSetsChecker.checkSourceSets(project) |
| |
| project.whenEvaluated { |
| checkSourceSetVisibilityRequirements(project) |
| } |
| } |
| |
| companion object { |
| const val METADATA_TARGET_NAME = "metadata" |
| |
| internal fun sourceSetFreeCompilerArgsPropertyName(sourceSetName: String) = |
| "kotlin.mpp.freeCompilerArgsForSourceSet.$sourceSetName" |
| |
| internal const val STABILITY_NOWARN_FLAG = "kotlin.mpp.stability.nowarn" |
| } |
| } |
| |
| /** |
| * The attributes attached to the targets and compilations need to be propagated to the relevant Gradle configurations: |
| * 1. Output configurations of each target need the corresponding compilation's attributes (and, indirectly, the target's attributes) |
| * 2. Resolvable configurations of each compilation need the compilation's attributes |
| */ |
| internal fun applyUserDefinedAttributes(target: AbstractKotlinTarget) { |
| val project = target.project |
| |
| project.whenEvaluated { |
| fun copyAttributes(from: AttributeContainer, to: AttributeContainer) { |
| fun <T> copyAttribute(key: Attribute<T>, from: AttributeContainer, to: AttributeContainer) { |
| to.attribute(key, from.getAttribute(key)!!) |
| } |
| |
| from.keySet().forEach { key -> copyAttribute(key, from, to) } |
| } |
| |
| // To copy the attributes to the output configurations, find those output configurations and their producing compilations |
| // based on the target's components: |
| val outputConfigurationsWithCompilations = |
| target.kotlinComponents.filterIsInstance<KotlinVariant>().flatMap { kotlinVariant -> |
| kotlinVariant.usages.mapNotNull { usageContext -> |
| project.configurations.findByName(usageContext.dependencyConfigurationName)?.let { configuration -> |
| configuration to usageContext.compilation |
| } |
| } |
| } + listOfNotNull( |
| target.compilations.findByName(KotlinCompilation.MAIN_COMPILATION_NAME)?.let { mainCompilation -> |
| project.configurations.findByName(target.defaultConfigurationName)?.to(mainCompilation) |
| } |
| ) |
| |
| outputConfigurationsWithCompilations.forEach { (configuration, compilation) -> |
| copyAttributes(compilation.attributes, configuration.attributes) |
| } |
| |
| target.compilations.all { compilation -> |
| val compilationAttributes = compilation.attributes |
| |
| compilation.relatedConfigurationNames |
| .mapNotNull { configurationName -> target.project.configurations.findByName(configurationName) } |
| .forEach { configuration -> copyAttributes(compilationAttributes, configuration.attributes) } |
| } |
| } |
| } |
| |
| internal fun sourcesJarTask(compilation: KotlinCompilation<*>, componentName: String?, artifactNameAppendix: String): TaskProvider<Jar> = |
| sourcesJarTask(compilation.target.project, lazy { compilation.allKotlinSourceSets }, componentName, artifactNameAppendix) |
| |
| internal fun sourcesJarTask( |
| project: Project, |
| sourceSets: Lazy<Set<KotlinSourceSet>>, |
| componentName: String?, |
| artifactNameAppendix: String |
| ): TaskProvider<Jar> { |
| val taskName = lowerCamelCaseName(componentName, "sourcesJar") |
| |
| project.locateTask<Jar>(taskName)?.let { |
| return it |
| } |
| |
| val result = project.registerTask<Jar>(taskName) { sourcesJar -> |
| sourcesJar.archiveAppendix.set(artifactNameAppendix) |
| sourcesJar.archiveClassifier.set("sources") |
| } |
| |
| project.whenEvaluated { |
| result.configure { |
| sourceSets.value.forEach { sourceSet -> |
| it.from(sourceSet.kotlin) { copySpec -> |
| copySpec.into(sourceSet.name) |
| } |
| } |
| } |
| } |
| |
| return result |
| } |
| |
| internal fun Project.setupGeneralKotlinExtensionParameters() { |
| val sourceSetsInMainCompilation by lazy { |
| CompilationSourceSetUtil.compilationsBySourceSets(project).filterValues { compilations -> |
| compilations.any { |
| // kotlin main compilation |
| it.isMain() |
| // android compilation which is NOT in tested variant |
| || (it as? KotlinJvmAndroidCompilation)?.let { getTestedVariantData(it.androidVariant) == null } == true |
| } |
| }.keys |
| } |
| |
| kotlinExtension.sourceSets.all { sourceSet -> |
| (sourceSet.languageSettings as? DefaultLanguageSettingsBuilder)?.run { |
| |
| // Set ad-hoc free compiler args from the internal project property |
| freeCompilerArgsProvider = project.provider { |
| val propertyValue = with(project.extensions.extraProperties) { |
| val sourceSetFreeCompilerArgsPropertyName = sourceSetFreeCompilerArgsPropertyName(sourceSet.name) |
| if (has(sourceSetFreeCompilerArgsPropertyName)) { |
| get(sourceSetFreeCompilerArgsPropertyName) |
| } else null |
| } |
| |
| mutableListOf<String>().apply { |
| when (propertyValue) { |
| is String -> add(propertyValue) |
| is Iterable<*> -> addAll(propertyValue.map { it.toString() }) |
| } |
| |
| val explicitApiState = project.kotlinExtension.explicitApi?.toCompilerArg() |
| // do not look into lazy set if explicitApiMode was not enabled |
| if (explicitApiState != null && sourceSet in sourceSetsInMainCompilation) |
| add(explicitApiState) |
| } |
| } |
| } |
| } |
| } |