Kotlin Module Configuration
This page goes into more detail about the various configuration options
for KotlinModule
.
Many of the APIs covered here are listed in the API documentation:
Common Configuration Overrides
package build
import mill._, kotlinlib._
object `package` extends RootModule with KotlinModule {
// You can have arbitrary numbers of third-party dependencies
def ivyDeps = Agg(
ivy"org.jetbrains.kotlinx:kotlinx-html-jvm:0.11.0"
)
def kotlinVersion = "1.9.24"
// Choose a main class to use for `.run` if there are multiple present
def mainClass = Some("foo.Foo2Kt")
// Add (or replace) source folders for the module to use
def sources = T.sources {
super.sources() ++ Seq(PathRef(millSourcePath / "custom-src"))
}
// Add (or replace) resource folders for the module to use
def resources = T.sources {
super.resources() ++ Seq(PathRef(millSourcePath / "custom-resources"))
}
// Generate sources at build time
def generatedSources: T[Seq[PathRef]] = Task {
for (name <- Seq("A", "B", "C")) os.write(
T.dest / s"Foo$name.kt",
s"""
package foo
object Foo$name {
val VALUE: String = "hello $name"
}
""".stripMargin
)
Seq(PathRef(T.dest))
}
// Pass additional JVM flags when `.run` is called or in the executable
// generated by `.assembly`
def forkArgs: T[Seq[String]] = Seq("-Dmy.custom.property=my-prop-value")
// Pass additional environmental variables when `.run` is called. Note that
// this does not apply to running externally via `.assembly
def forkEnv: T[Map[String, String]] = Map("MY_CUSTOM_ENV" -> "my-env-value")
}
If you want to better understand how the various upstream tasks feed into
a task of interest, such as run
, you can visualize their relationships via
> mill visualizePlan run
(right-click open in new tab to see full sized)
Compilation & Execution Flags
package build
import mill._, kotlinlib._
object `package` extends RootModule with KotlinModule {
def kotlinVersion = "1.9.24"
def mainClass = Some("foo.FooKt")
def forkArgs = Seq("-Xmx4g", "-Dmy.jvm.property=hello")
def forkEnv = Map("MY_ENV_VAR" -> "WORLD")
def kotlincOptions = super.kotlincOptions() ++ Seq("-Werror")
}
You can pass flags to the Kotlin compiler via kotlincOptions
.
> ./mill run
hello WORLD
> echo 'fun deprecatedMain(){Thread.currentThread().stop()}' >> src/foo/Foo.kt
> ./mill run
error: .../src/foo/Foo.kt... warning: 'stop(): Unit' is deprecated. Deprecated in Java
Classpath and Filesystem Resources
package build
import mill._, kotlinlib._
object foo extends KotlinModule {
def kotlinVersion = "1.9.24"
object test extends KotlinTests with TestModule.Junit5 {
def otherFiles = T.source(millSourcePath / "other-files")
def forkEnv = super.forkEnv() ++ Map(
"OTHER_FILES_DIR" -> otherFiles().path.toString
)
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
)
}
}
package foo
object Foo {
// Read `file.txt` from classpath
fun classpathResourceText(): String {
// Get the resource as an InputStream
return Foo::class.java.classLoader.getResourceAsStream("file.txt").use {
it.readAllBytes().toString(Charsets.UTF_8)
}
}
}
package foo
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import java.nio.file.Files
import java.nio.file.Paths
class FooTests :
FunSpec({
test("simple") {
// Reference app module's `Foo` class which reads `file.txt` from classpath
val appClasspathResourceText = Foo.classpathResourceText()
appClasspathResourceText shouldBe "Hello World Resource File"
// Read `test-file-a.txt` from classpath
val testClasspathResourceText =
Foo::class.java.classLoader.getResourceAsStream("test-file-a.txt").use {
it.readAllBytes().toString(Charsets.UTF_8)
}
testClasspathResourceText shouldBe "Test Hello World Resource File A"
// Use `MILL_TEST_RESOURCE_DIR` to read `test-file-b.txt` from filesystem
val testFileResourceDir = Paths.get(System.getenv("MILL_TEST_RESOURCE_DIR"))
val testFileResourceText =
Files.readString(
testFileResourceDir.resolve("test-file-b.txt"),
)
testFileResourceText shouldBe "Test Hello World Resource File B"
// Use `MILL_TEST_RESOURCE_DIR` to list files available in resource folder
val actualFiles = Files.list(testFileResourceDir).toList().sorted()
val expectedFiles =
listOf(
testFileResourceDir.resolve("test-file-a.txt"),
testFileResourceDir.resolve("test-file-b.txt"),
)
actualFiles shouldBe expectedFiles
// Use the `OTHER_FILES_DIR` configured in your build to access the
// files in `foo/test/other-files/`.
val otherFileText =
Files.readString(
Paths.get(System.getenv("OTHER_FILES_DIR"), "other-file.txt"),
)
otherFileText shouldBe "Other Hello World File"
}
})
Kotlin Compiler Plugins
The Kotlin compiler requires plugins to be passed explicitly. To do this, you can define
a module to contain the exact annotation processors you want, and pass
in -Xplugin
to kotlincOptions
:
package build
import mill._, kotlinlib._
import java.io.File
object foo extends KotlinModule {
def kotlinVersion = "1.9.24"
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3"
)
def processors = Task {
defaultResolver().resolveDeps(
Agg(
ivy"org.jetbrains.kotlin:kotlin-serialization-compiler-plugin:1.9.24"
)
)
}
def kotlincOptions = super.kotlincOptions() ++ Seq(
s"-Xplugin=${processors().head.path}"
)
object test extends KotlinTests with TestModule.Junit5 {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
)
}
}
> ./mill foo.test
Test foo.ProjectTest simple started
Test foo.ProjectTest simple finished...
...
Doc-Jar Generation
To generate API documenation you can use the docJar
task on the module you’d
like to create the documentation for, configured via dokkaOptions
:
package build
import mill._, kotlinlib._
object foo extends KotlinModule {
def kotlinVersion = "1.9.24"
}
> ./mill show foo.docJar
...
...Generation completed successfully...
> unzip -p out/foo/docJar.dest/out.jar root/foo/index.html
...
...My Awesome Docs for class Foo...
...
...My Awesome Docs for class Bar...
Custom Tasks
package build
import mill._, kotlinlib._
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")
def generatedSources: T[Seq[PathRef]] = Task {
val prettyIvyDeps = for (ivyDep <- ivyDeps()) yield {
val org = ivyDep.dep.module.organization.value
val name = ivyDep.dep.module.name.value
val version = ivyDep.dep.version
s""" "$org:$name:$version" """
}
val ivyDepsString = prettyIvyDeps.mkString(" + \"\\n\" + \n")
os.write(
T.dest / s"MyDeps.kt",
s"""
package foo
object MyDeps {
const val VALUE = $ivyDepsString;
}
""".stripMargin
)
Seq(PathRef(T.dest))
}
def lineCount: T[Int] = Task {
sources()
.flatMap(pathRef => os.walk(pathRef.path))
.filter(_.ext == "kt")
.map(os.read.lines(_).size)
.sum
}
def forkArgs: T[Seq[String]] = Seq(s"-Dmy.line.count=${lineCount()}")
def printLineCount() = T.command { println(lineCount()) }
}
> mill run --text hello
text: hello
MyDeps.value: com.github.ajalt.clikt:clikt-jvm:4.4.0
my.line.count: 17
> mill show lineCount
17
> mill printLineCount
17
Overriding Tasks
package build
import mill._, kotlinlib._
object foo extends KotlinModule {
def kotlinVersion = "1.9.24"
def mainClass = Some("foo.FooKt")
def sources = Task {
os.write(
T.dest / "Foo.kt",
"""package foo
fun main() = println("Hello World")
""".stripMargin
)
Seq(PathRef(T.dest))
}
def compile = Task {
println("Compiling...")
super.compile()
}
def run(args: Task[Args] = T.task(Args())) = T.command {
println("Running..." + args().value.mkString(" "))
super.run(args)()
}
}
object foo2 extends KotlinModule {
def kotlinVersion = "1.9.24"
def generatedSources = Task {
os.write(T.dest / "Foo.kt", """...""")
Seq(PathRef(T.dest))
}
}
object foo3 extends KotlinModule {
def kotlinVersion = "1.9.24"
def sources = Task {
os.write(T.dest / "Foo.kt", """...""")
super.sources() ++ Seq(PathRef(T.dest))
}
}
Native C Code with JNI
package build
import mill._, kotlinlib._, util.Jvm
object `package` extends RootModule with KotlinModule {
def mainClass = Some("foo.HelloWorldKt")
def kotlinVersion = "1.9.24"
// Additional source folder to put C sources
def nativeSources = T.sources(millSourcePath / "native-src")
// Compile C
def nativeCompiled = Task {
val cSourceFiles = nativeSources().map(_.path).flatMap(os.walk(_)).filter(_.ext == "c")
val output = "libhelloworld.so"
os.proc(
"clang",
"-shared",
"-fPIC",
"-I" + sys.props("java.home") + "/include/", // global JVM header files
"-I" + sys.props("java.home") + "/include/darwin",
"-I" + sys.props("java.home") + "/include/linux",
"-o",
T.dest / output,
cSourceFiles
)
.call(stdout = os.Inherit)
PathRef(T.dest / output)
}
def forkEnv = Map("HELLO_WORLD_BINARY" -> nativeCompiled().path.toString)
object test extends KotlinTests with TestModule.Junit5 {
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"io.kotest:kotest-runner-junit5-jvm:5.9.1"
)
def forkEnv = Map("HELLO_WORLD_BINARY" -> nativeCompiled().path.toString)
}
}
This is an example of how use Mill to compile C code together with your Kotlin
code using JNI. There are three two steps: defining the C source folder,
and then compiling the C code using clang
. After that we have the
libhelloworld.so
on disk ready to use, and in this example we use an
environment variable to pass the path of that file to the application
code to load it using System.load
.
The above builds expect the following project layout:
build.mill src/ foo/ HelloWorld.kt native-src/ HelloWorld.c test/ src/ foo/ HelloWorldTest.kt
This example is pretty minimal, but it demonstrates the core principles, and
can be extended if necessary to more elaborate use cases. The native*
tasks
can also be extracted out into a trait
for re-use if you have multiple
`KotlinModule`s that need native C components.
> ./mill run
Hello, World!
> ./mill test
Test foo.HelloWorldTest simple started
Test foo.HelloWorldTest simple finished...
...