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
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ install:
# - bash -c 'source "/home/travis/.sdkman/bin/sdkman-init.sh"; yes | sdk install java 8u144-zulu'
# - sudo apt-get install -y openjdk-8-jdk
- sdk install kotlin
- sdk install gradle
- sdk install gradle 4.10.2

- export KSCRIPT_HOME=${TRAVIS_BUILD_DIR}
- export PATH=${KSCRIPT_HOME}/build/libs:$PATH
Expand Down
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ dependencies {

testCompile("junit:junit:4.12")
testCompile( "io.kotlintest:kotlintest:2.0.7")
testImplementation("io.mockk:mockk:1.8.13")
testCompile(kotlin("script-runtime"))
}

Expand Down
40 changes: 35 additions & 5 deletions src/main/kotlin/kscript/app/AppHelpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,25 @@ data class ProcessResult(val command: String, val exitCode: Int, val stdout: Str
}
}

fun isWindows (): Boolean {
return System.getProperty("os.name").toLowerCase().startsWith("windows")
}

fun isCygwinOrMingw() : Boolean {
val result = evalCommand("uname")
// `uname` will fail on Windows cmd returning 1 as exitCode
return result.exitCode == 0 && (result.stdout.startsWith("CYGWIN") || result.stdout.startsWith("MINGW"))
}

fun evalCommand(cmd: String, wd: File? = null,
stdoutConsumer: Consumer<String> = StringBuilderConsumer(),
stderrConsumer: Consumer<String> = StringBuilderConsumer()): ProcessResult {

val (shell, shellArg0) = if (isWindows()) Pair("cmd", "/C") else Pair("bash", "-c")
return runProcess(shell, shellArg0, cmd,
wd = wd, stderrConsumer = stderrConsumer, stdoutConsumer = stdoutConsumer)
}

