Implement repl, script and expressions evaluation support in kotlin runner

This commit is contained in:
Ilya Chernikov
2019-11-18 12:38:36 +01:00
parent 531ff92791
commit 49de5e19fb
4 changed files with 204 additions and 23 deletions
@@ -36,19 +36,18 @@ object Main {
private fun run(args: Array<String>) {
val classpath = arrayListOf<URL>()
val compilerClasspath = arrayListOf<URL>()
var runner: Runner? = null
var collectingArguments = false
var collectingExpressions = false
var needsCompiler = false
val arguments = arrayListOf<String>()
val expressions = arrayListOf<String>()
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<URL>.addPath(path: String) {
@@ -19,5 +19,9 @@ package org.jetbrains.kotlin.runner
import java.net.URL
interface Runner {
fun run(classpath: List<URL>, arguments: List<String>)
fun run(
classpath: List<URL>,
arguments: List<String>,
compilerClasspath: List<URL>
)
}
@@ -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<File> {
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<String, String>? {
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)
@@ -32,7 +32,7 @@ abstract class AbstractRunner : Runner {
protected abstract fun createClassLoader(classpath: List<URL>): ClassLoader
override fun run(classpath: List<URL>, arguments: List<String>) {
override fun run(classpath: List<URL>, arguments: List<String>, compilerClasspath: List<URL>) {
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<URL>, arguments: List<String>) {
// TODO: run REPL instead
throw RunnerException("please specify at least one name or file to run")
abstract class RunnerWithCompiler : Runner {
fun runCompiler(compilerClasspath: List<URL>, arguments: List<String>) {
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<String>::class.java)
mainMethod.invoke(null, arguments.toTypedArray())
}
}
class ScriptRunner(private val path: String) : Runner {
override fun run(classpath: List<URL>, arguments: List<String>) {
// TODO
throw RunnerException("running Kotlin scripts is not yet supported")
private fun MutableList<String>.addClasspathArgIfNeeded(classpath: List<URL>) {
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<URL>, arguments: List<String>) {
// TODO
throw RunnerException("evaluating expressions is not yet supported")
class ReplRunner : RunnerWithCompiler() {
override fun run(classpath: List<URL>, arguments: List<String>, compilerClasspath: List<URL>) {
val compilerArgs = ArrayList<String>()
compilerArgs.addClasspathArgIfNeeded(classpath)
runCompiler(compilerClasspath, compilerArgs)
}
}
class ScriptRunner(private val path: String) : RunnerWithCompiler() {
override fun run(classpath: List<URL>, arguments: List<String>, compilerClasspath: List<URL>) {
val compilerArgs = ArrayList<String>().apply {
addClasspathArgIfNeeded(classpath)
add("-script")
add(path)
addAll(arguments)
}
runCompiler(compilerClasspath, compilerArgs)
}
}
class ExpressionRunner(private val code: List<String>) : RunnerWithCompiler() {
override fun run(classpath: List<URL>, arguments: List<String>, compilerClasspath: List<URL>) {
val compilerArgs = ArrayList<String>().apply {
addClasspathArgIfNeeded(classpath)
code.forEach {
add("-expression")
add(it)
}
addAll(arguments)
}
runCompiler(compilerClasspath, compilerArgs)
}
}