11import org.jetbrains.intellij.platform.gradle.TestFrameworkType
2+ import java.io.File
23
34plugins {
45 id(" org.jetbrains.kotlin.jvm" )
@@ -44,27 +45,33 @@ dependencies {
4445 // and the bridge then throws "Result type X is not serializable".
4546 compileOnly(" org.jetbrains.kotlinx:kotlinx-serialization-json:1.11.0" )
4647
47- // Phase 2: Kotlin runtime execution via JSR-223 ScriptEngine.
48- // kotlin-scripting-jsr223 pulls kotlin-compiler-embeddable transitively, which gives us
49- // a self-contained compiler inside the plugin's classloader, isolated from the IDE's
50- // bundled Kotlin plugin.
48+ // Phase 2: Kotlin runtime execution via embedded K2 compiler — same approach as
49+ // LivePlugin. We bundle kotlin-compiler-embeddable + scripting jars in the plugin zip,
50+ // but a post-prepareSandbox task (see below) MOVES them out of `lib/` into a sibling
51+ // `kotlin-compiler/` folder so the IntelliJ PluginClassLoader never sees them. At
52+ // runtime our KotlinExecutor builds a dedicated UrlClassLoader over those jars and
53+ // drives K2JVMCompiler.exec() through reflection — exactly like LivePlugin.
5154 //
52- // `runtimeOnly` (NOT `implementation`) is critical for two reasons:
53- // 1. The code uses only javax.script.* (standard JDK) — kotlin-scripting-jsr223 is
54- // discovered through META-INF/services at runtime, so we never need it at compile.
55- // 2. kotlin-compiler-embeddable bundles its OWN copy of IntelliJ platform resources
56- // (kotlinx-coroutines 1.8.x, an older `messages/JavaPsiBundle.properties`, etc.).
57- // If those land on testRuntimeClasspath, they shadow the real IDE's copies and
58- // every BasePlatformTestCase setUp dies at NoSuchMethodError / missing-resource.
59- // Marking it `runtimeOnly` keeps it on the production plugin jar but OFF the test
60- // classpath — so the IDE-provided coroutines and resources win in tests.
61- runtimeOnly(" org.jetbrains.kotlin:kotlin-scripting-jsr223:2.1.20" ) {
62- // Same shadowing problem: scripting transitively brings upstream coroutines 1.8.x
63- // which override the IDE's JetBrains-patched build, breaking the test JVM at
64- // `ContextKt.<clinit>` (NoSuchMethodError on limitedParallelism / runBlockingWith…).
65- exclude(group = " org.jetbrains.kotlinx" , module = " kotlinx-coroutines-core" )
66- exclude(group = " org.jetbrains.kotlinx" , module = " kotlinx-coroutines-core-jvm" )
67- }
55+ // `runtimeOnly` keeps these off the test classpath (they bundle older IntelliJ resources
56+ // that shadow the IDE's modern ones during BasePlatformTestCase). The
57+ // testRuntimeClasspath excludes below are defensive duplicates of the same intent.
58+ runtimeOnly(" org.jetbrains.kotlin:kotlin-compiler-embeddable:2.3.21" )
59+ runtimeOnly(" org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:2.3.21" )
60+ runtimeOnly(" org.jetbrains.kotlin:kotlin-scripting-jvm:2.3.21" )
61+ runtimeOnly(" org.jetbrains.kotlin:kotlin-scripting-common:2.3.21" )
62+ runtimeOnly(" org.jetbrains.kotlin:kotlin-stdlib:2.3.21" )
63+ runtimeOnly(" org.jetbrains.kotlin:kotlin-reflect:2.3.21" )
64+ // Our wrapper subproject — its jar carries EmbeddedCompiler.kt and is relocated
65+ // alongside the kotlin-* jars into `kotlin-compiler/` post-prepareSandbox.
66+ runtimeOnly(project(" :kotlin-compiler-wrapper" ))
67+
68+ // `@KotlinScript` annotation lives in kotlin-scripting-common; we need it at compile
69+ // time on our `IntrospectorScript` template class. The annotation class isn't on the
70+ // runtime classpath after the relocate task — but the JVM doesn't require annotation
71+ // classes to be loaded for instances to exist (RuntimeRetention only matters when
72+ // reflection tries to read it). The script compiler reads the annotation via its own
73+ // classpath in kotlin-compiler/, so no conflict.
74+ compileOnly(" org.jetbrains.kotlin:kotlin-scripting-common:2.3.21" )
6875
6976 // docs/MCP_TOOLS.md generator — runs as part of compileKotlin; see doc-processor/.
7077 ksp(project(" :doc-processor" ))
@@ -89,13 +96,69 @@ dependencies {
8996// safe because tests don't exec Kotlin scripts.
9097configurations.testRuntimeClasspath {
9198 exclude(group = " org.jetbrains.kotlin" , module = " kotlin-compiler-embeddable" )
92- exclude(group = " org.jetbrains.kotlin" , module = " kotlin-scripting-jsr223" )
9399 exclude(group = " org.jetbrains.kotlin" , module = " kotlin-scripting-compiler-embeddable" )
94100 exclude(group = " org.jetbrains.kotlin" , module = " kotlin-scripting-compiler-impl-embeddable" )
101+ exclude(group = " org.jetbrains.kotlin" , module = " kotlin-scripting-jvm" )
102+ exclude(group = " org.jetbrains.kotlin" , module = " kotlin-scripting-common" )
95103 exclude(group = " org.jetbrains.kotlin" , module = " kotlin-script-runtime" )
96104 exclude(group = " org.jetbrains.kotlin" , module = " kotlin-daemon-embeddable" )
97105}
98106
107+ // Move kotlin-compiler-embeddable + scripting + kotlin-stdlib/reflect + our wrapper jar
108+ // from `<sandbox>/plugins/ide-introspector/lib/` into a sibling `kotlin-compiler/` after
109+ // prepareSandbox. The IntelliJ PluginClassLoader only walks `lib/` — relocating these
110+ // keeps them OFF the plugin classpath, so there are no kotlin class duplicates between
111+ // our bundled compiler and the IDE's bundled Kotlin plugin. KotlinExecutor reads them via
112+ // `pluginPath/kotlin-compiler/` at runtime into a dedicated UrlClassLoader.
113+ //
114+ // Same approach as LivePlugin's "Move kotlin compiler jars from plugin classpath into a
115+ // separate folder so that there are no conflicts" task in its build.gradle.
116+ // The path is computed inside doLast purely with java.io.File ops (no Project script
117+ // references) but Gradle's configuration cache still flags closure capture. Opting out
118+ // for this task only is the pragmatic move — relocation is fast enough that recomputing
119+ // on every build is not a problem.
120+ val sandboxRootPath: String = layout.projectDirectory.dir(" .intellijPlatform/sandbox" ).asFile.absolutePath
121+ val relocateKotlinCompilerJars = tasks.register(" relocateKotlinCompilerJars" ) {
122+ notCompatibleWithConfigurationCache(" Walks live sandbox plugin tree" )
123+ dependsOn(" prepareSandbox" )
124+ doLast {
125+ val root = File (sandboxRootPath)
126+ val pluginDir: File = root.takeIf { it.isDirectory }
127+ ?.walkTopDown()
128+ ?.firstOrNull { it.name == " ide-introspector" && it.parentFile?.name == " plugins" }
129+ ? : error(" relocateKotlinCompilerJars: no sandbox plugin dir found under $sandboxRootPath " )
130+ val libDir = pluginDir.resolve(" lib" )
131+ val targetDir = pluginDir.resolve(" kotlin-compiler" ).apply { mkdirs() }
132+ val relocatablePrefixes = listOf (
133+ " kotlin-compiler-embeddable" ,
134+ " kotlin-scripting-compiler" ,
135+ " kotlin-scripting-common" ,
136+ " kotlin-scripting-jvm" ,
137+ " kotlin-daemon-embeddable" ,
138+ " kotlin-script-runtime" ,
139+ " kotlin-stdlib" ,
140+ " kotlin-reflect" ,
141+ " kotlinx-coroutines" ,
142+ " kotlin-compiler-wrapper" ,
143+ // org.jetbrains.annotations — required by Kotlin compiler's AnnotationCodegen
144+ // to emit @Nullable on generated bytecode; bundled as a Kotlin compiler
145+ // transitive but must live next to the compiler, NOT in plugin classpath.
146+ " annotations-" ,
147+ )
148+ val moved = mutableListOf<String >()
149+ libDir.listFiles()?.forEach { f ->
150+ if (f.isFile && f.name.endsWith(" .jar" ) && relocatablePrefixes.any { f.name.startsWith(it) }) {
151+ val dest = targetDir.resolve(f.name)
152+ if (dest.exists()) dest.delete()
153+ check(f.renameTo(dest)) { " Failed to move ${f.name} to kotlin-compiler/" }
154+ moved + = f.name
155+ }
156+ }
157+ logger.lifecycle(" Relocated ${moved.size} kotlin-* jars to ${targetDir.relativeTo(pluginDir)} /: ${moved.joinToString()} " )
158+ }
159+ }
160+ tasks.named(" prepareSandbox" ).configure { finalizedBy(relocateKotlinCompilerJars) }
161+
99162intellijPlatform {
100163 pluginConfiguration {
101164 ideaVersion {
0 commit comments