有办法在Gradle KTS中实例化KTS脚本引擎吗?

huangapple go评论115阅读模式
英文:

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

问题

我想在我的项目构建过程中使用第三方库。
库方法需要ScriptEngine。当我尝试实例化它时,我收到了以下错误:

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

我已经在一个简单的Gradle项目中重现了这个问题:
示例项目的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()
        }
    }
}

tasks.register("hello2", TestProjectTask::class) {
    projectFile.set(file("./project.kts"))
}

KotlinJsr223DefaultScriptEngineFactory().getScriptEngine() 总是抛出相同的异常。

英文:

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.&lt;init&gt;(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.&lt;init&gt;(build.gradle.kts:60)
...

I've reproduced issue with simple grdale project:
Sample project gradle:

import kotlin.script.experimental.jsr223.KotlinJsr223DefaultScriptEngineFactory
plugins {
kotlin(&quot;jvm&quot;) version &quot;1.8.21&quot;
application
}
repositories {
mavenCentral()
}
buildscript {
repositories {
mavenCentral()
google()
}
dependencies {
classpath(&quot;org.jetbrains.kotlin:kotlin-scripting-jsr223:1.8.10&quot;)
classpath(&quot;org.jetbrains.kotlin:kotlin-scripting-common:1.8.10&quot;)
classpath(&quot;org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.10&quot;)
classpath(&quot;org.jetbrains.kotlin:kotlin-reflect&quot;)
}
}
dependencies {
testImplementation(kotlin(&quot;test&quot;))
}
abstract class TestProjectTask : DefaultTask() {
@get: InputFile
abstract val projectFile: RegularFileProperty
@TaskAction
fun execute() {
try {
val eng2 = KotlinJsr223DefaultScriptEngineFactory().getScriptEngine()
println(&quot;Project file = ${projectFile.get()} $eng2&quot;)
val worker = Worker()
worker.doWork(eng2, projectFile.asFile.get().absolutePath)
} catch (err: Throwable) {
err.printStackTrace()
}
}
}
task(&quot;hello2&quot;, TestProjectTask::class) {
projectFile.set(File(&quot;./project.kts&quot;))
}

KotlinJsr223DefaultScriptEngineFactory().getScriptEngine() always throws same exception.

答案1

得分: 3

感谢贡献者在此问题链接的评论线程中的工作,答案相对简单,尽管发现如何做到这一点并不容易!我已经整理了提供的解决方法。

摘要:

  • 创建一个用于运行Kotlin脚本的Gradle任务
  • 获取所需的编译和运行依赖项
  • 运行K2JVMCompiler以生成源代码

我建议使用buildSrc约定插件来设置所需的逻辑。它有助于使构建脚本更清晰、更声明式,并且设置逻辑包含在buildSrc中。

Kotlin依赖

首先,确保K2JVMCompiler类可用。

如果您在单个build.gradle.kts中工作,那么可以通过应用Kotlin插件来实现:

// build.gradle.kts

plugins {
  kotlin("jvm") version "1.8.22"
}

或者如果编写一个插件/预编译脚本插件,可以在项目的build.gradle.kts中添加对Kotlin Gradle插件的依赖。

访问K2JVMCompiler类需要编译时依赖于kotlin-compiler-embeddable

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

⚠️ 请注意,在buildSrc/build.gradle.kts中添加对KGP的依赖意味着必须删除_所有_其他的KGP版本。

// build.gradle.kts
plugins {
  kotlin("jvm") // no version needed - it's set in buildSrc/build.gradle.kts
}

运行任务

接下来,让我们创建用于运行.main.kts文件的任务。

要运行Kotlin脚本,我们需要一些东西:

  • Kotlin脚本的位置(显而易见!)
  • 用于_编译_ Kotlin脚本的类路径
  • 用于_运行_ Kotlin脚本的类路径

为了遵循Gradle的最佳实践,跟踪任务的输入和输出文件也很重要(但不是严格要求的)。

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

(如前所述,最好在buildSrc目录中执行此操作,但您也可以将此任务粘贴到常规的build.gradle.kts中。)

编译和运行依赖项

让我们使用预编译约定插件来定义如何获取编译和运行Kotlin脚本所需的依赖项。

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

现在,我们有两个包含所需依赖项的[配置](https://docs.gradle.org/8.1.1/userguide/dependency_management_terminology.html#sub:

英文:

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(&quot;jvm&quot;) version &quot;1.8.22&quot;
}

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(&quot;org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22&quot;)

  // required for K2JVMCompiler::class - will be provided at runtime by Gradle
  compileOnly(&quot;org.jetbrains.kotlin:kotlin-compiler-embeddable:1.8.22&quot;)
}

> ⚠️ Note that adding a dependency on KGP in buildSrc/build.gradle.kts means that all other KGP versions must be removed.
>
> kotlin
&gt; // build.gradle.kts
&gt; plugins {
&gt; kotlin(&quot;jvm&quot;) // no version needed - it&#39;s set in buildSrc/build.gradle.kts
&gt; }
&gt;

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 = &quot;run kts&quot;
        description = &quot;Runs a Kotlin script&quot;
    }

    @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(
                &quot;-no-stdlib&quot;,
                &quot;-no-reflect&quot;,
                &quot;-classpath&quot;, runtimeClasspath,
                &quot;-script&quot;, 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(&quot;jvm&quot;) // no version needed - it&#39;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&lt;Configuration&gt; {
description = &quot;Dependencies used to compile Kotlin scripts&quot;
isCanBeConsumed = false
}
val ktsRuntimeClasspath by configurations.creating&lt;Configuration&gt; {
description = &quot;Dependencies used to run Kotlin scripts&quot;
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(&quot;compiler&quot;))
ktsCompileClasspath(kotlin(&quot;scripting-compiler&quot;))
// only depend on Kotlin main-kts for runtime
ktsRuntimeClasspath(kotlin(&quot;main-kts&quot;))
}

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&lt;RunKotlinScript&gt;().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&lt;RunKotlinScript&gt;(&quot;runDoSomethingKts&quot;) {
    script.set(file(&quot;scripts/do-something.main.kts&quot;))
    scriptOutputs.from(&quot;scripts/out.txt&quot;)    
    scriptInputs.from(&quot;script/input.txt&quot;)
}

and can be run, using Gradle

./gradlew runDoSomethingKts

huangapple
  • 本文由 发表于 2023年6月16日 03:31:54
  • 转载请务必保留本文链接:https://go.coder-hub.com/76484985.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定