diff --git a/kscript b/kscript index e2bcec94..ce8930c6 100755 --- a/kscript +++ b/kscript @@ -22,7 +22,47 @@ 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 +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 -exec $(kotlin -classpath ${jarPath} kscript.app.KscriptKt "$@") +# from 'kotlinc' + +[ -n "$JAVA_OPTS" ] || JAVA_OPTS="-Xmx256M -Xms32M" + +declare -a java_args +declare -a kscript_args + +while [ $# -gt 0 ]; do + case "$1" in + -D*) + java_args=("${java_args[@]}" "$1") + shift + ;; + -J*) + java_args=("${java_args[@]}" "$1") + shift + ;; + *) + kscript_args=("${kscript_args[@]}" "$1") + shift + ;; + esac +done + +followupFile=followup-$$ + +## run it +FOLLOWUP_FILE=$followupFile kotlin "${java_args[@]}" -classpath ${jarPath} kscript.app.KscriptKt "${kscript_args[@]}" +result=$? + +followupPath=${HOME}/.kscript/${followupFile} + +if [[ -f $followupPath ]]; then + cmd=$(cat $followupPath) + "${RMCMD:=rm}" -f $followupPath + exec $cmd +else + exit $result +fi diff --git a/src/main/kotlin/kscript/app/AppHelpers.kt b/src/main/kotlin/kscript/app/AppHelpers.kt index eb25db53..a04f8594 100644 --- a/src/main/kotlin/kscript/app/AppHelpers.kt +++ b/src/main/kotlin/kscript/app/AppHelpers.kt @@ -109,7 +109,6 @@ fun errorIf(value: Boolean, lazyMessage: () -> Any) { } fun quit(status: Int): Nothing { - print(if (status == 0) "true" else "false") exitProcess(status) } 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/Kotlinc.kt b/src/main/kotlin/kscript/app/Kotlinc.kt new file mode 100644 index 00000000..d66e75c4 --- /dev/null +++ b/src/main/kotlin/kscript/app/Kotlinc.kt @@ -0,0 +1,39 @@ +package kscript.app + +import java.io.File + +class Kotlinc(val KOTLIN_HOME: String) { + val cl = JarFileLoader() + val preloaderJar = File("${KOTLIN_HOME}${File.separatorChar}lib${File.separatorChar}kotlin-preloader.jar") + init { + cl.addFile(preloaderJar) + } + val baseArgs = listOf("-cp", "${KOTLIN_HOME}${File.separatorChar}lib${File.separatorChar}kotlin-compiler.jar", + "org.jetbrains.kotlin.cli.jvm.K2JVMCompiler") + val preloaderMain = cl.loadClass("org.jetbrains.kotlin.preloading.Preloader").getDeclaredMethod("main", Array::class.java) + + 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() + } + + fun runKotlinc(args: Collection) { + preloaderMain.invoke(cl, (baseArgs + args).toTypedArray()) + } + + fun compile(jarFile: File, scriptFile: File, wrapperFile: File?, classpath: String?) { + val baseArgs = listOf("-Xskip-runtime-version-check", + "-d", jarFile.absolutePath, scriptFile.absolutePath) + val wrapperArgs = listOfNotNull(wrapperFile?.absolutePath) + val cpArgs = classPathArgs(classpath) + runKotlinc(baseArgs + wrapperArgs + cpArgs) + } + + fun interactiveShell(jarFile: File, classpath: String?) { + val jarPath = if (jarFile.isFile) jarFile.absolutePath else null + val args = classPathArgs(classpath, jarPath) + runKotlinc(args) + } +} diff --git a/src/main/kotlin/kscript/app/Kscript.kt b/src/main/kotlin/kscript/app/Kscript.kt index e9f5b968..1365a69e 100644 --- a/src/main/kotlin/kscript/app/Kscript.kt +++ b/src/main/kotlin/kscript/app/Kscript.kt @@ -41,6 +41,7 @@ Options: -t --text Enable stdin support API for more streamlined text processing --idea Open script in temporary Intellij session -s --silent Suppress status logging to stderr + -J -J is stripped and passed to Java as-is Copyright : 2017 Holger Brandl License : MIT @@ -51,7 +52,17 @@ Website : https://github.com/holgerbrandl/kscript val KSCRIPT_CACHE_DIR = File(System.getenv("HOME")!!, ".kscript") val SCRIPT_TEMP_DIR = createTempDir() +val FOLLOWUP_FILE = run { + val fileName = System.getenv("FOLLOWUP_FILE") + if (fileName == null) null + else + File(KSCRIPT_CACHE_DIR, fileName) +} + + fun main(args: Array) { + FOLLOWUP_FILE?.let { if (it.isFile) it.delete() } + // skip org.docopt for version and help to allow for lazy version-check if (args.size == 1 && listOf("--help", "-h", "--version", "-v").contains(args[0])) { info(USAGE) @@ -83,28 +94,7 @@ fun main(args: Array) { // optionally self-update kscript ot the newest version // (if not local copy is not being maintained by sdkman) if (docopt.getBoolean(("self-update"))) { - if (true || evalBash("which kscript | grep .sdkman").stdout.isNotBlank()) { - info("Installing latest version of kscript...") - // println("sdkman_auto_answer=true && sdk install kscript") - - // create update script - val updateScript = File(KSCRIPT_CACHE_DIR, "self_update.sh").apply { - writeText(""" - #!/usr/bin/env bash - export SDKMAN_DIR="${"$"}{HOME}/.sdkman" - source "${"$"}{SDKMAN_DIR}/bin/sdkman-init.sh" - sdkman_auto_answer=true && sdk install kscript - """.trimIndent()) - setExecutable(true) - } - - println(updateScript.absolutePath) - } else { - info("Self-update is currently just supported via sdkman.") - info("Please download a new release from https://github.com/holgerbrandl/kscript") - // todo port sdkman-indpendent self-update - } - + selfUpdate() quit(0) } @@ -123,7 +113,7 @@ fun main(args: Array) { // Create temopary dev environment if (docopt.getBoolean("idea")) { - println(launchIdeaWithKscriptlet(scriptFile, dependencies, customRepos)) + evalBash(launchIdeaWithKscriptlet(scriptFile, dependencies, customRepos)) exitProcess(0) } @@ -133,18 +123,6 @@ fun main(args: Array) { // Extract kotlin arguments val kotlinOpts = script.collectRuntimeOptions() - - // Optionally enter interactive mode - if (docopt.getBoolean("interactive")) { - System.err.println("Creating REPL from ${scriptFile}") - // System.err.println("kotlinc ${kotlinOpts} -classpath '${classpath}'") - - val optionalCP = if (classpath != null && classpath.isNotEmpty()) "-classpath ${classpath}" else "" - println("kotlinc ${kotlinOpts} ${optionalCP}") - - exitProcess(0) - } - val scriptFileExt = scriptFile.extension val scriptCheckSum = md5(scriptFile) @@ -187,6 +165,8 @@ fun main(args: Array) { } + val kotlinc: Kotlinc by lazy { Kotlinc(KOTLIN_HOME!!)} + // If scriplet jar ist not cached yet, build it if (!jarFile.isFile) { // disabled logging because it seems too much @@ -199,12 +179,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 @@ -220,23 +197,69 @@ fun main(args: Array) { } } """.trimIndent()) + mainKotlin + } else null - "'${mainKotlin.absolutePath}'" - } else { - "" - } + kotlinc.compile(jarFile, scriptFile, wrapperFile, classpath) + } - val scriptCompileResult = evalBash("kotlinc -classpath '$classpath' -d '${jarFile.absolutePath}' '${scriptFile.absolutePath}' ${wrapperSrcArg}") - with(scriptCompileResult) { - errorIf(exitCode != 0) { "compilation of '$scriptResource' failed\n$stderr" } + // Optionally enter interactive mode + if (docopt.getBoolean("interactive")) { + System.err.println("Creating REPL from ${scriptFile}") + // System.err.println("kotlinc ${kotlinOpts} -classpath '${classpath}'") + + if (kotlinOpts.isNotEmpty()) { + val optionalCP = if (classpath != null && classpath.isNotEmpty()) "-classpath ${classpath}" else "" + writeToFollowUpFile("kotlinc ${kotlinOpts} ${optionalCP}") + exitProcess(0) } + kotlinc.interactiveShell(jarFile, classpath) + exitProcess(0) + } + + val scriptClassPath = "${jarFile}${CP_SEPARATOR_CHAR}${KOTLIN_HOME}${File.separatorChar}lib${File.separatorChar}kotlin-script-runtime.jar${CP_SEPARATOR_CHAR}${classpath}" + if (kotlinOpts.isNotEmpty()) { + // print the final command to be run by exec + val joinedUserArgs = userArgs.joinToString(" ") + writeToFollowUpFile("kotlin ${kotlinOpts} -classpath ${scriptClassPath} ${execClassName} ${joinedUserArgs} ") + exitProcess(0) } + // run the main method + val cl = JarFileLoader() + val runnerMain = cl.loadClass("org.jetbrains.kotlin.runner.Main").getDeclaredMethod("main", Array::class.java) + val runnerArgs = arrayOf("-cp", scriptClassPath, execClassName, *userArgs.toTypedArray()) + runnerMain.invoke(cl, runnerArgs) +} +fun writeToFollowUpFile(content: String) { + errorIf(FOLLOWUP_FILE == null) { + "Cannot determine FOLLOWUP_FILE" + } + FOLLOWUP_FILE!!.writeText(content) +} - // print the final command to be run by exec - val joinedUserArgs = userArgs.joinToString(" ") +private fun selfUpdate() { + if (true || evalBash("which kscript | grep .sdkman").stdout.isNotBlank()) { + info("Installing latest version of kscript...") + // println("sdkman_auto_answer=true && sdk install kscript") - println("kotlin ${kotlinOpts} -classpath ${jarFile}${CP_SEPARATOR_CHAR}${KOTLIN_HOME}${File.separatorChar}lib${File.separatorChar}kotlin-script-runtime.jar${CP_SEPARATOR_CHAR}${classpath} ${execClassName} ${joinedUserArgs} ") + // create update script + val updateScript = File(KSCRIPT_CACHE_DIR, "self_update.sh").apply { + writeText(""" + #!/usr/bin/env bash + export SDKMAN_DIR="${"$"}{HOME}/.sdkman" + source "${"$"}{SDKMAN_DIR}/bin/sdkman-init.sh" + sdkman_auto_answer=true && sdk install kscript + """.trimIndent()) + setExecutable(true) + } + + evalBash(updateScript.absolutePath) + } else { + info("Self-update is currently just supported via sdkman.") + info("Please download a new release from https://github.com/holgerbrandl/kscript") + // todo port sdkman-indpendent self-update + } } diff --git a/test/test_suite.sh b/test/test_suite.sh index 62b681dd..b13d735a 100755 --- a/test/test_suite.sh +++ b/test/test_suite.sh @@ -122,10 +122,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]"