Simple Scala Modules

This page documents the usage of simple config-based Scala 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.

Common Configuration Overrides for Simple Modules

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

build.mill.yaml (browse)
extends: [mill.scalalib.ScalaModule]
scalaVersion: 2.13.16
mvnDeps:
- com.lihaoyi::scalatags:0.13.1
- com.lihaoyi::os-lib:0.11.4

mainClass: "foo.Foo2"
sources: ["./src", "./custom-src"]
resources: ["./resources", "./custom-resources"]
forkArgs: ["-Dmy.custom.property=my-prop-value"]
forkEnv: { "MY_CUSTOM_ENV": "my-env-value" }
scalacOptions: ["-deprecation", "-Xfatal-warnings"]
> ./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
> sed -i.bak 's/Foo2 {/Foo2 { println(this + "hello")/g' custom-src/Foo2.scala

> ./mill compile # demonstrate -deprecation/-Xfatal-warnings flags
error: object Foo2 { println(this + "hello")
error:                       ^
error: ...Implicit injection of + is deprecated. Convert to String to call +...

Note that the .build.yaml config files only let you set simple configuration keys, and do not let you extend your build with code e.g. to generate sources. For more flexibility, see Programmatic Module Configuration.

Packaging and Publishing Simple Modules

Mill Single-File Projects have the full flexibility of the Mill build tool, just configured using the YAML build header syntax rather than a separate build.mill file. That means you can configure the JVM version, compile flags or runtime flags, create Executable Assemblies or Graal Native Images, Publishing to Maven Central etc.

The example below shows a few of those options configured in the YAML build header of a single-file project, and then made of use from the command line:

build.mill.yaml (browse)
extends: [mill.scalalib.ScalaModule, mill.scalalib.PublishModule, mill.scalalib.NativeImageModule]
scalaVersion: 3.7.3
jvmId: "graalvm-community:24"
nativeImageOptions: ["--no-fallback"]
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"}]
> ./mill nativeImage

> out/nativeImage.dest/native-executable
Hello Graal Native: 24...
> ./mill publishLocal
Publishing Artifact(com.lihaoyi,example...,0.0.1) to ivy repo ...

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

> ./mill mill.javalib.SonatypeCentralPublishModule/

Most configuration defs in Mill can be used to configure single-file projects, and most tasks and commands can also be used as well. This gives you a lot of flexibility in working with your single-file project until it becomes complex enough to need a dedicated build.mill file.

Custom Script Module Classes

By default, single-file Mill script modules inherit their behavior from the standard mill.javalib.JavaModule, mill.scalalib.ScalaModule, or mill.scalalib.KotlinModule. However, you can also customize it to inherit from a custom Module class that you define as part of your meta-build in mill-build/src/. For example, if we want to add a resource file generated by processing the source file of the script, this can be done in a custom LineCountScalaModule as shown below:

build.mill.yaml (browse)
extends: [millbuild.LineCountScalaModule]
scalaVersion: 3.7.3
mill-build/src/LineCountScalaModule.scala (browse)
package millbuild
import mill.*, scalalib.*

trait LineCountScalaModule extends mill.scalalib.ScalaModule {

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

Your custom LineCountScalaModule must be a class take two parameters val millScriptFile: os.Path, and override val moduleDeps: Seq[JavaModule] that will be populated by Mill, and inherit from mill.simple.Scala.Base or some other subclass of mill.simple.SimpleModule. This can then be used via //| extends: LineCountScalaModule in the header of your script file.

Custom script module classes allows you to customize the semantics of your Java, Scala, or Kotlin single-file script modules. If you have a large number of scripts with a similar configuration, or you need customizations that cannot be done in the YAML build header, placing these customizations in a custom script module class can let you centrally define the behavior and standardize it across all scripts that inherit it via extends.

Depending on Programmatic Modules from Simple Modules

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

object bar extends ScalaModule {
  def scalaVersion = "3.7.1"
  def mvnDeps = Seq(mvn"com.lihaoyi::scalatags:0.13.1")
}
foo/package.mill.yaml (browse)
extends: [mill.scalalib.ScalaModule]
moduleDeps: [build.bar]
scalaVersion: 3.7.3
mvnDeps: [com.lihaoyi::mainargs:0.7.6]
bar/src/Bar.scala (browse)
package bar
import scalatags.Text.all.*
object Bar {
  def generateHtml(text: String) = {
    val value = h1(text)
    value.toString
  }
}
> ./mill foo.run --text hello
<h1>hello</h1>

Single-File Scripts

Mill also allows you to run single-file Scala programs (often referred to as scripts) easily from the command-line, even those that contain third-party dependencies or other such configuration. These can be useful as a replacement for Bash scripts, letting you write small scripts or programs in Scala with full access to third-party libraries and other build-tool features.

For example, given the Scala program below, it can be run directly using Mill:

Foo.scala (browse)
//| mvnDeps:
//| - "com.lihaoyi::scalatags:0.13.1"
//| - "com.lihaoyi::mainargs:0.7.6"
import scalatags.Text.all.*
import mainargs.{main, ParserForMethods}

object Foo {
  def generateHtml(text: String) = {
    h1(text).toString
  }

  @main
  def main(text: String) = {
    println(generateHtml(text))
  }

  def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
> ./mill Foo.scala --text hello
compiling 1 Scala source to...
<h1>hello</h1>

The ./mill Foo.{language-ext} syntax is shorthand for ./mill Foo.{language-ext}:run. You can also call other tasks on your script modules, such as Foo.{langauge-ext}:assembly below:

> ./mill Foo.scala:run --text hello
<h1>hello</h1>
> ./mill show Foo.scala:assembly # show the output of the assembly task
".../out/Foo.scala/assembly.dest/out.jar"

> java -jar ./out/Foo.scala/assembly.dest/out.jar --text hello
<h1>hello</h1>

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

Script files can have test suites, usually written in a separate test script. The test script specifies what script it tests via moduleDeps, and can have its own mvnDeps in addition to those of the upstream script. The test script can then exercise functions from the upstream script as shown below:

FooTests.scala (browse)
//| moduleDeps: [Foo.scala]
//| mvnDeps:
//| - "com.google.guava:guava:33.3.0-jre"
import com.google.common.html.HtmlEscapers.htmlEscaper
object FooTests {
  def main(args: Array[String]): Unit = {
    val result = Foo.generateHtml("hello")
    assert(result == "<h1>hello</h1>")
    println(result)
    val result2 = Foo.generateHtml("<hello>")
    val expected2 = "<h1>" + htmlEscaper().escape("<hello>") + "</h1>"
    assert(result2 == expected2)
    println(result2)
  }
}
> ./mill FooTests.scala
<h1>hello</h1>
<h1>&lt;hello&gt;</h1>

Again, you can pass the name of the task explicitly via :, e.g. :run below

> ./mill FooTests.scala:run # specifying the test task explicitly
<h1>hello</h1>
<h1>&lt;hello&gt;</h1>

For more details on Mill script files, see Configuring Single-File Projects The rest of the examples below discuss the Mill config for building larger projects beyond the single-file project format discussed above, using a dedicated build.mill file to provide additional flexibility in setting up your project.