From 49de5e19fb3c5604cbaaf58f4a727c2a4b960df7 Mon Sep 17 00:00:00 2001 From: Ilya Chernikov Date: Mon, 18 Nov 2019 12:38:36 +0100 Subject: [PATCH] Implement repl, script and expressions evaluation support in kotlin runner --- .../src/org/jetbrains/kotlin/runner/Main.kt | 49 ++++++-- .../src/org/jetbrains/kotlin/runner/Runner.kt | 6 +- .../jetbrains/kotlin/runner/findCompiler.kt | 111 ++++++++++++++++++ .../org/jetbrains/kotlin/runner/runners.kt | 61 ++++++++-- 4 files changed, 204 insertions(+), 23 deletions(-) create mode 100644 compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/findCompiler.kt diff --git a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Main.kt b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Main.kt index a800c045546..6fc496d9cfa 100644 --- a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Main.kt +++ b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Main.kt @@ -36,19 +36,18 @@ object Main { private fun run(args: Array) { val classpath = arrayListOf() + val compilerClasspath = arrayListOf() var runner: Runner? = null var collectingArguments = false + var collectingExpressions = false + var needsCompiler = false val arguments = arrayListOf() + val expressions = arrayListOf() var noReflect = false var i = 0 while (i < args.size) { val arg = args[i] - if (collectingArguments) { - arguments.add(arg) - i++ - continue - } fun next(): String { if (++i == args.size) { @@ -57,6 +56,22 @@ object Main { return args[i] } + if (collectingExpressions) { + if ("-expression" == arg || "-e" == arg) { + expressions.add(next()) + i++ + continue + } else { + collectingArguments = true + } + } + + if (collectingArguments) { + arguments.add(arg) + i++ + continue + } + if ("-help" == arg || "-h" == arg) { printUsageAndExit() } @@ -68,9 +83,15 @@ object Main { classpath.addPath(path) } } + else if ("-compiler-path" == arg) { + for (path in next().split(File.pathSeparator).filter(String::isNotEmpty)) { + compilerClasspath.addPath(path) + } + } else if ("-expression" == arg || "-e" == arg) { - runner = ExpressionRunner(next()) - collectingArguments = true + expressions.add(next()) + collectingExpressions = true + needsCompiler = true } else if ("-no-reflect" == arg) { noReflect = true @@ -85,6 +106,7 @@ object Main { else if (arg.endsWith(".kts")) { runner = ScriptRunner(arg) collectingArguments = true + needsCompiler = true } else { runner = MainClassRunner(arg) @@ -103,11 +125,20 @@ object Main { classpath.addPath(KOTLIN_HOME.toString() + "/lib/kotlin-reflect.jar") } - if (runner == null) { + if (expressions.isNotEmpty()) { + runner = ExpressionRunner(expressions) + } else if (runner == null) { runner = ReplRunner() + needsCompiler = true } - runner.run(classpath, arguments) + if (needsCompiler && compilerClasspath.isEmpty()) { + findCompilerJar(this::class.java, KOTLIN_HOME.resolve("lib")).forEach { + compilerClasspath.add(it.absoluteFile.toURI().toURL()) + } + } + + runner.run(classpath, arguments, compilerClasspath) } private fun MutableList.addPath(path: String) { diff --git a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Runner.kt b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Runner.kt index fbb611e50c7..3626c98e13e 100644 --- a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Runner.kt +++ b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/Runner.kt @@ -19,5 +19,9 @@ package org.jetbrains.kotlin.runner import java.net.URL interface Runner { - fun run(classpath: List, arguments: List) + fun run( + classpath: List, + arguments: List, + compilerClasspath: List + ) } diff --git a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/findCompiler.kt b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/findCompiler.kt new file mode 100644 index 00000000000..757bec84dfc --- /dev/null +++ b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/findCompiler.kt @@ -0,0 +1,111 @@ +/* + * Copyright 2010-2019 JetBrains s.r.o. and Kotlin Programming Language contributors. + * Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file. + */ + +package org.jetbrains.kotlin.runner + +import java.io.File +import java.net.URL + +fun findCompilerJar(classFromJarInTheSameLocation: Class<*>, kotlinHome: File): List { + val baseDir = (tryGetResourcePathForClass(classFromJarInTheSameLocation)?.parentFile ?: kotlinHome).takeIf { it.isDirectory } + ?: return emptyList() + val compilerJars = baseDir.listFiles { f: File -> + COMPILER_JARS.any { expected -> + f.matchMaybeVersionedFile(expected) && f.extension == "jar" + } + }?.takeIf { it.size >= COMPILER_JARS.size }?.toList() + return compilerJars ?: emptyList() +} + +private val COMPILER_JARS = listOf("kotlin-compiler", "kotlin-stdlib", "kotlin-reflect") + +// below is a copy from kotlin.script.experimental.jvm.impl, but we do not want to introduce dependency to that implementation, and +// there is no other good place for sharing this functionality yet +// TODO: find a good place and put the shared code into it + +internal fun tryGetResourcePathForClass(aClass: Class<*>): File? { + val path = "/" + aClass.name.replace('.', '/') + ".class" + return getResourceRoot(aClass, path)?.let { + File(it).absoluteFile + } +} + +private fun getResourceRoot(context: Class<*>, path: String): String? { + var url: URL? = context.getResource(path) + if (url == null) { + url = ClassLoader.getSystemResource(path.substring(1)) + } + return if (url != null) extractRoot(url, path) else null +} + +private const val JAR_PROTOCOL = "jar" +private const val FILE_PROTOCOL = "file" +private const val JAR_SEPARATOR = "!/" +private const val SCHEME_SEPARATOR = "://" + +private fun extractRoot(resourceURL: URL, resourcePath: String): String? { + if (!resourcePath.startsWith('/') || resourcePath.startsWith('\\')) return null + + var resultPath: String? = null + val protocol = resourceURL.protocol + if (protocol == FILE_PROTOCOL) { + val path = resourceURL.toFileOrNull()!!.path + val testPath = path.replace('\\', '/') + val testResourcePath = resourcePath.replace('\\', '/') + if (testPath.endsWith(testResourcePath, ignoreCase = true)) { + resultPath = path.substring(0, path.length - resourcePath.length) + } + } else if (protocol == JAR_PROTOCOL) { + val paths = splitJarUrl(resourceURL.file) + if (paths?.first != null) { + resultPath = File(paths.first).canonicalPath + } + } + + return resultPath?.trimEnd(File.separatorChar) +} + +private fun splitJarUrl(url: String): Pair? { + val pivot = url.indexOf(JAR_SEPARATOR).takeIf { it >= 0 } ?: return null + + val resourcePath = url.substring(pivot + 2) + var jarPath = url.substring(0, pivot) + + if (jarPath.startsWith(JAR_PROTOCOL + ":")) { + jarPath = jarPath.substring(JAR_PROTOCOL.length + 1) + } + + if (jarPath.startsWith(FILE_PROTOCOL)) { + try { + jarPath = URL(jarPath).toFileOrNull()!!.path.replace('\\', '/') + } catch (e: Exception) { + jarPath = jarPath.substring(FILE_PROTOCOL.length) + if (jarPath.startsWith(SCHEME_SEPARATOR)) { + jarPath = jarPath.substring(SCHEME_SEPARATOR.length) + } else if (jarPath.startsWith(':')) { + jarPath = jarPath.substring(1) + } + } + + } + return Pair(jarPath, resourcePath) +} + +private fun URL.toFileOrNull() = + try { + File(toURI()) + } catch (e: IllegalArgumentException) { + null + } catch (e: java.net.URISyntaxException) { + null + } ?: run { + if (protocol != "file") null + else File(file) + } + +private fun File.matchMaybeVersionedFile(baseName: String) = + name == baseName || + name == baseName.removeSuffix(".jar") || // for classes dirs + Regex(Regex.escape(baseName.removeSuffix(".jar")) + "(-\\d.*)?\\.jar").matches(name) diff --git a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/runners.kt b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/runners.kt index db3ed63c54d..450612c2949 100644 --- a/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/runners.kt +++ b/compiler/cli/cli-runner/src/org/jetbrains/kotlin/runner/runners.kt @@ -32,7 +32,7 @@ abstract class AbstractRunner : Runner { protected abstract fun createClassLoader(classpath: List): ClassLoader - override fun run(classpath: List, arguments: List) { + override fun run(classpath: List, arguments: List, compilerClasspath: List) { val classLoader = createClassLoader(classpath) val mainClass = try { @@ -105,23 +105,58 @@ class JarRunner(private val path: String) : AbstractRunner() { } } -class ReplRunner : Runner { - override fun run(classpath: List, arguments: List) { - // TODO: run REPL instead - throw RunnerException("please specify at least one name or file to run") +abstract class RunnerWithCompiler : Runner { + + fun runCompiler(compilerClasspath: List, arguments: List) { + val classLoader = + if (arguments.isEmpty()) RunnerWithCompiler::class.java.classLoader + else URLClassLoader(compilerClasspath.toTypedArray(), null) + val compilerClass = classLoader.loadClass("org.jetbrains.kotlin.cli.jvm.K2JVMCompiler") + val mainMethod = compilerClass.getMethod("main", Array::class.java) + mainMethod.invoke(null, arguments.toTypedArray()) } } -class ScriptRunner(private val path: String) : Runner { - override fun run(classpath: List, arguments: List) { - // TODO - throw RunnerException("running Kotlin scripts is not yet supported") +private fun MutableList.addClasspathArgIfNeeded(classpath: List) { + if (classpath.isNotEmpty()) { + add("-cp") + add(classpath.map { + if (it.protocol == "file") it.path + else it.toExternalForm() + }.joinToString(File.pathSeparator)) } } -class ExpressionRunner(private val code: String) : Runner { - override fun run(classpath: List, arguments: List) { - // TODO - throw RunnerException("evaluating expressions is not yet supported") +class ReplRunner : RunnerWithCompiler() { + override fun run(classpath: List, arguments: List, compilerClasspath: List) { + val compilerArgs = ArrayList() + compilerArgs.addClasspathArgIfNeeded(classpath) + runCompiler(compilerClasspath, compilerArgs) + } +} + +class ScriptRunner(private val path: String) : RunnerWithCompiler() { + override fun run(classpath: List, arguments: List, compilerClasspath: List) { + val compilerArgs = ArrayList().apply { + addClasspathArgIfNeeded(classpath) + add("-script") + add(path) + addAll(arguments) + } + runCompiler(compilerClasspath, compilerArgs) + } +} + +class ExpressionRunner(private val code: List) : RunnerWithCompiler() { + override fun run(classpath: List, arguments: List, compilerClasspath: List) { + val compilerArgs = ArrayList().apply { + addClasspathArgIfNeeded(classpath) + code.forEach { + add("-expression") + add(it) + } + addAll(arguments) + } + runCompiler(compilerClasspath, compilerArgs) } }