VShcryabets
VShcryabets

Reputation: 361

Is there a way to instantinate KTS script engine in the Gradle KTS?

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.

Upvotes: 4

Views: 1252

Answers (1)

aSemy
aSemy

Reputation: 7159

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:

  • Create a Gradle task for running the Kotlin script
  • fetch the required compilation and runtime dependencies
  • Run K2JVMCompiler to generate the sources

I 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.

Kotlin dependencies

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
}

Run task

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:

  • the location of the Kotlin script (obviously!)
  • the classpath used to compile the Kotlin script
  • the classpath used to run the Kotlin script

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.)

Compile and Runtime dependencies

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)
}

Creating run tasks

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

Upvotes: 4

Related Questions