I want to use 3d party library in my project build process. Library methods requires ScriptEngine. When I'm trying to instantiate it i got an error:
java.lang.IllegalArgumentException: Unable to construct script definition: Unable to load base class kotlin.script.experimental.api.KotlinType@42842bb8
at kotlin.script.experimental.host.ConfigurationFromTemplateKt.getTemplateClass(configurationFromTemplate.kt:189)
at kotlin.script.experimental.host.ConfigurationFromTemplateKt.createScriptDefinitionFromTemplate(configurationFromTemplate.kt:36)
at kotlin.script.experimental.jsr223.KotlinJsr223DefaultScriptEngineFactory.<init>(KotlinJsr223DefaultScriptEngineFactory.kt:74)
at ce.domain.usecase.load.LoadMetaFilesForTargetUseCase.invoke(LoadMetaFilesUseCase.kt:17)
at ce.domain.usecase.entry.BuildProjectUseCase.invoke(BuildProjectUseCase.kt:24)
at ce.domain.usecase.entry.BuildProjectUseCase.invoke$default(BuildProjectUseCase.kt:18)
at Build_gradle$$$result$1.invoke(build.gradle.kts:68)
at Build_gradle$$$result$1.invoke(build.gradle.kts:60)
at org.gradle.kotlin.dsl.ProjectExtensionsKt$sam$org_gradle_api_Action$0.execute(ProjectExtensions.kt)
at org.gradle.api.internal.tasks.DefaultTaskContainer.create(DefaultTaskContainer.java:368)
at org.gradle.kotlin.dsl.ProjectExtensionsKt.task(ProjectExtensions.kt:147)
at Build_gradle.<init>(build.gradle.kts:60)
...
I've reproduced issue with simple grdale project: Sample project gradle:
import kotlin.script.experimental.jsr223.KotlinJsr223DefaultScriptEngineFactory
plugins {
kotlin("jvm") version "1.8.21"
application
}
repositories {
mavenCentral()
}
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-scripting-jsr223:1.8.10")
classpath("org.jetbrains.kotlin:kotlin-scripting-common:1.8.10")
classpath("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.10")
classpath("org.jetbrains.kotlin:kotlin-reflect")
}
}
dependencies {
testImplementation(kotlin("test"))
}
abstract class TestProjectTask : DefaultTask() {
@get: InputFile
abstract val projectFile: RegularFileProperty
@TaskAction
fun execute() {
try {
val eng2 = KotlinJsr223DefaultScriptEngineFactory().getScriptEngine()
println("Project file = ${projectFile.get()} $eng2")
val worker = Worker()
worker.doWork(eng2, projectFile.asFile.get().absolutePath)
} catch (err: Throwable) {
err.printStackTrace()
}
}
}
task("hello2", TestProjectTask::class) {
projectFile.set(File("./project.kts"))
}
KotlinJsr223DefaultScriptEngineFactory().getScriptEngine() always throws same exception.
Thanks to work done by contributors in this issue and the linked comment thread, the answer is relative simple, even if discovering how to do it was not! I've cleaned up the provided workaround.
Summary:
K2JVMCompiler
to generate the sourcesI recommend using a buildSrc convention plugin to set up the requisite logic. It helps keep the build scripts cleaner and more declarative, and setup-logic is contained within buildSrc.
First, make sure that the K2JVMCompiler
class is available.
If you're working in a single build.gradle.kts
, then this can be achieved by applying the Kotlin plugin:
// build.gradle.kts
plugins {
kotlin("jvm") version "1.8.22"
}
Or if writing a plugin/pre-compiled script plugin, add a dependency on the Kotlin Gradle Plugin in the project's build.gradle.kts
.
A compile-time dependency on kotlin-compiler-embeddable
is required for accessing the K2JVMCompiler
class.
// buildSrc/build.gradle.kts
plugins {
`kotlin-dsl`
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22")
// required for K2JVMCompiler::class - will be provided at runtime by Gradle
compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.22")
}
⚠️ Note that adding a dependency on KGP in
buildSrc/build.gradle.kts
means that all other KGP versions must be removed.// build.gradle.kts plugins { kotlin("jvm") // no version needed - it's set in buildSrc/build.gradle.kts }
Next, let's create the task that will be used to run the .main.kts
files.
In order to run a Kotlin script, we need a few things:
In order to follow Gradle best practices, tracking task's input and output files is also important (but is not strictly required).
// buildSrc/src/main/kotlin/RunKotlinScript.kt
import org.gradle.api.DefaultTask
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.file.RegularFileProperty
import org.gradle.api.tasks.*
import org.gradle.process.ExecOperations
import javax.inject.Inject
/** Task for running Kotlin Scripts */
abstract class RunKotlinScript @Inject constructor(
private val executor: ExecOperations
) : DefaultTask() {
/** Location of the `.kts` file (required) */
@get:InputFile
abstract val script: RegularFileProperty
/** (optional) Files that the script uses as an input */
@get:InputFiles
@get:Optional
abstract val scriptInputs: ConfigurableFileCollection
/** (optional) Files that the script produces as output */
@get:OutputFiles
abstract val scriptOutputs: ConfigurableFileCollection
@get:Classpath
abstract val compileClasspath: ConfigurableFileCollection
@get:Classpath
abstract val runtimeClasspath: ConfigurableFileCollection
init {
group = "run kts"
description = "Runs a Kotlin script"
}
@TaskAction
fun run() {
val scriptPath = script.get().asFile.invariantSeparatorsPath
val runtimeClasspath = runtimeClasspath.asPath
executor.javaexec {
classpath(compileClasspath)
mainClass.set(org.jetbrains.kotlin.cli.jvm.K2JVMCompiler::class.qualifiedName)
args(
"-no-stdlib",
"-no-reflect",
"-classpath", runtimeClasspath,
"-script", scriptPath,
)
}
}
}
(As previously mentioned, it's best to do this in a buildSrc directory, but you can paste this task into a regular build.gradle.kts
too.)
Let's use a pre-compiled convention plugin to define how to fetch the dependencies needed to compile and run a Kotlin script.
// buildSrc/src/main/kotlin/kotlin-script-runner.gradle.kts
plugins {
kotlin("jvm") // no version needed - it's set in buildSrc/build.gradle.kts
}
// Fetch dependencies necessary to compile and run kts scripts inside Gradle,
// so installing the kotlin CLI is not required (e.g. on CI/CD, or Heroku)
val ktsCompileClasspath by configurations.creating<Configuration> {
description = "Dependencies used to compile Kotlin scripts"
isCanBeConsumed = false
}
val ktsRuntimeClasspath by configurations.creating<Configuration> {
description = "Dependencies used to run Kotlin scripts"
isCanBeConsumed = false
// only fetch direct dependencies - the scripting context will pull in other dependencies
isTransitive = false
}
dependencies {
// add compile-time dependencies on the regular and scripting Kotlin compilers
ktsCompileClasspath(kotlin("compiler"))
ktsCompileClasspath(kotlin("scripting-compiler"))
// only depend on Kotlin main-kts for runtime
ktsRuntimeClasspath(kotlin("main-kts"))
}
We now have two Configurations that contain the requisite dependencies. In the same convention plugin, let's add those dependencies to all RunKotlinScript tasks.
// buildSrc/src/main/kotlin/kotlin-script-runner.gradle.kts
// ...
tasks.withType<RunKotlinScript>().configureEach {
runtimeClasspath.from(ktsRuntimeClasspath)
compileClasspath.from(ktsCompileClasspath)
}
This convention plugin can be applied to any script in the project:
// my-subproject/build.gradle.kts
plugins {
`kotlin-script-runner`
}
and then you can create a task, which will be correctly configured
// my-subproject/build.gradle.kts
tasks.register<RunKotlinScript>("runDoSomethingKts") {
script.set(file("scripts/do-something.main.kts"))
scriptOutputs.from("scripts/out.txt")
scriptInputs.from("script/input.txt")
}
and can be run, using Gradle
./gradlew runDoSomethingKts