From aa997a43a078ac3bc2ae6da3c253ccf34f3727a5 Mon Sep 17 00:00:00 2001 From: Hwijae Lee Date: Sat, 22 Dec 2018 12:54:47 -0500 Subject: [PATCH 1/3] Add KOTLIN_OPTS and @file:KotlinOpts annotation test --- test/resources/kotlin_opts_test.kts | 7 +++++++ test/test_suite.sh | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 test/resources/kotlin_opts_test.kts diff --git a/test/resources/kotlin_opts_test.kts b/test/resources/kotlin_opts_test.kts new file mode 100644 index 00000000..711f6864 --- /dev/null +++ b/test/resources/kotlin_opts_test.kts @@ -0,0 +1,7 @@ +@file:KotlinOpts("-J-Xmx5g -Dkscript.opt1=1") +//KOTLIN_OPTS -Dkscript.opt2=foo -Dkscript.opt3=bar + +// https://stackoverflow.com/questions/7611987/how-to-check-the-configured-xmx-value-for-a-running-java-application + +//println("arg is " + args[0]) +println("mem_${Runtime.getRuntime().maxMemory()}_${System.getProperty("kscript.opt1")}_${System.getProperty("kscript.opt2")}_${System.getProperty("kscript.opt3")}") \ No newline at end of file diff --git a/test/test_suite.sh b/test/test_suite.sh index 06d79aac..e8e3d5d8 100755 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -260,6 +260,8 @@ assert "source ${KSCRIPT_HOME}/test/resources/home_dir_include.sh" "42" ## prevent regression of #173 assert "source ${KSCRIPT_HOME}/test/resources/compiler_opts_with_includes.sh" "hello42" +## KOTLIN_OPTS +assert "kscript ${KSCRIPT_HOME}/test/resources/kotlin_opts_test.kts" "mem_4772593664_1_foo_bar" kscript_nocall() { kotlin -classpath ${KSCRIPT_HOME}/build/libs/kscript.jar kscript.app.KscriptKt "$@";} export -f kscript_nocall From 36c7dd2cd06bf50380cefd667b0d8e76e492a69b Mon Sep 17 00:00:00 2001 From: Hwijae Lee Date: Sat, 22 Dec 2018 13:51:37 -0500 Subject: [PATCH 2/3] Add docopt class access test kscript.jar classes should not be accessable by the script without explicit dependency. --- test/test_suite.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/test_suite.sh b/test/test_suite.sh index e8e3d5d8..0a4641c6 100755 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -263,6 +263,10 @@ assert "source ${KSCRIPT_HOME}/test/resources/compiler_opts_with_includes.sh" "h ## KOTLIN_OPTS assert "kscript ${KSCRIPT_HOME}/test/resources/kotlin_opts_test.kts" "mem_4772593664_1_foo_bar" +## kscript.jar class access test +assert_raises 'kscript "println(org.docopt.Docopt::class)"' 1 + + kscript_nocall() { kotlin -classpath ${KSCRIPT_HOME}/build/libs/kscript.jar kscript.app.KscriptKt "$@";} export -f kscript_nocall From 8a671831306cdad25959347f045717baa11b9bad Mon Sep 17 00:00:00 2001 From: Hwijae Lee Date: Mon, 15 Jan 2018 12:16:41 -0500 Subject: [PATCH 3/3] Use classloader to run the script Supports for -J and -D arguments were added. --- src/kscript | 54 +++++++++++++-- src/main/kotlin/kscript/app/AppHelpers.kt | 10 ++- src/main/kotlin/kscript/app/DependencyUtil.kt | 2 +- src/main/kotlin/kscript/app/JarFileLoader.kt | 14 ++++ src/main/kotlin/kscript/app/KotlinRunner.kt | 61 +++++++++++++++++ src/main/kotlin/kscript/app/Kscript.kt | 66 +++++++------------ test/test_suite.sh | 19 +++--- 7 files changed, 166 insertions(+), 60 deletions(-) create mode 100644 src/main/kotlin/kscript/app/JarFileLoader.kt create mode 100644 src/main/kotlin/kscript/app/KotlinRunner.kt diff --git a/src/kscript b/src/kscript index 1fbede4a..24c7540e 100755 --- a/src/kscript +++ b/src/kscript @@ -22,13 +22,57 @@ abs_kscript_path=$(resolve_symlink $(absolute_path $0)) ## resolve application jar path from script location and convert to windows path when using cygwin jarPath=$(dirname $abs_kscript_path)/kscript.jar -if [[ $(uname) == CYGWIN* ]]; then jarPath=$(cygpath -w ${jarPath}); fi ## prefer KOTLIN_HOME instead of PATH to resolve `kotlin` location (see #145) if [[ -z "$KOTLIN_HOME" ]]; then KOTLIN_EXEC="kotlin"; else KOTLIN_EXEC="$KOTLIN_HOME/bin/kotlin"; fi -## expose the name of the script being run to the script itself -export KSCRIPT_FILE="$1" +if [[ $(uname) == CYGWIN* ]]; then + jarPath=$(cygpath -w ${jarPath}) + [[ ! -z $KOTLIN_HOME ]] && KOTLIN_HOME=$(cygpath -w ${KOTLIN_HOME}) +fi -## run it using command substitution to have just the user process once kscript is done -eval "exec $(${KOTLIN_EXEC} -classpath ${jarPath} kscript.app.KscriptKt "$@")" +## -J -D (borrowed from 'kotlinc') + +declare -a java_args +declare -a kscript_args + +export KSCRIPT_FILE= + +while [ $# -gt 0 ]; do + is_java_arg= + case "$1" in + -D*) + [[ -z $KSCRIPT_FILE ]] && is_java_arg=1 + ;; + -J*) + [[ -z $KSCRIPT_FILE ]] && is_java_arg=1 + ;; + *) + if [[ ! $1 =~ ^-. ]]; then + export KSCRIPT_FILE=$1 + fi + ;; + esac + if [[ -n $is_java_arg ]]; then + java_args=("${java_args[@]}" "$1") + else + kscript_args=("${kscript_args[@]}" "$1") + fi + shift +done + +## KotlinOpts + +if [[ -r $KSCRIPT_FILE ]]; then + commentOpts=(`grep '^//KOTLIN_OPTS ' $KSCRIPT_FILE | sed 's/^\/\/KOTLIN_OPTS \(.*\)/\1/'`) + if [[ -n $commentOpts ]]; then + java_args=("${java_args[@]}" "${commentOpts[@]}") + fi + fileOpts=(`grep '^@file:KotlinOpts(' $KSCRIPT_FILE | sed 's/.*"\(.*\)".*/\1/'`) + if [[ -n $fileOpts ]]; then + java_args=("${java_args[@]}" "${fileOpts[@]}") + fi +fi + +## run it +exec ${KOTLIN_EXEC} "${java_args[@]}" -classpath ${jarPath} kscript.app.KscriptKt "${kscript_args[@]}" diff --git a/src/main/kotlin/kscript/app/AppHelpers.kt b/src/main/kotlin/kscript/app/AppHelpers.kt index 50b036fe..f2b7fdfc 100644 --- a/src/main/kotlin/kscript/app/AppHelpers.kt +++ b/src/main/kotlin/kscript/app/AppHelpers.kt @@ -9,6 +9,11 @@ import java.security.MessageDigest import java.util.function.Consumer import kotlin.system.exitProcess +val IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("windows") + +fun joinToPathString(vararg parts: String) = parts.joinToString("${File.separatorChar}") + +fun argsToList(args: String): List = if (args.isEmpty()) listOf() else args.split("\\s+".toRegex()) data class ProcessResult(val command: String, val exitCode: Int, val stdout: String, val stderr: String) { @@ -116,7 +121,6 @@ fun errorIf(value: Boolean, lazyMessage: () -> Any) { } fun quit(status: Int): Nothing { - print(if (status == 0) "true" else "false") exitProcess(status) } @@ -282,7 +286,7 @@ private fun createSymLink(link: File, target: File) { * Create and use a temporary gradle project to package the compiled script using capsule. * See https://github.com/puniverse/capsule */ -fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List, customRepos: List, runtimeOptions: String, appName: String) { +fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List, customRepos: List, runtimeOptions: List, appName: String) { requireInPath("gradle", "gradle is required to package kscripts") infoMsg("Packaging script '$appName' into standalone executable...") @@ -295,7 +299,7 @@ fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List val stringifiedDeps = dependencies.map { " compile \"$it\"" }.joinToString("\n") val stringifiedRepos = customRepos.map { " maven {\n url '${it.url}'\n }\n" }.joinToString("\n") - val jvmOptions = runtimeOptions.split(" ") + val jvmOptions = runtimeOptions .filter { it.startsWith("-J") } .map { it.removePrefix("-J") } .map { '"' + it + '"' } diff --git a/src/main/kotlin/kscript/app/DependencyUtil.kt b/src/main/kotlin/kscript/app/DependencyUtil.kt index 255fdff1..4c208871 100644 --- a/src/main/kotlin/kscript/app/DependencyUtil.kt +++ b/src/main/kotlin/kscript/app/DependencyUtil.kt @@ -11,7 +11,7 @@ import java.io.File val DEP_LOOKUP_CACHE_FILE = File(KSCRIPT_CACHE_DIR, "dependency_cache.txt") -val CP_SEPARATOR_CHAR = if (System.getProperty("os.name").toLowerCase().contains("windows")) ";" else ":" +val CP_SEPARATOR_CHAR = if (IS_WINDOWS) ";" else ":" fun resolveDependencies(depIds: List, customRepos: List = emptyList(), loggingEnabled: Boolean): String? { diff --git a/src/main/kotlin/kscript/app/JarFileLoader.kt b/src/main/kotlin/kscript/app/JarFileLoader.kt new file mode 100644 index 00000000..fb14c23d --- /dev/null +++ b/src/main/kotlin/kscript/app/JarFileLoader.kt @@ -0,0 +1,14 @@ +package kscript.app + +import java.io.File +import java.net.URL +import java.net.URLClassLoader + +// https://dzone.com/articles/add-jar-file-java-load-path + +class JarFileLoader(urls: Array = arrayOf()) : URLClassLoader(urls) { + fun addFile(file: File) { + addURL(file.toURI().toURL()) + } +} + diff --git a/src/main/kotlin/kscript/app/KotlinRunner.kt b/src/main/kotlin/kscript/app/KotlinRunner.kt new file mode 100644 index 00000000..9e090d29 --- /dev/null +++ b/src/main/kotlin/kscript/app/KotlinRunner.kt @@ -0,0 +1,61 @@ +package kscript.app + +import java.io.File + +class KotlinRunner(private val kotlinHome: String) { + companion object { + private const val PRELOADER_CLASS = "org.jetbrains.kotlin.preloading.Preloader" + private const val RUNNER_MAIN_CLASS = "org.jetbrains.kotlin.runner.Main" + } + + private val jarFileLoader by lazy { JarFileLoader() } + + private val compilerBaseArgs = listOf("-cp", joinToPathString(kotlinHome, "lib", "kotlin-compiler.jar"), + "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler") + + private val preloaderMainMethod by lazy { + val preloaderJar = File(joinToPathString(kotlinHome, "lib", "kotlin-preloader.jar")) + jarFileLoader.addFile(preloaderJar) + jarFileLoader.loadClass(PRELOADER_CLASS).getDeclaredMethod("main", Array::class.java) + } + private val runnerMainMethod by lazy { + System.getProperties().setProperty("kotlin.home", kotlinHome) + jarFileLoader.addFile(File(joinToPathString(kotlinHome, "lib", "kotlin-runner.jar"))) + jarFileLoader.loadClass(RUNNER_MAIN_CLASS).getDeclaredMethod("main", Array::class.java) + } + + fun compile(compilerOpts: List, targetJarFile: File, sourceFiles: List, classpath: String?) { + val baseArgs = listOf("-Xskip-runtime-version-check") + + compilerOpts + listOf("-d", targetJarFile.absolutePath) + val cpArgs = classPathArgs(classpath) + runKotlinc(baseArgs + sourceFiles.map { it.absolutePath } + cpArgs) + } + + fun runScript(scriptClassPath: String, execClassName: String, userArgs: List): Int { + val runnerArgs = listOf("-cp", scriptClassPath, execClassName) + userArgs + runKotlin(runnerArgs) + return 0 + } + + fun interactiveShell(jarFile: File, classpath: String?, compilerOpts: List) { + val jarPath = if (jarFile.isFile) jarFile.absolutePath else null + val args = compilerOpts + classPathArgs(classpath, jarPath) + runKotlinc(args) + } + + private fun classPathArgs(vararg paths: String?): List { + val combinedClasspath = listOfNotNull(*paths).filter { it.isNotEmpty() }.joinToString(CP_SEPARATOR_CHAR) + return if (combinedClasspath.isNotEmpty()) { + listOf("-cp", combinedClasspath) + } else listOf() + } + + private fun runKotlinc(args: Collection) { + preloaderMainMethod.invoke(jarFileLoader, (compilerBaseArgs + args).toTypedArray()) + } + + private fun runKotlin(args: Collection) { + runnerMainMethod.invoke(jarFileLoader, args.toTypedArray()) + } +} + diff --git a/src/main/kotlin/kscript/app/Kscript.kt b/src/main/kotlin/kscript/app/Kscript.kt index e853a95a..178ca0f5 100644 --- a/src/main/kotlin/kscript/app/Kscript.kt +++ b/src/main/kotlin/kscript/app/Kscript.kt @@ -1,6 +1,5 @@ package kscript.app -import kscript.app.ShellUtils.requireInPath import org.docopt.DocOptWrapper import org.intellij.lang.annotations.Language import java.io.BufferedReader @@ -43,7 +42,8 @@ Options: -s --silent Suppress status logging to stderr --package Package script and dependencies into self-dependent binary --add-bootstrap-header Prepend bash header that installs kscript if necessary - + -J -J is stripped and passed to Java as-is (if no KOTLIN_OPTS is given) + -Dname=value Set a system JVM property (if no KOTLIN_OPTS is given) Copyright : 2017 Holger Brandl License : MIT @@ -150,28 +150,16 @@ fun main(args: Array) { // Create temporary dev environment if (docopt.getBoolean("idea")) { - println(launchIdeaWithKscriptlet(rawScript, dependencies, customRepos, includeURLs)) + evalBash(launchIdeaWithKscriptlet(rawScript, dependencies, customRepos, includeURLs)) exitProcess(0) } val classpath = resolveDependencies(dependencies, customRepos, loggingEnabled) ?: "" - val optionalCpArg = if (classpath.isNotEmpty()) "-classpath '${classpath}'" else "" // Extract kotlin arguments - val kotlinOpts = script.collectRuntimeOptions() - val compilerOpts = script.collectCompilerOptions() - - - // Optionally enter interactive mode - if (docopt.getBoolean("interactive")) { - infoMsg("Creating REPL from ${scriptFile}") - // System.err.println("kotlinc ${kotlinOpts} -classpath '${classpath}'") - - println("kotlinc ${compilerOpts} ${kotlinOpts} ${optionalCpArg}") - - exitProcess(0) - } + val kotlinOpts by lazy { argsToList(script.collectRuntimeOptions()) } + val compilerOpts = argsToList(script.collectCompilerOptions()) val scriptFileExt = scriptFile.extension val scriptCheckSum = md5(scriptFile) @@ -211,12 +199,12 @@ fun main(args: Array) { // infer KOTLIN_HOME if not set - val KOTLIN_HOME = System.getenv("KOTLIN_HOME") ?: guessKotlinHome() - - errorIf(KOTLIN_HOME == null) { - "KOTLIN_HOME is not set and could not be inferred from context" + val KOTLIN_HOME = System.getenv("KOTLIN_HOME") ?: guessKotlinHome() ?: run { + errorMsg("KOTLIN_HOME is not set and could not be inferred from context") + quit(1) } + val kotlinRunner = KotlinRunner(KOTLIN_HOME) // If scriplet jar ist not cached yet, build it if (!jarFile.isFile) { @@ -230,12 +218,9 @@ fun main(args: Array) { // }).forEach { it.delete() } - requireInPath("kotlinc") - - // create main-wrapper for kts scripts - val wrapperSrcArg = if (scriptFileExt == "kts") { + val wrapperFile = if (scriptFileExt == "kts") { val mainKotlin = File(createTempDir("kscript"), execClassName + ".kt") val classReference = (script.pckg ?: "") + className @@ -251,22 +236,12 @@ fun main(args: Array) { } } """.trimIndent()) - - "'${mainKotlin.absolutePath}'" - } else { - "" - } - - val scriptCompileResult = evalBash("kotlinc ${compilerOpts} ${optionalCpArg} -d '${jarFile.absolutePath}' '${scriptFile.absolutePath}' ${wrapperSrcArg}") - with(scriptCompileResult) { - errorIf(exitCode != 0) { "compilation of '$scriptResource' failed\n$stderr" } - } + mainKotlin + } else null + val sourceFiles = listOfNotNull(scriptFile, wrapperFile) + kotlinRunner.compile(compilerOpts, jarFile, sourceFiles, classpath) } - - // print the final command to be run by eval+exec - val joinedUserArgs = userArgs.map { "\"${it.replace("\"", "\\\"")}\"" }.joinToString(" ") - //if requested try to package the into a standalone binary if (docopt.getBoolean("package")) { val binaryName = if (File(scriptResource).run { canRead() && listOf("kts", "kt").contains(extension) }) { @@ -280,11 +255,16 @@ fun main(args: Array) { quit(0) } - var extClassPath = "${jarFile}${CP_SEPARATOR_CHAR}${KOTLIN_HOME}${File.separatorChar}lib${File.separatorChar}kotlin-script-runtime.jar" - if (classpath.isNotEmpty()) - extClassPath += kscript.app.CP_SEPARATOR_CHAR + classpath + // Optionally enter interactive mode + if (docopt.getBoolean("interactive")) { + System.err.println("Creating REPL from ${scriptFile}") + kotlinRunner.interactiveShell(jarFile, classpath, compilerOpts) + exitProcess(0) + } + + val scriptClassPath = listOf(jarFile, joinToPathString(KOTLIN_HOME, "lib", "kotlin-script-runtime.jar"), classpath).joinToString(CP_SEPARATOR_CHAR) - println("kotlin ${kotlinOpts} -classpath \"${extClassPath}\" ${execClassName} ${joinedUserArgs} ") + exitProcess(kotlinRunner.runScript(scriptClassPath, execClassName, userArgs)) } diff --git a/test/test_suite.sh b/test/test_suite.sh index 0a4641c6..c1ced977 100755 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -133,10 +133,10 @@ export -f resolve_deps assert_stderr "resolve_deps log4j:log4j:1.2.14" "${HOME}/.m2/repository/log4j/log4j/1.2.14/log4j-1.2.14.jar" ## impossible version -assert "resolve_deps log4j:log4j:9.8.76" "false" +assert_raises "resolve_deps log4j:log4j:9.8.76" 1 ## wrong format should exit with 1 -assert "resolve_deps log4j:1.0" "false" +assert_raises "resolve_deps log4j:1.0" 1 assert_stderr "resolve_deps log4j:1.0" "[ERROR] Invalid dependency locator: 'log4j:1.0'. Expected format is groupId:artifactId:version[:classifier][@type]" @@ -267,14 +267,17 @@ assert "kscript ${KSCRIPT_HOME}/test/resources/kotlin_opts_test.kts" "mem_477259 assert_raises 'kscript "println(org.docopt.Docopt::class)"' 1 -kscript_nocall() { kotlin -classpath ${KSCRIPT_HOME}/build/libs/kscript.jar kscript.app.KscriptKt "$@";} -export -f kscript_nocall +## +## --idea tests were commented out as they do not make sense anymore -## temp projects with include symlinks -assert_raises 'tmpDir=$(kscript_nocall --idea test/resources/includes/include_variations.kts | cut -f2 -d" " | xargs echo); cd $tmpDir && gradle build' 0 +# kscript_nocall() { kotlin -classpath ${KSCRIPT_HOME}/build/libs/kscript.jar kscript.app.KscriptKt "$@";} +# export -f kscript_nocall -## support diamond-shaped include schemes (see #133) -assert_raises 'tmpDir=$(kscript_nocall --idea test/resources/includes/diamond.kts | cut -f2 -d" " | xargs echo); cd $tmpDir && gradle build' 0 +# ## temp projects with include symlinks +# assert_raises 'tmpDir=$(kscript_nocall --idea test/resources/includes/include_variations.kts | cut -f2 -d" " | xargs echo); cd $tmpDir && gradle build' 0 + +# ## support diamond-shaped include schemes (see #133) +# assert_raises 'tmpDir=$(kscript_nocall --idea test/resources/includes/diamond.kts | cut -f2 -d" " | xargs echo); cd $tmpDir && gradle build' 0 ## todo reenable interactive mode tests using kscript_nocall