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
9 changes: 7 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
plugins {
kotlin("jvm") version "1.2.70"
id("com.github.johnrengelman.shadow") version "2.0.4"
application
}

application {
mainClassName = "kscript.app.KscriptKt"
}

group = "com.github.holgerbrandl.kscript.launcher"

dependencies {
compileOnly("org.jetbrains.kotlin:kotlin-stdlib")
compile("org.jetbrains.kotlin:kotlin-stdlib")

compile("com.offbytwo:docopt:0.6.0.20150202")

Expand Down Expand Up @@ -50,4 +55,4 @@ val jar by tasks.getting {
// Build shadowJar when
val assemble by tasks.getting {
dependsOn(shadowJar)
}
}
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-4.7-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
37 changes: 31 additions & 6 deletions src/kscript
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,38 @@ 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"

## run it using command substitution to have just the user process once kscript is done
eval "exec $(${KOTLIN_EXEC} -classpath ${jarPath} kscript.app.KscriptKt "$@")"
if [[ $(uname) == CYGWIN* ]]; then
jarPath=$(cygpath -w ${jarPath})
[[ ! -z $KOTLIN_HOME ]] && KOTLIN_HOME=$(cygpath -w ${KOTLIN_HOME})
fi

# 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:2}")
shift
;;
*)
kscript_args=("${kscript_args[@]}" "$1")
shift
;;
esac
done

## run it
exec ${JAVACMD:=java} "${java_args[@]}" -jar ${jarPath} "${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())
}
}

103 changes: 103 additions & 0 deletions src/main/kotlin/kscript/app/KotlinRunner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
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>, kotlinOpts: List<String>): Int {
val runnerArgs = listOf("-cp", scriptClassPath, execClassName) + userArgs
return if (kotlinOpts.isNotEmpty()) {
execKotlin(kotlinOpts, runnerArgs)
} else {
runKotlin(runnerArgs)
// it would be unreachable if the script calls exitProcess()
0
}
}

fun interactiveShell(jarFile: File, classpath: String?, compilerOpts: List<String>, kotlinOpts: List<String>) {
val jarPath = if (jarFile.isFile) jarFile.absolutePath else null
val args = compilerOpts + classPathArgs(classpath, jarPath)
if (kotlinOpts.isNotEmpty()) {
execKotlinc(kotlinOpts, args)
} else {
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 execKotlinc(kotlinOpts: List<String>, args: List<String>): Int {
return if (IS_WINDOWS) {
execJava(kotlinOpts, compilerBaseArgs, args)
} else {
exec(listOf("kotlinc") + kotlinOpts + args)
}
}

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

private fun execKotlin(kotlinOpts: List<String>, args: List<String>): Int {
return if (IS_WINDOWS) {
val runnerJar = joinToPathString(kotlinHome, "lib", "kotlin-runner.jar")
val kotlinAppArgs = listOf("-Dkotlin.home=$kotlinHome", "-cp", runnerJar, RUNNER_MAIN_CLASS)
execJava(kotlinOpts, kotlinAppArgs, args)
} else {
exec(listOf("kotlin") + kotlinOpts + args)
}
}

private fun execJava(kotlinOpts: List<String>, kotlinAppArgs: List<String>, args: List<String>): Int {
val (javaOpts, pureKotlinOpts) = kotlinOpts.partition { it.startsWith("-J") || it.startsWith("-D") }
val pureJavaOpts = javaOpts.map { if (it.startsWith("-J")) it.substring(2) else it }
val javaCmd = System.getenv("JAVACMD") ?: "java"
return exec(listOf(javaCmd) + pureJavaOpts + kotlinAppArgs + pureKotlinOpts + args)
}

private fun exec(commandList: List<String>) =
ProcessBuilder(commandList)
.inheritIO()
.apply { environment()["KOTLIN_RUNNER"] = "" }
.start()
.waitFor()
}

Loading