org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPlugin.kt Maven / Gradle / Ivy
/*
* Copyright 2010-2016 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jetbrains.kotlin.gradle.plugin
import org.gradle.api.*
import org.gradle.api.artifacts.Configuration
import org.gradle.api.artifacts.ProjectDependency
import org.gradle.api.file.SourceDirectorySet
import org.gradle.api.tasks.SourceSet
import org.jetbrains.kotlin.gradle.dsl.kotlinExtension
import org.jetbrains.kotlin.gradle.logging.kotlinWarn
import org.jetbrains.kotlin.gradle.plugin.diagnostics.KotlinToolingDiagnostics
import org.jetbrains.kotlin.gradle.plugin.diagnostics.reportDiagnosticOncePerBuild
import org.jetbrains.kotlin.gradle.plugin.internal.JavaSourceSetsAccessor
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import org.jetbrains.kotlin.gradle.utils.androidPluginIds
import org.jetbrains.kotlin.gradle.utils.getOrPut
import java.util.concurrent.atomic.AtomicBoolean
abstract class KotlinPlatformPluginBase(protected val platformName: String) : Plugin {
companion object {
@JvmStatic
protected inline fun > Project.applyPlugin() {
pluginManager.apply(T::class.java)
}
}
}
const val EXPECTED_BY_CONFIG_NAME = "expectedBy"
const val IMPLEMENT_CONFIG_NAME = "implement"
const val IMPLEMENT_DEPRECATION_WARNING = "The '$IMPLEMENT_CONFIG_NAME' configuration is deprecated and will be removed. " +
"Use '$EXPECTED_BY_CONFIG_NAME' instead."
open class KotlinPlatformImplementationPluginBase(platformName: String) : KotlinPlatformPluginBase(platformName) {
private val commonProjects = arrayListOf()
private fun configurationsForCommonModuleDependency(project: Project): List =
listOf(project.configurations.getByName("api"))
override fun apply(project: Project) {
warnAboutKotlin12xMppDeprecation(project)
val implementConfig = project.configurations.create(IMPLEMENT_CONFIG_NAME)
val expectedByConfig = project.configurations.create(EXPECTED_BY_CONFIG_NAME)
implementConfig.dependencies.whenObjectAdded {
if (!implementConfigurationIsUsed) {
implementConfigurationIsUsed = true
project.logger.kotlinWarn(IMPLEMENT_DEPRECATION_WARNING)
}
}
listOf(implementConfig, expectedByConfig).forEach { config ->
config.isTransitive = false
config.dependencies.whenObjectAdded { dep ->
if (dep is ProjectDependency) {
addCommonProject(dep.dependencyProject, project)
// Needed for the projects that depend on this one to recover the common module sources through
// the transitive dependency (also, it will be added to the POM generated by Gradle):
configurationsForCommonModuleDependency(project).forEach { configuration ->
configuration.dependencies.add(dep)
}
} else {
throw GradleException("$project '${config.name}' dependency is not a project: $dep")
}
}
}
val incrementalMultiplatform = PropertiesProvider(project).incrementalMultiplatform ?: true
project.afterEvaluate {
project.tasks.withType(AbstractKotlinCompile::class.java).all { task ->
if (task.incremental && !incrementalMultiplatform) {
task.logger.debug("IC is turned off for task '${task.path}' because multiplatform IC is not enabled")
}
task.incremental = task.incremental && incrementalMultiplatform
}
}
}
private var implementConfigurationIsUsed = false
private fun addCommonProject(commonProject: Project, platformProject: Project) {
commonProjects.add(commonProject)
commonProject.whenEvaluated {
if (!commonProject.pluginManager.hasPlugin("kotlin-platform-common")) {
throw GradleException(
"Platform project $platformProject has an " +
"'$EXPECTED_BY_CONFIG_NAME'${if (implementConfigurationIsUsed) "/'$IMPLEMENT_CONFIG_NAME'" else ""} " +
"dependency to non-common project $commonProject"
)
}
// Since the two projects may add source sets in arbitrary order, and both may do that after the plugin is applied,
// we need to handle all source sets of the two projects and connect them once we get a match:
// todo warn if no match found
matchSymmetricallyByNames(
getKotlinSourceSetsSafe(commonProject),
namedSourceSetsContainer(platformProject)
) { commonSourceSet: Named, _ ->
addCommonSourceSetToPlatformSourceSet(commonSourceSet, platformProject)
// Workaround for older versions of Kotlin/Native overriding the old signature
commonProject.variantImplementationFactory()
.getInstance(commonProject)
.sourceSetsIfAvailable
?.findByName(commonSourceSet.name)
?.let { javaSourceSet ->
@Suppress("DEPRECATION")
addCommonSourceSetToPlatformSourceSet(javaSourceSet, platformProject)
}
}
}
}
/**
* Applies [whenMatched] to pairs of items with the same name in [containerA] and [containerB],
* regardless of the order in which they are added to the containers.
*/
private fun matchSymmetricallyByNames(
containerA: NamedDomainObjectCollection,
containerB: NamedDomainObjectCollection,
whenMatched: (A, B) -> Unit
) {
val matchedNames = mutableSetOf()
fun NamedDomainObjectCollection.matchAllWith(other: NamedDomainObjectCollection, match: (T, R) -> Unit) {
[email protected] { item ->
val itemName = [email protected](item)
if (itemName !in matchedNames) {
val otherItem = other.findByName(itemName)
if (otherItem != null) {
matchedNames += itemName
match(item, otherItem)
}
}
}
}
containerA.matchAllWith(containerB) { a, b -> whenMatched(a, b) }
containerB.matchAllWith(containerA) { b, a -> whenMatched(a, b) }
}
protected open fun namedSourceSetsContainer(project: Project): NamedDomainObjectContainer<*> =
project.kotlinExtension.sourceSets
protected open fun addCommonSourceSetToPlatformSourceSet(commonSourceSet: Named, platformProject: Project) {
platformProject.whenEvaluated {
// At the point when the source set in the platform module is created, the task does not exist
val platformTasks = platformProject.tasks
.withType(AbstractKotlinCompile::class.java)
.filter { it.sourceSetName.get() == commonSourceSet.name } // TODO use strict check once this code is not run in K/N
val commonSources = getKotlinSourceDirectorySetSafe(commonSourceSet)!!
for (platformTask in platformTasks) {
platformTask.setSource(commonSources)
platformTask.commonSourceSet.from(commonSources)
}
}
}
private fun getKotlinSourceSetsSafe(project: Project): NamedDomainObjectCollection {
// Access through reflection, because another project's KotlinProjectExtension might be loaded by a different class loader:
val kotlinExt = project.extensions.getByName("kotlin")
@Suppress("UNCHECKED_CAST")
val sourceSets = kotlinExt.javaClass.getMethod("getSourceSets").invoke(kotlinExt) as NamedDomainObjectCollection
return sourceSets
}
protected fun getKotlinSourceDirectorySetSafe(from: Any): SourceDirectorySet? {
val getKotlin = from.javaClass.getMethod("getKotlin")
return getKotlin(from) as? SourceDirectorySet
}
@Deprecated("Migrate to the new Kotlin source sets and use the addCommonSourceSetToPlatformSourceSet(Named, Project) overload")
protected open fun addCommonSourceSetToPlatformSourceSet(sourceSet: SourceSet, platformProject: Project) = Unit
@Deprecated("Retained for older Kotlin/Native MPP plugin binary compatibility", level = DeprecationLevel.ERROR)
protected val SourceSet.kotlin: SourceDirectorySet?
get() {
@Suppress("DEPRECATION")
return getExtension(KOTLIN_DSL_NAME) ?: getExtension(KOTLIN_JS_DSL_NAME)
}
}
internal fun Project.whenEvaluated(fn: Project.() -> T) {
if (state.executed) {
fn()
return
}
/** If there's already an Android plugin applied, just dispatch the action to `afterEvaluate`, it gets executed after AGP's actions */
if (androidPluginIds.any { pluginManager.hasPlugin(it) }) {
afterEvaluate { fn() }
return
}
val isDispatchedAfterAndroid = AtomicBoolean(false)
/**
* This queue holds all actions submitted to `whenEvaluated` in this project, waiting for one of the Android plugins to be applied.
* After (and if) an Android plugin gets applied, we dispatch all the actions in the queue to `afterEvaluate`, so that they are
* executed after what AGP scheduled to `afterEvaluate`. There are different Android plugins, so actions in the queue also need to check
* if it's the first Android plugin, using `isDispatched` (each has its own instance).
*/
val afterAndroidDispatchQueue = project.extensions.extraProperties.getOrPut("org.jetbrains.kotlin.whenEvaluated") {
val queue = mutableListOf<() -> Unit>()
// Trigger the actions on any plugin applied; the actions themselves ensure that they only dispatch the fn once.
androidPluginIds.forEach { id ->
pluginManager.withPlugin(id) { queue.forEach { it() } }
}
queue
}
afterAndroidDispatchQueue.add {
if (!isDispatchedAfterAndroid.getAndSet(true)) {
afterEvaluate { fn() }
}
}
afterEvaluate {
/** If no Android plugin was loaded, then the action was not dispatched, and we can freely execute it now */
if (!isDispatchedAfterAndroid.getAndSet(true)) {
fn()
}
}
}
private const val KOTLIN_12X_MPP_DEPRECATION_SUPPRESS_FLAG = "kotlin.internal.mpp12x.deprecation.suppress"
internal fun warnAboutKotlin12xMppDeprecation(project: Project) {
if (project.findProperty(KOTLIN_12X_MPP_DEPRECATION_SUPPRESS_FLAG) != "true") {
project.reportDiagnosticOncePerBuild(KotlinToolingDiagnostics.Kotlin12XMppDeprecation())
}
}