diff --git a/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt b/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt index 71d6684c9af..a902db3e1c6 100644 --- a/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt +++ b/compiler/backend/src/org/jetbrains/kotlin/codegen/ScriptCodegen.kt @@ -21,6 +21,7 @@ import org.jetbrains.kotlin.resolve.BindingContext import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassNotAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperClassOrAny import org.jetbrains.kotlin.resolve.descriptorUtil.getSuperInterfaces +import org.jetbrains.kotlin.resolve.jvm.AsmTypes import org.jetbrains.kotlin.resolve.jvm.AsmTypes.OBJECT_TYPE import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin import org.jetbrains.kotlin.resolve.jvm.diagnostics.JvmDeclarationOrigin.Companion.NO_ORIGIN @@ -222,19 +223,43 @@ class ScriptCodegen private constructor( } private fun genMembers() { + var hasMain = false for (declaration in scriptDeclaration.declarations) { - if (declaration is KtProperty || declaration is KtNamedFunction || declaration is KtTypeAlias) { - genSimpleMember(declaration) - } - else if (declaration is KtClassOrObject) { - genClassOrObject(declaration) - } - else if (declaration is KtDestructuringDeclaration) { - for (entry in declaration.entries) { + when (declaration) { + is KtNamedFunction -> { + genSimpleMember(declaration) + // temporary way to avoid name clashes + // TODO: remove as soon as main generation become an explicit configuration option + if (declaration.name == "main") { + hasMain = true + } + } + is KtProperty, is KtNamedFunction, is KtTypeAlias -> genSimpleMember(declaration) + is KtClassOrObject -> genClassOrObject(declaration) + is KtDestructuringDeclaration -> for (entry in declaration.entries) { genSimpleMember(entry) } } } + if (!hasMain) { + genMain() + } + } + + private fun genMain() { + val mainMethodArgsType = AsmUtil.getArrayType(AsmTypes.JAVA_STRING_TYPE) + val mainMethodDescriptor = Type.getMethodDescriptor(Type.VOID_TYPE, mainMethodArgsType) + val runMethodDescriptor = Type.getMethodDescriptor(Type.VOID_TYPE, AsmTypes.JAVA_CLASS_TYPE, mainMethodArgsType) + InstructionAdapter( + v.newMethod(NO_ORIGIN, ACC_STATIC or ACC_FINAL or ACC_PUBLIC, "main", mainMethodDescriptor, null, null) + ).apply { + visitCode() + visitLdcInsn(classAsmType) + load(0, mainMethodArgsType) + visitMethodInsn(INVOKESTATIC, "kotlin/script/experimental/jvm/RunnerKt", "runCompiledScript", runMethodDescriptor, false) + areturn(Type.VOID_TYPE) + visitEnd() + } } companion object { diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/KotlinPaths.kt b/compiler/util/src/org/jetbrains/kotlin/utils/KotlinPaths.kt index be71b6a0ffc..2c8cdd261e6 100644 --- a/compiler/util/src/org/jetbrains/kotlin/utils/KotlinPaths.kt +++ b/compiler/util/src/org/jetbrains/kotlin/utils/KotlinPaths.kt @@ -96,6 +96,7 @@ interface KotlinPaths { ScriptingImpl(PathUtil.KOTLIN_SCRIPTING_IMPL_NAME), ScriptingLib(PathUtil.KOTLIN_SCRIPTING_COMMON_NAME), ScriptingJvmLib(PathUtil.KOTLIN_SCRIPTING_JVM_NAME), + CoroutinesCore(PathUtil.KOTLINX_COROUTINES_CORE_NAME), } enum class ClassPaths(val contents: List = emptyList()) { diff --git a/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt b/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt index 0a790cb68b3..f5b25b23cba 100644 --- a/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt +++ b/compiler/util/src/org/jetbrains/kotlin/utils/PathUtil.kt @@ -69,6 +69,8 @@ object PathUtil { const val KOTLIN_SCRIPTING_COMPILER_PLUGIN_JAR = "$KOTLIN_SCRIPTING_COMPILER_PLUGIN_NAME.jar" const val KOTLIN_SCRIPTING_IMPL_NAME = "kotlin-scripting-impl" const val KOTLIN_SCRIPTING_IMPL_JAR = "$KOTLIN_SCRIPTING_IMPL_NAME.jar" + const val KOTLINX_COROUTINES_CORE_NAME = "kotlinx-coroutines-core" + const val KOTLINX_COROUTINES_CORE_JAR = "$KOTLINX_COROUTINES_CORE_NAME.jar" const val KOTLIN_TEST_NAME = "kotlin-test" const val KOTLIN_TEST_JAR = "$KOTLIN_TEST_NAME.jar" diff --git a/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt b/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt index ad191753641..5bf9e181fb2 100644 --- a/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt +++ b/libraries/scripting/common/src/kotlin/script/experimental/api/errorHandling.kt @@ -7,6 +7,8 @@ package kotlin.script.experimental.api +import java.io.File + /** * The single script diagnostic report * @param message diagnostic message @@ -25,6 +27,27 @@ data class ScriptDiagnostic( * The diagnostic severity */ enum class Severity { FATAL, ERROR, WARNING, INFO, DEBUG } + + override fun toString(): String = buildString { + append(severity.name) + append(' ') + append(message) + if (sourcePath != null || location != null) { + append(" (") + sourcePath?.let { append(it.substringAfterLast(File.separatorChar)) } + location?.let { + append(':') + append(it.start.line) + append(':') + append(it.start.col) + } + append(')') + } + if (exception != null) { + append(": ") + append(exception) + } + } } /** diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/BasicJvmScriptingHost.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/BasicJvmScriptingHost.kt index 40cfad86898..e6ccf01360e 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/BasicJvmScriptingHost.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/BasicJvmScriptingHost.kt @@ -48,6 +48,6 @@ inline fun createJvmEvaluationConfigurationFromTemplate( ): ScriptEvaluationConfiguration = createEvaluationConfigurationFromTemplate( KotlinType(T::class), hostConfiguration, - ScriptCompilationConfiguration::class, + ScriptEvaluationConfiguration::class, body ) diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt index 34d46af5802..63f171ebda3 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/impl/KJvmCompilerImpl.kt @@ -352,7 +352,6 @@ class KJvmCompilerImpl(val hostConfiguration: ScriptingHostConfiguration) : KJvm sourceDependencies: List, getScriptConfiguration: (KtFile) -> ScriptCompilationConfiguration ): KJvmCompiledScript { - val module = makeCompiledModule(generationState) val scriptDependenciesStack = ArrayDeque() fun makeOtherScripts(script: KtScript): List> { @@ -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), diff --git a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt index 52527b7609b..64d76863ae1 100644 --- a/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt +++ b/libraries/scripting/jvm-host/src/kotlin/script/experimental/jvmhost/jvmScriptSaving.kt @@ -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() + ?.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() - ?.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( diff --git a/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt b/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt index ade7792f5bd..254828a1a6e 100644 --- a/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt +++ b/libraries/scripting/jvm-host/test/kotlin/script/experimental/jvmhost/test/ScriptingHostTest.kt @@ -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() + 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()) + }.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 ResultWithDiagnostics.throwOnFailure(): ResultWithDiagnostics = 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 { 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() } + diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt index 0b18dada7cc..48f295b00ec 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/BasicJvmScriptEvaluator.kt @@ -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 { diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledModule.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledModule.kt index b809239d95d..53556c275eb 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledModule.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledModule.kt @@ -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) : 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 = 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 = + 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) } diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledScript.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledScript.kt index 1ae08f907c1..d4b5add8972 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledScript.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/impl/KJvmCompiledScript.kt @@ -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( - sourceLocationId: String?, - compilationConfiguration: ScriptCompilationConfiguration, - private var scriptClassFQName: String, - otherScripts: List> = emptyList(), - var compiledModule: KJvmCompiledModule +internal class KJvmCompiledScriptData( + var sourceLocationId: String?, + var compilationConfiguration: ScriptCompilationConfiguration, + var scriptClassFQName: String, + var otherScripts: List> = 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> + scriptClassFQName = inputStream.readObject() as String + } + + companion object { + @JvmStatic + private val serialVersionUID = 3L + } +} + +class KJvmCompiledScript 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, Serializable { - private var _sourceLocationId: String? = sourceLocationId + constructor( + sourceLocationId: String?, + compilationConfiguration: ScriptCompilationConfiguration, + scriptClassFQName: String, + otherScripts: List> = 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> = otherScripts + get() = data.compilationConfiguration override val otherScripts: List> - get() = _otherScripts + get() = data.otherScripts + + val scriptClassFQName: String + get() = data.scriptClassFQName override suspend fun getClass(scriptEvaluationConfiguration: ScriptEvaluationConfiguration?): ResultWithDiagnostics> = 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( } 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> - 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>() fun seq(res: Sequence>, script: CompiledScript<*>): Sequence> { 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 KJvmCompiledScript.copyWithoutModule(): KJvmCompiledScript = 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 +} diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEvaluation.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEvaluation.kt index 7fcc42d94c7..dde81a6a472 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEvaluation.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/jvmScriptEvaluation.kt @@ -29,6 +29,11 @@ val JvmScriptEvaluationConfigurationKeys.baseClassLoader by PropertiesCollection */ val JvmScriptEvaluationConfigurationKeys.loadDependencies by PropertiesCollection.key(true) +/** + * Arguments of the main call, if script is executed via its main method + */ +val JvmScriptEvaluationConfigurationKeys.mainArguments by PropertiesCollection.key>() + internal val JvmScriptEvaluationConfigurationKeys.actualClassLoader by PropertiesCollection.key() internal val JvmScriptEvaluationConfigurationKeys.scriptsInstancesSharingMap by PropertiesCollection.key, EvaluationResult>>() diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/runner.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/runner.kt new file mode 100644 index 00000000000..b790c6a4dea --- /dev/null +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/runner.kt @@ -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) + } +} \ No newline at end of file diff --git a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt index 70f51422695..8e5b0b47d7e 100644 --- a/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt +++ b/libraries/scripting/jvm/src/kotlin/script/experimental/jvm/util/jvmClasspathUtil.kt @@ -119,8 +119,8 @@ internal fun List.filterIfContainsAll(vararg keyNames: String): List val res = hashMapOf() 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