Scala Build Examples

On this page, we will explore the Mill build tool via a series of simple Scala example projects. Each project demonstrates one particular feature of the Mill build tool, and is also an executable codebase you can download and run. By the end of this page, you will be familiar with how to configure Mill to work with realistic Scala codebases: cross-building, testing, and publishing them.

Many of the APIs covered here are listed in the Scaladoc:

Common Configuration Overrides

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

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

object foo extends RootModule with ScalaModule {
  def scalaVersion = "2.13.8"

  // You can have arbitrary numbers of third-party dependencies
  def ivyDeps = Agg(
    ivy"com.lihaoyi::scalatags:0.8.2",
    ivy"com.lihaoyi::os-lib:0.9.1",
  )

  // Choose a main class to use for `.run` if there are multiple present
  def mainClass: T[Option[String]] = Some("foo.Foo2")

  // 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]] = T {
    for(name <- Seq("A", "B", "C")) os.write(
      T.dest / s"Foo$name.scala",
      s"""
package foo
object Foo$name {
  val value = "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")

  // Additional Scala compiler options, e.g. to turn warnings into errors
  def scalacOptions: T[Seq[String]] = Seq("-deprecation", "-Xfatal-warnings")
}

Note the use of millSourcePath, T.dest, and PathRef when preforming various filesystem operations:

  1. millSourcePath refers to the base path of the module. For the root module, this is the root of the repo, and for inner modules it would be the module path e.g. for module foo.bar.qux the millSourcePath would be foo/bar/qux. This can also be overriden if necessary

  2. T.dest refers to the destination folder for a task in the out/ folder. This is unique to each task, and can act as both a scratch space for temporary computations as well as a place to put "output" files, without worrying about filesystem conflicts with other tasks

  3. PathRef is a way to return the contents of a file or folder, rather than just its path as a string. This ensures that downstream tasks properly invalidate when the contents changes even when the path stays the same

> mill run
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
FooA.value: hello A
FooB.value: hello B
FooC.value: hello C
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>
FooA.value: hello A
FooB.value: hello B
FooC.value: hello C
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
> sed -i '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 +...

Custom Tasks

This example shows how to define target that depend on other tasks:

  1. For generatedSources, we override an the task and make it depend directly on ivyDeps to generate its source files. In this example, to include the list of dependencies as tuples within a static object

  2. For lineCount, we define a brand new task that depends on sources, and then override forkArgs to use it. That lets us access the line count at runtime using sys.props and print it when the program runs

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

object foo extends RootModule with ScalaModule {
  def scalaVersion = "2.13.8"
  def ivyDeps = Agg(ivy"com.lihaoyi::mainargs:0.4.0")

  def generatedSources: T[Seq[PathRef]] = T {
    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")"""
    }
    os.write(
      T.dest / s"MyDeps.scala",
      s"""
package foo
object MyDeps {
  val value = List(
    ${prettyIvyDeps.mkString(",\n")}
  )
}
      """.stripMargin
    )

    Seq(PathRef(T.dest))
  }

  def lineCount: T[Int] = T {
    sources()
      .flatMap(pathRef => os.walk(pathRef.path))
      .filter(_.ext == "scala")
      .map(os.read.lines(_).size)
      .sum
  }

  def forkArgs: T[Seq[String]] = Seq(s"-Dmy.line.count=${lineCount()}")

  def printLineCount() = T.command { println(lineCount()) }
}

