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

co.touchlab.skie.plugin.SkieLoaderPlugin.kt Maven / Gradle / Ivy

package co.touchlab.skie.plugin

import co.touchlab.skie.gradle.KotlinCompilerVersion
import co.touchlab.skie.gradle_plugin_loader.BuildConfig
import co.touchlab.skie.plugin.configuration.SkieExtension
import org.gradle.api.Named
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.api.attributes.Category
import org.gradle.api.attributes.LibraryElements
import org.gradle.api.attributes.Usage
import org.gradle.api.attributes.plugin.GradlePluginApiVersion
import org.gradle.api.model.ObjectFactory
import org.gradle.configurationcache.extensions.serviceOf
import org.gradle.internal.classloader.HashingClassLoaderFactory
import org.gradle.internal.classpath.DefaultClassPath
import org.gradle.util.GradleVersion
import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin

@Suppress("unused")
abstract class SkieLoaderPlugin : Plugin {

    override fun apply(project: Project) {
        // We need to register the extension here, so that Gradle knows the type of it in the build script.
        with(SkieExtension) {
            project.createExtension()
        }

        val kotlinVersion = project.getValidKotlinVersion() ?: return

        project.loadSkieGradlePlugin(kotlinVersion)
    }

    private fun Project.getValidKotlinVersion(): String? {
        val kotlinVersion = getKotlinVersionString()
        val kgpVersion = kotlinVersion?.let { BuildConfig.KOTLIN_TO_SKIE_KGP_VERSION[it] }

        if (kotlinGradlePluginVersionOverride != null) {
            logger.error(
                """
                    Warning:
                    skie.kgpVersion is used to override automatic Kotlin version resolution for SKIE plugin.
                    Usage of this property in production is highly discouraged as it may lead to non-obvious compiler errors caused by SKIE incompatibility with the used Kotlin compiler version.
                """.trimIndent(),
            )
        }

        val error = when {
            kotlinVersion == null -> {
                """
                    SKIE could not infer Kotlin plugin version.
                    Make sure you have Kotlin Multiplatform plugin applied in the same module as SKIE and that the plugin works - for example by calling the link task that produces the Obj-C framework.
                    If that is the case, then this problem is likely caused by a bug in SKIE - please report it to the SKIE developers.
                    You can try to workaround this issue by providing the Kotlin version manually via 'skie.kgpVersion' property in your gradle.properties.
                """.trimIndent()
            }
            kgpVersion == null -> {
                val supportedKotlinVersions = BuildConfig.KOTLIN_TO_SKIE_KGP_VERSION.keys.sorted()

                """
                    SKIE ${BuildConfig.SKIE_VERSION} does not support Kotlin $kotlinVersion.
                    Supported versions are: ${supportedKotlinVersions}.
                    Check if you have the most recent version of SKIE and if so, please wait for the SKIE developers to add support for this Kotlin version.
                    New Kotlin versions are usually supported within a few days after they are released.
                    Note that there are no plans for supporting early access versions like Beta, RC, etc.
                """.trimIndent()
            }
            else -> return kgpVersion
        }

        reportSkieLoaderError(error)

        return null
    }

    private fun Project.reportSkieLoaderError(error: String) {
        logger.error("Error:\n$error\nSKIE cannot not be used until this error is resolved.\n")

        gradle.taskGraph.whenReady {
            val hasLinkTask = allTasks.any { it.name.startsWith("link") && it.project == project }
            val isSkieEnabled = extensions.findByType(SkieExtension::class.java)?.isEnabled?.get() == true

            if (hasLinkTask && isSkieEnabled) {
                error("$error\nTo proceed with the compilation, please remove or explicitly disable SKIE by adding 'skie { isEnabled.set(false) }' to your Gradle configuration.")
            }
        }
    }

    private fun Project.getKotlinVersionString(): String? =
        (project.kotlinGradlePluginVersionOverride ?: project.kotlinGradlePluginVersion ?: project.rootProject.kotlinGradlePluginVersion)

