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,