CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-org-jetbrains-kotlin--kotlin-scripting-jvm-host

Kotlin Scripting JVM host for executing and compiling Kotlin scripts in JVM environments with JSR-223 integration and comprehensive caching mechanisms

Pending
Overview
Eval results
Files

script-persistence.mddocs/

Script Persistence

The script persistence system provides utilities for saving compiled scripts as executable JAR files or individual class files for deployment, distribution, and standalone execution.

Capabilities

BasicJvmScriptJarGenerator

Generates executable JAR files from compiled scripts with embedded dependencies and proper manifest configuration.

/**
 * Generates executable JAR files from compiled scripts
 * @param outputJar Target JAR file location
 */
open class BasicJvmScriptJarGenerator(val outputJar: File) : ScriptEvaluator {
    
    /**
     * Generates JAR file from compiled script
     * @param compiledScript Compiled script to package
     * @param scriptEvaluationConfiguration Evaluation configuration (not used for generation)
     * @returns Success with NotEvaluated result or failure with diagnostics
     */
    override suspend operator fun invoke(
        compiledScript: CompiledScript,
        scriptEvaluationConfiguration: ScriptEvaluationConfiguration
    ): ResultWithDiagnostics<EvaluationResult>
}

BasicJvmScriptClassFilesGenerator

Generates individual class files from compiled scripts for integration into existing projects or custom deployment scenarios.

/**
 * Generates individual class files from compiled scripts
 * @param outputDir Target directory for class files
 */
open class BasicJvmScriptClassFilesGenerator(val outputDir: File) : ScriptEvaluator {
    
    /**
     * Generates class files from compiled script
     * @param compiledScript Compiled script to extract classes from
     * @param scriptEvaluationConfiguration Evaluation configuration (not used for generation)
     * @returns Success with NotEvaluated result or failure with diagnostics
     */
    override suspend operator fun invoke(
        compiledScript: CompiledScript,
        scriptEvaluationConfiguration: ScriptEvaluationConfiguration
    ): ResultWithDiagnostics<EvaluationResult>
}

Usage Examples:

import kotlin.script.experimental.jvmhost.*
import kotlin.script.experimental.api.*
import java.io.File

// Compile a script first
val compiler = JvmScriptCompiler()
val script = """
    data class Person(val name: String, val age: Int)
    
    val people = listOf(
        Person("Alice", 30),
        Person("Bob", 25)
    )
    
    println("People: \$people")
    people.filter { it.age > 25 }.map { it.name }
""".trimIndent()

val compilationResult = compiler(
    script.toScriptSource("people-script.kts"),
    ScriptCompilationConfiguration {
        dependencies(JvmDependency(kotlinStdlib))
    }
)

when (compilationResult) {
    is ResultWithDiagnostics.Success -> {
        val compiledScript = compilationResult.value
        
        // Generate executable JAR
        val jarGenerator = BasicJvmScriptJarGenerator(File("output/people-script.jar"))
        val jarResult = jarGenerator(compiledScript, ScriptEvaluationConfiguration.Default)
        
        when (jarResult) {
            is ResultWithDiagnostics.Success -> {
                println("JAR generated successfully: output/people-script.jar")
                
                // The JAR can now be executed with: java -jar people-script.jar
            }
            is ResultWithDiagnostics.Failure -> {
                jarResult.reports.forEach { println("JAR generation error: ${it.message}") }
            }
        }
        
        // Generate individual class files
        val classGenerator = BasicJvmScriptClassFilesGenerator(File("output/classes"))
        val classResult = classGenerator(compiledScript, ScriptEvaluationConfiguration.Default)
        
        when (classResult) {
            is ResultWithDiagnostics.Success -> {
                println("Class files generated in: output/classes/")
                
                // List generated class files
                File("output/classes").walkTopDown()
                    .filter { it.isFile && it.extension == "class" }
                    .forEach { println("Generated: ${it.relativeTo(File("output/classes"))}") }
            }
            is ResultWithDiagnostics.Failure -> {
                classResult.reports.forEach { println("Class generation error: ${it.message}") }
            }
        }
    }
    is ResultWithDiagnostics.Failure -> {
        println("Compilation failed")
    }
}

Script JAR Extension Functions

