Building Scala with Mill

This page contains a quick introduction to getting start with using Mill to build a simple Scala program. We will walk 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 on Scala go into more depth into individual features, with more examples of how to use Mill for Scala and more details of how the Mill build tool works. They aren’t intended to be read comprehensively top-to-bottom, but rather looked up when you have a particular interest e.g. in testing, linting, publishing, and so on.

Simple Scala Module

build.mill (download, browse)
package build
import mill._, scalalib._

object foo extends ScalaModule {
  def scalaVersion = "2.13.11"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::scalatags:0.12.0",
    ivy"com.lihaoyi::mainargs:0.6.2"
  )

  object test extends ScalaTests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
    def testFramework = "utest.runner.Framework"
  }
}

This is a basic Mill build for a single ScalaModule, with two third-party dependencies and a test suite using the uTest framework.

You can download this example project using the download link above if you want to try out the commands below yourself, or browse the full sources of the example (including supporting files) via the browse link. The only requirement is that you have some version of the JVM installed; the ./mill script takes care of any further dependencies that need to be downloaded. All examples in this documentation site are executable and are continually exercised as part of Mill’s CI workflows, and they range from the simple hello-world projects on this page to more sophisticated web build examples or example builds for real-world projects

The source code for this module lives in the src/ folder. Output for this module (compiled files, resolved dependency lists, …​) lives in out/.

build.mill
foo/
    src/
        Foo.scala
    resources/
        ...
    test/
        src/
            FooTests.scala
out/foo/
    compile.json
    compile.dest/
    ...
    test/
        compile.json
        compile.dest/
        ...
The default Mill source folder layout foo/src/ differs from that of SBT’s foo/src/main/scala. If you wish to use the SBT source folder layout, e.g. for migrating an existing codebase, you should use SBT-Compatible Modules

This example project uses two third-party dependencies - MainArgs for CLI argument parsing, Scalatags for HTML generation - and uses them to wrap a given input string in HTML templates with proper escaping.

Typical usage of a ScalaModule is shown below:

> ./mill resolve foo._ # List what tasks are available to run
foo.assembly
...
foo.compile
...
foo.run
...

> ./mill inspect foo.compile # Show documentation and inputs of a task
foo.compile(ScalaModule.scala:...)
    Compiles the current module to generate compiled classfiles/bytecode.
Inputs:
    foo.scalaVersion
    foo.upstreamCompileOutput
    foo.allSourceFiles
    foo.compileClasspath

> ./mill foo.compile # compile sources into classfiles
...
compiling 1 Scala source to...

> ./mill foo.run # run the main method, if any
error: Missing argument: --text <str>

> ./mill foo.run --text hello
<h1>hello</h1>

> ./mill foo.test
...
+ foo.FooTests...simple ...  <h1>hello</h1>
+ foo.FooTests...escaping ...  <h1>&lt;hello&gt;</h1>

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

> ./out/foo/assembly.dest/out.jar --text hello # mac/linux
<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 make Mill print out the metadata output for a particular task.

Additional Mill tasks you would likely need include:

$ 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 foo/launcher.dest/run you can run later

$ mill jar           # bundle the classfiles into a jar suitable for publishing

$ mill -i console    # start a Scala console within your project

$ mill -i repl       # start an Ammonite Scala REPL within your project

$ mill -w compile    # watch input files and re-compile whenever a file changes

You can run mill resolve __ to see a full list of the different tasks that are available, mill resolve _ to see the tasks within foo, mill inspect compile to inspect a task’s doc-comment documentation or what it depends on, or mill show foo.scalaVersion to show the output of any task.

The most common tasks that Mill can run are cached tasks, such as compile, and un-cached commands such as foo.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.

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.

Custom Build Logic

Mill makes 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 ScalaModule - normally the resources/ folder - to instead contain a single generated text file containing the line count of all the source files in that module

build.mill (download, browse)
package build
import mill._, scalalib._

object foo extends ScalaModule {
  def scalaVersion = "2.13.11"