Mill lets you define new cached Targets using the T {…​} syntax, depending on existing Targets e.g. foo.sources via the foo.sources() syntax to extract their current value, as shown in lineCount above. The return-type of a Target has to be JSON-serializable (using uPickle) and the Target is cached when first run until its inputs change (in this case, if someone edits the foo.sources files which live in foo/src. Cached Targets cannot take parameters.

Note that depending on a task requires use of parentheses after the task name, e.g. ivyDeps(), sources() and lineCount(). This converts the task of type T[V] into a value of type V you can make use in your task implementation.

This example can be run as follows:

> mill run --text hello
text: hello
MyDeps.value: List((com.lihaoyi,mainargs,0.4.0))
my.line.count: 14

> mill show lineCount
14

> mill printLineCount
14

Custom targets and commands can contain arbitrary code. Whether you want to download files using requests.get, shell-out to Webpack to compile some Javascript, generate sources to feed into a compiler, or create some custom jar/zip assembly with the files you want , all of these can simply be custom targets with your code running in the T {…​} block.

You can create arbitrarily long chains of dependent targets, and Mill will handle the re-evaluation and caching of the targets' output for you. Mill also provides you a T.dest folder for you to use as scratch space or to store files you want to return: all files a task creates should live within T.dest, and any files you want to modify should be copied into T.dest before being modified. That ensures that the files belonging to a particular target all live in one place, avoiding file-name conflicts and letting Mill automatically invalidate the files when the target’s inputs change.

Overriding Tasks

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

object foo extends ScalaModule {
  def scalaVersion = "2.13.8"

  def sources = T{
    os.write(
      T.dest / "Foo.scala",
      """package foo
object Foo {
  def main(args: Array[String]): Unit = {
    println("Hello World")
  }
}
      """.stripMargin
    )
    Seq(PathRef(T.dest))
  }

  def compile = T {
    println("Compiling...")
    super.compile()
  }

  def run(args: Task[Args] = T.task(Args())) = T.command {
    println("Running..." + args().value.mkString(" "))
    super.run(args)()
  }
}

You can re-define targets and commands to override them, and use super if you want to refer to the originally defined task. The above example shows how to override compile and run to add additional logging messages, and we override sources which was T.sources for the src/ folder with a plain T{…​} target that generates the necessary source files on-the-fly.

Note that this example replaces your src/ folder with the generated sources. If you want to add generated sources, you can either override generatedSources, or you can override sources and use super to include the original source folder:

object foo2 extends ScalaModule {
  def scalaVersion = "2.13.8"

  def generatedSources = T{
    os.write(T.dest / "Foo.scala", """...""")
    Seq(PathRef(T.dest))
  }
}

object foo3 extends ScalaModule {
  def scalaVersion = "2.13.8"

  def sources = T{
    os.write(T.dest / "Foo.scala", """...""")
    super.sources() ++ Seq(PathRef(T.dest))
  }
}

In Mill builds the override keyword is optional.

> mill foo.run
Compiling...
Running...
Hello World

Nesting Modules

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

trait MyModule extends ScalaModule {
  def scalaVersion = "2.13.8"
  def ivyDeps = Agg(
    ivy"com.lihaoyi::scalatags:0.8.2",
    ivy"com.lihaoyi::mainargs:0.4.0"
  )
}

object foo extends MyModule {
  def moduleDeps = Seq(bar, qux)

  object bar extends MyModule
  object qux extends MyModule {
    def moduleDeps = Seq(bar)
  }
}

object baz extends MyModule {
  def moduleDeps = Seq(foo.bar, foo.qux, foo)
}

Modules can be nested arbitrarily deeply within each other. The outer module can be the same kind of module as the ones within, or it can be a plain Module if we just need a wrapper to put the modules in without any tasks defined on the wrapper.

The outer module can also depend on the inner module (as shown above), and vice versa, but not both at the same.

Running tasks on the nested modules requires the full module path foo.bar.run

> mill resolve __.run
foo.bar.run
foo.qux.run
baz.run

> mill foo.run --bar-text hello --qux-text world --foo-text today
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>
Foo.value: <p>today</p>

> mill baz.run --bar-text hello --qux-text world --foo-text today --baz-text yay
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>
Foo.value: <p>today</p>
Baz.value: <p>yay</p>

> mill foo.qux.run --bar-text hello --qux-text world
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>

Scala Module With Test Suite

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

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

This build defines a single module with a test suite, configured to use "uTest" as the testing framework. Test suites are themselves ScalaModules, nested within the enclosing module, and have all the normal tasks like foo.test.compile available to run, but with an additional .test task that runs the tests. You can also run the test suite directly, in which case it will run the .test task as the default task for that module.

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

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

> mill foo.test.test
...foo.FooTests.hello ...
...foo.FooTests.world ...

> mill foo.test
...foo.FooTests.hello ...
...foo.FooTests.world ...

For convenience, you can also use one of the predefined test frameworks:

  • TestModule.Junit4

  • TestModule.Junit5

  • TestModule.TestNg

  • TestModule.Munit

  • TestModule.ScalaTest

  • TestModule.Specs2

  • TestModule.Utest

  • TestModule.ZioTest

object bar extends ScalaModule {
  def scalaVersion = "2.13.8"

  object test extends ScalaTests with TestModule.Utest {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
  }
}
> mill bar.test
...bar.BarTests.hello ...
...bar.BarTests.world ...

By default, tests are run in a subprocess, and forkArg and forkEnv can be overridden to pass JVM flags & environment variables. You can also use

mill foo.test.testLocal

To run tests in-process in an isolated classloader.

If you want to pass any arguments to the test framework, simply put them after foo.test in the command line. e.g. uTest lets you pass in a selector to decide which test to run, which in Mill would be:

> mill bar.test bar.BarTests.hello
...bar.BarTests.hello ...

You can also define multiple test suites if you want, e.g.:

object qux extends ScalaModule {
  def scalaVersion = "2.13.8"

  object test extends ScalaTests with TestModule.Utest {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
  }
  object integration extends ScalaTests with TestModule.Utest {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest:0.8.4")
  }
}

Each of which will expect their sources to be in their respective foo/test and foo/integration folder.

> mill qux.test
...qux.QuxTests...hello...
...qux.QuxTests...world...

> mill qux.integration
...qux.QuxIntegrationTests...helloworld...

> mill qux.{test,integration}
...qux.QuxTests...hello...
...qux.QuxTests...world...
...qux.QuxIntegrationTests...helloworld...

Publish Module

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

object foo extends ScalaModule with PublishModule {
  def scalaVersion = "2.13.8"
  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")
    )
  )
}