Extension functions for working with compiled scripts and JAR files directly.

KJvmCompiledScript.saveToJar

Saves a compiled script directly to a JAR file with proper manifest and dependency information.

/**
 * Saves compiled script to JAR file with manifest and dependencies
 * @param outputJar Target JAR file
 */
fun KJvmCompiledScript.saveToJar(outputJar: File)

File.loadScriptFromJar

Loads a previously saved script from a JAR file for execution or inspection.

/**
 * Loads compiled script from JAR file
 * @param checkMissingDependencies Whether to validate all dependencies exist
 * @returns Loaded compiled script or null if invalid/missing dependencies
 */
fun File.loadScriptFromJar(checkMissingDependencies: Boolean = true): CompiledScript?

Direct JAR Operations Example:

import kotlin.script.experimental.jvmhost.saveToJar
import kotlin.script.experimental.jvmhost.loadScriptFromJar
import kotlin.script.experimental.jvm.impl.KJvmCompiledScript

// Save compiled script directly to JAR
val compiledScript = compilationResult.value as KJvmCompiledScript
val jarFile = File("direct-save.jar")
compiledScript.saveToJar(jarFile)

println("Script saved to: ${jarFile.absolutePath}")

// Load script back from JAR
val loadedScript = jarFile.loadScriptFromJar()
when (loadedScript) {
    null -> println("Failed to load script from JAR")
    else -> {
        println("Successfully loaded script from JAR")
        
        // Execute loaded script
        val evaluator = BasicJvmScriptEvaluator()
        val evalResult = evaluator(loadedScript, ScriptEvaluationConfiguration.Default)
        // Handle evaluation...
    }
}

// Load without dependency checking for faster loading
val quickLoadedScript = jarFile.loadScriptFromJar(checkMissingDependencies = false)

JAR File Structure

Generated JAR files follow a specific structure to ensure proper execution and dependency management.

Generated JAR Layout

script.jar
├── META-INF/
│   └── MANIFEST.MF              # Executable manifest with Main-Class and Class-Path
├── Script_12345.class           # Main script class (name includes hash)
├── Script_12345$InnerClass.class# Inner classes if any
├── Script_12345$Data.class      # Data classes defined in script
└── script_metadata_Script_12345 # Serialized script metadata

Manifest Configuration

The generated manifest includes:

  • Main-Class: Points to the compiled script class
  • Class-Path: Lists all dependencies with their locations
  • Manifest-Version: Standard JAR manifest version
  • Created-By: Identifies Kotlin as the creator

Example Manifest:

Manifest-Version: 1.0
Created-By: JetBrains Kotlin
Main-Class: Script_a1b2c3d4
Class-Path: /path/to/kotlin-stdlib-2.2.0.jar /path/to/kotlin-reflect-2.2.0.jar

Dependency Management

Dependencies are handled in several ways:

  1. External References: Dependencies remain as external JAR files listed in Class-Path
  2. Relative Paths: Paths are converted to URIs for proper resolution
  3. Missing Dependencies: Optional validation ensures all dependencies are accessible

Advanced JAR Generation with Custom Dependencies:

// Create script with custom dependencies
val scriptWithDeps = """
    @file:DependsOn("org.apache.commons:commons-lang3:3.12.0")
    
    import org.apache.commons.lang3.StringUtils
    
    val text = "  hello world  "
    val result = StringUtils.capitalize(StringUtils.trim(text))
    println("Result: \$result")
    result
""".trimIndent()

val configWithMavenDeps = ScriptCompilationConfiguration {
    dependencies(
        JvmDependency("org.apache.commons:commons-lang3:3.12.0"),
        JvmDependency(kotlinStdlib)
    )
    
    // Custom dependency resolver
    refineConfiguration {
        onAnnotations(DependsOn::class) { context ->
            val dependsOnList = context.collectedData?.get(DependsOn::class)
            if (dependsOnList != null) {
                ScriptCompilationConfiguration(context.compilationConfiguration) {
                    dependencies.append(JvmDependency(dependsOnList.flatMap { it.artifacts }))
                }.asSuccess()
            } else {
                context.compilationConfiguration.asSuccess()
            }
        }
    }
}

