Skip to content

Commit c551db6

Browse files
committed
Add simple-main-kts project
1 parent d20f4fa commit c551db6

21 files changed

+713
-0
lines changed

Diff for: ReadMe.md

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ when needed
2020
- [Simple script definition](jvm/basic/jvm-simple-script/SimpleScript.md)
2121
- [Script with Dynamic dependencies from Maven](jvm/basic/jvm-maven-deps/MavenDeps.md)
2222
- [Scripting Host with Kotlin Compiler Embeddable](jvm/basic/jvm-embeddable-host/EmbeddableCompiler.md)
23+
- [Simplified main-kts](jvm/simple-main-kts/SimpleMainKts.md)
2324

2425
## External examples
2526

Diff for: jvm/simple-main-kts/SimpleMainKts.md

+29
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
# Kotlin Scripting Examples: Simplified main-kts
3+
4+
This is a simplified version of the `main-kts` script support, distributed with the Kotlin compiler.
5+
6+
*Simplifications mostly related to the removal of the JSR-223 support, since it uses non-public API. In the future,
7+
when a public API will become available, the support could be added back. Also the packaging into a singe jar is not
8+
implemented.*
9+
10+
## Description
11+
12+
The purpose of the `simple-main-kts` (as well as the original `main-kts`) is to allow writing simple but extendable
13+
utility scripts for the usage in the command line, replacing the simple Kotlin programs with `main` function (hence
14+
the `main` in its name).
15+
16+
The script definition implementation demonstrates many aspects of the script definition functionality and
17+
scripting API usage, in particular:
18+
- refinement of the compilation configuration with the parameters passed to the annotations
19+
- dynamic script dependencies resolved via ivy library
20+
- import scripts with possibility of sharing instances (diamond-shaped import)
21+
- compilation cache, where cached compiled script are saved as executable jars
22+
- discovery file configuration, that could be used with IntelliJ IDEA and command-line compiler
23+
24+
and others.
25+
26+
The original implementation of the `kotlin-main-kts` contains all the dependencies in the single jar, that simplify
27+
the usage in the command line. This simplified version does not implement it, therefore all dependencies should be
28+
specified explicitly in the command line, to use it with the `kotlinc`.
29+
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
plugins {
3+
kotlin("jvm")
4+
}
5+
6+
val kotlinVersion: String by rootProject.extra
7+
8+
dependencies {
9+
testImplementation(project(":jvm:simple-main-kts:simple-main-kts"))
10+
testImplementation("org.jetbrains.kotlin:kotlin-scripting-jvm-host:$kotlinVersion")
11+
testImplementation("junit:junit:4.12")
12+
}
13+
14+
sourceSets {
15+
main {}
16+
}
17+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
/*
2+
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
3+
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
4+
*/
5+
package org.jetbrains.kotlin.script.examples.simpleMainKts.test
6+
7+
import org.jetbrains.kotlin.script.examples.simpleMainKts.COMPILED_SCRIPTS_CACHE_DIR_PROPERTY
8+
import org.jetbrains.kotlin.script.examples.simpleMainKts.SimpleMainKtsScript
9+
import org.junit.Assert
10+
import org.junit.Test
11+
import java.io.*
12+
import java.net.URLClassLoader
13+
import kotlin.script.experimental.api.*
14+
import kotlin.script.experimental.host.toScriptSource
15+
import kotlin.script.experimental.jvm.baseClassLoader
16+
import kotlin.script.experimental.jvm.jvm
17+
import kotlin.script.experimental.jvmhost.BasicJvmScriptingHost
18+
import kotlin.script.experimental.jvmhost.createJvmCompilationConfigurationFromTemplate
19+
20+
fun evalFile(scriptFile: File, cacheDir: File? = null): ResultWithDiagnostics<EvaluationResult> =
21+
withMainKtsCacheDir(cacheDir?.absolutePath ?: "") {
22+
val scriptDefinition = createJvmCompilationConfigurationFromTemplate<SimpleMainKtsScript>()
23+
24+
val evaluationEnv = ScriptEvaluationConfiguration {
25+
jvm {
26+
baseClassLoader(null)
27+
}
28+
constructorArgs(emptyArray<String>())
29+
enableScriptsInstancesSharing()
30+
}
31+
32+
BasicJvmScriptingHost().eval(scriptFile.toScriptSource(), scriptDefinition, evaluationEnv)
33+
}
34+
35+
36+
const val TEST_DATA_ROOT = "testData"
37+
38+
class SimpleMainKtsTest {
39+
40+
@Test
41+
fun testResolveJunit() {
42+
val res = evalFile(File("$TEST_DATA_ROOT/hello-resolve-junit.smain.kts"))
43+
assertSucceeded(res)
44+
}
45+
46+
@Test
47+
fun testResolveJunitDynamicVer() {
48+
val errRes = evalFile(File("$TEST_DATA_ROOT/hello-resolve-junit-dynver-error.smain.kts"))
49+
assertFailed("Unresolved reference: assertThrows", errRes)
50+
51+
val res = evalFile(File("$TEST_DATA_ROOT/hello-resolve-junit-dynver.smain.kts"))
52+
assertSucceeded(res)
53+
}
54+
55+
@Test
56+
fun testUnresolvedJunit() {
57+
val res = evalFile(File("$TEST_DATA_ROOT/hello-unresolved-junit.smain.kts"))
58+
assertFailed("Unresolved reference: junit", res)
59+
}
60+
61+
@Test
62+
fun testResolveError() {
63+
val res = evalFile(File("$TEST_DATA_ROOT/hello-resolve-error.smain.kts"))
64+
assertFailed("File 'abracadabra' not found", res)
65+
}
66+
67+
@Test
68+
fun testResolveLog4jAndDocopt() {
69+
val res = evalFile(File("$TEST_DATA_ROOT/resolve-log4j-and-docopt.smain.kts"))
70+
assertSucceeded(res)
71+
}
72+
73+
private val outFromImportTest = listOf("Hi from common", "Hi from middle", "sharedVar == 5")
74+
75+
@Test
76+
fun testImport() {
77+
78+
val out = captureOut {
79+
val res = evalFile(File("$TEST_DATA_ROOT/import-test.smain.kts"))
80+
assertSucceeded(res)
81+
}.lines()
82+
83+
Assert.assertEquals(outFromImportTest, out)
84+
}
85+
86+
@Test
87+
fun testCompilerOptions() {
88+
89+
val out = captureOut {
90+
val res = evalFile(File("$TEST_DATA_ROOT/compile-java6.smain.kts"))
91+
assertSucceeded(res)
92+
assertIsJava6Bytecode(res)
93+
}.lines()
94+
95+
Assert.assertEquals(listOf("Hi from sub", "Hi from super", "Hi from random"), out)
96+
}
97+
98+
@Test
99+
fun testCache() {
100+
val script = File("$TEST_DATA_ROOT/import-test.smain.kts")
101+
val cache = createTempDir("main.kts.test")
102+
103+
try {
104+
Assert.assertTrue(cache.exists() && cache.listFiles { f: File -> f.extension == "jar" }?.isEmpty() == true)
105+
val out1 = evalSuccessWithOut(script)
106+
Assert.assertEquals(outFromImportTest, out1)
107+
Assert.assertTrue(cache.listFiles { f: File -> f.extension.equals("jar", ignoreCase = true) }?.isEmpty() == true)
108+
109+
val out2 = evalSuccessWithOut(script, cache)
110+
Assert.assertEquals(outFromImportTest, out2)
111+
val casheFile = cache.listFiles { f: File -> f.extension.equals("jar", ignoreCase = true) }?.firstOrNull()
112+
Assert.assertTrue(casheFile != null && casheFile.exists())
113+
114+
val out3 = captureOut {
115+
val classLoader = URLClassLoader(arrayOf(casheFile!!.toURI().toURL()), null)
116+
val clazz = classLoader.loadClass("Import_test_smain")
117+
val mainFn = clazz.getDeclaredMethod("main", Array<String>::class.java)
118+
mainFn.invoke(null, arrayOf<String>())
119+
}.lines()
120+
Assert.assertEquals(outFromImportTest, out3)
121+
} finally {
122+
cache.deleteRecursively()
123+
}
124+
}
125+
126+
@Test
127+
fun testKotlinxHtml() {
128+
val out = captureOut {
129+
val res = evalFile(File("$TEST_DATA_ROOT/kotlinx-html.smain.kts"))
130+
assertSucceeded(res)
131+
}.lines()
132+
133+
Assert.assertEquals(listOf("<html>", " <body>", " <h1>Hello, World!</h1>", " </body>", "</html>"), out)
134+
}
135+
136+
private fun assertIsJava6Bytecode(res: ResultWithDiagnostics<EvaluationResult>) {
137+
val scriptClassResource = res.valueOrThrow().returnValue.scriptClass!!.java.run {
138+
getResource("$simpleName.class")
139+
}
140+
141+
DataInputStream(ByteArrayInputStream(scriptClassResource.readBytes())).use { stream ->
142+
val header = stream.readInt()
143+
if (0xCAFEBABE.toInt() != header) throw IOException("Invalid header class header: $header")
144+
stream.readUnsignedShort() // minor
145+
val major = stream.readUnsignedShort()
146+
Assert.assertTrue(major == 50)
147+
}
148+
}
149+
150+
private fun assertSucceeded(res: ResultWithDiagnostics<EvaluationResult>) {
151+
Assert.assertTrue(
152+
"test failed:\n ${res.reports.joinToString("\n ") { it.message + if (it.exception == null) "" else ": ${it.exception}" }}",
153+
res is ResultWithDiagnostics.Success
154+
)
155+
}
156+
157+
private fun assertFailed(expectedError: String, res: ResultWithDiagnostics<EvaluationResult>) {
158+
Assert.assertTrue(
159+
"test failed - expecting a failure with the message \"$expectedError\" but received " +
160+
(if (res is ResultWithDiagnostics.Failure) "failure" else "success") +
161+
":\n ${res.reports.joinToString("\n ") { it.message + if (it.exception == null) "" else ": ${it.exception}" }}",
162+
res is ResultWithDiagnostics.Failure && res.reports.any { it.message.contains(expectedError) }
163+
)
164+
}
165+
166+
private fun evalSuccessWithOut(scriptFile: File, cacheDir: File? = null): List<String> =
167+
captureOut {
168+
val res = evalFile(scriptFile, cacheDir)
169+
assertSucceeded(res)
170+
}.lines()
171+
}
172+
173+
private fun captureOut(body: () -> Unit): String {
174+
val outStream = ByteArrayOutputStream()
175+
val prevOut = System.out
176+
System.setOut(PrintStream(outStream))
177+
try {
178+
body()
179+
} finally {
180+
System.out.flush()
181+
System.setOut(prevOut)
182+
}
183+
return outStream.toString().trim()
184+
}
185+
186+
private fun <T> withMainKtsCacheDir(value: String?, body: () -> T): T {
187+
val prevCacheDir = System.getProperty(COMPILED_SCRIPTS_CACHE_DIR_PROPERTY)
188+
if (value == null) System.clearProperty(COMPILED_SCRIPTS_CACHE_DIR_PROPERTY)
189+
else System.setProperty(COMPILED_SCRIPTS_CACHE_DIR_PROPERTY, value)
190+
try {
191+
return body()
192+
} finally {
193+
if (prevCacheDir == null) System.clearProperty(COMPILED_SCRIPTS_CACHE_DIR_PROPERTY)
194+
else System.setProperty(COMPILED_SCRIPTS_CACHE_DIR_PROPERTY, prevCacheDir)
195+
}
196+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
@file:CompilerOptions("-jvm-target", "1.6")
2+
3+
interface Test {
4+
fun print()
5+
fun printSuper() = println("Hi from super")
6+
}
7+
8+
class TestImpl : Test {
9+
override fun print() = println("Hi from sub")
10+
}
11+
12+
inline fun printRandom() = println("Hi from random")
13+
14+
TestImpl().run {
15+
print()
16+
printSuper()
17+
printRandom()
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
@file:DependsOn("abracadabra")
3+
4+
println("Hello, World!")
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
@file:DependsOn("junit:junit:(4.11,4.12]")
3+
4+
org.junit.Assert.assertThrows(NullPointerException::class.java) {
5+
throw null!!
6+
}
7+
8+
println("Hello, World!")
9+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
@file:DependsOn("junit:junit:(4.12,5.0)")
3+
4+
org.junit.Assert.assertThrows(NullPointerException::class.java) {
5+
throw null!!
6+
}
7+
8+
println("Hello, World!")
9+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
@file:DependsOn("junit:junit:4.11")
3+
4+
org.junit.Assert.assertTrue(true)
5+
6+
println("Hello, World!")
7+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
org.junit.Assert.assertTrue(true)
3+
4+
println("Hello, World!")
5+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
2+
var sharedVar = 2
3+
4+
println("Hi from common")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
2+
@file:Import("import-common.smain.kts")
3+
4+
sharedVar *= 2
5+
6+
println("Hi from middle")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
2+
@file:Import("import-common.smain.kts")
3+
@file:Import("import-middle.smain.kts")
4+
5+
sharedVar = sharedVar + 1
6+
7+
println("sharedVar == $sharedVar")
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#!/usr/bin/env kotlinc -cp dist/kotlinc/lib/kotlin-main-kts.jar -script
2+
3+
@file:Repository("https://jcenter.bintray.com")
4+
@file:DependsOn("org.jetbrains.kotlinx:kotlinx-html-jvm:0.6.11")
5+
6+
import kotlinx.html.*; import kotlinx.html.stream.*; import kotlinx.html.attributes.*
7+
8+
print(createHTML().html {
9+
body {
10+
h1 { +"Hello, World!" }
11+
}
12+
})
13+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
// NOTE: copied for kscript tests
3+
4+
@file:DependsOn("log4j:log4j:1.2.12")
5+
@file:DependsOn("com.offbytwo:docopt:0.6.0.20150202")
6+
7+
// some pointless comment
8+
9+
import org.docopt.Docopt
10+
11+
// test the docopt dependency
12+
val docopt = Docopt("Usage: jl <command> [options] [<joblist_file>]")
13+
14+
// instantiate a logger to test the log4j dependency
15+
org.apache.log4j.Logger.getRootLogger()
16+
17+
18+
println("Succeeded!")

Diff for: jvm/simple-main-kts/simple-main-kts/build.gradle.kts

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
2+
plugins {
3+
kotlin("jvm")
4+
}
5+
6+
val kotlinVersion: String by rootProject.extra
7+
8+
dependencies {
9+
implementation("org.jetbrains.kotlin:kotlin-scripting-jvm-host:$kotlinVersion")
10+
implementation("org.jetbrains.kotlin:kotlin-scripting-dependencies:$kotlinVersion")
11+
implementation("org.jetbrains.kotlin:kotlin-script-util:$kotlinVersion")
12+
implementation("org.apache.ivy:ivy:2.5.0")
13+
}
14+
15+
sourceSets {
16+
test {}
17+
}

0 commit comments

Comments
 (0)