diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml
index ed862f35a90..777268dbc1d 100644
--- a/.github/workflows/run-tests.yml
+++ b/.github/workflows/run-tests.yml
@@ -58,11 +58,11 @@ jobs:
# For most tests, run them arbitrarily on Java 11 or Java 17 on Linux, and
# on the opposite version on Windows below, so we get decent coverage of
# each test on each Java version and each operating system
- # We also try to group tests together to manuaully balance out the runtimes of each jobs
+ # We also try to group tests together to manually balance out the runtimes of each jobs
- java-version: 17
millargs: "'{main,scalalib,testrunner,bsp,testkit}.__.testCached'"
- java-version: '11'
- millargs: "'{scalajslib,scalanativelib}.__.testCached'"
+ millargs: "'{scalajslib,scalanativelib,kotlinlib}.__.testCached'"
- java-version: 17
millargs: "contrib.__.testCached"
@@ -70,6 +70,8 @@ jobs:
millargs: "'example.javalib.__.local.testCached'"
- java-version: 17
millargs: "'example.scalalib.__.local.testCached'"
+ - java-version: 17
+ millargs: "'example.kotlinlib.__.local.testCached'"
- java-version: '11'
millargs: "'example.thirdparty[{mockito,acyclic,commons-io}].local.testCached'"
- java-version: 17
diff --git a/build.mill b/build.mill
index 820784d4f1b..99a53841460 100644
--- a/build.mill
+++ b/build.mill
@@ -188,6 +188,7 @@ object Deps {
val requests = ivy"com.lihaoyi::requests:0.9.0"
val logback = ivy"ch.qos.logback:logback-classic:1.5.7"
val sonatypeCentralClient = ivy"com.lumidion::sonatype-central-client-requests:0.3.0"
+ val kotlinCompiler = ivy"org.jetbrains.kotlin:kotlin-compiler:1.9.24"
object RuntimeDeps {
val errorProneCore = ivy"com.google.errorprone:error_prone_core:2.31.0"
@@ -757,6 +758,7 @@ object dist extends MillPublishJavaModule {
genTask(build.main.eval)() ++
genTask(build.main)() ++
genTask(build.scalalib)() ++
+ genTask(build.kotlinlib)() ++
genTask(build.scalajslib)() ++
genTask(build.scalanativelib)()
diff --git a/example/kotlinlib/basic/1-simple/build.mill b/example/kotlinlib/basic/1-simple/build.mill
new file mode 100644
index 00000000000..8361f78a117
--- /dev/null
+++ b/example/kotlinlib/basic/1-simple/build.mill
@@ -0,0 +1,102 @@
+//// SNIPPET:BUILD
+package build
+import mill._, kotlinlib._, scalalib._
+
+object `package` extends RootModule with KotlinModule {
+
+ def kotlinVersion = "1.9.24"
+
+ def mainClass = Some("foo.FooKt")
+
+ def ivyDeps = Agg(
+ ivy"com.github.ajalt.clikt:clikt-jvm:4.4.0",
+ ivy"org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0"
+ )
+
+ object test extends KotlinModuleTests with TestModule.Junit5 {
+ def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
+ )
+ }
+}
+
+// This is a basic Mill build for a single `KotlinModule`, with two
+// third-party dependencies and a test suite using the JUnit framework. As a
+// single-module project, it `extends RootModule` to mark `object foo` as the
+// top-level module in the build. This lets us directly perform operations
+// `./mill compile` or `./mill run` without needing to prefix it as
+// `foo.compile` or `foo.run`.
+//
+//// SNIPPET:DEPENDENCIES
+//
+// This example project uses two third-party dependencies - Clikt for CLI
+// argument parsing, Apache Commons Text for HTML escaping - and uses them to wrap a
+// given input string in HTML templates with proper escaping.
+//
+// You can run `assembly` to generate a standalone executable jar, which then
+// can be run from the command line or deployed to be run elsewhere.
+
+/** Usage
+
+> ./mill resolve _ # List what tasks are available to run
+assembly
+...
+clean
+...
+compile
+...
+run
+...
+show
+...
+inspect
+...
+
+> ./mill inspect compile # Show documentation and inputs of a task
+compile(KotlinModule...)
+ Compiles all the sources to JVM class files.
+ Compiles the current module to generate compiled classfiles/bytecode.
+ When you override this, you probably also want/need to override [[bspCompileClassesPath]],
+ as that needs to point to the same compilation output path.
+ Keep in sync with [[bspCompileClassesPath]]
+Inputs:
+ allJavaSourceFiles
+ allKotlinSourceFiles
+ compileClasspath
+ upstreamCompileOutput
+ javacOptions
+ zincReportCachedProblems
+ kotlincOptions
+ kotlinCompilerClasspath
+...
+
+> ./mill compile # compile sources into classfiles
+...
+Compiling 1 Kotlin sources to...
+
+> ./mill run # run the main method, if any
+error: Error: missing option --text
+...
+
+> ./mill run --text hello
+
hello
+
+> ./mill test
+...
+Test foo.FooTesttestSimple finished, ...
+Test foo.FooTesttestEscaping finished, ...
+Test foo.FooTest finished, ...
+Test run finished: 0 failed, 0 ignored, 2 total, ...
+
+> ./mill assembly # bundle classfiles and libraries into a jar for deployment
+
+> ./mill show assembly # show the output of the assembly task
+".../out/assembly.dest/out.jar"
+
+> java -jar ./out/assembly.dest/out.jar --text hello
+hello
+
+> ./out/assembly.dest/out.jar --text hello # mac/linux
+hello
+
+*/
diff --git a/example/kotlinlib/basic/1-simple/src/foo/Foo.kt b/example/kotlinlib/basic/1-simple/src/foo/Foo.kt
new file mode 100644
index 00000000000..1f9f6931105
--- /dev/null
+++ b/example/kotlinlib/basic/1-simple/src/foo/Foo.kt
@@ -0,0 +1,21 @@
+package foo
+
+import kotlinx.html.h1
+import kotlinx.html.stream.createHTML
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.required
+
+class Foo: CliktCommand() {
+ val text by option("-t", "--text", help="text to insert").required()
+
+ override fun run() {
+ echo(generateHtml(text))
+ }
+}
+
+fun generateHtml(text: String): String {
+ return createHTML(prettyPrint = false).h1 { text(text) }.toString()
+}
+
+fun main(args: Array) = Foo().main(args)
diff --git a/example/kotlinlib/basic/1-simple/test/src/foo/FooTest.kt b/example/kotlinlib/basic/1-simple/test/src/foo/FooTest.kt
new file mode 100644
index 00000000000..c978e934c1b
--- /dev/null
+++ b/example/kotlinlib/basic/1-simple/test/src/foo/FooTest.kt
@@ -0,0 +1,15 @@
+package foo
+
+import foo.generateHtml
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class FooTest : FunSpec({
+ test("testSimple") {
+ generateHtml("hello") shouldBe "hello
"
+ }
+
+ test("testEscaping") {
+ generateHtml("") shouldBe "<hello>
"
+ }
+})
diff --git a/example/kotlinlib/basic/2-custom-build-logic/build.mill b/example/kotlinlib/basic/2-custom-build-logic/build.mill
new file mode 100644
index 00000000000..e6f3b5c62dc
--- /dev/null
+++ b/example/kotlinlib/basic/2-custom-build-logic/build.mill
@@ -0,0 +1,27 @@
+//// SNIPPET:BUILD
+package build
+import mill._, kotlinlib._, scalalib._
+
+object `package` extends RootModule with KotlinModule {
+
+ def kotlinVersion = "1.9.24"
+
+ def mainClass = Some("foo.FooKt")
+
+ /** Total number of lines in module's source files */
+ def lineCount = T{
+ allSourceFiles().map(f => os.read.lines(f.path).size).sum
+ }
+
+ /** Generate resources using lineCount of sources */
+ override def resources = T{
+ os.write(T.dest / "line-count.txt", "" + lineCount())
+ Seq(PathRef(T.dest))
+ }
+
+ object test extends KotlinModuleTests with TestModule.Junit5 {
+ def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
+ )
+ }
+}
diff --git a/example/kotlinlib/basic/2-custom-build-logic/src/foo/Foo.kt b/example/kotlinlib/basic/2-custom-build-logic/src/foo/Foo.kt
new file mode 100644
index 00000000000..f40a128ef3c
--- /dev/null
+++ b/example/kotlinlib/basic/2-custom-build-logic/src/foo/Foo.kt
@@ -0,0 +1,17 @@
+package foo
+
+import java.io.IOException
+
+fun getLineCount(): String? {
+ return try {
+ String(
+ ::main.javaClass.classLoader.getResourceAsStream("line-count.txt").readAllBytes()
+ )
+ } catch (e: IOException) {
+ null
+ }
+}
+
+fun main() {
+ println("Line Count: " + getLineCount())
+}
diff --git a/example/kotlinlib/basic/2-custom-build-logic/test/src/foo/FooTests.kt b/example/kotlinlib/basic/2-custom-build-logic/test/src/foo/FooTests.kt
new file mode 100644
index 00000000000..aa0d089a440
--- /dev/null
+++ b/example/kotlinlib/basic/2-custom-build-logic/test/src/foo/FooTests.kt
@@ -0,0 +1,14 @@
+package foo
+
+import foo.getLineCount
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class FooTests : FunSpec({
+
+ test("testSimple") {
+ val expectedLineCount = 12
+ val actualLineCount = getLineCount()?.trim().let { Integer.parseInt(it) }
+ actualLineCount shouldBe expectedLineCount
+ }
+})
diff --git a/example/kotlinlib/basic/3-multi-module/bar/src/bar/Bar.kt b/example/kotlinlib/basic/3-multi-module/bar/src/bar/Bar.kt
new file mode 100644
index 00000000000..7d764903605
--- /dev/null
+++ b/example/kotlinlib/basic/3-multi-module/bar/src/bar/Bar.kt
@@ -0,0 +1,12 @@
+package bar
+
+import kotlinx.html.h1
+import kotlinx.html.stream.createHTML
+
+fun generateHtml(text: String): String {
+ return createHTML(prettyPrint = false).h1 { text(text) }.toString()
+}
+
+fun main(args: Array) {
+ println("Bar.value: " + generateHtml(args[0]))
+}
diff --git a/example/kotlinlib/basic/3-multi-module/bar/test/src/bar/BarTests.kt b/example/kotlinlib/basic/3-multi-module/bar/test/src/bar/BarTests.kt
new file mode 100644
index 00000000000..aeb26106298
--- /dev/null
+++ b/example/kotlinlib/basic/3-multi-module/bar/test/src/bar/BarTests.kt
@@ -0,0 +1,18 @@
+package bar
+
+import bar.generateHtml
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class BarTests : FunSpec({
+
+ test("simple") {
+ val result = generateHtml("hello")
+ result shouldBe "hello
"
+ }
+
+ test("escaping") {
+ val result = generateHtml("")
+ result shouldBe "<hello>
"
+ }
+})
diff --git a/example/kotlinlib/basic/3-multi-module/build.mill b/example/kotlinlib/basic/3-multi-module/build.mill
new file mode 100644
index 00000000000..e8eeb597ccc
--- /dev/null
+++ b/example/kotlinlib/basic/3-multi-module/build.mill
@@ -0,0 +1,57 @@
+//// SNIPPET:BUILD
+package build
+import mill._, kotlinlib._, scalalib._
+
+trait MyModule extends KotlinModule {
+
+ def kotlinVersion = "1.9.24"
+
+}
+
+object foo extends MyModule {
+ def mainClass = Some("foo.FooKt")
+ def moduleDeps = Seq(bar)
+ def ivyDeps = Agg(
+ ivy"com.github.ajalt.clikt:clikt-jvm:4.4.0"
+ )
+}
+
+object bar extends MyModule {
+ def mainClass = Some("bar.BarKt")
+ def ivyDeps = Agg(
+ ivy"org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0"
+ )
+
+ object test extends KotlinModuleTests with TestModule.Junit5 {
+ def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
+ )
+ }
+}
+
+//// SNIPPET:TREE
+// ----
+// build.mill
+// foo/
+// src/
+// foo/
+// Foo.java
+// resources/
+// ...
+// bar/
+// src/
+// bar/
+// Bar.java
+// resources/
+// ...
+// out/
+// foo/
+// compile.json
+// compile.dest/
+// ...
+// bar/
+// compile.json
+// compile.dest/
+// ...
+// ----
+//
diff --git a/example/kotlinlib/basic/3-multi-module/foo/src/foo/Foo.kt b/example/kotlinlib/basic/3-multi-module/foo/src/foo/Foo.kt
new file mode 100644
index 00000000000..bc022e40c49
--- /dev/null
+++ b/example/kotlinlib/basic/3-multi-module/foo/src/foo/Foo.kt
@@ -0,0 +1,23 @@
+package foo
+
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.required
+
+const val VALUE: String = "hello"
+
+class Foo: CliktCommand() {
+ val fooText by option("--foo-text").required()
+ val barText by option("--bar-text").required()
+
+ override fun run() {
+ mainFunction(fooText, barText)
+ }
+}
+
+fun mainFunction(fooText: String, barText: String) {
+ println("Foo.value: " + VALUE)
+ println("Bar.value: " + bar.generateHtml(barText))
+}
+
+fun main(args: Array) = Foo().main(args)
diff --git a/example/kotlinlib/basic/4-builtin-commands/bar/src/bar/Bar.kt b/example/kotlinlib/basic/4-builtin-commands/bar/src/bar/Bar.kt
new file mode 100644
index 00000000000..be74135c57b
--- /dev/null
+++ b/example/kotlinlib/basic/4-builtin-commands/bar/src/bar/Bar.kt
@@ -0,0 +1,12 @@
+package bar
+
+import kotlinx.html.h1
+import kotlinx.html.stream.createHTML
+
+fun generateHtml(text: String): String {
+ return createHTML(prettyPrint = false).h1 { text("world") }.toString()
+}
+
+fun main(args: Array) {
+ println("Bar.value: " + generateHtml(args[0]))
+}
\ No newline at end of file
diff --git a/example/kotlinlib/basic/4-builtin-commands/bar/test/src/bar/BarTests.kt b/example/kotlinlib/basic/4-builtin-commands/bar/test/src/bar/BarTests.kt
new file mode 100644
index 00000000000..f47be7c3b6b
--- /dev/null
+++ b/example/kotlinlib/basic/4-builtin-commands/bar/test/src/bar/BarTests.kt
@@ -0,0 +1,18 @@
+package bar
+
+import bar.generateHtml
+import io.kotest.core.spec.style.FunSpec
+import io.kotest.matchers.shouldBe
+
+class BarTests : FunSpec({
+
+ test("simple") {
+ val result = generateHtml("hello")
+ result shouldBe "hello
"
+ }
+
+ test("escaping") {
+ val result = generateHtml("")
+ result shouldBe "<hello>
"
+ }
+})
\ No newline at end of file
diff --git a/example/kotlinlib/basic/4-builtin-commands/build.mill b/example/kotlinlib/basic/4-builtin-commands/build.mill
new file mode 100644
index 00000000000..5918a33db9c
--- /dev/null
+++ b/example/kotlinlib/basic/4-builtin-commands/build.mill
@@ -0,0 +1,29 @@
+//// SNIPPET:BUILD
+package build
+import mill._, kotlinlib._, scalalib._
+
+trait MyModule extends KotlinModule {
+
+ def kotlinVersion = "1.9.24"
+}
+
+object foo extends MyModule {
+ def mainClass = Some("foo.FooKt")
+ def moduleDeps = Seq(bar)
+ def ivyDeps = Agg(
+ ivy"com.github.ajalt.clikt:clikt-jvm:4.4.0"
+ )
+}
+
+object bar extends MyModule {
+ def mainClass = Some("bar.BarKt")
+ def ivyDeps = Agg(
+ ivy"org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0"
+ )
+
+ object test extends KotlinModuleTests with TestModule.Junit5 {
+ def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
+ )
+ }
+}
diff --git a/example/kotlinlib/basic/4-builtin-commands/foo/src/foo/Foo.kt b/example/kotlinlib/basic/4-builtin-commands/foo/src/foo/Foo.kt
new file mode 100644
index 00000000000..abc8fe7492c
--- /dev/null
+++ b/example/kotlinlib/basic/4-builtin-commands/foo/src/foo/Foo.kt
@@ -0,0 +1,24 @@
+package foo
+
+import com.github.ajalt.clikt.core.CliktCommand
+import com.github.ajalt.clikt.parameters.options.option
+import com.github.ajalt.clikt.parameters.options.required
+
+const val VALUE: String = "hello"
+
+class Foo: CliktCommand() {
+ val fooText by option("--foo-text").required()
+ val barText by option("--bar-text").required()
+
+ override fun run() {
+ mainFunction(fooText, barText)
+ }
+}
+
+fun mainFunction(fooText: String, barText: String) {
+ println("Foo.value: " + VALUE)
+ println("Bar.value: " + bar.generateHtml(barText))
+}
+
+fun main(args: Array) = Foo().main(args)
+
diff --git a/example/package.mill b/example/package.mill
index 0ab4f788c8b..a6c712752c6 100644
--- a/example/package.mill
+++ b/example/package.mill
@@ -35,6 +35,9 @@ object `package` extends RootModule with Module {
object module extends Cross[ExampleCrossModuleJava](build.listIn(millSourcePath / "module"))
object web extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "web"))
}
+ object kotlinlib extends Module {
+ object basic extends Cross[ExampleCrossModuleKotlin](build.listIn(millSourcePath / "basic"))
+ }
object scalalib extends Module {
object basic extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "basic"))
object builds extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "builds"))
@@ -61,6 +64,15 @@ object `package` extends RootModule with Module {
object libraries extends Cross[ExampleCrossModule](build.listIn(millSourcePath / "libraries"))
}
+ trait ExampleCrossModuleKotlin extends ExampleCrossModuleJava {
+ override def lineTransform(line: String) = this.millModuleSegments.parts.last match {
+ case "3-multi-module" => line.replace("bar.BarTests.simple", "bar.BarTestssimple")
+ .replace("bar.BarTests.escaping", "bar.BarTestsescaping")
+ case "4-builtin-commands" => line.replace("compile.dest/zinc", "compile.dest/kotlin.analysis.dummy")
+ case _ => line
+ }
+ }
+
trait ExampleCrossModuleJava extends ExampleCrossModule {
def upstreamCross(s: String) = s match {
@@ -79,7 +91,7 @@ object `package` extends RootModule with Module {
case Some(upstream) => T{
os.copy.over(super.testRepoRoot().path, T.dest)
val upstreamRoot = upstream.testRepoRoot().path
- val suffix = Seq("build.mill", "build.mill").find(s => os.exists(upstreamRoot / s)).head
+ val suffix = Seq("build.sc", "build.mill").find(s => os.exists(upstreamRoot / s)).head
for(lines <- buildScLines()) {
os.write.over(T.dest / suffix, lines.mkString("\n"))
}
@@ -102,7 +114,7 @@ object `package` extends RootModule with Module {
case s"//// SNIPPET:$name" =>
current = Some(name)
groupedLines(name) = mutable.Buffer()
- case s => current.foreach(groupedLines(_).append(s))
+ case s => current.foreach(groupedLines(_).append(lineTransform(s)))
}
current = None
@@ -116,11 +128,13 @@ object `package` extends RootModule with Module {
Nil
}
- case s => if (current.nonEmpty) None else Some(s)
+ case s => if (current.nonEmpty) None else Some(lineTransform(s))
}
}
}
}
+
+ def lineTransform(line: String) = line
}
trait ExampleCrossModule extends build.integration.IntegrationTestModule {
diff --git a/kotlinlib/package.mill b/kotlinlib/package.mill
new file mode 100644
index 00000000000..8f7c88c10cc
--- /dev/null
+++ b/kotlinlib/package.mill
@@ -0,0 +1,38 @@
+package build.kotlinlib
+
+// imports
+import mill._
+import mill.scalalib._
+
+// TODO change MillPublishScalaModule to MillStableScalaModule after mill version with kotlinlib is released,
+// because currently there is no previous artifact version
+object `package` extends RootModule with build.MillPublishScalaModule {
+
+ def moduleDeps = Seq(build.main, build.scalalib, build.testrunner, worker)
+ def testTransitiveDeps = super.testTransitiveDeps() ++ Seq(worker.impl.testDep())
+
+ trait MillKotlinModule extends build.MillPublishScalaModule {
+ override def javacOptions = {
+ val release =
+ if (scala.util.Properties.isJavaAtLeast(11)) Seq("-release", "8")
+ else Seq("-source", "1.8", "-target", "1.8")
+ release ++ Seq("-encoding", "UTF-8", "-deprecation")
+ }
+ }
+
+ object worker extends MillKotlinModule {
+ def moduleDeps = Seq(build.main, build.testrunner)
+
+ override def compileIvyDeps: T[Agg[Dep]] = Agg(
+ build.Deps.osLib
+ )
+
+ object impl extends MillKotlinModule {
+ override def moduleDeps: Seq[PublishModule] = Seq(worker)
+ override def compileIvyDeps: T[Agg[Dep]] = Agg(
+ build.Deps.osLib,
+ build.Deps.kotlinCompiler
+ )
+ }
+ }
+}
diff --git a/kotlinlib/src/mill/kotlinlib/KotlinModule.scala b/kotlinlib/src/mill/kotlinlib/KotlinModule.scala
new file mode 100644
index 00000000000..4269424f104
--- /dev/null
+++ b/kotlinlib/src/mill/kotlinlib/KotlinModule.scala
@@ -0,0 +1,265 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package mill
+package kotlinlib
+
+import mill.api.{PathRef, Result}
+import mill.define.{Command, ModuleRef, Task}
+import mill.kotlinlib.worker.api.KotlinWorker
+import mill.scalalib.api.{CompilationResult, ZincWorkerApi}
+import mill.scalalib.{Dep, DepSyntax, JavaModule, Lib, ZincWorkerModule}
+import mill.util.Util.millProjectModule
+import mill.{Agg, T}
+
+import java.io.File
+
+trait KotlinModule extends JavaModule { outer =>
+
+ /**
+ * All individual source files fed into the compiler.
+ */
+ override def allSourceFiles = T {
+ Lib.findSourceFiles(allSources(), Seq("kt", "kts", "java")).map(PathRef(_))
+ }
+
+ /**
+ * All individual Java source files fed into the compiler.
+ * Subset of [[allSourceFiles]].
+ */
+ def allJavaSourceFiles = T {
+ allSourceFiles().filter(_.path.ext.toLowerCase() == "java")
+ }
+
+ /**
+ * All individual Kotlin source files fed into the compiler.
+ * Subset of [[allSourceFiles]].
+ */
+ def allKotlinSourceFiles = T {
+ allSourceFiles().filter(path => Seq("kt", "kts").exists(path.path.ext.toLowerCase() == _))
+ }
+
+ /**
+ * The Kotlin version to be used (for API and Language level settings).
+ */
+ def kotlinVersion: T[String]
+
+ /**
+ * The dependencies of this module.
+ * Defaults to add the kotlin-stdlib dependency matching the [[kotlinVersion]].
+ */
+ override def mandatoryIvyDeps: T[Agg[Dep]] = T {
+ super.mandatoryIvyDeps() ++ Agg(
+ ivy"org.jetbrains.kotlin:kotlin-stdlib:${kotlinVersion()}"
+ )
+ }
+
+ /**
+ * The version of the Kotlin compiler to be used.
+ * Default is derived from [[kotlinVersion]].
+ */
+ def kotlinCompilerVersion: T[String] = T { kotlinVersion() }
+
+ type CompileProblemReporter = mill.api.CompileProblemReporter
+
+ protected def zincWorkerRef: ModuleRef[ZincWorkerModule] = zincWorker
+
+ protected def kotlinWorkerRef: ModuleRef[KotlinWorkerModule] = ModuleRef(KotlinWorkerModule)
+
+ private[kotlinlib] def kotlinWorkerClasspath = T {
+ millProjectModule(
+ "mill-kotlinlib-worker-impl",
+ repositoriesTask(),
+ resolveFilter = _.toString.contains("mill-kotlinlib-worker-impl")
+ )
+ }
+
+ /**
+ * The Java classpath resembling the Kotlin compiler.
+ * Default is derived from [[kotlinCompilerIvyDeps]].
+ */
+ def kotlinCompilerClasspath: T[Seq[PathRef]] = T {
+ resolveDeps(
+ T.task { kotlinCompilerIvyDeps().map(bindDependency()) }
+ )().toSeq ++ kotlinWorkerClasspath()
+ }
+
+ /**
+ * The Ivy/Coursier dependencies resembling the Kotlin compiler.
+ * Default is derived from [[kotlinCompilerVersion]].
+ */
+ def kotlinCompilerIvyDeps: T[Agg[Dep]] = T {
+ Agg(ivy"org.jetbrains.kotlin:kotlin-compiler:${kotlinCompilerVersion()}") ++
+// (
+// if (Seq("1.0.", "1.1.", "1.2").exists(prefix => kotlinVersion().startsWith(prefix)))
+// Agg(ivy"org.jetbrains.kotlin:kotlin-runtime:${kotlinCompilerVersion()}")
+// else Seq()
+// ) ++
+ (
+ if (
+ !Seq("1.0.", "1.1.", "1.2.0", "1.2.1", "1.2.2", "1.2.3", "1.2.4").exists(prefix =>
+ kotlinVersion().startsWith(prefix)
+ )
+ )
+ Agg(ivy"org.jetbrains.kotlin:kotlin-scripting-compiler:${kotlinCompilerVersion()}")
+ else Seq()
+ )
+// ivy"org.jetbrains.kotlin:kotlin-scripting-compiler-impl:${kotlinCompilerVersion()}",
+// ivy"org.jetbrains.kotlin:kotlin-scripting-common:${kotlinCompilerVersion()}",
+ }
+
+// @Deprecated("Use kotlinWorkerTask instead, as this does not need to be cached as Worker")
+// def kotlinWorker: Worker[KotlinWorker] = T.worker {
+// kotlinWorkerTask()
+// }
+
+ def kotlinWorkerTask: Task[KotlinWorker] = T.task {
+ kotlinWorkerRef().kotlinWorkerManager().get(kotlinCompilerClasspath())
+ }
+
+ /**
+ * Compiles all the sources to JVM class files.
+ */
+ override def compile: T[CompilationResult] = T {
+ kotlinCompileTask()()
+ }
+
+ /**
+ * Runs the Kotlin compiler with the `-help` argument to show you the built-in cmdline help.
+ * You might want to add additional arguments like `-X` to see extra help.
+ */
+ def kotlincHelp(args: String*): Command[Unit] = T.command {
+ kotlinCompileTask(Seq("-help") ++ args)()
+ ()
+ }
+
+ protected def when(cond: Boolean)(args: String*): Seq[String] = if (cond) args else Seq()
+
+ /**
+ * The actual Kotlin compile task (used by [[compile]] and [[kotlincHelp]]).
+ */
+ protected def kotlinCompileTask(extraKotlinArgs: Seq[String] = Seq()): Task[CompilationResult] =
+ T.task {
+ val ctx = T.ctx()
+ val dest = ctx.dest
+ val classes = dest / "classes"
+ os.makeDir.all(classes)
+
+ val javaSourceFiles = allJavaSourceFiles().map(_.path)
+ val kotlinSourceFiles = allKotlinSourceFiles().map(_.path)
+
+ val isKotlin = kotlinSourceFiles.nonEmpty
+ val isJava = javaSourceFiles.nonEmpty
+ val isMixed = isKotlin && isJava
+
+ val compileCp = compileClasspath().map(_.path).filter(os.exists)
+ val updateCompileOutput = upstreamCompileOutput()
+
+ def compileJava: Result[CompilationResult] = {
+ ctx.log.info(
+ s"Compiling ${javaSourceFiles.size} Java sources to ${classes} ..."
+ )
+ // The compile step is lazy, but its dependencies are not!
+ internalCompileJavaFiles(
+ worker = zincWorkerRef().worker(),
+ upstreamCompileOutput = updateCompileOutput,
+ javaSourceFiles = javaSourceFiles,
+ compileCp = compileCp,
+ javacOptions = javacOptions(),
+ compileProblemReporter = ctx.reporter(hashCode),
+ reportOldProblems = internalReportOldProblems()
+ )
+ }
+
+ if (isMixed || isKotlin) {
+ ctx.log.info(
+ s"Compiling ${kotlinSourceFiles.size} Kotlin sources to ${classes} ..."
+ )
+ val compilerArgs: Seq[String] = Seq(
+ // destdir
+ Seq("-d", classes.toIO.getAbsolutePath()),
+ // classpath
+ when(compileCp.iterator.nonEmpty)(
+ "-classpath",
+ compileCp.iterator.mkString(File.pathSeparator)
+ ),
+ kotlincOptions(),
+ extraKotlinArgs,
+ // parameters
+ (kotlinSourceFiles ++ javaSourceFiles).map(_.toIO.getAbsolutePath())
+ ).flatten
+
+ val workerResult = kotlinWorkerTask().compile(compilerArgs: _*)
+
+ val analysisFile = dest / "kotlin.analysis.dummy"
+ os.write(target = analysisFile, data = "", createFolders = true)
+
+ workerResult match {
+ case Result.Success(_) =>
+ val cr = CompilationResult(analysisFile, PathRef(classes))
+ if (!isJava) {
+ // pure Kotlin project
+ cr
+ } else {
+ // also run Java compiler and use it's returned result
+ compileJava
+ }
+ case Result.Failure(reason, _) =>
+ Result.Failure(reason, Some(CompilationResult(analysisFile, PathRef(classes))))
+ case e: Result.Exception => e
+ case Result.Aborted => Result.Aborted
+ case Result.Skipped => Result.Skipped
+ // case x => x
+ }
+ } else {
+ // it's Java only
+ compileJava
+ }
+ }
+
+ /**
+ * Additional Kotlin compiler options to be used by [[compile]].
+ */
+ def kotlincOptions: T[Seq[String]] = T {
+ Seq("-no-stdlib") ++
+ when(!kotlinVersion().startsWith("1.0"))(
+ "-language-version",
+ kotlinVersion().split("[.]", 3).take(2).mkString("."),
+ "-api-version",
+ kotlinVersion().split("[.]", 3).take(2).mkString(".")
+ )
+ }
+
+ private[kotlinlib] def internalCompileJavaFiles(
+ worker: ZincWorkerApi,
+ upstreamCompileOutput: Seq[CompilationResult],
+ javaSourceFiles: Seq[os.Path],
+ compileCp: Agg[os.Path],
+ javacOptions: Seq[String],
+ compileProblemReporter: Option[CompileProblemReporter],
+ reportOldProblems: Boolean
+ )(implicit ctx: ZincWorkerApi.Ctx): Result[CompilationResult] = {
+ worker.compileJava(
+ upstreamCompileOutput = upstreamCompileOutput,
+ sources = javaSourceFiles,
+ compileClasspath = compileCp,
+ javacOptions = javacOptions,
+ reporter = compileProblemReporter,
+ reportCachedProblems = reportOldProblems
+ )
+ }
+
+ private[kotlinlib] def internalReportOldProblems: Task[Boolean] = zincReportCachedProblems
+
+ /**
+ * A test sub-module linked to its parent module best suited for unit-tests.
+ */
+ trait KotlinModuleTests extends JavaModuleTests with KotlinModule {
+ override def kotlinVersion: T[String] = T { outer.kotlinVersion() }
+ override def kotlinCompilerVersion: T[String] = T { outer.kotlinCompilerVersion() }
+ override def kotlincOptions: T[Seq[String]] = T { outer.kotlincOptions() }
+ override def defaultCommandName(): String = super.defaultCommandName()
+ }
+
+}
diff --git a/kotlinlib/src/mill/kotlinlib/KotlinWorkerManager.scala b/kotlinlib/src/mill/kotlinlib/KotlinWorkerManager.scala
new file mode 100644
index 00000000000..53296127d8b
--- /dev/null
+++ b/kotlinlib/src/mill/kotlinlib/KotlinWorkerManager.scala
@@ -0,0 +1,12 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package mill.kotlinlib
+
+import mill.api.{Ctx, PathRef}
+import mill.kotlinlib.worker.api.KotlinWorker
+
+trait KotlinWorkerManager {
+ def get(toolsClasspath: Seq[PathRef])(implicit ctx: Ctx): KotlinWorker
+}
diff --git a/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala b/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala
new file mode 100644
index 00000000000..7e8fadcdc27
--- /dev/null
+++ b/kotlinlib/src/mill/kotlinlib/KotlinWorkerManagerImpl.scala
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package mill.kotlinlib
+
+import mill.PathRef
+import mill.api.Ctx
+import mill.kotlinlib.worker.api.KotlinWorker
+
+import java.net.{URL, URLClassLoader}
+
+class KotlinWorkerManagerImpl(ctx: Ctx) extends KotlinWorkerManager with AutoCloseable {
+
+ private[this] var workerCache: Map[Seq[PathRef], (KotlinWorker, Int)] = Map.empty
+
+ override def get(toolsClasspath: Seq[PathRef])(implicit ctx: Ctx): KotlinWorker = {
+ val toolsCp = toolsClasspath.distinct
+ val (worker, count) = workerCache.get(toolsCp) match {
+ case Some((w, count)) =>
+ ctx.log.debug(s"Reusing existing AspectjWorker for classpath: ${toolsCp}")
+ w -> count
+ case None =>
+ ctx.log.debug(s"Creating Classloader with classpath: [${toolsCp}]")
+ val classLoader = new URLClassLoader(
+ toolsCp.map(_.path.toNIO.toUri().toURL()).toArray[URL],
+ getClass().getClassLoader()
+ )
+
+ val className =
+ classOf[KotlinWorker].getPackage().getName().split("\\.").dropRight(1).mkString(
+ "."
+ ) + ".impl." + classOf[KotlinWorker].getSimpleName() + "Impl"
+ ctx.log.debug(s"Creating ${className} from classpath: ${toolsCp}")
+ val impl = classLoader.loadClass(className)
+ val worker = impl.getConstructor().newInstance().asInstanceOf[KotlinWorker]
+ if (worker.getClass().getClassLoader() != classLoader) {
+ ctx.log.error(
+ """Worker not loaded from worker classloader.
+ |You should not add the mill-kotlin-worker JAR to the mill build classpath""".stripMargin
+ )
+ }
+ if (worker.getClass().getClassLoader() == classOf[KotlinWorker].getClassLoader()) {
+ ctx.log.error("Worker classloader used to load interface and implementation")
+ }
+ worker -> 0
+ }
+ workerCache += toolsCp -> (worker -> (1 + count))
+ ctx.log.debug(stats())
+ worker
+ }
+
+ def stats(): String = {
+ s"""Cache statistics of ${this.toString()}:
+ |${
+ workerCache.map { case (cp, (worker, count)) =>
+ s"""- worker: ${worker.toString()}
+ | used: ${count}
+ |""".stripMargin
+ }.mkString
+ }""".stripMargin
+ }
+
+ override def close(): Unit = {
+ ctx.log.debug(stats())
+
+ // We drop cached worker instances
+ workerCache = Map.empty
+ }
+}
diff --git a/kotlinlib/src/mill/kotlinlib/KotlinWorkerModule.scala b/kotlinlib/src/mill/kotlinlib/KotlinWorkerModule.scala
new file mode 100644
index 00000000000..c701a9eb53d
--- /dev/null
+++ b/kotlinlib/src/mill/kotlinlib/KotlinWorkerModule.scala
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package mill.kotlinlib
+
+import mill.T
+import mill.define.{Discover, ExternalModule, Module, Worker}
+
+trait KotlinWorkerModule extends Module {
+ def kotlinWorkerManager: Worker[KotlinWorkerManager] = T.worker {
+ new KotlinWorkerManagerImpl(T.ctx())
+ }
+}
+
+object KotlinWorkerModule extends ExternalModule with KotlinWorkerModule {
+ override def millDiscover: Discover = Discover[this.type]
+}
diff --git a/kotlinlib/test/resources/hello-world-kotlin/main/src/Hello.kt b/kotlinlib/test/resources/hello-world-kotlin/main/src/Hello.kt
new file mode 100644
index 00000000000..76ccba8ed2a
--- /dev/null
+++ b/kotlinlib/test/resources/hello-world-kotlin/main/src/Hello.kt
@@ -0,0 +1,14 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package hello
+
+fun getHelloString() : String {
+ return "Hello, world!"
+}
+
+fun main(args : Array) {
+ println(getHelloString())
+}
+
diff --git a/kotlinlib/test/resources/hello-world-kotlin/main/test/src/HelloTest.kt b/kotlinlib/test/resources/hello-world-kotlin/main/test/src/HelloTest.kt
new file mode 100644
index 00000000000..acd822d848c
--- /dev/null
+++ b/kotlinlib/test/resources/hello-world-kotlin/main/test/src/HelloTest.kt
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package hello.tests
+
+import hello.getHelloString
+import kotlin.test.assertEquals
+import org.junit.Test
+
+class HelloTest {
+ @Test fun testSuccess() : Unit {
+ assertEquals("Hello, world!", getHelloString())
+ }
+ @Test fun testFailure() : Unit {
+ assertEquals("world!", getHelloString())
+ }
+}
+
diff --git a/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/src/hello/JavaHello.java b/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/src/hello/JavaHello.java
new file mode 100644
index 00000000000..7397d53095b
--- /dev/null
+++ b/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/src/hello/JavaHello.java
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package hello;
+
+public class JavaHello {
+ public static String JavaHelloString = "Hello from Java!";
+
+ public static String getHelloStringFromKotlin() {
+ return KotlinHelloKt.getKotlinHelloString();
+ }
+
+ public static void main(String[] args) {
+ System.out.println(getHelloStringFromKotlin());
+ System.out.println(KotlinHelloKt.getHelloStringFromJava());
+ }
+}
diff --git a/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/src/hello/KotlinHello.kt b/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/src/hello/KotlinHello.kt
new file mode 100644
index 00000000000..5bc04d3278a
--- /dev/null
+++ b/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/src/hello/KotlinHello.kt
@@ -0,0 +1,11 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package hello
+
+val KotlinHelloString : String = "Hello from Kotlin!"
+
+fun getHelloStringFromJava() : String {
+ return JavaHello.JavaHelloString!!;
+}
\ No newline at end of file
diff --git a/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/test/src/HelloTest.java b/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/test/src/HelloTest.java
new file mode 100644
index 00000000000..e0e173995b9
--- /dev/null
+++ b/kotlinlib/test/resources/mixed-code-hello-world-kotlin/main/test/src/HelloTest.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package hello.tests;
+
+import hello.JavaHello;
+import junit.framework.TestCase;
+
+public class HelloTest extends TestCase {
+ public void testSuccess() {
+ assertEquals("Hello from Kotlin!", JavaHello.getHelloStringFromKotlin());
+ assertEquals("Hello from Java!", hello.KotlinHelloKt.getHelloStringFromJava());
+ }
+
+ public void testFailure() {
+ assertEquals("Hello from Java!", JavaHello.getHelloStringFromKotlin());
+ assertEquals("Hello from Kotlin!", hello.KotlinHelloKt.getHelloStringFromJava());
+ }
+}
diff --git a/kotlinlib/test/src/mill/kotlinlib/HelloWorldTests.scala b/kotlinlib/test/src/mill/kotlinlib/HelloWorldTests.scala
new file mode 100644
index 00000000000..6b0d3476e19
--- /dev/null
+++ b/kotlinlib/test/src/mill/kotlinlib/HelloWorldTests.scala
@@ -0,0 +1,90 @@
+package mill
+package kotlinlib
+
+import mill.scalalib.{DepSyntax, TestModule}
+import mill.testkit.{TestBaseModule, UnitTester}
+import mill.api.Result
+import utest._
+
+object HelloWorldTests extends TestSuite {
+
+ val kotlinVersions = if (scala.util.Properties.isJavaAtLeast(9)) {
+ Seq("1.9.24", "2.0.20")
+ } else {
+ Seq("1.0.0", "1.9.24", "2.0.20")
+ }
+
+ object HelloWorldKotlin extends TestBaseModule {
+ trait MainCross extends KotlinModule with Cross.Module[String] {
+ def kotlinVersion = crossValue
+ override def mainClass = Some("hello.HelloKt")
+
+ object test extends KotlinModuleTests with TestModule.Junit4 {
+ override def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"org.jetbrains.kotlin:kotlin-test-junit:${this.kotlinVersion()}"
+ )
+ }
+ }
+ object main extends Cross[MainCross](kotlinVersions)
+ }
+
+ val resourcePath = os.Path(sys.env("MILL_TEST_RESOURCE_FOLDER")) / "hello-world-kotlin"
+
+ def testEval() = UnitTester(HelloWorldKotlin, resourcePath)
+ def tests: Tests = Tests {
+ test("compile") {
+ val eval = testEval()
+
+ HelloWorldKotlin.main.crossModules.foreach(m => {
+ val Right(result) = eval.apply(m.compile)
+
+ assert(
+ os.walk(result.value.classes.path).exists(_.last == "HelloKt.class")
+ )
+ })
+ }
+ test("testCompile") {
+ val eval = testEval()
+
+ HelloWorldKotlin.main.crossModules.foreach(m => {
+ val Right(result1) = eval.apply(m.test.compile)
+
+ assert(
+ os.walk(result1.value.classes.path).exists(_.last == "HelloTest.class")
+ )
+ })
+ }
+ test("test") {
+ val eval = testEval()
+
+ HelloWorldKotlin.main.crossModules.foreach(m => {
+ val Left(Result.Failure(_, Some(v1))) = eval.apply(m.test.test())
+
+ assert(
+ v1._2(0).fullyQualifiedName == "hello.tests.HelloTest.testFailure",
+ v1._2(0).status == "Failure",
+ v1._2(1).fullyQualifiedName == "hello.tests.HelloTest.testSuccess",
+ v1._2(1).status == "Success"
+ )
+ })
+ }
+ test("failures") {
+ val eval = testEval()
+
+ val mainJava = HelloWorldKotlin.millSourcePath / "main" / "src" / "Hello.kt"
+
+ HelloWorldKotlin.main.crossModules.foreach(m => {
+
+ val Right(_) = eval.apply(m.compile)
+
+ os.write.over(mainJava, os.read(mainJava) + "}")
+
+ val Left(_) = eval.apply(m.compile)
+
+ os.write.over(mainJava, os.read(mainJava).dropRight(1))
+
+ val Right(_) = eval.apply(m.compile)
+ })
+ }
+ }
+}
diff --git a/kotlinlib/test/src/mill/kotlinlib/MixedHelloWorldTests.scala b/kotlinlib/test/src/mill/kotlinlib/MixedHelloWorldTests.scala
new file mode 100644
index 00000000000..c3a67f82f60
--- /dev/null
+++ b/kotlinlib/test/src/mill/kotlinlib/MixedHelloWorldTests.scala
@@ -0,0 +1,93 @@
+package mill
+package kotlinlib
+
+import mill.api.Result
+import mill.scalalib.{DepSyntax, TestModule}
+import mill.testkit.{TestBaseModule, UnitTester}
+import utest._
+
+object MixedHelloWorldTests extends TestSuite {
+
+ val kotlinVersions = if (scala.util.Properties.isJavaAtLeast(9)) {
+ Seq("1.9.24", "2.0.20")
+ } else {
+ Seq("1.0.0", "1.9.24", "2.0.20")
+ }
+
+ object MixedHelloWorldKotlin extends TestBaseModule {
+ trait MainCross extends KotlinModule with Cross.Module[String] {
+ def kotlinVersion = crossValue
+ override def mainClass = Some("hello.JavaHello")
+
+ object test extends KotlinModuleTests with TestModule.Junit4 {
+ override def ivyDeps = super.ivyDeps() ++ Agg(
+ ivy"org.jetbrains.kotlin:kotlin-test-junit:${this.kotlinVersion()}"
+ )
+ }
+ }
+ object main extends Cross[MainCross](kotlinVersions)
+ }
+
+ val resourcePath =
+ os.Path(sys.env("MILL_TEST_RESOURCE_FOLDER")) / "mixed-code-hello-world-kotlin"
+
+ def testEval() = UnitTester(MixedHelloWorldKotlin, resourcePath)
+ def tests: Tests = Tests {
+ test("compile") {
+ val eval = testEval()
+
+ MixedHelloWorldKotlin.main.crossModules.foreach(m => {
+ val Right(result) = eval.apply(m.compile)
+
+ assert(
+ os.walk(result.value.classes.path).exists(_.last == "KotlinHelloKt.class"),
+ os.walk(result.value.classes.path).exists(_.last == "JavaHello.class")
+ )
+ })
+ }
+ test("testCompile") {
+ val eval = testEval()
+
+ MixedHelloWorldKotlin.main.crossModules.foreach(m => {
+ val Right(result1) = eval.apply(m.test.compile)
+
+ assert(
+ os.walk(result1.value.classes.path).exists(_.last == "HelloTest.class")
+ )
+ })
+ }
+ test("test") {
+ val eval = testEval()
+ MixedHelloWorldKotlin.main.crossModules.foreach(m => {
+
+ val Left(Result.Failure(_, Some(v1))) = eval.apply(m.test.test())
+
+ assert(
+ v1._2(0).fullyQualifiedName == "hello.tests.HelloTest.testFailure",
+ v1._2(0).status == "Failure",
+ v1._2(1).fullyQualifiedName == "hello.tests.HelloTest.testSuccess",
+ v1._2(1).status == "Success"
+ )
+ })
+ }
+ test("failures") {
+ val eval = testEval()
+
+ MixedHelloWorldKotlin.main.crossModules.foreach(m => {
+
+ val mainJava =
+ MixedHelloWorldKotlin.millSourcePath / "main" / "src" / "hello" / "KotlinHello.kt"
+
+ val Right(_) = eval.apply(m.compile)
+
+ os.write.over(mainJava, os.read(mainJava) + "}")
+
+ val Left(_) = eval.apply(m.compile)
+
+ os.write.over(mainJava, os.read(mainJava).dropRight(1))
+
+ val Right(_) = eval.apply(m.compile)
+ })
+ }
+ }
+}
diff --git a/kotlinlib/worker/impl/src/mill/kotlinlib/worker/impl/KotlinWorkerImpl.scala b/kotlinlib/worker/impl/src/mill/kotlinlib/worker/impl/KotlinWorkerImpl.scala
new file mode 100644
index 00000000000..99fcb41f6e7
--- /dev/null
+++ b/kotlinlib/worker/impl/src/mill/kotlinlib/worker/impl/KotlinWorkerImpl.scala
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package mill.kotlinlib.worker.impl
+
+import mill.api.{Ctx, Result}
+import mill.kotlinlib.worker.api.KotlinWorker
+import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler
+
+class KotlinWorkerImpl extends KotlinWorker {
+
+ def compile(args: String*)(implicit ctx: Ctx): Result[Unit] = {
+ ctx.log.debug("Using kotlin compiler arguments: " + args.map(v => s"'${v}'").mkString(" "))
+
+ val compiler = new K2JVMCompiler()
+ val exitCode = compiler.exec(ctx.log.errorStream, args: _*)
+ if (exitCode.getCode() != 0) {
+ Result.Failure(s"Kotlin compiler failed with exit code ${exitCode.getCode()} (${exitCode})")
+ } else {
+ Result.Success(())
+ }
+ }
+
+}
diff --git a/kotlinlib/worker/src/mill/kotlinlib/worker/api/KotlinWorker.scala b/kotlinlib/worker/src/mill/kotlinlib/worker/api/KotlinWorker.scala
new file mode 100644
index 00000000000..0c323d1e88b
--- /dev/null
+++ b/kotlinlib/worker/src/mill/kotlinlib/worker/api/KotlinWorker.scala
@@ -0,0 +1,13 @@
+/*
+ * Copyright 2020-Present Original lefou/mill-kotlin repository contributors.
+ */
+
+package mill.kotlinlib.worker.api
+
+import mill.api.{Ctx, Result}
+
+trait KotlinWorker {
+
+ def compile(args: String*)(implicit ctx: Ctx): Result[Unit]
+
+}
diff --git a/runner/package.mill b/runner/package.mill
index f27e1a26b0a..c30b9ebd2d9 100644
--- a/runner/package.mill
+++ b/runner/package.mill
@@ -11,6 +11,7 @@ object `package` extends RootModule with build.MillPublishScalaModule {
def moduleDeps = Seq(
build.scalalib,
+ build.kotlinlib,
build.scalajslib,
build.scalanativelib,
build.bsp,