Scala Build Examples

On this page, we will explore the Mill build tool via a series of simple Scala example projects.

Nesting Modules

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

SBT-Compatible Modules

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

Cross-Scala-Version Modules

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

Realistic Scala Example Project

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

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.mill (download, browse)
package build
import mill._, scalalib._, publish._

acyclic test suite assumes files are on disk at specific paths relative to os.pwd. To avoid changing the test code, we instead copy the necessary files into the os.pwd when preparing the resources for test suite execution

os.copy.over(
  interp.watch(mill.api.WorkspaceRoot.workspaceRoot / "acyclic"),
  os.pwd / "acyclic",
  createFolders = true
)

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 = Task.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.mill (download, browse)
package build
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