val depsCompilationResult = compiler(scriptWithDeps.toScriptSource(), configWithMavenDeps)
when (depsCompilationResult) {
    is ResultWithDiagnostics.Success -> {
        val jarGenerator = BasicJvmScriptJarGenerator(File("script-with-deps.jar"))
        jarGenerator(depsCompilationResult.value, ScriptEvaluationConfiguration.Default)
        
        // The generated JAR will include proper Class-Path entries for Maven dependencies
    }
}

Class File Generation

Individual class file generation provides fine-grained control over script deployment and integration.

Class File Organization

Generated class files maintain the package structure and naming conventions of the original script:

output/classes/
├── Script_12345.class           # Main script class
├── Script_12345$Person.class    # Data class defined in script
├── Script_12345$Helper.class    # Helper class or function
└── Script_12345$WhenMappings.class # Compiler-generated utility classes

Custom Class Generation Example:

class CustomScriptClassGenerator(
    private val outputDir: File,
    private val packageName: String? = null
) : ScriptEvaluator {
    
    override suspend operator fun invoke(
        compiledScript: CompiledScript,
        scriptEvaluationConfiguration: ScriptEvaluationConfiguration
    ): ResultWithDiagnostics<EvaluationResult> {
        
        return try {
            if (compiledScript !is KJvmCompiledScript) {
                return ResultWithDiagnostics.Failure(
                    listOf("Cannot generate classes: unsupported compiled script type".asErrorDiagnostics())
                )
            }
            
            val module = (compiledScript.getCompiledModule() as? KJvmCompiledModuleInMemory)
                ?: return ResultWithDiagnostics.Failure(
                    listOf("Cannot generate classes: unsupported module type".asErrorDiagnostics())
                )
            
            // Create package directory structure if specified
            val targetDir = if (packageName != null) {
                File(outputDir, packageName.replace('.', '/'))
            } else {
                outputDir
            }
            
            if (!targetDir.exists()) {
                targetDir.mkdirs()
            }
            
            // Write all class files
            for ((path, bytes) in module.compilerOutputFiles) {
                val classFile = File(targetDir, File(path).name)
                classFile.writeBytes(bytes)
                println("Generated class file: ${classFile.relativeTo(outputDir)}")
            }
            
            ResultWithDiagnostics.Success(
                EvaluationResult(ResultValue.NotEvaluated, scriptEvaluationConfiguration)
            )
            
        } catch (e: Throwable) {
            ResultWithDiagnostics.Failure(
                listOf(e.asErrorDiagnostics("Cannot generate script classes: ${e.message}"))
            )
        }
    }
}

// Use custom generator
val customGenerator = CustomScriptClassGenerator(
    outputDir = File("custom-output"),
    packageName = "com.example.scripts"
)

val customResult = customGenerator(compiledScript, ScriptEvaluationConfiguration.Default)

Integration with Build Systems

Generated artifacts can be integrated into existing build systems and deployment pipelines.

Maven Integration

<plugin>
    <groupId>org.codehaus.mojo</groupId>
    <artifactId>exec-maven-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <mainClass>kotlin.script.experimental.jvmhost.GenerateScriptJar</mainClass>
        <args>
            <arg>src/main/scripts/my-script.kts</arg>
            <arg>target/generated-scripts/my-script.jar</arg>
        </args>
    </configuration>
</plugin>

Gradle Integration

tasks.register<JavaExec>("generateScriptJars") {
    classpath = configurations.runtimeClasspath.get()
    mainClass.set("kotlin.script.experimental.jvmhost.GenerateScriptJar")
    args = listOf(
        "src/main/scripts/",
        "build/generated-jars/"
    )
}

Standalone Execution

Generated JAR files can be executed directly:

# Execute generated script JAR
java -jar generated-script.jar

# Execute with additional JVM options
java -Xmx2g -jar data-processing-script.jar

# Execute with custom classpath additions
java -cp "lib/*:generated-script.jar" Script_12345

Install with Tessl CLI

npx tessl i tessl/maven-org-jetbrains-kotlin--kotlin-scripting-jvm-host

docs

core-scripting-host.md

index.md

jsr223-integration.md

repl-support.md

script-caching.md

script-compilation.md

script-persistence.md

tile.json