This is an example ScalaModule with added publishing capabilities via PublishModule. This requires that you define an additional publishVersion and pomSettings with the relevant metadata, and provides the .publishLocal and publishSigned tasks for publishing locally to the machine or to the central maven repository

> mill foo.publishLocal
Publishing Artifact(com.lihaoyi,foo_2.13,0.0.1) to ivy repo...

The artifactName defaults to the name of your module (in this case foo) but can be overridden. The organization is defined in pomSettings.

You may also check and update the values of sonatypeUri and sonatypeSnapshotUri, which may not be correct if you have a newer Sonatype account (when created after Feb. 2021).

Staging Releases

Once you’ve mixed in PublishModule, you can publish your libraries to maven central via:

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b

This uploads them to oss.sonatype.org where you can log-in and stage/release them manually. You can also pass in the --release true flag to perform the staging/release automatically:

Recent versions of gpg (versions 2.1 and above) require an additional argument in order to accept a passphrase on the command line. Add --pinentry-mode=loopback to the comma-separated list of gpg arguments after --gpgArgs, so it becomes --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b,--pinentry-mode=loopback. Without the additional argument, you may see errors like gpg: signing failed: Inappropriate ioctl for device.

Sonatype credentials can be passed via environment variables (SONATYPE_USERNAME and SONATYPE_PASSWORD) or by passing second or --sonatypeCreds argument in format username:password. Consider using environment variables over the direct CLI passing due to security risks.

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b \
        --release true

If you want to publish/release multiple modules, you can use the or _ wildcard syntax:

mill mill.scalalib.PublishModule/publishAll \
        __.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b \
        --release true

To publish to repository other than oss.sonaytype.org such as internal hosted nexus at example.company.com, you can pass in the --sonatypeUri and --sonatypeSnapshotUri parameters to uploads to different site:

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --sonatypeUri http://example.company.com/release \
        --sonatypeSnaphostUri http://example.company.com/snapshot

Since Feb. 2021 any new Sonatype accounts have been created on s01.oss.sonatype.org, so you’ll want to ensure you set the relevant URIs to match.

The symptom of using the "wrong" URL for publishling is typically a 403 error code, in response to the publish request.

Typically

Non-Staging Releases (classic Maven uploads)

If the site does not support staging releases as oss.sonatype.org and s01.oss.sonatype.org do (for example, a self-hosted OSS nexus site), you can pass in the --stagingRelease false option to simply upload release artifacts to corresponding maven path under sonatypeUri instead of staging path.

mill mill.scalalib.PublishModule/publishAll \
        foo.publishArtifacts \
        lihaoyi:$SONATYPE_PASSWORD \
        --sonatypeUri http://example.company.com/release \
        --stagingRelease false

