Large Builds and Monorepos

This section walks through Mill features and techniques used for managing large builds. While Mill works great for small single-module projects, it is also able to work effectively with large projects with hundreds of modules. Mill’s own build for the com-lihaoyi/mill project has ~400 modules, and other proprietary projects may have many more.

Mill modules are cheap. Having more modules does not significantly impact performance or resource usage, build files are incrementally re-compiled when modified, and modules are lazily loaded and initialized only when needed. So you are encouraged to break up your project into modules to manage the layering of your codebase or benefit from parallelism.

Multi-file Builds

Mill allows you to break up your build.mill file into smaller files by defining the build-related logic for any particular subfolder as a package.mill file in that subfolder. This can be very useful to keep large Mill builds maintainable, as each folder’s build logic gets co-located with the files that need to be built, and speeds up compilation of the build logic since each build.mill or package.mill file can be compiled independently when it is modified without re-compiling all the others.

Usage of sub-folder package.mill files is enabled by the magic import import $packages._

build.mill (download, browse)
package build
import $packages._

import mill._, scalalib._

trait MyModule extends ScalaModule {
  def scalaVersion = "2.13.11"
}
foo/package.mill (browse)
package build.foo
import mill._, scalalib._

object `package` extends RootModule with build.MyModule {
  def moduleDeps = Seq(build.bar.qux.mymodule)
  def ivyDeps = Agg(ivy"com.lihaoyi::mainargs:0.4.0")
}
bar/qux/package.mill (browse)
package build.bar.qux
import mill._, scalalib._

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

In this example, the root build.mill only contains the trait MyModule, but it is foo/package.mill and bar/qux/package.mill that define modules using it. The modules defined in foo/package.mill and bar/qux/package.mill are automatically nested within foo and bar.qux respectively, and can be referenced from the command line as below:

> ./mill resolve __
bar
...
bar.qux.mymodule
...
bar.qux.mymodule.compile
...
foo
...
foo.compile

> ./mill bar.qux.mymodule.compile

> ./mill foo.compile

> ./mill foo.run --foo-text hello --bar-qux-text world
Foo.value: hello
BarQux.value: <p>world</p>

Note that in this example, foo/package.mill defines object module extends RootModule, and so the name .module does not need to be provided at the command line. In contrast, bar/qux/package.mill defines object mymodule that does not extend RootModule, and so we need to explicitly reference it with a .mymodule suffix.

package.mill files are only discovered in direct subfolders of the root build.mill or subfolders of another folder containing a package.mill; Hence in this example, we need an bar/package.mill to be present for bar/qux/package.mill to be discovered, even though bar/package.mill is empty

Helper Files

Apart from having package files in subfolders to define modules, Mill also allows you to have helper code in any *.mill file in the same folder as your build.mill or a package.mill.

build.mill (download, browse)
package build
import $packages._
import mill._, scalalib._
import $file.foo.versions
import $file.util.MyModule

object `package` extends RootModule with MyModule {
  def forkEnv = Map(
    "MY_SCALA_VERSION" -> build.scalaVersion(),
    "MY_PROJECT_VERSION" -> versions.myProjectVersion
  )
}
util.mill (browse)
package build
import mill._, scalalib._

def myScalaVersion = "2.13.14"

trait MyModule extends ScalaModule {
  def scalaVersion = myScalaVersion
}
foo/package.mill (browse)
package build.foo
import mill._, scalalib._
import $file.util
import $file.foo.versions.myProjectVersion
object `package` extends RootModule with build_.util.MyModule {
  def forkEnv = Map(
    "MY_SCALA_VERSION" -> util.myScalaVersion,
    "MY_PROJECT_VERSION" -> myProjectVersion
  )
}
foo/versions.mill (browse)
package build.foo

def myProjectVersion = "0.0.1"

Different helper scripts and build.mill/package files can all refer to each other using the build object, which marks the root object of your build. In this example:

  • build.mill can be referred to as simple build

  • util.mill can be referred to as simple $file.util

  • foo/package can be referred to as simple build.foo

  • foo/versions.mill can be referred to as simple $file.foo.versions

> ./mill run
Main Env build.util.myScalaVersion: 2.13.14
Main Env build.foo.versions.myProjectVersion: 0.0.1

> ./mill foo.run
Foo Env build.util.myScalaVersion: 2.13.14
Foo Env build.foo.versions.myProjectVersion: 0.0.1

Legacy .sc extension

To ease the migration from Mill 0.11.x, the older .sc file extension is also supported for Mill build files, and the package declaration is optional in such files. Note that this means that IDE support using .sc files will not be as good as IDE support using the current .mill extension with package declaration, so you should use .mill whenever possible

build.mill (download, browse)
import mill._, scalalib._
import $packages._
import $file.foo.versions
import $file.util, util.MyModule

object `package` extends RootModule with MyModule {
  def forkEnv = T {
    Map(
      "MY_SCALA_VERSION" -> build.scalaVersion(),
      "MY_PROJECT_VERSION" -> versions.myProjectVersion
    )
  }
}
util.sc (browse)
import mill._, scalalib._

def myScalaVersion = "2.13.14"

trait MyModule extends ScalaModule {
  def scalaVersion = myScalaVersion
}
foo/package.sc (browse)
import mill._, scalalib._
import $file.^.util
import $file.versions, versions.myProjectVersion
object `package` extends RootModule with build_.util.MyModule {
  def forkEnv = Map(
    "MY_SCALA_VERSION" -> util.myScalaVersion,
    "MY_PROJECT_VERSION" -> myProjectVersion
  )
}
foo/versions.sc (browse)
def myProjectVersion = "0.0.1"
> ./mill run
Main Env build.util.myScalaVersion: 2.13.14
Main Env build.foo.versions.myProjectVersion: 0.0.1

> ./mill foo.run
Foo Env build.util.myScalaVersion: 2.13.14
Foo Env build.foo.versions.myProjectVersion: 0.0.1