Skip to content

Commit a74917c

Browse files
committed
fix #1? Dynamically cast KGP classes if necessary
1 parent 619a35a commit a74917c

File tree

3 files changed

+188
-17
lines changed

3 files changed

+188
-17
lines changed

modules/bcv-gradle-plugin-functional-tests/src/functionalTest/kotlin/kotlinx/validation/test/SettingsPluginDslTest.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ private fun kotlinMultiplatformProjectWithBcvSettingsPlugin() =
148148
private val settingsGradleKtsWithBcvPlugin = """
149149
buildscript {
150150
dependencies {
151-
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.7.20")
151+
//classpath("org.jetbrains.kotlin:kotlin-gradle-plugin-api:1.7.20")
152152
}
153153
}
154154
@@ -222,5 +222,5 @@ val printBCVTargets by tasks.registering {
222222
}
223223
}
224224
}
225-
225+
226226
"""

modules/bcv-gradle-plugin/src/main/kotlin/BCVProjectPlugin.kt

+63-15
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,8 @@ import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.API_
77
import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.EXTENSION_NAME
88
import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.RUNTIME_CLASSPATH_CONFIGURATION_NAME
99
import dev.adamko.kotlin.binary_compatibility_validator.BCVPlugin.Companion.RUNTIME_CLASSPATH_RESOLVER_CONFIGURATION_NAME
10-
import dev.adamko.kotlin.binary_compatibility_validator.internal.BCVInternalApi
11-
import dev.adamko.kotlin.binary_compatibility_validator.internal.declarable
12-
import dev.adamko.kotlin.binary_compatibility_validator.internal.resolvable
13-
import dev.adamko.kotlin.binary_compatibility_validator.internal.sourceSets
10+
import dev.adamko.kotlin.binary_compatibility_validator.internal.*
11+
import dev.adamko.kotlin.binary_compatibility_validator.internal.Dynamic.Companion.Dynamic
1412
import dev.adamko.kotlin.binary_compatibility_validator.tasks.BCVApiCheckTask
1513
import dev.adamko.kotlin.binary_compatibility_validator.tasks.BCVApiDumpTask
1614
import dev.adamko.kotlin.binary_compatibility_validator.tasks.BCVApiGenerateTask
@@ -181,29 +179,79 @@ constructor(
181179
extension: BCVProjectExtension,
182180
) {
183181
project.pluginManager.withPlugin("kotlin-multiplatform") {
184-
val kotlinTargetsContainer = project.extensions.getByType<KotlinTargetsContainer>()
182+
try {
183+
val kotlinTargetsContainer = project.extensions.getByType<KotlinTargetsContainer>()
185184

186-
kotlinTargetsContainer.targets
187-
.matching {
188-
it.platformType in arrayOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm)
189-
}.all {
190-
val targetPlatformType = platformType
185+
kotlinTargetsContainer.targets
186+
.matching {
187+
it.platformType in arrayOf(KotlinPlatformType.jvm, KotlinPlatformType.androidJvm)
188+
}.all {
189+
val targetPlatformType = platformType
191190

191+
extension.targets.register(targetName) {
192+
enabled.convention(true)
193+
compilations
194+
.matching {
195+
when (targetPlatformType) {
196+
KotlinPlatformType.jvm -> it.name == "main"
197+
KotlinPlatformType.androidJvm -> it.name == "release"
198+
else -> false
199+
}
200+
}.all {
201+
inputClasses.from(output.classesDirs)
202+
}
203+
}
204+
}
205+
} catch (e: Throwable) {
206+
when (e) {
207+
is NoClassDefFoundError,
208+
is TypeNotPresentException -> {
209+
logger.info("Failed to apply BCVProjectPlugin to project ${project.path} with plugin $id using KGP classes $e")
210+
createKotlinMultiplatformTargetsHack(project, extension)
211+
}
212+
213+
else -> throw e
214+
}
215+
}
216+
}
217+
}
218+
219+
private fun createKotlinMultiplatformTargetsHack(
220+
project: Project,
221+
extension: BCVProjectExtension,
222+
) {
223+
logger.info("Falling back to dynamic access to KGP classes ${project.path} https://github.com/adamko-dev/kotlin-binary-compatibility-validator-mu/issues/1")
224+
val kmpExtAny = project.extensions.findByName("kotlin")
225+
?: return
226+
227+
val kmpExt by Dynamic<KotlinTargetsContainerWrapped>(kmpExtAny)
228+
229+
kmpExt.targets
230+
.matching { rawTarget ->
231+
val target by Dynamic<KotlinTargetWrapped>(rawTarget)
232+
val platformType by Dynamic<KotlinPlatformTypeWrapped>(target.platformType)
233+
platformType.name in arrayOf("jvm", "androidJvm")
234+
}.all {
235+
val it by Dynamic<KotlinTargetWrapped>(this)
236+
with(it) {
237+
val targetPlatformType by Dynamic<KotlinPlatformTypeWrapped>(platformType)
192238
extension.targets.register(targetName) {
193239
enabled.convention(true)
194240
compilations
195241
.matching {
196-
when (targetPlatformType) {
197-
KotlinPlatformType.jvm -> it.name == "main"
198-
KotlinPlatformType.androidJvm -> it.name == "release"
199-
else -> false
242+
when (targetPlatformType.name) {
243+
"jvm" -> it.name == "main"
244+
"androidJvm" -> it.name == "release"
245+
else -> false
200246
}
201247
}.all {
248+
val comp by Dynamic<KotlinCompilationWrapped>(this)
249+
val output by Dynamic<KotlinCompilationOutputWrapped>(comp.output)
202250
inputClasses.from(output.classesDirs)
203251
}
204252
}
205253
}
206-
}
254+
}
207255
}
208256

