Building Kotlin with Mill
Mill supports three main ways of defining Kotlin projects:
-
Config-Based Modules: simple projects that require basic setting of dependencies and versions
-
Single-File Scripts: small programs with both code and build configuration contained within a single file
-
Programmatic Modules: builds which require custom logic, e.g. code generation or resource processing
This page walks through a series of Mill builds of increasing complexity to show you the key features and usage of the Mill build tool. The other pages of this section of the docs go into more depth into individual Mill features: testing, publishing, and so on. Every example can be downloaded to try it yourself.
The API reference for Mill’s Kotlin toolchain can be found at mill.kotlinlib
Config-Based Modules
extends: [mill.kotlinlib.KotlinModule]
kotlinVersion: "2.0.20"
mvnDeps:
- "com.github.ajalt.clikt:clikt:4.4.0"
- "org.jetbrains.kotlinx:kotlinx-html:0.11.0"
extends: [build.KotlinTests, mill.javalib.TestModule.Junit5]
mvnDeps:
- "io.kotest:kotest-runner-junit5:5.9.1"
- "com.github.sbt.junit:jupiter-interface:0.11.2"
This is a basic Mill build for a single KotlinModule, with two
third-party dependencies and a test suite using the JUnit framework.
This uses Mill’s config-based .mill.yaml files for defining the
modules:
-
build.mill.yamldefines the configuration for the root module -
test/package.mill.yamldefines the submoduletest/containing the test suites.
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.
The test/ submodule contains its own mvnDeps
for dependencies that are only used in the test suite.
Basic Folder Layout
The source code for this module lives in the src/ folder.
Output for this module (compiled files, resolved dependency lists, …)
lives in out/. A typical filesystem layout is shown below:
build.mill.yaml
src/
foo/Foo.kt
resources/
...
test/
package.mill.yaml
src/
foo/FooTest.kt
out/
compile.json
compile.dest/
...
test/
compile.json
compile.dest/
...
The default Mill source folder layout src/ differs from that of Maven/Gradle’s
src/main/java/.
If you wish to use the Maven source folder layout, e.g. for migrating
an existing codebase, you should use
Maven-Compatible Modules
|
Typical Usage
Typical usage of this example project from the command line is shown below:
> ./mill resolve _ # List what tasks are available to run
assembly
...
compile
...
run
...
> ./mill inspect compile # Show documentation and inputs of a task
compile(KotlinModule.scala:...)
Compiles the current module to generate compiled classfiles/bytecode.
Inputs:
upstreamCompileOutput
allJavaSourceFiles
allKotlinSourceFiles
compileClasspath
> ./mill compile # compile sources into classfiles
...
Compiling 1 Kotlin sources to...
> ./mill test
Test foo.FooTest testSimple finished, ...
Test foo.FooTest testEscaping finished, ...
Test foo.FooTest finished, ...
Test run finished: 0 failed, 0 ignored, 2 total, ...
> ./mill run --text hello
<h1>hello</h1>
> ./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>
> # Note that on windows you need to rename `out.jar` to `out.bat` to run it without `java -jar`
> cp ./out/assembly.dest/out.jar out.bat # windows
> ./out.bat --text hello # windows
<h1>hello</h1>
The output of every Mill task is stored in the out/ folder under a name
corresponding to the task that created it. e.g. The assembly task puts its
metadata output in out/assembly.json, and its output files in
out/assembly.dest. You can also use show to print out the
metadata output for a particular task, or inspect to print metadata about
the task itself (docs, source location, etc.)
Additional Mill tasks you would likely need include:
> ./mill resolve __ # recursively list all tasks and modules that are available
> ./mill runBackground # run the main method in the background
> ./mill clean <task> # delete the cached output of a task, terminate any runBackground
> ./mill launcher # prepares a out/launcher.dest/run you can run later
> ./mill jar # bundle the classfiles into a jar suitable for publishing
> ./mill -w compile # watch input files and re-compile whenever a file changes
The most common tasks that Mill can run are cached tasks, such as
compile, and un-cached commands such as run. Cached tasks do not
re-evaluate unless one of their inputs changes, whereas commands re-run every
time. See the documentation for Tasks for
details on the different task types.
This example uses Mill’s config-based YAML syntax, which is a good fit for simple builds where you are just setting some configuration keys. This is discussed more in the documentation for:
Multi-Module Projects
This example contains a simple Mill build with two modules, foo and bar,
defined by their respective package.mill.yaml config
files, and on which you can run tasks on such as foo.run or
bar.run. There is also a bar.test module defined by bar/test/package.mill.yaml.
extends: [mill.kotlinlib.KotlinModule]
moduleDeps: [bar]
kotlinVersion: 2.0.20
mvnDeps:
- com.github.ajalt.clikt:clikt:4.4.0
extends: [mill.kotlinlib.KotlinModule]
kotlinVersion: 2.0.20
mvnDeps:
- org.jetbrains.kotlinx:kotlinx-html:0.11.0
extends: [build.bar.KotlinTests, mill.javalib.TestModule.Junit5]
mvnDeps:
- io.kotest:kotest-runner-junit5:5.9.1
- com.github.sbt.junit:jupiter-interface:0.11.2
Inter-module dependencies are defined by the moduleDeps key, and modules
can also be nested within each other, as bar.test is nested
within bar.
Multi-Module Folder Layout
This multi-module example build expects the following folder layout:
build.mill
foo/
src/
foo/Foo.kt
bar/
src/
bar/Bar.kt
test/
src/
bar/BarTests.kt
out/
foo/
compile.json
compile.dest/
...
bar/
compile.json
compile.dest/
...
test/
compile.json
compile.dest/
...
Typically, source, output files, and task names in Mill follow the module
hierarchy, so e.g. input to the foo module lives in foo/src/, can be
compiled via foo.compile and, and its compiled output files live in
out/foo/compile.dest.
You can use mill resolve to list out what tasks you can run, e.g.
mill resolve __.run below which lists out all the run tasks:
> ./mill resolve __.run
foo.run
bar.run
> ./mill foo.run --text "hello-world"
<h1>hello-world</h1>
> ./mill bar.test
...
...bar.BarTests...simple...
...bar.BarTests...escaping...
In general, Mill will ensure that the specified tasks and their upstream
dependencies are executed in the right order, and re-executed as necessary when
source code in each module changes. For example if you run foo.compile:
-
Mill will automatically run
bar.compileand any other upstream tasks first if they have not been executed yet, or are out of date -
Mill will automatically re-use the cached output for
bar.compileif it was executed earlier and its upstream source files have not changed
Task Query Syntax
You can use wildcards and brace-expansion to select
multiple tasks at once or to shorten the path to deeply nested tasks,
as we saw with __.run above. Some more examples of this are shown below:
Wildcard |
Function |
|
matches a single segment of the task path |
|
matches arbitrary segments of the task path |
|
is equal to specifying two tasks |
You can use the + symbol to add another task with optional arguments.
If you need to feed a + as argument to your task, you can mask it by
preceding it with a backslash (\).
> ./mill bar._.compile # Runs `compile` for all direct sub-modules of `foo`
> ./mill bar.__.test # Runs `test` for all transitive sub-modules of `foo`
> ./mill {foo,bar}.compile # Runs `compile` for `foo` and `bar`
> ./mill __.compile + bar.__.test # Runs all `compile` tasks and all tests under `foo`.
If you provide optional task arguments and your wildcard or brace-expansion is resolved to multiple tasks, the arguments will be applied to each of the tasks. For more details on the query syntax, see the page Task Query Syntax
Maven Compatible Modules
Mill’s default folder layout of src/ and test/src differs from the src/main/scala/
and src/test/scala/ common in Maven, Gradle, or SBT. If you are
adopting Mill in an existing codebase, you can use Mill’s KotlinMavenModule and
KotlinMavenTests as shown below to preserve filesystem compatibility with an existing
build:
extends: [mill.kotlinlib.KotlinMavenModule]
kotlinVersion: "2.0.20"
mvnDeps:
- "com.github.ajalt.clikt:clikt:4.4.0"
- "org.jetbrains.kotlinx:kotlinx-html:0.11.0"
object test:
extends: [KotlinMavenTests, mill.javalib.TestModule.Junit5]
mvnDeps:
- "io.kotest:kotest-runner-junit5:5.9.1"
- "com.github.sbt.junit:jupiter-interface:0.11.2"
object integration:
extends: [KotlinMavenTests, mill.javalib.TestModule.Junit5]
mvnDeps:
- "io.kotest:kotest-runner-junit5:5.9.1"
- "com.github.sbt.junit:jupiter-interface:0.11.2"
Note the object test: and object integration: keys in the above file: these allow
you to define submodules in the same build.mill.yaml file as the parent module without
needing a separate package.mill.yaml in a subfolder. This is necessary in scenarios
like this one where we want test and integration submodules but the filesystem layout
means we do not have test/ or integration/ folders to put a package.mill.yaml in.
KotlinMavenModule is a variant of KotlinModule
that uses the more verbose folder layout of Maven, SBT, and other tools:
-
src/main/java -
src/main/kotlin -
src/test/java -
src/test/kotlin -
src/integration/java -
src/integration/kotlin
Rather than Mill’s
-
src -
test/src
One use case of these compatibility modules is migrations: while migrating to Mill, a
project may be built using your previous build tool and Mill at the same time. Using
KotlinMavenModules means that during migration, you can leave all your source files
in place while setting up your Mill build, and do not need to invasively move them
around to match the Mill default module layout.
Although the source layout of these compatibility modules is different from the
default KotlinModule, the command-line usage is the same:
> ./mill compile
Compiling 1 Kotlin source...
> ./mill test.compile
Compiling 1 Kotlin source...
> ./mill test.testForked
...foo.FooTests hello ...
> ./mill test
...foo.FooTests hello ...
> ./mill integration
...foo.FooIntegrationTests hello ...
For more details on migrating from other build tools, see Migrating to Mill
Single-File Scripts
Mill allows you to run single-file Kotlin programs
easily from the command-line, even those that contain third-party dependencies, specific
JVM versions, or other such build configuration. Unlike Kotlin programs built with other
tools, Mill scripts run via a ./mill
bootstrap script run reproducibly
without needing any prior installation or setup.
This can be useful for several purposes:
-
A replacement for Bash scripts: Mill instead lets you write small Kotlin scripts or programs in Kotlin with full access to third-party libraries that run in a reproducible way across diverse dev/test/prod environments
-
Self-contained examples or issue reproductions, as you can include both the code and dependencies necessary in a self-contained file that can be run using
./millwithout manual setup or installation -
Small projects or experiments, where setting up and maintaining a separate build file can be a hassle, and putting both code and config in a single file makes things easier to manage
For example the Kotlin program below can be run directly using Mill, which will automatically download and cache the specified third-party dependencies as necessary:
//| jvmId: 11.0.28
//| mvnDeps:
//| - "com.github.ajalt.clikt:clikt:4.4.0"
//| - "org.jetbrains.kotlinx:kotlinx-html:0.11.0"
import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.parameters.options.required
import kotlinx.html.h1
import kotlinx.html.stream.createHTML
class Foo : CliktCommand() {
val text by option("-t", "--text", help = "text to insert").required()
override fun run() {
echo("Jvm Version: " + System.getProperty("java.version"))
echo(generateHtml(text))
}
}
fun generateHtml(text: String): String = createHTML(prettyPrint = false).h1 { text(text) }.toString()
fun main(args: Array<String>) = Foo().main(args)
Apart from the mvnDeps config that allows you to use third-party libraries in
your script, the jvmId config lets you specify exactly what JVM version you wish
to use in this script. Like other Mill modules, Mill scripts will default to a Mill’s
own default JVM version of zulu:21 if a jvmId is not provided.
If you want to use the environmentally installed java command available on your path,
you must explicitly set jvmId: system.
This script can be run as shown below:
> ./mill Foo.kt --text hello
Compiling 1 Kotlin sources to...
<h1>hello</h1>
Mill will automatically download and cache the necessary artifacts
on your behalf, allowing you run these scripts via ./mill and have them behave
the same regardless of what environment it is running in without needing any setup.
The ./mill Foo.kt syntax is shorthand for ./mill Foo.kt:run.
You can also call other tasks on your script modules, such as Foo.kt:assembly
below:
> ./mill Foo.kt:run --text hello
<h1>hello</h1>
> ./mill show Foo.kt:assembly # show the output of the assembly task
".../out/Foo.kt/assembly.dest/out.jar"
> java -jar ./out/Foo.kt/assembly.dest/out.jar --text hello
<h1>hello</h1>
> ./out/Foo.kt/assembly.dest/out.jar --text hello # mac/linux
<h1>hello</h1>
Kotlin scripts support the same configuration keys as
Config-Based Modules as part of their //| header comment, and support most
the tasks via the :run :assembly etc. command-line syntax shown above. You can list
these tasks via ./mill resolve as shown below:
> ./mill resolve Foo.kt:_
run
runMain
compile
assembly
...
Testing Scripts
Script files can have test suites, usually written in a separate test script. The test script configuration specifies
-
What test framework it uses via
extends -
What script it tests via
moduleDeps -
Any
mvnDepsneeded by the code, in addition to those of the upstream script.
The test script can then run tests that exercise the upstream script as shown below:
//| extends: [mill.script.KotlinModule.Junit5]
//| moduleDeps: [./Foo.kt]
//| mvnDeps:
//| - "io.kotest:kotest-runner-junit5:5.9.1"
//| - "com.github.sbt.junit:jupiter-interface:0.11.2"
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><hello></h1>"
}
})
> ./mill FooTest.kt
Test FooTest testSimple started
Test FooTest testEscaping started
Again, you can pass the name of the task explicitly via :, e.g. :testForked below
> ./mill FooTest.kt:testForked # specifying the test task explicitly
Test FooTest testSimple started
Test FooTest testEscaping started
The testing framework used in a script is defined by the class specified in the
extends clause. The different testing frameworks supported in Testing Kotlin Projects
can be used for your scripts: Junit4, Junit5, TestNg, Munit, ScalaTest, Specs2,
Utest, Weaver, ZioTest. If you need something not on this list, you can define
a Custom Script Module Class.
For scripts that grow larger than a single file, you should convert them to Config-Based Modules.
Programmatic Modules
package build
import mill.*, kotlinlib.*
object foo extends KotlinModule {
def kotlinVersion = "1.9.24"
def mvnDeps = Seq(
mvn"com.github.ajalt.clikt:clikt:4.4.0",
mvn"org.jetbrains.kotlinx:kotlinx-html:0.11.0"
)
object test extends KotlinTests, TestModule.Junit5 {
def mvnDeps = Seq(
mvn"io.kotest:kotest-runner-junit5:5.9.1"
)
}
}
This is an example of Mill’s programmatic configuration syntax.
This is slightly more verbose than the YAML syntax shown above, but
in exchange allows more flexibility in how tasks are defined and their
values are computed. Keys such as mvnDeps: in the YAML syntax correspond
directly to the def mvnDeps methods in the programmatic syntax.
build.mill
foo/
src/
foo/Foo.kt
resources/
...
test/
src/
foo/FooTest.kt
out/foo/
compile.json
compile.dest/
...
test/
compile.json
compile.dest/
...
This example places the KotlinModule in the foo/ subfolder, but you can also use
define a Root Module to place it at the
root of your repository similar to the build.mill.yaml examples discussed in
Config-Based Modules
Usage of programmatic Mill builds is similar to usage of simple config-based Mill builds:
> ./mill resolve foo._ # List what tasks are available to run
foo.assembly
...
foo.compile
...
foo.run
...
> ./mill foo.run --text hello
<h1>hello</h1>
> ./mill foo.test
Test foo.FooTest testSimple finished, ...
Test foo.FooTest testEscaping finished, ...
Test foo.FooTest finished, ...
> ./mill foo.assembly # bundle classfiles and libraries into a jar for deployment
> ./mill show foo.assembly # show the output of the assembly task
".../out/foo/assembly.dest/out.jar"
> java -jar ./out/foo/assembly.dest/out.jar --text hello
<h1>hello</h1>
Programmatic Mill build files are written in Scala, but you do not need to have prior experience in Scala to read or write them. Like Gradle Groovy or Maven XML, anyone can learn enough Scala for Mill without needing to become an expert in the language. For simpler builds that do not need the flexibility that programmatic builds provide, YAML-based Config-Based Module definitions are a great alternative.
Custom Build Logic
Programmatic Mill builds make it very easy to customize your build graph, overriding portions
of it with custom logic. In this example, we override the JVM resources of
our KotlinModule - normally the resources/ folder - to also contain a
single generated text file containing the line count of all the source files
in that module
package build
import mill.*, kotlinlib.*
object foo extends KotlinModule {
def kotlinVersion = "1.9.24"
def mainClass = Some("foo.FooKt")
/** 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))
}
object test extends KotlinTests, TestModule.Junit5 {
def mvnDeps = super.mvnDeps() ++ Seq(
mvn"io.kotest:kotest-runner-junit5:5.9.1"
)
}
}
-
override def resourcesreplaces the previousresourcefolder provided byKotlinModule(labelledresources.superbelow), including both the previous resource foldersuper.resources()together with theTask.destfolder of the new task which contains alint-count.txtfile we write.
-
os.read.linesandos.writecome from the OS-Lib library, which is one of Mill’s Bundled Libraries. You can also import any JVM library you want from Maven Central using //| mvnDeps, so you are not limited to what is bundled with Mill. -
The
overridekeyword is optional in Mill. It is shown above for clarity, but can be elided for conciseness.
In general, this use of method defs, method calls, override, and super should be
very familiar to anyone with prior experience working in Kotlin. These are the same
concepts that have underpinned object-oriented programming for decades, and work exactly
the same here as they work anywhere else.
This generated line-count.txt file can then be loaded and used at runtime, as see
in the output of mill run below.
> ./mill foo.run
...
Line Count: 17
> ./mill show foo.lineCount
17
> ./mill inspect foo.lineCount
foo.lineCount(build.mill:...)
Total number of lines in module source files
Inputs:
foo.allSourceFiles
If you’re not familiar with what tasks you can override or how they are related,
you can explore the existing tasks via autocomplete in your IDE, or use the
mill visualize.
Custom user-defined tasks in Mill such as def lineCount above benefit from all
the same things that built-in tasks do: automatic caching (in the
out/ folder), parallelism (configurable via -j/--jobs
flag), inspectability (via show and inspect), and so on.
While these things may not matter for such a simple example that runs
quickly, they ensure that custom build logic remains performant and
maintainable even as the complexity of your project grows.
Programmatic Multi-Module Project
package build
import mill.*, kotlinlib.*
trait MyModule extends KotlinModule {
def kotlinVersion = "1.9.24"
object test extends KotlinTests, TestModule.Junit5 {
def mvnDeps = super.mvnDeps() ++ Seq(
mvn"io.kotest:kotest-runner-junit5:5.9.1"
)
}
}
object foo extends MyModule {
def moduleDeps = Seq(bar)
def mvnDeps = Seq(
mvn"com.github.ajalt.clikt:clikt:4.4.0"
)
}
object bar extends MyModule {
def mainClass = Some("bar.BarKt")
def mvnDeps = Seq(
mvn"org.jetbrains.kotlinx:kotlinx-html:0.11.0"
)
}
This is similar to the Multi-Module Projects example above, but using Mill’s
programmatic configuration syntax. You can define multiple modules the same way you
define a single module, using def moduleDeps to define the relationship between them.
Modules can also be nested within each other, as foo.test and bar.test are nested
within foo and bar respectively
Note that we split out the test submodule configuration common to both
modules into a separate trait MyModule. This Trait Module
works like a class in Java, and lets us avoid the need to copy-paste common
settings, while still letting us define any per-module configuration such as
mvnDeps specific to a particular module. This is a common pattern within Mill builds.
> ./mill bar.test
Test bar.BarTests simple finished...
Test bar.BarTests escaping finished...
> ./mill foo.run --foo-text hello --bar-text world
Foo.value: hello
Bar.value: <h1>world</h1>
You can also put the configuration for each submodule in it’s respective
folder’s package.mill file, as described in Multi-File Builds.
This can be helpful in larger projects to avoid having your build.mill grow
large, and aid in discoverability by keeping the build configuration for each
module close to the code it is configuring
Programmatic Compatibility Modules
This example is similar to the Maven-Compatible Modules
above, but using Mill’s programmatic build.mill files. These are more flexible than
the config-driven build.mill.yaml files, and can contain
Custom Build Logic and other programmatic
configuration
package build
import mill.*, kotlinlib.*
object foo extends KotlinModule, KotlinMavenModule {
def kotlinVersion = "1.9.24"
object test extends KotlinMavenTests, TestModule.Junit5 {
def mvnDeps = Seq(mvn"io.kotest:kotest-runner-junit5:5.9.1")
}
object integration extends KotlinMavenTests, TestModule.Junit5 {
def mvnDeps = Seq(mvn"io.kotest:kotest-runner-junit5:5.9.1")
}
}
> ./mill foo.compile
Compiling 1 Kotlin source...
> ./mill foo.test.compile
Compiling 1 Kotlin source...
> ./mill foo.test.testForked
...foo.FooTests hello ...
> ./mill foo.test
...foo.FooTests hello ...
> ./mill foo.integration
...foo.FooIntegrationTests hello ...
Realistic Java Example Project
Below, we should a realistic example of a build for a Kotlin project. This example touches on library dependencies, testing, publishing, code generation, and other topics covered in more detail in the Kotlin section of the Mill docs, and you can browse each respective page if you want to learn more.
package build
import mill.*, kotlinlib.*, publish.*
trait MyModule extends KotlinModule, PublishModule {
def publishVersion = "0.0.1"
def pomSettings = PomSettings(
description = "Hello",
organization = "com.lihaoyi",
url = "https://github.com/lihaoyi/example",
licenses = Seq(License.MIT),
versionControl = VersionControl.github("lihaoyi", "example"),
developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
)
def kotlinVersion = "1.9.24"
def mvnDeps = Seq(mvn"org.jetbrains.kotlinx:kotlinx-html:0.11.0")
object test extends KotlinTests, TestModule.Junit5 {
def mvnDeps = super.mvnDeps() ++ Seq(
mvn"io.kotest:kotest-runner-junit5:5.9.1"
)
}
}
object foo extends MyModule {
def moduleDeps = Seq(bar, qux)
def generatedSources = Task {
os.write(
Task.dest / "Version.kt",
s"""
|package foo
|
|object Version {
| fun value() = "${publishVersion()}"
|}
""".stripMargin
)
Seq(PathRef(Task.dest))
}
}
object bar extends MyModule {
def moduleDeps = Seq(qux)
}
object qux extends MyModule {
def mainClass = Some("qux.QuxKt")
}
A semi-realistic build setup, combining all the individual Mill concepts:
-
Three
KotlinModules that depend on each other -
With unit testing and publishing set up
-
With generated sources to include the
publishVersionas a string in the code, so it can be printed at runtime
Note that for multi-module builds like this, using using queries to run tasks on multiple modules at once can be very convenient:
> mill __.test
> mill __.publishLocal
Also note how you can use traits to bundle together common combinations of
modules: MyModule not only defines a KotlinModule with some common
configuration, but it also defines a object test module within it with its
own configuration. This is a very useful technique for managing the often
repetitive module structure in a typical project
> ./mill resolve __.run
bar.run
bar.test.run
foo.run
foo.test.run
qux.run
> ./mill foo.run
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: 31337
> ./mill bar.test
...bar.BarTests world ...
> ./mill qux.run
Qux.value: 31337
> ./mill __.compile
> ./mill __.test
...bar.BarTests world ...
...foo.FooTests hello ...
> ./mill __.publishLocal
Publishing Artifact(com.lihaoyi,foo,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux,0.0.1) to ivy repo...
...
> ./mill show foo.assembly # mac/linux
".../out/foo/assembly.dest/out.jar"
> ./out/foo/assembly.dest/out.jar # mac/linux
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: 31337
History
Mill’s Kotlin support originated as the third-party plugin lefou/mill-kotlin, which was later included with the main Mill codebase under its Apache 2.0 License.