Cross-Scala-Version Modules

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

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

object foo extends Cross[FooModule](scalaVersions)
trait FooModule extends CrossScalaModule{
  def moduleDeps = Seq(bar())
}

object bar extends Cross[BarModule](scalaVersions)
trait BarModule extends CrossScalaModule

This is an example of cross-building a module across multiple Scala versions. Each module is replaced by a Cross module, which is given a list of strings you want the cross-module to be replicated for. You can then specify the cross-modules with square brackets when you want to run tasks on them.

CrossScalaModule supports both shared sources within src/ as well as version specific sources in src-x/, src-x.y/, or src-x.y.z/ that apply to the cross-module with that version prefix.

> mill resolve __.run
foo[2.12.17].run
foo[2.13.8].run
bar[2.12.17].run
bar[2.13.8].run

> mill foo[2.12.17].run
Foo.value: Hello World Scala library version 2.12.17...
Bar.value: bar-value
Specific code for Scala 2.x
Specific code for Scala 2.12.x

> mill foo[2.13.8].run
Foo.value: Hello World Scala library version 2.13.8...
Bar.value: bar-value
Specific code for Scala 2.x
Specific code for Scala 2.13.x

> mill bar[2.13.8].run
Bar.value: bar-value

CrossScalaModules can depend on each other using moduleDeps, but require the () suffix in moduleDeps to select the appropriate instance of the cross-module to depend on. You can also pass the crossScalaVersion explicitly to select the right version of the cross-module:

object foo2 extends Cross[Foo2Module](scalaVersions)
trait Foo2Module extends CrossScalaModule{
  def moduleDeps = Seq(bar(crossScalaVersion))
}

object bar2 extends Cross[Bar2Module](scalaVersions)
trait Bar2Module extends CrossScalaModule

SBT-Compatible Modules

build.sc (download, browse)
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