209257
private fun createJavaTestFixtureTargets(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
package dev.adamko.kotlin.binary_compatibility_validator.internal
2+
3+
import java.lang.reflect.InvocationHandler
4+
import java.lang.reflect.Method
5+
import java.lang.reflect.Proxy
6+
import kotlin.properties.ReadOnlyProperty
7+
import kotlin.reflect.KClass
8+
import kotlin.reflect.KProperty
9+
import org.gradle.api.Named
10+
import org.gradle.api.NamedDomainObjectCollection
11+
import org.gradle.api.NamedDomainObjectContainer
12+
import org.gradle.api.file.ConfigurableFileCollection
13+
14+
15+
/**
16+
* Wrap a [target] instance, allowing dynamic calls.
17+
*/
18+
internal class Dynamic<out T : Any> private constructor(
19+
private val cls: KClass<T>,
20+
private val target: Any,
21+
) : InvocationHandler, ReadOnlyProperty<Any?, T> {
22+
23+
private val targetName: String = target.javaClass.name
24+
private val targetMethods: List<Method> = target.javaClass.methods.asList()
25+
26+
private val proxy: T by lazy {
27+
val proxy = Proxy.newProxyInstance(
28+
target.javaClass.classLoader,
29+
arrayOf(cls.java),
30+
this,
31+
)
32+
@Suppress("UNCHECKED_CAST")
33+
proxy as T
34+
}
35+
36+
override fun invoke(
37+
proxy: Any,
38+
method: Method,
39+
args: Array<out Any?>?
40+
): Any? {
41+
for (delegateMethod in targetMethods) {
42+
if (method matches delegateMethod) {
43+
return if (args == null)
44+
delegateMethod.invoke(target)
45+
else
46+
delegateMethod.invoke(target, *args)
47+
}
48+
}
49+
throw UnsupportedOperationException("$targetName : $method args:[${args?.joinToString()}]")
50+
}
51+
52+
/** Delegated value provider */
53+
override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = proxy
54+
55+
56+
companion object {
57+
private infix fun Method.matches(other: Method): Boolean =
58+
this.name == other.name && this.parameterTypes.contentEquals(other.parameterTypes)
59+
60+
internal inline fun <reified T : Any> Dynamic(target: Any): Dynamic<T> =
61+
Dynamic(T::class, target)
62+
}
63+
}
64+
65+
66+
//private class A {
67+
// val name: String = "Team A"
68+
// fun shout() = println("go team A!")
69+
// fun echo(input: String) = input.repeat(5)
70+
//}
71+
//
72+
//private class B {
73+
// val name: String = "Team B"
74+
// fun shout() = println("go team B!")
75+
// fun echo(call: String) = call.repeat(2)
76+
//}
77+
//
78+
//private interface Shoutable {
79+
// val name: String
80+
// fun shout()
81+
// fun echo(call: String): String
82+
//}
83+
//
84+
//
85+
//private fun main() {
86+
// val a = A()
87+
// val b = B()
88+
//
89+
// val sa by DuckTyper<Shoutable>(a)
90+
// val sb: Shoutable? by DuckTyper(b)
91+
//
92+
// sa?.shout()
93+
// sb?.shout()
94+
// println(sa?.echo("hello..."))
95+
// println(sb?.echo("hello..."))
96+
// println(sa?.name)
97+
// println(sb?.name)
98+
//}
99+
100+
/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinTargetsContainer] */
101+
internal interface KotlinTargetsContainerWrapped {
102+
val targets: NamedDomainObjectCollection<Any>
103+
}
104+
105+
/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinTarget] */
106+
internal interface KotlinTargetWrapped {
107+
val platformType: Any
108+
val targetName: String
109+
val compilations: NamedDomainObjectContainer<Named>
110+
}
111+
112+
/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinPlatformType] */
113+
internal interface KotlinPlatformTypeWrapped : Named
114+
115+
/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinCompilation] */
116+
internal interface KotlinCompilationWrapped {
117+
val output: Any
118+
}
119+
120+
/** Wrap [org.jetbrains.kotlin.gradle.plugin.KotlinCompilationOutput] */
121+
internal interface KotlinCompilationOutputWrapped {
122+
val classesDirs: ConfigurableFileCollection
123+
}

0 commit comments

Comments
 (0)