All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.jetbrains.kotlin.gradle.plugin.KotlinMultiplatformPlugin.kt Maven / Gradle / Ivy

There is a newer version: 2.0.0
Show newest version
/*
 * 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())
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy