diff --git a/modules/bcv-gradle-plugin-functional-tests/src/functionalTest/kotlin/kotlinx/validation/test/SettingsPluginDslTest.kt b/modules/bcv-gradle-plugin-functional-tests/src/functionalTest/kotlin/kotlinx/validation/test/SettingsPluginDslTest.kt index bdc6029..56e9e6e 100644 --- a/modules/bcv-gradle-plugin-functional-tests/src/functionalTest/kotlin/kotlinx/validation/test/SettingsPluginDslTest.kt +++ b/modules/bcv-gradle-plugin-functional-tests/src/functionalTest/kotlin/kotlinx/validation/test/SettingsPluginDslTest.kt @@ -148,7 +148,7 @@ private fun kotlinMultiplatformProjectWithBcvSettingsPlugin() = private val settingsGradleKtsWithBcvPlugin = """ buildscript { dependencies { - classpath("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.7.20") + //classpath("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.7.20") } } @@ -222,5 +222,5 @@ val printBCVTargets by tasks.registering { } } } - + """ diff --git a/modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt b/modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt index 080fd12..ed14f49 100644 --- a/modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt +++ b/modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt @@ -7,10 +7,8 @@ import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.API_ import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.EXTENSION_NAME import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.RUNTIME_CLASSPATH_CONFIGURATION_NAME import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.RUNTIME_CLASSPATH_RESOLVER_CONFIGURATION_NAME -import dev.adamko.kotlin.binary_compatibility_validator.internal.BCVInternalApi -import dev.adamko.kotlin.binary_compatibility_validator.internal.declarable -import dev.adamko.kotlin.binary_compatibility_validator.internal.resolvable -import dev.adamko.kotlin.binary_compatibility_validator.internal.sourceSets +import dev.adamko.kotlin.binary_compatibility_validator.internal.* +import dev.adamko.kotlin.binary_compatibility_validator.internal.Dynamic.Companion.Dynamic import dev.adamko.kotlin.binary_compatibility_validator.tasks.BCVApiCheckTask import dev.adamko.kotlin.binary_compatibility_validator.tasks.BCVApiDumpTask import dev.adamko.kotlin.binary_compatibility_validator.tasks.BCVApiGenerateTask @@ -181,29 +179,79 @@ constructor( extension: BCVProjectExtension, ) { project.pluginManager.withPlugin("kotlin-multiplatform") { - val kotlinTargetsContainer = project.extensions.getByType() + try { + val kotlinTargetsContainer = project.extensions.getByType() - kotlinTargetsContainer.targets - .matching { - it.platformType in arrayOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm) - }.all { - val targetPlatformType = platformType + kotlinTargetsContainer.targets + .matching { + it.platformType in arrayOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm) + }.all { + val targetPlatformType = platformType + extension.targets.register(targetName) { + enabled.convention(true) + compilations + .matching { + when (targetPlatformType) { + KotlinPlatformType.jvm -> it.name == "main" + KotlinPlatformType.androidJvm -> it.name == "release" + else -> false + } + }.all { + inputClasses.from(output.classesDirs) + } + } + } + } catch (e: Throwable) { + when (e) { + is NoClassDefFoundError, + is TypeNotPresentException -> { + logger.info("Failed to apply BCVProjectPlugin to project ${project.path} with plugin $id using KGP classes $e") + createKotlinMultiplatformTargetsHack(project, extension) + } + + else -> throw e + } + } + } + } + + private fun createKotlinMultiplatformTargetsHack( + project: Project, + extension: BCVProjectExtension, + ) { + logger.info("Falling back to dynamic access to KGP classes ${project.path} https://github.com/adamko-dev/kotlin-binary-compatibility-validator-mu/issues/1") + val kmpExtAny = project.extensions.findByName("kotlin") + ?: return + + val kmpExt by Dynamic(kmpExtAny) + + kmpExt.targets + .matching { rawTarget -> + val target by Dynamic(rawTarget) + val platformType by Dynamic(target.platformType) + platformType.name in arrayOf("jvm", "androidJvm") + }.all { + val it by Dynamic(this) + with(it) { + val targetPlatformType by Dynamic(platformType) extension.targets.register(targetName) { enabled.convention(true) compilations .matching { - when (targetPlatformType) { - KotlinPlatformType.jvm -> it.name == "main" - KotlinPlatformType.androidJvm -> it.name == "release" - else -> false + when (targetPlatformType.name) { + "jvm" -> it.name == "main" + "androidJvm" -> it.name == "release" + else -> false } }.all { + val comp by Dynamic(this) + val output by Dynamic(comp.output) inputClasses.from(output.classesDirs) } } } - } + } } private fun createJavaTestFixtureTargets( diff --git a/modules/bcv-gradle-plugin/src/main/kotlin/internal/Dynamic.kt b/modules/bcv-gradle-plugin/src/main/kotlin/internal/Dynamic.kt new file mode 100644 index 0000000..4545e39 --- /dev/null +++ b/modules/bcv-gradle-plugin/src/main/kotlin/internal/Dynamic.kt @@ -0,0 +1,123 @@ +package dev.adamko.kotlin.binary_compatibility_validator.internal + +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Method +import java.lang.reflect.Proxy +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import org.gradle.api.Named +import org.gradle.api.NamedDomainObjectCollection +import org.gradle.api.NamedDomainObjectContainer +import org.gradle.api.file.ConfigurableFileCollection + + +/** + * Wrap a [target] instance, allowing dynamic calls. + */ +internal class Dynamic private constructor( + private val cls: KClass, + private val target: Any, +) : InvocationHandler, ReadOnlyProperty { + + private val targetName: String = target.javaClass.name + private val targetMethods: List = target.javaClass.methods.asList() + + private val proxy: T by lazy { + val proxy = Proxy.newProxyInstance( + target.javaClass.classLoader, + arrayOf(cls.java), + this, + ) + @Suppress("UNCHECKED_CAST") + proxy as T + } + + override fun invoke( + proxy: Any, + method: Method, + args: Array? + ): Any? { + for (delegateMethod in targetMethods) { + if (method matches delegateMethod) { + return if (args == null) + delegateMethod.invoke(target) + else + delegateMethod.invoke(target, *args) + } + } + throw UnsupportedOperationException("$targetName : $method args:[${args?.joinToString()}]") + } + + /** Delegated value provider */ + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = proxy + + + companion object { + private infix fun Method.matches(other: Method): Boolean = + this.name == other.name && this.parameterTypes.contentEquals(other.parameterTypes) + + internal inline fun Dynamic(target: Any): Dynamic = + Dynamic(T::class, target) + } +} + + +//private class A { +// val name: String = "Team A" +// fun shout() = println("go team A!") +// fun echo(input: String) = input.repeat(5) +//} +// +//private class B { +// val name: String = "Team B" +// fun shout() = println("go team B!") +// fun echo(call: String) = call.repeat(2) +//} +// +//private interface Shoutable { +// val name: String +// fun shout() +// fun echo(call: String): String +//} +// +// +//private fun main() { +// val a = A() +// val b = B() +// +// val sa by DuckTyper(a) +// val sb: Shoutable? by DuckTyper(b) +// +// sa?.shout() +// sb?.shout() +// println(sa?.echo("hello...")) +// println(sb?.echo("hello...")) +// println(sa?.name) +// println(sb?.name) +//} + +/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinTargetsContainer] */ +internal interface KotlinTargetsContainerWrapped { + val targets: NamedDomainObjectCollection +} + +/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinTarget] */ +internal interface KotlinTargetWrapped { + val platformType: Any + val targetName: String + val compilations: NamedDomainObjectContainer +} + +/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType] */ +internal interface KotlinPlatformTypeWrapped : Named + +/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinCompilation] */ +internal interface KotlinCompilationWrapped { + val output: Any +} + +/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinCompilationOutput] */ +internal interface KotlinCompilationOutputWrapped { + val classesDirs: ConfigurableFileCollection +}