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.

Custom Targets & Commands

The simplest way of adding custom functionality to Mill is to define a custom Target or Command:

def foo = T { ... }
def bar(x: Int, s: String) = T.command { ... }

These can depend on other Targets, contain arbitrary code, and be placed top-level or within any module. If you have something you just want to do that isn’t covered by the built-in ScalaModules/ScalaJSModules, simply write a custom Target (for cached computations) or Command (for un-cached actions) and you’re done.

For subprocess/filesystem operations, you can use the os-lib library that comes bundled with Mill, or even plain java.nio/java.lang.Process. Each target gets its own T.dest folder that you can use to place files without worrying about colliding with other targets.

This covers use cases like:

Compile some Javascript with Webpack and put it in your runtime classpath:

def doWebpackStuff(sources: Seq[PathRef]): PathRef = ???

def javascriptSources = T.sources { millSourcePath / "js" }
def compiledJavascript = T { doWebpackStuff(javascriptSources()) }
object foo extends ScalaModule {
  def runClasspath = T { super.runClasspath() ++ compiledJavascript() }

Deploy your compiled assembly to AWS

object foo extends ScalaModule {}

def deploy(assembly: PathRef, credentials: String) = ???

def deployFoo(credentials: String) = T.command {
  deploy(foo.assembly(), credentials)

Custom Workers

Custom Targets & Commands are re-computed from scratch each time; sometimes you want to keep values around in-memory when using --watch or the Build REPL. E.g. you may want to keep a webpack process running so webpack’s own internal caches are hot and compilation is fast:

def webpackWorker = T.worker {
  // Spawn a process using java.lang.Process and return it

def javascriptSources = T.sources { millSourcePath / "js" }

def doWebpackStuff(webpackProcess: Process, sources: Seq[PathRef]): PathRef =

def compiledJavascript = T {
  doWebpackStuff(webpackWorker(), javascriptSources())

Mill itself uses T.workers for its built-in Scala support: we keep the Scala compiler in memory between compilations, rather than discarding it each time, in order to improve performance.

Custom Modules

trait FooModule extends mill.Module {
  def bar = T { "hello" }
  def baz = T { "world" }

Custom modules are useful if you have a common set of tasks that you want to re-used across different parts of your build. You simply define a trait inheriting from mill.Module, and then use that trait as many times as you want in various objects:

object foo1 extends FooModule
object foo2 extends FooModule {
  def qux = T { "I am Cow" }

You can also define a trait extending the built-in ScalaModule if you have common configuration you want to apply to all your ScalaModules:

trait FooModule extends ScalaModule {
  def scalaVersion = "2.11.11"
  object test extends Tests with TestModule.ScalaTest {
    def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.4")

import $file

If you want to define some functionality that can be used both inside and outside the build, you can create a new foo.sc file next to your build.sc, import $file.foo, and use it in your build.sc file:

def fooValue() = 31337
import $file.foo
def printFoo() = T.command { println(foo.fooValue()) }

Mill’s import $file syntax supports the full functionality of Ammonite Scripts

import $ivy

If you want to pull in artifacts from the public repositories (e.g. Maven Central) for use in your build, you can simply use import $ivy.

Example build.sc: Using scalatags library to generate HTML
import $ivy.`com.lihaoyi::scalatags:0.6.2` (1)

def generatedHtml = T {
  import scalatags.Text.all._ (2)
1 Import the scalatags library from Mavel Central repository.
2 Creates the generatedHtml target which is used here to generate a simple Hello World HTML document. It can be used however you would like: written to a file, further processed, etc.

Please also read the next section Using Mill Plugins (import $ivy)].

For more information, see Ammonite’s Ivy Dependencies documentation.

Using Mill Plugins (import $ivy)

Mill plugins are ordinary jars and are loaded as any other external dependency with import $ivy.

Mill plugins are typically bound to a specific version or version range of Mill. To ease the use of the correct versions and avoid runtime issues (caused by binary incompatible plugins, which are hard to debug) you can apply one of the following techniques:

Use the specific Mill Binary Platform notation

// for classic Scala dependencies
import $ivy.`<group>::<plugin>::<version>` (1)
// for dependencies specific to the exact Scala version
import $ivy.`<group>:::<plugin>::<version>` (2)
1 This is equivalent to
import $ivy.`<group>::<plugin>_mill$MILL_BIN_PLATFORM:<version>`
2 This is equivalent to
import $ivy.`<group>:::<plugin>_mill$MILL_BIN_PLATFORM:<version>`

Use special placeholders in your import $ivy


to substitute the currently used Mill version. This is typical required for Mill contrib modules, which are developed in the Mill repository and highly bound to the current Mill version.

Example: Use mill-contrib-bloop plugin matching the current Mill version
import $ivy.`com.lihaoyi:mill-contrib-bloop:$MILL_VERSION`

There is the even more convenient option to leave the version completely empty. Mill will substitute it with its current version. But don’t forget to provide the trailing colon!

Example: Use mill-contrib-bloop plugin matching the current Mill version
import $ivy.`com.lihaoyi:mill-contrib-bloop:`

to substitute the currently used Mill binary platform.

Example: Using mill-vcs-version plugin matching the current Mill Binary Platfrom
import $ivy.`de.tototec::de.tobiasroeser.mill.vcs.version_mill$MILL_BIN_PLATFORM:0.1.2`
If you want to publish re-usable libraries that other people can use in their builds, simply publish your code as a library to maven central.

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 {

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.