    private fun Project.loadSkieGradlePlugin(kotlinVersion: String) {
        val gradleVersion = GradleVersion.current().version
        logger.info("Resolving SKIE gradle plugin for Kotlin plugin version $kotlinVersion and Gradle version $gradleVersion")

        KotlinCompilerVersion.registerIn(project.dependencies, kotlinVersion)
        KotlinCompilerVersion.registerIn(buildscript.dependencies, kotlinVersion)
        val skieGradleConfiguration = buildscript.configurations.detachedConfiguration(
            buildscript.dependencies.create(BuildConfig.SKIE_GRADLE_PLUGIN),
        ).apply {
            this.isCanBeConsumed = false
            this.isCanBeResolved = true

            exclude(
                mapOf(
                    "group" to "org.jetbrains.kotlin",
                ),
            )

            attributes {
                attribute(Category.CATEGORY_ATTRIBUTE, objects.named(Category.LIBRARY))
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
                attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.JAR))
                attribute(Usage.USAGE_ATTRIBUTE, objects.named(Usage.JAVA_RUNTIME))
                attribute(KotlinCompilerVersion.attribute, objects.named(kotlinVersion))
                if (GradleVersion.current() >= GradleVersion.version("7.0")) {
                    attribute(
                        GradlePluginApiVersion.GRADLE_PLUGIN_API_VERSION_ATTRIBUTE,
                        objects.named(GradleVersion.current().version),
                    )
                }
            }
        }

        skieGradleConfiguration.resolvedConfiguration.rethrowFailure()

        val classLoaderFactory = serviceOf()
        val skieGradleClassLoader = classLoaderFactory.createChildClassLoader(
            "skieGradleClassLoader",
            buildscript.classLoader,
            DefaultClassPath.of(skieGradleConfiguration.resolve()),
            null,
        )

        val probablySkiePluginClass = skieGradleClassLoader.loadClass("co.touchlab.skie.plugin.SkieGradlePlugin")
        if (!Plugin::class.java.isAssignableFrom(probablySkiePluginClass)) {
            reportSkieLoaderError(
                """
                    Loaded class ${probablySkiePluginClass.name} does not implement ${Plugin::class.java.name}!
                    This is a bug in SKIE - please report it to the SKIE developers.
                """.trimIndent(),
            )
            return
        }

        @Suppress("UNCHECKED_CAST")
        val shimPlugin: Class> = probablySkiePluginClass as Class>
        plugins.apply(shimPlugin)
    }

    private val Project.kotlinGradlePluginVersion: String?
        get() = kotlinGradlePluginVersionFromPlugin() ?: kotlinGradlePluginVersionFromClasspathConfiguration()

    private val Project.kotlinGradlePluginVersionOverride: String?
        get() = findProperty("skie.kgpVersion") as? String

    private fun Project.kotlinGradlePluginVersionFromPlugin(): String? {
        return try {
            plugins.filterIsInstance().firstOrNull()?.pluginVersion
        } catch (e: NoClassDefFoundError) {
            // This happens when kotlin-gradle-plugin-api is not on classpath. SKIE loader doesn't add it to make sure we don't lock it to a specific version.
            null
        } catch (e: ClassNotFoundException) {
            // We'll probably never get here, but we want to be sure not to crash when we can't find the KotlinBasePlugin class.
            null
        }
    }

    private fun Project.kotlinGradlePluginVersionFromClasspathConfiguration(): String? {
        val classpathConfiguration = buildscript.configurations.getByName("classpath")
        val artifact = classpathConfiguration.resolvedConfiguration.resolvedArtifacts.singleOrNull { artifact ->
            artifact.moduleVersion.id.let { it.group == "org.jetbrains.kotlin" && it.name == "kotlin-gradle-plugin" }
        }
        return artifact?.moduleVersion?.id?.version
    }

    private inline fun  ObjectFactory.named(name: String): T =
        named(T::class.java, name)
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy