Implement main method generation in scripts and runnable jar saving
refactor necessary parts on the way
This commit is contained in:
+1
-1
@@ -48,6 +48,6 @@ inline fun <reified T : Any> createJvmEvaluationConfigurationFromTemplate(
|
||||
): ScriptEvaluationConfiguration = createEvaluationConfigurationFromTemplate(
|
||||
KotlinType(T::class),
|
||||
hostConfiguration,
|
||||
ScriptCompilationConfiguration::class,
|
||||
ScriptEvaluationConfiguration::class,
|
||||
body
|
||||
)
|
||||
|
||||
+2
-2
@@ -352,7 +352,6 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm
|
||||
sourceDependencies: List<ScriptsCompilationDependencies.SourceDependencies>,
|
||||
getScriptConfiguration: (KtFile) -> ScriptCompilationConfiguration
|
||||
): KJvmCompiledScript<Any> {
|
||||
val module = makeCompiledModule(generationState)
|
||||
val scriptDependenciesStack = ArrayDeque<KtScript>()
|
||||
|
||||
fun makeOtherScripts(script: KtScript): List<KJvmCompiledScript<*>> {
|
||||
@@ -371,7 +370,7 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm
|
||||
getScriptConfiguration(sourceFile),
|
||||
it.fqName.asString(),
|
||||
makeOtherScripts(it),
|
||||
module
|
||||
null
|
||||
)
|
||||
}
|
||||
} ?: emptyList()
|
||||
@@ -380,6 +379,7 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm
|
||||
return otherScripts
|
||||
}
|
||||
|
||||
val module = makeCompiledModule(generationState)
|
||||
return KJvmCompiledScript(
|
||||
script.locationId,
|
||||
getScriptConfiguration(ktScript.containingKtFile),
|
||||
|
||||
+40
-26
@@ -5,15 +5,16 @@
|
||||
|
||||
package kotlin.script.experimental.jvmhost
|
||||
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import org.jetbrains.kotlin.utils.KotlinPaths
|
||||
import java.io.*
|
||||
import java.util.jar.JarEntry
|
||||
import java.util.jar.JarOutputStream
|
||||
import java.util.jar.Manifest
|
||||
import kotlin.script.experimental.api.*
|
||||
import kotlin.script.experimental.jvm.JvmDependency
|
||||
import kotlin.script.experimental.jvm.impl.*
|
||||
import kotlin.script.experimental.jvm.util.scriptCompilationClasspathFromContext
|
||||
import kotlin.script.experimental.jvmhost.impl.KJvmCompiledModuleInMemory
|
||||
import kotlin.script.experimental.jvm.impl.KJvmCompiledScript
|
||||
|
||||
// TODO: generate execution code (main)
|
||||
|
||||
@@ -45,6 +46,41 @@ open class BasicJvmScriptClassFilesGenerator(val outputDir: File) : ScriptEvalua
|
||||
}
|
||||
}
|
||||
|
||||
fun KJvmCompiledScript<*>.saveToJar(outputJar: File) {
|
||||
val module = (compiledModule as? KJvmCompiledModuleInMemory)
|
||||
?: throw IllegalArgumentException("Unsupported module type $compiledModule")
|
||||
val dependenciesFromScript = compilationConfiguration[ScriptCompilationConfiguration.dependencies]
|
||||
?.filterIsInstance<JvmDependency>()
|
||||
?.flatMap { it.classpath }
|
||||
.orEmpty()
|
||||
val dependenciesForMain = scriptCompilationClasspathFromContext(
|
||||
KotlinPaths.Jar.ScriptingLib.baseName, KotlinPaths.Jar.ScriptingJvmLib.baseName, KotlinPaths.Jar.CoroutinesCore.baseName,
|
||||
classLoader = this::class.java.classLoader,
|
||||
wholeClasspath = false
|
||||
)
|
||||
val dependencies = (dependenciesFromScript + dependenciesForMain).distinct()
|
||||
FileOutputStream(outputJar).use { fileStream ->
|
||||
val manifest = Manifest()
|
||||
manifest.mainAttributes.apply {
|
||||
putValue("Manifest-Version", "1.0")
|
||||
putValue("Created-By", "JetBrains Kotlin")
|
||||
if (dependencies.isNotEmpty()) {
|
||||
// TODO: implement options for various cases - paths as is (now), absolute paths (local execution only), names only (most likely as a hint only), fat jar
|
||||
putValue("Class-Path", dependencies.joinToString(" "))
|
||||
}
|
||||
putValue("Main-Class", scriptClassFQName)
|
||||
}
|
||||
// TODO: fat jar/dependencies
|
||||
val jarStream = JarOutputStream(fileStream, manifest)
|
||||
jarStream.putNextEntry(JarEntry(scriptMetadataPath(scriptClassFQName)))
|
||||
jarStream.write(copyWithoutModule().toBytes())
|
||||
for ((path, bytes) in module.compilerOutputFiles) {
|
||||
jarStream.putNextEntry(JarEntry(path))
|
||||
jarStream.write(bytes)
|
||||
}
|
||||
jarStream.finish()
|
||||
}
|
||||
}
|
||||
|
||||
open class BasicJvmScriptJarGenerator(val outputJar: File) : ScriptEvaluator {
|
||||
|
||||
@@ -55,29 +91,7 @@ open class BasicJvmScriptJarGenerator(val outputJar: File) : ScriptEvaluator {
|
||||
try {
|
||||
if (compiledScript !is KJvmCompiledScript<*>)
|
||||
return failure("Cannot generate jar: unsupported compiled script type $compiledScript")
|
||||
val module = (compiledScript.compiledModule as? KJvmCompiledModuleInMemory)
|
||||
?: return failure("Cannot generate jar: unsupported module type ${compiledScript.compiledModule}")
|
||||
val dependencies = compiledScript.compilationConfiguration[ScriptCompilationConfiguration.dependencies]
|
||||
?.filterIsInstance<JvmDependency>()
|
||||
?.flatMap { it.classpath }
|
||||
.orEmpty()
|
||||
FileOutputStream(outputJar).use { fileStream ->
|
||||
val manifest = Manifest()
|
||||
manifest.mainAttributes.apply {
|
||||
putValue("Manifest-Version", "1.0")
|
||||
putValue("Created-By", "JetBrains Kotlin")
|
||||
if (dependencies.isNotEmpty()) {
|
||||
putValue("Class-Path", dependencies.joinToString(" ") { it.name })
|
||||
}
|
||||
}
|
||||
// TODO: fat jar/dependencies
|
||||
val jarStream = JarOutputStream(fileStream, manifest)
|
||||
for ((path, bytes) in module.compilerOutputFiles) {
|
||||
jarStream.putNextEntry(JarEntry(path))
|
||||
jarStream.write(bytes)
|
||||
}
|
||||
jarStream.finish()
|
||||
}
|
||||
compiledScript.saveToJar(outputJar)
|
||||
return ResultWithDiagnostics.Success(EvaluationResult(ResultValue.Unit, scriptEvaluationConfiguration))
|
||||
} catch (e: Throwable) {
|
||||
return ResultWithDiagnostics.Failure(
|
||||
|
||||
+67
-3
@@ -13,6 +13,8 @@ import java.io.*
|
||||
import java.net.URLClassLoader
|
||||
import java.nio.file.Files
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.jar.JarFile
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.script.experimental.api.*
|
||||
import kotlin.script.experimental.host.BasicScriptingHost
|
||||
@@ -75,6 +77,60 @@ class ScriptingHostTest : TestCase() {
|
||||
Assert.assertEquals(greeting, output)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSaveToRunnableJar() {
|
||||
val greeting = "Hello from script jar!"
|
||||
val outJar = Files.createTempFile("saveToRunnableJar", ".jar").toFile()
|
||||
val compilationConfiguration = createJvmCompilationConfigurationFromTemplate<SimpleScriptTemplate>()
|
||||
val compiler = JvmScriptCompiler(defaultJvmScriptingHostConfiguration)
|
||||
val scriptName = "SavedRunnableScript"
|
||||
val compiledScript = runBlocking {
|
||||
compiler("println(\"$greeting\")".toScriptSource(name = "$scriptName.kts"), compilationConfiguration).throwOnFailure()
|
||||
.resultOrNull()!!
|
||||
}
|
||||
val saver = BasicJvmScriptJarGenerator(outJar)
|
||||
runBlocking {
|
||||
saver(compiledScript, ScriptEvaluationConfiguration.Default).throwOnFailure()
|
||||
}
|
||||
|
||||
val classpathFromJar = run {
|
||||
val manifest = JarFile(outJar).manifest
|
||||
manifest.mainAttributes.getValue("Class-Path").split(" ") // TODO: quoted paths
|
||||
.map { File(it).toURI().toURL() }
|
||||
} + outJar.toURI().toURL()
|
||||
|
||||
fun checkInvokeMain(baseClassLoader: ClassLoader?) {
|
||||
val classloader = URLClassLoader(classpathFromJar.toTypedArray(), baseClassLoader)
|
||||
val scriptClass = classloader.loadClass(scriptName)
|
||||
val mainMethod = scriptClass.methods.find { it.name == "main" }
|
||||
Assert.assertNotNull(mainMethod)
|
||||
val output = captureOutAndErr {
|
||||
mainMethod!!.invoke(null, emptyArray<String>())
|
||||
}.toList().filterNot(String::isEmpty).joinToString("\n")
|
||||
Assert.assertEquals(greeting, output)
|
||||
}
|
||||
|
||||
checkInvokeMain(null) // isolated
|
||||
checkInvokeMain(Thread.currentThread().contextClassLoader)
|
||||
|
||||
val javaExecutable = File(File(System.getProperty("java.home"), "bin"), "java")
|
||||
val args = listOf(javaExecutable.absolutePath, "-jar", outJar.path)
|
||||
val processBuilder = ProcessBuilder(args)
|
||||
processBuilder.redirectErrorStream(true)
|
||||
val outputFromProcess = run {
|
||||
val process = processBuilder.start()
|
||||
process.waitFor(10, TimeUnit.SECONDS)
|
||||
val out = process.inputStream.reader().readText()
|
||||
if (process.isAlive) {
|
||||
process.destroyForcibly()
|
||||
"Error: timeout, killing script process\n$out"
|
||||
} else {
|
||||
out
|
||||
}
|
||||
}.trim()
|
||||
Assert.assertEquals(greeting, outputFromProcess)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSimpleRequire() {
|
||||
val greeting = "Hello from required!"
|
||||
@@ -348,7 +404,7 @@ class ScriptingHostTest : TestCase() {
|
||||
}
|
||||
}
|
||||
|
||||
fun ResultWithDiagnostics<*>.throwOnFailure(): ResultWithDiagnostics<*> = apply {
|
||||
fun <T> ResultWithDiagnostics<T>.throwOnFailure(): ResultWithDiagnostics<T> = apply {
|
||||
if (this is ResultWithDiagnostics.Failure) {
|
||||
val firstExceptionFromReports = reports.find { it.exception != null }?.exception
|
||||
throw Exception(
|
||||
@@ -470,15 +526,23 @@ private class FileBasedScriptCache(val baseDir: File) : ScriptingCacheWithCounte
|
||||
get() = _retrievedScripts
|
||||
}
|
||||
|
||||
private fun captureOut(body: () -> Unit): String {
|
||||
private fun captureOut(body: () -> Unit): String = captureOutAndErr(body).first
|
||||
|
||||
private fun captureOutAndErr(body: () -> Unit): Pair<String, String> {
|
||||
val outStream = ByteArrayOutputStream()
|
||||
val errStream = ByteArrayOutputStream()
|
||||
val prevOut = System.out
|
||||
val prevErr = System.err
|
||||
System.setOut(PrintStream(outStream))
|
||||
System.setErr(PrintStream(errStream))
|
||||
try {
|
||||
body()
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
System.setOut(prevOut)
|
||||
System.setErr(prevErr)
|
||||
}
|
||||
return outStream.toString().trim()
|
||||
return outStream.toString().trim() to errStream.toString().trim()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user