Config-Based Kotlin Modules

This page documents the usage of config-based Kotlin modules defined by build.mill.yaml and package.mill.yaml files. These are less flexible but easier to get started with than the full build.mill or package.mill build files, which makes them ideal for small projects which do not need additional flexibility.

This page is not meant to be comprehensive reference, but rather is intended to give an introduction to Mill’s config-based module syntax. For more details on these topics, see the dedicated pages for each topic linked from the left navigation bar.

Common Config Overrides

This example shows some of the common tasks you may want to override on a KotlinModule: specifying the mainClass, adding additional sources/resources, and setting compilation/run options.

build.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule]
kotlinVersion: 1.9.24
mvnDeps:
- org.jetbrains.kotlinx:kotlinx-html:0.11.0

# Add a custom maven repository URL
repositories: ["https://oss.sonatype.org/content/repositories/releases"]

# Set an explicit main class
mainClass: "foo.Foo2Kt"

# Add an additional source folder and resource folder, in addition to the default one
sources: ["./src", "./custom-src"]
resources: ["./resources", "./custom-resources"]

# Configure JVM runtime options and env vars
forkArgs: ["-Dmy.custom.property=my-prop-value"]
forkEnv: { "MY_CUSTOM_ENV": "my-env-value" }
> ./mill run
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
MY_CUSTOM_ENV: my-env-value

> ./mill show assembly
".../out/assembly.dest/out.jar"

> ./out/assembly.dest/out.jar # mac/linux
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value

Paths such as ./custom-src or ./custom-resources are relative to the moduleDir of the module being configured. In the example above this is the root folder of your codebase, but in multi-module projects this may be a subfolder containing the current package.mill.yaml file.

Most Tasks that you can define programmatically in a build.mill file can be specified in a build.mill.yaml, as long as they involve simple configuration (strings, lists, maps, etc.) and do not need Custom Build Logic. For example, you can use the various configuration methods discussed in Kotlin Library Dependencies: runMvnDeps, compileMvnDeps, unmanagedClasspath, etc. For more flexibility, e.g. if you need to generate sources or resource files at build time, you can instead use Programmatic Modules.

Unit & Integration Testing

This example demonstrates more details about testing in Mill:

build.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule]
kotlinVersion: 1.9.24
test/package.mill.yaml (download, browse)
extends: [build.KotlinTests, mill.kotlinlib.TestModule.Junit5]
mvnDeps: [io.kotest:kotest-runner-junit5:5.9.1]
integration/package.mill.yaml (download, browse)
extends: [build.KotlinTests, mill.kotlinlib.TestModule.Junit5]
mvnDeps: [io.kotest:kotest-runner-junit5:5.9.1]
moduleDeps: [test]
  • The ability to have multiple test suites, e.g. unit tests in test/ and integration tests in integration/

  • Each test suite having its own set of mvnDeps to define the third-party libraries that it needs

  • Test-module dependencies, here the integration module has a moduleDeps on test, giving access to code and utilities defined in test/src/

These two test modules will expect their sources to be in their respective test/ and integration/ folders respectively. You can use Mill’s task query syntax to select the test modules which you want to run

> ./mill '{test,integration}' # run both test suites
Test qux.QuxTests hello finished...
Test qux.QuxTests world finished...
Test qux.QuxIntegrationTests helloworld finished...

> ./mill __.integration # run all integration test suites

> ./mill __.test # run all normal test suites

> ./mill __.testForked # run all test suites of any kind

For more details on testing in Mill, see Testing Kotlin Projects

Linting & Autoformatting

Mill config-based build.mill.yaml modules support all the same linting features as the more flexible programmatic module definitions, as described in Linting Kotlin Projects. This section covers two common workflows: autoformatting and linting your Kotlin module.

Autoformatting

To autoformat your Kotlin code with KtFmt, you can run ./mill mill.kotlinlib.ktfmt/ as shown below:

> cat src/foo/Foo.kt
package example
import kotlin.random.Random
fun main() {
if (Random.nextBoolean()) {
if (Random.nextBoolean()) {
if (Random.nextBoolean()) {
if (Random.nextBoolean()) {
if (Random.nextBoolean()) {
println("Hello World")
}
}
}
}
}
}

> ./mill mill.kotlinlib.ktfmt/
Done formatting .../src/foo/Foo.kt

> cat src/foo/Foo.kt
package example
import kotlin.random.Random
fun main() {
    if (Random.nextBoolean()) {
        if (Random.nextBoolean()) {
            if (Random.nextBoolean()) {
                if (Random.nextBoolean()) {
                    if (Random.nextBoolean()) {
                        println("Hello World")
                    }
                }
            }
        }
    }
}

Linting

To lint your code with Detekt, you can make yor module extend mill.javalib.errorprone.ErrorProneModule, which will allow you to run checks via ./mill __.detekt

build.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule, kotlinlib.detekt.DetektModule]
kotlinVersion: "2.2.20"
> ./mill __.detekt
error: ...NestedBlockDepth - 5/4 - [Function main is nested too deeply.] at .../src/foo/Foo.kt:5:5

Packaging & Publishing

Graal Native Image

