Extending Mill

There are different ways of extending Mill, depending on how much customization and flexibility you need. This page will go through your options from the easiest/least-flexible to the hardest/most-flexible.

import $file and import $ivy

build.sc (download, browse)
import mill._, scalalib._
import $ivy.`com.lihaoyi::scalatags:0.12.0`, scalatags.Text.all._
import $file.scalaversion, scalaversion.myScalaVersion

object foo extends RootModule with ScalaModule {
  def scalaVersion = myScalaVersion

  def ivyDeps = Agg(ivy"com.lihaoyi::os-lib:0.9.1")
  def htmlSnippet = T{ div(h1("hello"), p("world")).toString }
  def resources = T.sources{
    os.write(T.dest / "snippet.txt", htmlSnippet())
    super.resources() ++ Seq(PathRef(T.dest))
  }
}
scalaversion.sc (browse)
def myScalaVersion = "2.13.8"

This example illustrates usage of import $file and import $ivy. These allow you to pull in code from outside your build.sc file:

  1. import $file lets you import other *.sc files on disk. This lets you split up your build.sc logic if the file is growing too large. In this tiny example case, we move myScalaVersion to another versions.sc file and import it for use.

  2. import $ivy lets you import ivy dependencies into your build.sc, so you can use arbitrary third-party libraries at build-time. This makes lets you perform computations at build-time rather than run-time, speeding up your application start up. In this case, we move the Scalatags rendering logic to build time, so the application code gets a pre-rendere string it can directly print without further work.

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

> mill run
generated snippet.txt resource: <div><h1>hello</h1><p>world</p></div>

> mill show assembly
".../out/assembly.dest/out.jar"

> ./out/assembly.dest/out.jar # mac/linux
generated snippet.txt resource: <div><h1>hello</h1><p>world</p></div>

The Mill Meta-Build

The meta-build manages the compilation of the build.sc. If you don’t configure it explicitly, a built-in synthetic meta-build is used.

To customize it, you need to explicitly enable it with import $meta._. Once enabled, the meta-build lives in the mill-build/ directory. It needs to contain a top-level module of type MillBuildRootModule.

Meta-builds are recursive, which means, it can itself have a nested meta-builds, and so on.

To run a task on a meta-build, you specifying the --meta-level option to select the meta-build level.

Example: Format the build.sc

As an example of running a task on the meta-build, you can format the build.sc with Scalafmt. Everything is already provided by Mill. You only need a .scalafmt.conf config file which at least needs configure the Scalafmt version.

Run Scalafmt on the build.sc (and potentially included files)
$ mill --meta-level 1 mill.scalalib.scalafmt.ScalafmtModule/reformatAll sources
  • --meta-level 1 selects the first meta-build. Without any customization, this is the only built-in meta-build.

  • mill.scalalib.scalafmt.ScalafmtModule/reformatAll is a generic task to format scala source files with Scalafmt. It requires the targets that refer to the source files as argument

  • sources this selects the sources targets of the meta-build, which at least contains the build.sc.

Example: Find plugin updates

Mill plugins are defined as ivyDeps in the meta-build. Hence, you can easily search for updates with the external mill.scalalib.Dependency module.

Check for Mill Plugin updates
$ mill --meta-level 1 mill.scalalib.Dependency/showUpdates
Found 1 dependency update for
  de.tototec:de.tobiasroeser.mill.vcs.version_mill0.11_2.13 : 0.3.1-> 0.4.0

Example: Customizing the Meta-Build

build.sc (download, browse)
import $meta._
import mill._, scalalib._
import scalatags.Text.all._

object foo extends RootModule with ScalaModule {
  def scalaVersion = millbuild.ScalaVersion.myScalaVersion
  def ivyDeps = Agg(ivy"com.lihaoyi::os-lib:0.9.1")

  def htmlSnippet = T{ h1("hello").toString }
  def resources = T.sources{
    os.write(T.dest / "snippet.txt", htmlSnippet())
    super.resources() ++ Seq(PathRef(T.dest))
  }

  def forkArgs = Seq(
    s"-Dmill.scalatags.version=${millbuild.DepVersions.scalatagsVersion}"
  )
}

