Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 49 additions & 5 deletions src/kscript
Original file line number Diff line number Diff line change
Expand Up @@ -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[@]}"
10 changes: 7 additions & 3 deletions src/main/kotlin/kscript/app/AppHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> = if (args.isEmpty()) listOf() else args.split("\\s+".toRegex())

data class ProcessResult(val command: String, val exitCode: Int, val stdout: String, val stderr: String) {

Expand Down Expand Up @@ -116,7 +121,6 @@ fun errorIf(value: Boolean, lazyMessage: () -> Any) {
}

fun quit(status: Int): Nothing {
print(if (status == 0) "true" else "false")
exitProcess(status)
}

Expand Down Expand Up @@ -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<String>, customRepos: List<MavenRepo>, runtimeOptions: String, appName: String) {
fun packageKscript(scriptJar: File, wrapperClassName: String, dependencies: List<String>, customRepos: List<MavenRepo>, runtimeOptions: List<String>, appName: String) {
requireInPath("gradle", "gradle is required to package kscripts")

infoMsg("Packaging script '$appName' into standalone executable...")
Expand All @@ -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 + '"' }
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/kscript/app/DependencyUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<String>, customRepos: List<MavenRepo> = emptyList(), loggingEnabled: Boolean): String? {
Expand Down
14 changes: 14 additions & 0 deletions src/main/kotlin/kscript/app/JarFileLoader.kt
Original file line number Diff line number Diff line change
@@ -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<URL> = arrayOf()) : URLClassLoader(urls) {
fun addFile(file: File) {
addURL(file.toURI().toURL())
}
}

61 changes: 61 additions & 0 deletions src/main/kotlin/kscript/app/KotlinRunner.kt
Original file line number Diff line number Diff line change
@@ -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<String>::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<String>::class.java)
}

fun compile(compilerOpts: List<String>, targetJarFile: File, sourceFiles: List<File>, 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<String>): Int {
val runnerArgs = listOf("-cp", scriptClassPath, execClassName) + userArgs
runKotlin(runnerArgs)
return 0
}

fun interactiveShell(jarFile: File, classpath: String?, compilerOpts: List<String>) {
val jarPath = if (jarFile.isFile) jarFile.absolutePath else null
val args = compilerOpts + classPathArgs(classpath, jarPath)
runKotlinc(args)
}

private fun classPathArgs(vararg paths: String?): List<String> {
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<String>) {
preloaderMainMethod.invoke(jarFileLoader, (compilerBaseArgs + args).toTypedArray())
}

private fun runKotlin(args: Collection<String>) {
runnerMainMethod.invoke(jarFileLoader, args.toTypedArray())
}
}

66 changes: 23 additions & 43 deletions src/main/kotlin/kscript/app/Kscript.kt
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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<arg> -J is stripped and <arg> 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
Expand Down Expand Up @@ -150,28 +150,16 @@ fun main(args: Array<String>) {

// 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)
Expand Down Expand Up @@ -211,12 +199,12 @@ fun main(args: Array<String>) {


// 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) {
Expand All @@ -230,12 +218,9 @@ fun main(args: Array<String>) {
// }).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
Expand All @@ -251,22 +236,12 @@ fun main(args: Array<String>) {
}
}
""".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) }) {
Expand All @@ -280,11 +255,16 @@ fun main(args: Array<String>) {
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))
}


Expand Down
7 changes: 7 additions & 0 deletions test/resources/kotlin_opts_test.kts
Original file line number Diff line number Diff line change
@@ -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")}")
25 changes: 17 additions & 8 deletions test/test_suite.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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]"

Expand Down Expand Up @@ -260,15 +260,24 @@ 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
## kscript.jar class access test
assert_raises 'kscript "println(org.docopt.Docopt::class)"' 1

## 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
##
## --idea tests were commented out as they do not make sense anymore

# kscript_nocall() { kotlin -classpath ${KSCRIPT_HOME}/build/libs/kscript.jar kscript.app.KscriptKt "$@";}
# export -f kscript_nocall

# ## 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

Expand Down