fun evalBash(cmd: String, wd: File? = null,
stdoutConsumer: Consumer<String> = StringBuilderConsumer(),
stderrConsumer: Consumer<String> = StringBuilderConsumer()): ProcessResult {
Expand Down Expand Up @@ -89,7 +108,7 @@ internal open class StringBuilderConsumer : Consumer<String> {

object ShellUtils {

fun isInPath(tool: String) = evalBash("which $tool").stdout.trim().isNotBlank()
fun isInPath(tool: String) = evalCommand( (if (isWindows()) "where" else "which") + " $tool").stdout.trim().isNotBlank()

fun requireInPath(tool: String, msg: String = "$tool is not in PATH") = errorIf(!isInPath(tool)) { msg }

Expand Down Expand Up @@ -122,9 +141,19 @@ fun quit(status: Int): Nothing {

/** see discussion on https://github.com/holgerbrandl/kscript/issues/15*/
fun guessKotlinHome(): String? {
return evalBash("KOTLIN_RUNNER=1 JAVACMD=echo kotlinc").stdout.run {
"kotlin.home=([^\\s]*)".toRegex()
.find(this)?.groups?.get(1)?.value
if (isWindows()) {
return evalCommand("where kotlinc").run {
if (exitCode == 1)
null
else
File(stdout.split("\n")[0].trim())
.parentFile.parent
}
} else {
return evalBash("KOTLIN_RUNNER=1 JAVACMD=echo kotlinc").stdout.run {
"kotlin.home=([^\\s]*)".toRegex()
.find(this)?.groups?.get(1)?.value
}
}
}

Expand Down Expand Up @@ -352,7 +381,8 @@ exec java -jar ${'$'}0 "${'$'}@"

File(tmpProjectDir, "build.gradle").writeText(gradleScript)

val pckgResult = evalBash("cd '${tmpProjectDir}' && gradle simpleCapsule")
// wrap chained commands (&&) with additional " for Windows
val pckgResult = evalCommand("gradle --no-daemon -p \"${tmpProjectDir}\" simpleCapsule")

with(pckgResult) {
kscript.app.errorIf(exitCode != 0) { "packaging of '$appName' failed:\n$pckgResult" }
Expand Down
7 changes: 3 additions & 4 deletions 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 (isWindows()) ";" else ":"


fun resolveDependencies(depIds: List<String>, customRepos: List<MavenRepo> = emptyList(), loggingEnabled: Boolean): String? {
Expand All @@ -34,12 +34,11 @@ fun resolveDependencies(depIds: List<String>, customRepos: List<MavenRepo> = emp
if (cache.containsKey(depsHash)) {
val cachedCP = cache.get(depsHash)!!


// Make sure that local dependencies have not been wiped since resolving them (like by deleting .m2) (see #146)
if (cachedCP.split(":").all { File(it).exists() }) {
if (cachedCP.split(CP_SEPARATOR_CHAR).all { File(it).exists() }) {
return cachedCP
} else {
System.err.println("[kscript] Detected missing dependencies in cache.")
System.err.println("[kscript] Detected missing dependencies in cache")
}
}
}
Expand Down
26 changes: 18 additions & 8 deletions src/main/kotlin/kscript/app/Kscript.kt
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Website : https://github.com/holgerbrandl/kscript
// see https://stackoverflow.com/questions/585534/what-is-the-best-way-to-find-the-users-home-directory-in-java
// See #146 "allow kscript cache dir to be configurable" for details
val KSCRIPT_CACHE_DIR = System.getenv("KSCRIPT_CACHE_DIR")?.let { File(it) }
?: File(System.getProperty("user.home")!!, ".kscript")
?: File(System.getProperty("user.home")!!, ".kscript")

// use lazy here prevent empty dirs for regular scripts https://github.com/holgerbrandl/kscript/issues/130
val SCRIPT_TEMP_DIR by lazy { createTempDir() }
Expand Down Expand Up @@ -83,9 +83,7 @@ fun main(args: Array<String>) {

// optionally clear up the jar cache
if (docopt.getBoolean("clear-cache")) {
info("Cleaning up cache...")
KSCRIPT_CACHE_DIR.listFiles().forEach { it.delete() }
// evalBash("rm -f ${KSCRIPT_CACHE_DIR}/*")
clearCache()
quit(0)
}

Expand Down Expand Up @@ -146,7 +144,7 @@ fun main(args: Array<String>) {


val classpath = resolveDependencies(dependencies, customRepos, loggingEnabled) ?: ""
val optionalCpArg = if (classpath.isNotEmpty()) "-classpath '${classpath}'" else ""
val optionalCpArg = if (classpath.isNotEmpty()) """-classpath "${classpath}"""" else ""

// Extract kotlin arguments
val kotlinOpts = script.collectRuntimeOptions()
Expand Down Expand Up @@ -242,12 +240,19 @@ fun main(args: Array<String>) {
}
""".trimIndent())

"'${mainKotlin.absolutePath}'"
"${mainKotlin.absolutePath}"
} else {
""
}

val scriptCompileResult = evalBash("kotlinc ${compilerOpts} ${optionalCpArg} -d '${jarFile.absolutePath}' '${scriptFile.absolutePath}' ${wrapperSrcArg}")
val cmd= StringBuilder()
.append("kotlinc ${compilerOpts} ${optionalCpArg} -d \"${jarFile.absolutePath}\" \"${scriptFile.absolutePath}\"")
.apply {
if (wrapperSrcArg.isNotEmpty())
this.append(" \"${wrapperSrcArg}\"")
}
.toString()
val scriptCompileResult = evalCommand(cmd)
with(scriptCompileResult) {
errorIf(exitCode != 0) { "compilation of '$scriptResource' failed\n$stderr" }
}
Expand All @@ -274,9 +279,14 @@ fun main(args: Array<String>) {
if (classpath.isNotEmpty())
extClassPath += kscript.app.CP_SEPARATOR_CHAR + classpath

println("kotlin ${kotlinOpts} -classpath \"${extClassPath}\" ${execClassName} ${joinedUserArgs} ")
println("kotlin ${kotlinOpts} -classpath \"${extClassPath}\" ${execClassName} ${joinedUserArgs}")
}

fun clearCache() {
info("Cleaning up cache...")
KSCRIPT_CACHE_DIR.listFiles().forEach { it.delete() }
// evalBash("rm -f ${KSCRIPT_CACHE_DIR}/*")
}

/** Determine the latest version by checking github repo and print info if newer version is available. */
private fun versionCheck() {
Expand Down
6 changes: 5 additions & 1 deletion src/main/kotlin/kscript/app/ResolveIncludes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ fun resolveIncludes(template: File, includeContext: URI = template.parentFile.to
isUrl(include) -> URL(include)
include.startsWith("/") -> File(include).toURI().toURL()
include.startsWith("~/") -> File(System.getenv("HOME")!! + include.substring(1)).toURI().toURL()
else -> includeContext.resolve(URI(include.removePrefix("./"))).toURL()
else -> {
val includeFile = if (File(include).isAbsolute) { File(include) } else File(template.parent, include)
// TODO is `resolve` really necessary?
includeContext.resolve(includeFile.toURI()).toURL()
}
}

// test if include was processed already (aka include duplication, see #151)
Expand Down
191 changes: 191 additions & 0 deletions src/test/kotlin/KscriptTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import io.kotlintest.matchers.shouldBe
import io.kotlintest.matchers.shouldNotBe
import io.mockk.every
import io.mockk.mockkStatic
import kscript.app.*
import org.junit.Assume.assumeFalse
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import java.io.ByteArrayOutputStream
import java.io.PrintStream

// We can assert messages thrown during pre-processing but not the script result, which is currently in a separate process
class KscriptTest {

@Before
fun before() {
clearCache()
}

@Test
fun `should run a piped script`() {
assumeTrue(kotlinIsAvailableInPath())

main(arrayOf("""println("kotlin rocks")"""))
}

@Test
fun `should run a script with only println`() {
assumeTrue(kotlinIsAvailableInPath())
// setup
val (originalErr, newErr) = captureStdErr()
// when
main(arrayOf("test/resources/dot.Test.kts"))
// then
newErr.toString().lines() shouldNotBe listOf(
"[kscript] Resolving dependencies...",
""
)
// clean up
restoreStdErr(originalErr)
}

@Test
fun `should package a piped script`() {
assumeTrue(kotlinIsAvailableInPath())
assumeTrue(gradleIsAvailableInPath())

val result: Int = exitStatus {
main(arrayOf("--package", """println("kotlin rocks")"""))
}

result shouldBe 0
}

@Test
fun `should run a script with multiple dependencies as comments`() {
assumeTrue(kotlinIsAvailableInPath())
// setup
val (originalErr, newErr) = captureStdErr()
// when
main(arrayOf("test/resources/multi_line_deps.kts"))
// then
newErr.toString().lines() shouldBe listOf(
"[kscript] Resolving dependencies...",
"[kscript] Resolving com.offbytwo:docopt:0.6.0.20150202...Done",
"[kscript] Resolving log4j:log4j:1.2.14...Done",
"[kscript] Dependencies resolved",
""
)
// clean up
restoreStdErr(originalErr)
}

@Test
fun `should run a script with dependencies as annotations`() {
assumeTrue(kotlinIsAvailableInPath())
// setup
val (originalErr, newErr) = captureStdErr()
// when
main(arrayOf("test/resources/depends_on_with_type.kts"))
// then
println(newErr)
newErr.toString().lines() shouldBe listOf(
"[kscript] Resolving dependencies...",
"[kscript] Resolving org.javamoney:moneta:1.3@pom...Done",
"[kscript] Resolving com.github.holgerbrandl:kscript-annotations:1.2...Done",
"[kscript] Dependencies resolved",
""
)
// clean up
restoreStdErr(originalErr)
}

@Test
fun `should run a script with support API`() {
assumeTrue(kotlinIsAvailableInPath())
// setup
val (originalErr, newErr) = captureStdErr()
// when
main(arrayOf("-t", """println("hello")"""))
// then
newErr.toString().lines() shouldBe listOf(
"[kscript] Resolving dependencies...",
"[kscript] Resolving com.github.holgerbrandl:kscript-support:1.2.5...Done",
"[kscript] Dependencies resolved",
""
)
// clean up
restoreStdErr(originalErr)
}

@Test
fun `should run a script from an url`() {
assumeTrue(kotlinIsAvailableInPath())
// setup
val (originalErr, newErr) = captureStdErr()
// when
main(arrayOf("https://raw.githubusercontent.com/holgerbrandl/kscript/master/test/resources/url_test.kts"))
// then
newErr.toString().trim().length shouldBe 0
// clean up
restoreStdErr(originalErr)
}

@Test
fun `should resolve dependencies from script from an url`() {
assumeTrue(kotlinIsAvailableInPath())
// setup
val (originalErr, newErr) = captureStdErr()
// when
main(arrayOf("https://git.io/fxHBv"))
// then
newErr.toString().lines() shouldBe listOf(
"[kscript] Resolving dependencies...",
"[kscript] Resolving log4j:log4j:1.2.14...Done",
"[kscript] Dependencies resolved",
""
)
// clean up
restoreStdErr(originalErr)
}

@Test
fun `should exit if kotlinc is not in PATH`() {
assumeFalse(kotlinIsAvailableInPath())

val result = exitStatus {
main(arrayOf("println(\"kotlin rocks\")"))
}

result shouldBe 1
}

private fun kotlinIsAvailableInPath() = guessKotlinHome() != null

private fun gradleIsAvailableInPath() = evalCommand("where gradle").exitCode != 1

private fun captureStdErr(): Pair<PrintStream, ByteArrayOutputStream> {
val originalErr = System.err
val newErr = ByteArrayOutputStream()
restoreStdErr(PrintStream(newErr))
return Pair(originalErr, newErr)
}

private fun restoreStdErr(originalErr: PrintStream) {
System.setErr(originalErr)
}

private fun exitStatus(arg: () -> Unit): Int {
mockkStatic("kscript.app.AppHelpersKt")
// workaround for `Nothing` methods: https://github.com/mockk/mockk/issues/187
every {
quit(1)
} answers {
throw RuntimeException("1")
}
every {
quit(0)
} answers {
throw RuntimeException("0")
}

try {
arg.invoke()
} catch (e: RuntimeException) {
return e.message?.toInt() ?: 0
}
return 0
}
}
Loading