This example illustrates usage of the mill-build/ folder. Mill’s build.sc file and it’s import $file and $ivy are a shorthand syntax for defining a Mill ScalaModule, with sources and ivyDeps and so on, which is compiled and executed to perform your build. This module lives in mill-build/, and can be enabled via the import $meta._ statement above.

mill-build/build.sc (browse)
import mill._, scalalib._

object millbuild extends MillBuildRootModule{
  val scalatagsVersion = "0.12.0"
  def ivyDeps = Agg(ivy"com.lihaoyi::scalatags:$scalatagsVersion")

  def generatedSources = T {
    os.write(
      T.dest / "DepVersions.scala",
      s"""package millbuild
object DepVersions{
  def scalatagsVersion = "$scalatagsVersion"
}
""".stripMargin
    )
    super.generatedSources() ++ Seq(PathRef(T.dest))
  }
}
mill-build/src/ScalaVersion.scala (browse)
package millbuild
object ScalaVersion{
  def myScalaVersion = "2.13.10"
}

In this example:

  1. Our myScalaVersion value comes from mill-build/src/Versions.scala, while the Scalatags library we use in build.sc comes from the def ivyDeps in mill-build/build.sc.

  2. We also use generatedSources in mill-build/build.sc to create a DepVersions object that the build.sc can use to pass the scalatagsVersion to the application without having to copy-paste the version and keep the two copies in sync

You can customize the mill-build/ module with more flexibility than is provided by import $ivy or import $file, overriding any tasks that are present on a typical ScalaModule: scalacOptions, generatedSources, etc. This is useful for large projects where the build itself is a non-trivial module which requires its own non-trivial customization.

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

> mill run
Foo.value: <h1>hello</h1>
scalatagsVersion: 0.12.0

> mill show assembly
".../out/assembly.dest/out.jar"

> ./out/assembly.dest/out.jar # mac/linux
Foo.value: <h1>hello</h1>
scalatagsVersion: 0.12.0

You can also run tasks on the meta-build by using the --meta-level cli option.

> mill --meta-level 1 show sources
[
.../build.sc",
.../mill-build/src"
]

> mill --meta-level 2 show sources
.../mill-build/build.sc"

Using ScalaModule.run as a task

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

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

  def barWorkingDir = T{ T.dest }
  def sources = T{
    bar.run(T.task(Args(barWorkingDir(), super.sources().map(_.path))))()
    Seq(PathRef(barWorkingDir()))
  }
}

object bar extends ScalaModule{
  def scalaVersion = "2.13.8"
  def ivyDeps = Agg(ivy"com.lihaoyi::os-lib:0.9.1")
}

This example demonstrates using Mill ScalaModules as build tasks: rather than defining the task logic in the build.sc, we instead put the build logic within the bar module as bar/src/Bar.scala. In this example, we use Bar.scala as a source-code pre-processor on the foo module source code: we override foo.sources, passing the super.sources() to bar.run along with a barWorkingDir, and returning a PathRef(barWorkingDir()) as the new foo.sources.

> mill foo.run
...
Foo.value: HELLO

This example does a trivial string-replace of "hello" with "HELLO", but is enough to demonstrate how you can use Mill ScalaModules to implement your own arbitrarily complex transformations. This is useful for build logic that may not fit nicely inside a build.sc file, whether due to the sheer lines of code or due to dependencies that may conflict with the Mill classpath present in build.sc

Evaluator Commands (experimental)

Evaluator Command are experimental and suspected to change. See issue #502 for details.

You can define a command that takes in the current Evaluator as an argument, which you can use to inspect the entire build, or run arbitrary tasks. For example, here is the mill.scalalib.GenIdea/idea command which uses this to traverse the module-tree and generate an Intellij project config for your build.

def idea(ev: Evaluator) = T.command {
  mill.scalalib.GenIdea(
    implicitly,
    ev.rootModule,
    ev.discover
  )
}

Many built-in tools are implemented as custom evaluator commands: inspect, resolve, show. If you want a way to run Mill commands and programmatically manipulate the tasks and outputs, you do so with your own evaluator command.