build.sc (download, browse)
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 = T {
    os.write(
      T.dest / "Version.scala",
      s"""
package foo
object Version{
  def value = "${publishVersion()}"
}
      """.stripMargin
    )
    Seq(PathRef(T.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 queries to run tasks on multiple targets 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

Example Builds for Real Projects

Mill comes bundled with example builds for real-world open-source projects, demonstrating how Mill can be used to build code outside of tiny example codebases:

Acyclic

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

object Deps {
  def acyclic = ivy"com.lihaoyi:::acyclic:0.3.6"
  def scalaCompiler(scalaVersion: String) = ivy"org.scala-lang:scala-compiler:$scalaVersion"
  val utest = ivy"com.lihaoyi::utest:0.8.4"
}

val crosses =
  Seq("2.11.12") ++
  Range.inclusive(8, 17).map("2.12." + _) ++
  Range.inclusive(0, 10).map("2.13." + _)

object acyclic extends Cross[AcyclicModule](crosses)
trait AcyclicModule extends CrossScalaModule with PublishModule {
  def crossFullScalaVersion = true
  def artifactName = "acyclic"
  def publishVersion = "1.3.3.7"

  def pomSettings = PomSettings(
    description = artifactName(),
    organization = "com.lihaoyi",
    url = "https://github.com/com-lihaoyi/acyclic",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github(owner = "com-lihaoyi", repo = "acyclic"),
    developers = Seq(
      Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")
    )
  )

  def compileIvyDeps = Agg(Deps.scalaCompiler(crossScalaVersion))

  object test extends ScalaTests with TestModule.Utest {
    def sources = T.sources(millSourcePath / "src", millSourcePath / "resources")
    def ivyDeps = Agg(Deps.utest, Deps.scalaCompiler(crossScalaVersion))
  }
}

Acyclic is an example of a very small project that is a Scala compiler plugin. It is cross-built against all point versions of Scala from 2.11.12 to 2.13.10, and has a dependency on the org.scala-lang:scala-compiler

> ./mill resolve acyclic[_].compile
acyclic[2.11.12].compile
acyclic[2.12.10].compile
acyclic[2.12.11].compile
acyclic[2.12.12].compile
acyclic[2.12.13].compile
acyclic[2.12.14].compile
acyclic[2.12.15].compile
acyclic[2.12.16].compile
acyclic[2.12.8].compile
acyclic[2.12.9].compile
acyclic[2.13.0].compile
acyclic[2.13.1].compile
acyclic[2.13.2].compile
acyclic[2.13.3].compile
acyclic[2.13.4].compile
acyclic[2.13.5].compile
acyclic[2.13.6].compile
acyclic[2.13.7].compile
acyclic[2.13.8].compile
acyclic[2.13.9].compile

> ./mill acyclic[2.12.17].compile
compiling 6 Scala sources...
...

> ./mill acyclic[2.13.10].test.testLocal # acyclic tests need testLocal due to classloader assumptions
-------------------------------- Running Tests --------------------------------
...

Fansi

build.sc (download, browse)
import mill._, scalalib._, scalajslib._, scalanativelib._, publish._

val dottyCommunityBuildVersion = sys.props.get("dottyVersion").toList

val scalaVersions = Seq("2.12.17", "2.13.8", "2.11.12", "3.1.3") ++ dottyCommunityBuildVersion

trait FansiModule extends PublishModule with CrossScalaModule with PlatformScalaModule {
  def publishVersion = "1.3.3.7"

  def pomSettings = PomSettings(
    description = artifactName(),
    organization = "com.lihaoyi",
    url = "https://github.com/com-lihaoyi/Fansi",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github(owner = "com-lihaoyi", repo = "fansi"),
    developers = Seq(
      Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi")
    )
  )

  def ivyDeps = Agg(ivy"com.lihaoyi::sourcecode::0.3.0")

  trait FansiTests extends ScalaTests with TestModule.Utest {
    def ivyDeps = Agg(ivy"com.lihaoyi::utest::0.8.4")
  }
}

object fansi extends Module {
  object jvm extends Cross[JvmFansiModule](scalaVersions)
  trait JvmFansiModule extends FansiModule with ScalaModule {
    object test extends FansiTests with ScalaTests
  }

  object js extends Cross[JsFansiModule](scalaVersions)
  trait JsFansiModule extends FansiModule with ScalaJSModule {
    def scalaJSVersion = "1.16.0"
    object test extends FansiTests with ScalaJSTests
  }

  object native extends Cross[NativeFansiModule](scalaVersions)
  trait NativeFansiModule extends FansiModule with ScalaNativeModule {
    def scalaNativeVersion = "0.4.5"
    object test extends FansiTests with ScalaNativeTests
  }
}

Fansi is an example of a small library that is cross built against every minor version of Scala (including Scala 3.x) and every platform: JVM, JS, and Native. Both the library and the test suite are duplicated across all entries in the {version}x{platform} matrix, and you can select which one you want to compile, test, or publish

> ./mill resolve __.compile
fansi.js[2.11.12].test.compile
fansi.js[2.12.17].compile
fansi.js[2.12.17].test.compile
fansi.js[2.13.8].compile
fansi.js[2.13.8].test.compile
fansi.js[3.1.3].compile
fansi.js[3.1.3].test.compile
fansi.jvm[2.11.12].compile
fansi.jvm[2.11.12].test.compile
fansi.jvm[2.12.17].compile
fansi.jvm[2.12.17].test.compile
fansi.jvm[2.13.8].compile
fansi.jvm[2.13.8].test.compile
fansi.jvm[3.1.3].compile
fansi.jvm[3.1.3].test.compile
fansi.native[2.11.12].compile
fansi.native[2.11.12].test.compile
fansi.native[2.12.17].compile
fansi.native[2.12.17].test.compile
fansi.native[2.13.8].compile
fansi.native[2.13.8].test.compile
fansi.native[3.1.3].compile
fansi.native[3.1.3].test.compile

> ./mill fansi.jvm[2.12.17].compile
compiling 1 Scala source...
...

> ./mill fansi.js[2.13.8].test
Starting process: node
-------------------------------- Running Tests --------------------------------
...

> ./mill fansi.native[3.1.3].publishLocal
Publishing Artifact(com.lihaoyi,fansi_native0.4_3,1.3.3.7) to ivy repo...
...

Real World Mill Builds

Ammonite

Ammonite is an ergonomic Scala REPL.

Scala-CLI

Scala-CLI is the primary CLI tool that runs when you enter scala in the terminal. It is able to compile, test, run, and package your Scala code in a variety of different ways

Coursier

Coursier is a fast JVM dependency resolver, used in many build tools down resolve and download third party dependencies