  /** 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))
  }
}

The addition of lineCount and resources overrides the previous resource folder provided by ScalaModule (labelled resources.super below), replacing it with the destination folder of the new resources task, which is wired up to lineCount:

G allSourceFiles allSourceFiles lineCount lineCount allSourceFiles->lineCount resources resources lineCount->resources ... ... resources->... run run ...->run resources.super resources.super resources.super->resources
> 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

Above, def lineCount is a new build task we define, which makes use of allSourceFiles (an existing task) and is in-turn used in our override of resources (also an existing task). The override keyword is optional in Mill. This generated file can then be loaded and used at runtime, as see in the output of mill run.

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.

os.read.lines and os.write come from the OS-Lib library, which is one of Mill’s Bundled Libraries. You can also import any other library you want from Maven Central using import $ivy, so you are not limited to what is bundled with Mill.

Custom user-defined tasks in Mill benefit from all the same things that built-in tasks do: automatic caching (in the out/ folder), parallelism (with the -j/--jobs flag), inspectability (via show/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.

Multi-Module Project

build.mill (download, browse)
package build
import mill._, scalalib._

trait MyModule extends ScalaModule {
  def scalaVersion = "2.13.11"
  object test extends ScalaTests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
    def testFramework = "utest.runner.Framework"
  }
}

object foo extends MyModule {
  def moduleDeps = Seq(bar)
  def ivyDeps = Agg(ivy"com.lihaoyi::mainargs:0.4.0")
}

object bar extends MyModule {
  def ivyDeps = Agg(ivy"com.lihaoyi::scalatags:0.8.2")
}

This example contains a simple Mill build with two modules, foo and bar, which you can run tasks on such as foo.run or bar.run. 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 lets us avoid the need to copy-paste common settings, while still letting us define any per-module configuration such as ivyDeps specific to a particular module. This is a common pattern within Mill builds.

The above builds expect the following project layout:

build.mill
foo/
    src/
        Foo.scala
bar/
    src/
        Bar.scala
    test/
        src/
            BarTests.scala
out/
    foo/
        compile.json
        compile.dest/
        ...
    bar/
        compile.json
        compile.dest/
        ...
        test/
            compile.json
            compile.dest/
            ...

Typically, both source code and output files in Mill follow the module hierarchy, so e.g. input to the foo module lives in foo/src/ and 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 --foo-text hello --bar-text world
Foo.value: hello
Bar.value: <h1>world</h1>

> mill bar.run world
Bar.value: <h1>world</h1>

> mill bar.test
...
...bar.BarTests...simple...
...bar.BarTests...escaping...

Mill’s evaluator will ensure that the modules are compiled in the right order, and recompiled as necessary when source code in each module changes. The unique path on disk that Mill automatically assigns each task also ensures you do not need to worry about choosing a path on disk to cache outputs, or filesystem collisions if multiple tasks write to the same path.

You can use wildcards and brace-expansion to select multiple tasks at once or to shorten the path to deeply nested tasks. 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.

Table 1. Wildcards and brace-expansion

Wildcard

Function

_

matches a single segment of the task path

__

matches arbitrary segments of the task path

{a,b}

is equal to specifying two tasks a and b

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 foo._.compile # Runs `compile` for all direct sub-modules of `foo`

> mill foo.__.test # Runs `test` for all transitive sub-modules of `foo`

> mill {foo,bar}.__.testCached # Runs `testCached` for all sub-modules of `foo` and `bar`

> mill __.compile + foo.__.test # Runs all `compile` tasks and all tests under `foo`.

For more details on the query syntax, check out the query syntax documentation

SBT-Compatible Modules

Mill’s default folder layout of foo/src/ and foo/test/src differs from that of SBT’s foo/src/main/scala/ and foo/src/test/scala/. If you are migrating an existing codebase from SBT, you can use Mill’s SbtModule and SbtTests as shown below to preserve filesystem compatibility with an existing SBT build:

build.mill (download, browse)
package build
import mill._, scalalib._

object foo extends SbtModule {
  def scalaVersion = "2.13.8"
  object test extends SbtTests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
    def testFramework = "utest.runner.Framework"
  }
}

object bar extends Cross[BarModule]("2.12.17", "2.13.8")
trait BarModule extends CrossSbtModule {
  object test extends CrossSbtTests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
    def testFramework = "utest.runner.Framework"
  }
}

SbtModule/CrossSbtModule are variants of ScalaModule/CrossScalaModule that use the more verbose folder layout of SBT, Maven, and other tools:

  • foo/src/main/scala

  • foo/src/main/scala-2.12

  • foo/src/main/scala-2.13

  • foo/src/test/scala

Rather than Mill’s

  • foo/src

  • foo/src-2.12

  • foo/src-2.13

  • foo/test/src

This is especially useful if you are migrating from SBT to Mill (or vice versa), during which a particular module may be built using both SBT and Mill at the same time

> mill foo.compile
compiling 1 Scala source...

> mill foo.test.compile
compiling 1 Scala source...

> mill foo.test.test
+ foo.FooTests...hello ...

> mill foo.test
+ foo.FooTests.hello ...

> mill bar[2.13.8].run
Bar.value: Hello World Scala library version 2.13.8...

> mill bar[2.12.17].run
Bar.value: Hello World Scala library version 2.12.17...

Realistic Scala Example Project

Below, we should a realistic example of a build for a Scala project. This example touches on library dependencies, testing, publishing, code generation, and other topics covered in more detail in the Scala section of the Mill docs, and you can browse each respective page if you want to learn more.

build.mill (download, browse)
package build
import mill._, scalalib._, publish._

trait MyModule extends 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"))
  )
}

trait MyScalaModule extends MyModule with CrossScalaModule {
  def ivyDeps = Agg(ivy"com.lihaoyi::scalatags:0.12.0")
  object test extends ScalaTests {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
    def testFramework = "utest.runner.Framework"
  }
}

val scalaVersions = Seq("2.13.8", "3.3.3")

object foo extends Cross[FooModule](scalaVersions)
trait FooModule extends MyScalaModule {
  def moduleDeps = Seq(bar(), qux)

  def generatedSources = Task {
    os.write(
      Task.dest / "Version.scala",
      s"""
package foo
object Version{
  def value = "${publishVersion()}"
}
      """.stripMargin
    )
    Seq(PathRef(Task.dest))
  }
}

object bar extends Cross[BarModule](scalaVersions)
trait BarModule extends MyScalaModule {
  def moduleDeps = Seq(qux)
}

object qux extends JavaModule with MyModule

A semi-realistic build setup, combining all the individual Mill concepts:

  • Two CrossScalaModules compiled against two Scala versions, that depend on each other as well as on a JavaModule

  • With unit testing and publishing set up

  • With version-specific sources

  • With generated sources to include the publishVersion as 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:

__.test
__.publishLocal

Also note that ScalaModules can depend on JavaModules, and when multiple inter-dependent modules are published they automatically will include the inter-module dependencies in the publish metadata.

Also note how you can use traits to bundle together common combinations of modules: MyScalaModule not only defines a ScalaModule 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[2.13.8].run
bar[2.13.8].test.run
bar[3.3.3].run
bar[3.3.3].test.run
foo[2.13.8].run
foo[2.13.8].test.run
foo[3.3.3].run
foo[3.3.3].test.run
qux.run

> mill foo[2.13.8].run
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world Specific code for Scala 2.x</p>
Qux.value: 31337

> mill bar[3.3.3].test
+ bar.BarTests.test ... <p>world Specific code for Scala 3.x</p>

> mill qux.run
Qux.value: 31337

> mill __.compile

> mill __.test
+ bar.BarTests.test ... <p>world Specific code for Scala 2.x</p>
+ bar.BarTests.test ... <p>world Specific code for Scala 3.x</p>
+ foo.FooTests.test ... <h1>hello</h1>
+ foo.FooTests.test ... <h1>hello</h1>

> mill __.publishLocal
Publishing Artifact(com.lihaoyi,foo_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_2.13,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,foo_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar_3,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux,0.0.1) to ivy repo...

> mill show foo[2.13.8].assembly # mac/linux
".../out/foo/2.13.8/assembly.dest/out.jar"

> ./out/foo/2.13.8/assembly.dest/out.jar # mac/linux
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world Specific code for Scala 2.x</p>
Qux.value: 31337