Skip to content

Commit

Permalink
Add Kotlin integration (#3514)
Browse files Browse the repository at this point in the history
This PR solves Part 1 from
#3451:

* It in-sources https://github.com/lefou/mill-kotlin with minimal
changes
* It adds basic samples

I still need to finalize it (especially automated test for example
`4-builtin-commands` fails right now), but most of the work is done.

Things I noticed in the original implementation
(https://github.com/lefou/mill-kotlin):

* Kotlin stdlib is added by overriding `ivyDeps`, meaning that if the
user of `KotlinModule` wants to add its own deps and forget to call
`super.ivyDeps() ++ <their deps>` and just does `def ivyDeps = <their
deps>`, then no stdlib will be in the classpath during the compilation
and it will fail with cryptic error. Such approach is thus error-prone.
* No Kotlin Reflection library is added automatically to the compile
classpath, meaning that if code to compile contains any Kotlin
Reflection classes, compilation will fail (because `-nostdlib`
[flag](https://kotlinlang.org/docs/compiler-reference.html#nostdlib) is
used for compilation)

@lihaoyi Feel free to give your feedback while this is in a Draft state
if you want.

---------

Co-authored-by: 0xnm <0xnm@users.noreply.github.com>
  • Loading branch information
0xnm and 0xnm authored Sep 14, 2024
1 parent 7b4468e commit 756dd37
Show file tree
Hide file tree
Showing 32 changed files with 1,119 additions and 5 deletions.
6 changes: 4 additions & 2 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,18 +58,20 @@ 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"

- java-version: 17
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
Expand Down
2 changes: 2 additions & 0 deletions build.mill
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -758,6 +759,7 @@ object dist extends MillPublishJavaModule {
genTask(build.main.eval)() ++
genTask(build.main)() ++
genTask(build.scalalib)() ++
genTask(build.kotlinlib)() ++
genTask(build.scalajslib)() ++
genTask(build.scalanativelib)()

Expand Down
102 changes: 102 additions & 0 deletions example/kotlinlib/basic/1-simple/build.mill
Original file line number Diff line number Diff line change
@@ -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
<h1>hello</h1>

> ./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
<h1>hello</h1>

> ./out/assembly.dest/out.jar --text hello # mac/linux
<h1>hello</h1>

*/
21 changes: 21 additions & 0 deletions example/kotlinlib/basic/1-simple/src/foo/Foo.kt
Original file line number Diff line number Diff line change
@@ -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<String>) = Foo().main(args)
15 changes: 15 additions & 0 deletions example/kotlinlib/basic/1-simple/test/src/foo/FooTest.kt
Original file line number Diff line number Diff line change
@@ -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 "<h1>hello</h1>"
}

test("testEscaping") {
generateHtml("<hello>") shouldBe "<h1>&lt;hello&gt;</h1>"
}
})
27 changes: 27 additions & 0 deletions example/kotlinlib/basic/2-custom-build-logic/build.mill
Original file line number Diff line number Diff line change
@@ -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"
)
}
}
17 changes: 17 additions & 0 deletions example/kotlinlib/basic/2-custom-build-logic/src/foo/Foo.kt
Original file line number Diff line number Diff line change
@@ -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())
}
Original file line number Diff line number Diff line change
@@ -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
}
})
12 changes: 12 additions & 0 deletions example/kotlinlib/basic/3-multi-module/bar/src/bar/Bar.kt
Original file line number Diff line number Diff line change
@@ -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<String>) {
println("Bar.value: " + generateHtml(args[0]))
}
Original file line number Diff line number Diff line change
@@ -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 "<h1>hello</h1>"
}

test("escaping") {
val result = generateHtml("<hello>")
result shouldBe "<h1>&lt;hello&gt;</h1>"
}
})
57 changes: 57 additions & 0 deletions example/kotlinlib/basic/3-multi-module/build.mill
Original file line number Diff line number Diff line change
@@ -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/
// ...
// ----
//
23 changes: 23 additions & 0 deletions example/kotlinlib/basic/3-multi-module/foo/src/foo/Foo.kt
Original file line number Diff line number Diff line change
@@ -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<String>) = Foo().main(args)
12 changes: 12 additions & 0 deletions example/kotlinlib/basic/4-builtin-commands/bar/src/bar/Bar.kt
Original file line number Diff line number Diff line change
@@ -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<String>) {
println("Bar.value: " + generateHtml(args[0]))
}
Original file line number Diff line number Diff line change
@@ -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 "<h1>hello</h1>"
}

test("escaping") {
val result = generateHtml("<hello>")
result shouldBe "<h1>&lt;hello&gt;</h1>"
}
})
Loading

0 comments on commit 756dd37

Please sign in to comment.