Skip to content

Commit

Permalink
fix #1? Dynamically cast KGP classes if necessary
Browse files Browse the repository at this point in the history
  • Loading branch information
aSemy committed Jan 13, 2024
1 parent 619a35a commit a74917c
Show file tree
Hide file tree
Showing 3 changed files with 188 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
}
Expand Down Expand Up @@ -222,5 +222,5 @@ val printBCVTargets by tasks.registering {
}
}
}
"""
78 changes: 63 additions & 15 deletions modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -181,29 +179,79 @@ constructor(
extension: BCVProjectExtension,
) {
project.pluginManager.withPlugin("kotlin-multiplatform") {
val kotlinTargetsContainer = project.extensions.getByType<KotlinTargetsContainer>()
try {
val kotlinTargetsContainer = project.extensions.getByType<KotlinTargetsContainer>()

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<KotlinTargetsContainerWrapped>(kmpExtAny)

kmpExt.targets
.matching { rawTarget ->
val target by Dynamic<KotlinTargetWrapped>(rawTarget)
val platformType by Dynamic<KotlinPlatformTypeWrapped>(target.platformType)
platformType.name in arrayOf("jvm", "androidJvm")
}.all {
val it by Dynamic<KotlinTargetWrapped>(this)
with(it) {
val targetPlatformType by Dynamic<KotlinPlatformTypeWrapped>(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<KotlinCompilationWrapped>(this)
val output by Dynamic<KotlinCompilationOutputWrapped>(comp.output)
inputClasses.from(output.classesDirs)
}
}
}
}
}
}

private fun createJavaTestFixtureTargets(
Expand Down
123 changes: 123 additions & 0 deletions modules/bcv-gradle-plugin/src/main/kotlin/internal/Dynamic.kt
Original file line number Diff line number Diff line change
@@ -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<out T : Any> private constructor(
private val cls: KClass<T>,
private val target: Any,
) : InvocationHandler, ReadOnlyProperty<Any?, T> {

private val targetName: String = target.javaClass.name
private val targetMethods: List<Method> = 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<out Any?>?
): 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 <reified T : Any> Dynamic(target: Any): Dynamic<T> =
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<Shoutable>(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<Any>
}

/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinTarget] */
internal interface KotlinTargetWrapped {
val platformType: Any
val targetName: String
val compilations: NamedDomainObjectContainer<Named>
}

/** 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
}

0 comments on commit a74917c

Please sign in to comment.