This example demonstrates how to define a Mill project with a build.mill.yaml that can be packaged into a Graal native executable. To do so, your module must extend mill.scalalib.NativeImageModule, and set your jvmId to a GraalVM distribution as shown below. You can pass native-image specific command-line flags via nativeImageOptions

build.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule, mill.kotlinlib.NativeImageModule]
kotlinVersion: 2.0.20
jvmId: "graalvm-community:24"
nativeImageOptions: ["--no-fallback"]
> ./mill nativeImage

> out/nativeImage.dest/native-executable
Hello Graal Native: 24...

This generates an out/nativeImage.dest/native-executable binary that you can run later. These native binaries do not need a JVM installed in order to run, but they do require that the OS/CPU architecture that they are run on is the same as that on which they were built. See Building Native Image Binaries with Graal VM for more details.

Publishing

This example demonstrates how to publish a Mill project to to Maven Central. To do so, your module must extend mill.scalalib.PublishModule, and configure the publishing metadata shown below:

build.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule, mill.kotlinlib.PublishModule]
kotlinVersion: 2.0.20
publishVersion: "0.0.1"
artifactName: "example"
pomSettings:
  description: "Example"
  organization: "com.lihaoyi"
  url: "https://github.com/com.lihaoyi/example"
  licenses: ["MIT"]
  versionControl: "https://github.com/com.lihaoyi/example"
  developers: [{"name": "Li Haoyi", "email": "example@example.com"}]

You can publish this locally to your ~/.ivy2 filesystem artifact repository via:

> ./mill publishLocal
Publishing Artifact(com.lihaoyi,example...,0.0.1) to ivy repo ...

Apart from publishing locally, you can also publish this project to Sonatype Maven Central via:

> ./mill mill.javalib.SonatypeCentralPublishModule/

For more details on packaging and publishing in Mill, see Kotlin Packaging & Publishing

Example Web Project

This example shows off running a simple webserver in a module configured via build.mill.yaml

build.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule]
kotlinVersion: 2.0.20
mvnDeps: [io.ktor:ktor-server-core:2.3.7, io.ktor:ktor-server-cio:2.3.7]

In this example, the webserver script uses the Ktor framework to run the web server

src/WebServer.kt (download, browse)
package example
import io.ktor.server.application.*
import io.ktor.server.cio.*
import io.ktor.server.engine.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*

fun main() {
    embeddedServer(CIO, port = 8080) {
        routing {
            post("/reverse-string") {
                val body = call.receiveText()
                call.respondText(body.reversed())
            }
        }
    }.start(wait = true)
}
> ./mill runBackground

> curl -d 'helloworld' localhost:8080/reverse-string
dlrowolleh

> ./mill clean runBackground # shut down webserver
> ./mill runBackground

> curl -d 'helloworld' localhost:8080/reverse-string
dlrowolleh

> ./mill clean runBackground # shut down webserver

Testing Web Projects

This example web project comes with a small test suite in the test/ folder, configured by test/package.mill.yaml. This test suite spins up the server and makes a single HTTP reqeust against and checks it behaves as expected.

test/package.mill.yaml (download, browse)
extends: [build.KotlinTests, mill.javalib.TestModule.Junit5]
mvnDeps:
- io.ktor:ktor-client-core:2.3.7
- io.ktor:ktor-client-cio:2.3.7
- org.junit.jupiter:junit-jupiter-api:5.10.1
- org.junit.jupiter:junit-jupiter-engine:5.10.1
> ./mill test
Test example.WebServerTests#testReverseString() finished...

For more detailed examples of web development with Mill, see Kotlin Web Project Examples

Custom Module Traits

Config-based Kotlin modules can inherit from user-defined traits via extends, and are not limited to the builtin traits provided by Mill:

build.mill.yaml (download, browse)
extends: [millbuild.LineCountKotlinModule]
kotlinVersion: 2.0.20
mill-build/src/LineCountKotlinModule.scala (download, browse)
package millbuild
import mill.*, kotlinlib.*

trait LineCountKotlinModule extends mill.kotlinlib.KotlinModule {

  /** Total number of lines in module source files */
  def lineCount = Task {
    allSourceFiles().map(f => os.read.lines(f.path).size).sum
  }

  /** Generate resources using lineCount of sources */
  override def resources = Task {
    os.write(Task.dest / "line-count.txt", "" + lineCount())
    super.resources() ++ Seq(PathRef(Task.dest))
  }
}
> ./mill run
...
Line Count: 17

> ./mill show lineCount
17

Programmatic moduleDeps

Config-based modules can depend on programmatic modules, and vice versa. This allows you to use simple config-based build.mill.yaml files for modules without any special requirements, use programmatic build.mill files for modules with custom tasks or other more advanced customizations, and have them inter-operate with each other seamlessly.

build.mill (download, browse)
package build
import mill.*, kotlinlib.*

object bar extends KotlinModule {
  def kotlinVersion = "1.9.24"
  def mvnDeps = Seq(
    mvn"org.jetbrains.kotlinx:kotlinx-html:0.11.0"
  )
}
foo/package.mill.yaml (download, browse)
extends: [mill.kotlinlib.KotlinModule]
moduleDeps: [bar]
kotlinVersion: 2.0.20
mvnDeps: [com.github.ajalt.clikt:clikt:4.4.0]
> ./mill foo.run --text hello
<h1>hello</h1>