Implement main method generation in scripts and runnable jar saving

refactor necessary parts on the way
This commit is contained in:
Ilya Chernikov
2019-04-09 08:28:53 +02:00
parent c896b4873b
commit 67ad3773de
14 changed files with 355 additions and 81 deletions
@@ -8,7 +8,6 @@ package kotlin.script.experimental.jvm
import kotlin.reflect.KClass
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.impl.getConfigurationWithClassloader
import kotlin.script.experimental.jvm.impl.sharedScripts
open class BasicJvmScriptEvaluator : ScriptEvaluator {
@@ -23,7 +22,7 @@ open class BasicJvmScriptEvaluator : ScriptEvaluator {
// run as SAM
// return res
val sharedScripts = configuration.sharedScripts
val sharedScripts = configuration[ScriptEvaluationConfiguration.jvm.scriptsInstancesSharingMap]
sharedScripts?.get(scriptClass)?.asSuccess()
?: compiledScript.otherScripts.mapSuccess {
@@ -6,7 +6,10 @@
package kotlin.script.experimental.jvm.impl
import java.io.File
import java.io.InputStream
import java.net.URL
import java.net.URLClassLoader
import java.util.*
interface KJvmCompiledModule {
fun createClassLoader(baseClassLoader: ClassLoader?): ClassLoader
@@ -18,8 +21,55 @@ class KJvmCompiledModuleFromClassPath(val classpath: Collection<File>) : KJvmCom
URLClassLoader(classpath.map { it.toURI().toURL() }.toTypedArray(), baseClassLoader)
}
class KJvmCompiledModuleFromLoadedClasses : KJvmCompiledModule {
class KJvmCompiledModuleFromClassLoader(val moduleClassLoader: ClassLoader) : KJvmCompiledModule {
override fun createClassLoader(baseClassLoader: ClassLoader?): ClassLoader =
baseClassLoader ?: KJvmCompiledModuleFromLoadedClasses::class.java.classLoader
if (baseClassLoader == null) moduleClassLoader
else DualClassLoader(moduleClassLoader, baseClassLoader)
}
private class DualClassLoader(fallbackLoader: ClassLoader, parentLoader: ClassLoader?) :
ClassLoader(singleClassLoader(fallbackLoader, parentLoader) ?: parentLoader) {
private class Wrapper(parent: ClassLoader) : ClassLoader(parent) {
fun openFindResources(name: String): Enumeration<URL> = super.findResources(name)
fun openFindResource(name: String): URL? = super.findResource(name)
}
companion object {
private fun singleClassLoader(fallbackLoader: ClassLoader, parentLoader: ClassLoader?): ClassLoader? {
tailrec fun ClassLoader.isAncestorOf(other: ClassLoader?): Boolean = when {
other == null -> false
this === other -> true
else -> isAncestorOf(other.parent)
}
return when {
parentLoader == null -> fallbackLoader
parentLoader.isAncestorOf(fallbackLoader) -> fallbackLoader
fallbackLoader.isAncestorOf(parentLoader) -> parentLoader
else -> null
}
}
}
private val fallbackClassLoader: Wrapper? =
if (/* optimization */ parentLoader == null || singleClassLoader(fallbackLoader, parentLoader) != null) null
else Wrapper(fallbackLoader)
override fun findClass(name: String): Class<*> = try {
super.findClass(name)
} catch (e: ClassNotFoundException) {
fallbackClassLoader?.loadClass(name) ?: throw e
}
override fun getResourceAsStream(name: String): InputStream? =
super.getResourceAsStream(name) ?: fallbackClassLoader?.getResourceAsStream(name)
override fun findResources(name: String): Enumeration<URL> =
if (fallbackClassLoader == null) super.findResources(name)
else Collections.enumeration(super.findResources(name).toList() + fallbackClassLoader.openFindResources(name).asSequence())
override fun findResource(name: String): URL? =
super.findResource(name) ?: fallbackClassLoader?.openFindResource(name)
}
@@ -5,49 +5,77 @@
package kotlin.script.experimental.jvm.impl
import java.io.ObjectInputStream
import java.io.ObjectOutputStream
import java.io.Serializable
import java.io.*
import java.net.URLClassLoader
import kotlin.reflect.KClass
import kotlin.script.experimental.api.*
import kotlin.script.experimental.jvm.*
import kotlin.script.experimental.jvm.actualClassLoader
class KJvmCompiledScript<out ScriptBase : Any>(
sourceLocationId: String?,
compilationConfiguration: ScriptCompilationConfiguration,
private var scriptClassFQName: String,
otherScripts: List<CompiledScript<*>> = emptyList(),
var compiledModule: KJvmCompiledModule
internal class KJvmCompiledScriptData(
var sourceLocationId: String?,
var compilationConfiguration: ScriptCompilationConfiguration,
var scriptClassFQName: String,
var otherScripts: List<CompiledScript<*>> = emptyList()
) : Serializable {
private fun writeObject(outputStream: ObjectOutputStream) {
outputStream.writeObject(compilationConfiguration)
outputStream.writeObject(sourceLocationId)
outputStream.writeObject(otherScripts)
outputStream.writeObject(scriptClassFQName)
}
@Suppress("UNCHECKED_CAST")
private fun readObject(inputStream: ObjectInputStream) {
compilationConfiguration = inputStream.readObject() as ScriptCompilationConfiguration
sourceLocationId = inputStream.readObject() as String?
otherScripts = inputStream.readObject() as List<CompiledScript<*>>
scriptClassFQName = inputStream.readObject() as String
}
companion object {
@JvmStatic
private val serialVersionUID = 3L
}
}
class KJvmCompiledScript<out ScriptBase : Any> internal constructor(
internal var data: KJvmCompiledScriptData,
var compiledModule: KJvmCompiledModule? // module should be null for imported (other) scripts, so only one reference to the module is kept
) : CompiledScript<ScriptBase>, Serializable {
private var _sourceLocationId: String? = sourceLocationId
constructor(
sourceLocationId: String?,
compilationConfiguration: ScriptCompilationConfiguration,
scriptClassFQName: String,
otherScripts: List<CompiledScript<*>> = emptyList(),
compiledModule: KJvmCompiledModule? // module should be null for imported (other) scripts, so only one reference to the module is kept
): this(KJvmCompiledScriptData(sourceLocationId, compilationConfiguration, scriptClassFQName, otherScripts), compiledModule)
override val sourceLocationId: String?
get() = _sourceLocationId
private var _compilationConfiguration: ScriptCompilationConfiguration? = compilationConfiguration
get() = data.sourceLocationId
override val compilationConfiguration: ScriptCompilationConfiguration
get() = _compilationConfiguration!!
private var _otherScripts: List<CompiledScript<*>> = otherScripts
get() = data.compilationConfiguration
override val otherScripts: List<CompiledScript<*>>
get() = _otherScripts
get() = data.otherScripts
val scriptClassFQName: String
get() = data.scriptClassFQName
override suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics<KClass<*>> = try {
// ensuring proper defaults are used
val actualEvaluationConfiguration = scriptEvaluationConfiguration ?: ScriptEvaluationConfiguration()
val classLoader = getOrCreateActualClassloader(actualEvaluationConfiguration)
val clazz = classLoader.loadClass(scriptClassFQName).kotlin
val clazz = classLoader.loadClass(data.scriptClassFQName).kotlin
clazz.asSuccess()
} catch (e: Throwable) {
ResultWithDiagnostics.Failure(
ScriptDiagnostic(
"Unable to instantiate class $scriptClassFQName",
"Unable to instantiate class ${data.scriptClassFQName}",
sourcePath = sourceLocationId,
exception = e
)
@@ -55,35 +83,32 @@ class KJvmCompiledScript<out ScriptBase : Any>(
}
private fun writeObject(outputStream: ObjectOutputStream) {
outputStream.writeObject(compilationConfiguration)
outputStream.writeObject(sourceLocationId)
outputStream.writeObject(otherScripts)
outputStream.writeObject(data)
outputStream.writeObject(compiledModule)
outputStream.writeObject(scriptClassFQName)
}
@Suppress("UNCHECKED_CAST")
private fun readObject(inputStream: ObjectInputStream) {
_compilationConfiguration = inputStream.readObject() as ScriptCompilationConfiguration
_sourceLocationId = inputStream.readObject() as String?
_otherScripts = inputStream.readObject() as List<CompiledScript<*>>
compiledModule = inputStream.readObject() as KJvmCompiledModule
scriptClassFQName = inputStream.readObject() as String
data = inputStream.readObject() as KJvmCompiledScriptData
compiledModule = inputStream.readObject() as KJvmCompiledModule?
}
companion object {
@JvmStatic
private val serialVersionUID = 2L
private val serialVersionUID = 3L
}
}
fun KJvmCompiledScript<*>.getOrCreateActualClassloader(evaluationConfiguration: ScriptEvaluationConfiguration): ClassLoader =
evaluationConfiguration[ScriptEvaluationConfiguration.jvm.actualClassLoader] ?: run {
val module = compiledModule
?: throw IllegalStateException("Illegal call sequence, actualClassloader should be set before calling function on the class without module")
val baseClassLoader = evaluationConfiguration[ScriptEvaluationConfiguration.jvm.baseClassLoader]
val moduleClassLoader = module.createClassLoader(baseClassLoader)
val classLoaderWithDeps =
if (evaluationConfiguration[ScriptEvaluationConfiguration.jvm.loadDependencies] == false) baseClassLoader
else makeClassLoaderFromDependencies(baseClassLoader)
compiledModule.createClassLoader(classLoaderWithDeps)
if (evaluationConfiguration[ScriptEvaluationConfiguration.jvm.loadDependencies] == false) moduleClassLoader
else makeClassLoaderFromDependencies(moduleClassLoader)
return classLoaderWithDeps
}
fun getConfigurationWithClassloader(
@@ -105,9 +130,7 @@ fun getConfigurationWithClassloader(
}
}
val ScriptEvaluationConfiguration.sharedScripts get() = get(ScriptEvaluationConfiguration.jvm.scriptsInstancesSharingMap)
private fun CompiledScript<*>.makeClassLoaderFromDependencies(baseClassLoader: ClassLoader?): ClassLoader? {
private fun CompiledScript<*>.makeClassLoaderFromDependencies(baseClassLoader: ClassLoader): ClassLoader {
val processedScripts = mutableSetOf<CompiledScript<*>>()
fun seq(res: Sequence<CompiledScript<*>>, script: CompiledScript<*>): Sequence<CompiledScript<*>> {
if (processedScripts.contains(script)) return res
@@ -127,3 +150,35 @@ private fun CompiledScript<*>.makeClassLoaderFromDependencies(baseClassLoader: C
return if (dependencies.none()) baseClassLoader
else URLClassLoader(dependencies.toList().toTypedArray(), baseClassLoader)
}
const val KOTLIN_SCRIPT_METADATA_PATH = "META-INF/kotlin/script"
const val KOTLIN_SCRIPT_METADATA_EXTENSION_WITH_DOT = ".kotlin_script"
fun scriptMetadataPath(scriptClassFQName: String) =
"$KOTLIN_SCRIPT_METADATA_PATH/$scriptClassFQName$KOTLIN_SCRIPT_METADATA_EXTENSION_WITH_DOT"
fun <T : Any> KJvmCompiledScript<T>.copyWithoutModule(): KJvmCompiledScript<T> = KJvmCompiledScript(data, null)
fun KJvmCompiledScript<*>.toBytes(): ByteArray {
val bos = ByteArrayOutputStream()
var oos: ObjectOutputStream? = null
try {
oos = ObjectOutputStream(bos)
oos.writeObject(this)
oos.flush()
return bos.toByteArray()!!
} finally {
try {
oos?.close()
} catch (e: IOException) {}
}
}
fun createScriptFromClassLoader(scriptClassFQName: String, classLoader: ClassLoader): KJvmCompiledScript<*> {
val scriptDataStream = classLoader.getResourceAsStream(scriptMetadataPath(scriptClassFQName))
?: throw IllegalArgumentException("Cannot find metadata for script $scriptClassFQName")
val script = ObjectInputStream(scriptDataStream).use {
it.readObject() as KJvmCompiledScript<*>
}
script.compiledModule = KJvmCompiledModuleFromClassLoader(classLoader)
return script
}
@@ -29,6 +29,11 @@ val JvmScriptEvaluationConfigurationKeys.baseClassLoader by PropertiesCollection
*/
val JvmScriptEvaluationConfigurationKeys.loadDependencies by PropertiesCollection.key<Boolean>(true)
/**
* Arguments of the main call, if script is executed via its main method
*/
val JvmScriptEvaluationConfigurationKeys.mainArguments by PropertiesCollection.key<Array<out String>>()
internal val JvmScriptEvaluationConfigurationKeys.actualClassLoader by PropertiesCollection.key<ClassLoader?>()
internal val JvmScriptEvaluationConfigurationKeys.scriptsInstancesSharingMap by PropertiesCollection.key<MutableMap<KClass<*>, EvaluationResult>>()
@@ -0,0 +1,36 @@
/*
* Copyright 2010-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license
* that can be found in the license/LICENSE.txt file.
*/
package kotlin.script.experimental.jvm
import kotlinx.coroutines.runBlocking
import kotlin.script.experimental.api.ScriptCompilationConfiguration
import kotlin.script.experimental.api.ScriptEvaluationConfiguration
import kotlin.script.experimental.api.baseClass
import kotlin.script.experimental.api.onFailure
import kotlin.script.experimental.host.createEvaluationConfigurationFromTemplate
import kotlin.script.experimental.jvm.impl.createScriptFromClassLoader
@Suppress("unused") // script codegen generates a call to it
fun runCompiledScript(scriptClass: Class<*>, vararg args: String) {
val script = createScriptFromClassLoader(scriptClass.name, scriptClass.classLoader)
val evaluator = BasicJvmScriptEvaluator()
val baseEvaluationConfiguration =
createEvaluationConfigurationFromTemplate(
script.compilationConfiguration[ScriptCompilationConfiguration.baseClass]!!,
defaultJvmScriptingHostConfiguration,
scriptClass.kotlin
)
val evaluationConfiguration = ScriptEvaluationConfiguration(baseEvaluationConfiguration) {
jvm {
mainArguments(args)
}
}
val result = runBlocking {
evaluator(script, evaluationConfiguration)
}.onFailure {
it.reports.forEach(System.err::println)
}
}
@@ -119,8 +119,8 @@ internal fun List<File>.filterIfContainsAll(vararg keyNames: String): List<File>
val res = hashMapOf<String, File>()
for (cpentry in this) {
for (prefix in keyNames) {
if (cpentry.matchMaybeVersionedFile(prefix) ||
(cpentry.isDirectory && cpentry.hasParentNamed(prefix))
if (!res.containsKey(prefix) &&
(cpentry.matchMaybeVersionedFile(prefix) || (cpentry.isDirectory && cpentry.hasParentNamed(prefix)))
) {
res[prefix] = cpentry
break