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.

In such a case, you can use a Worker. Workers keep their state between multiple runs. Workers are created with the T.worker macro.

Example: 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-use 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")
  }
}

In fact, the above example of a configuration trait is so convenient, that it is found in almost any non-trivial Mill project, in once form or another.

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:

foo.sc
def fooValue() = 31337
build.sc
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)
  html(
    head(),
    body(
      h1("Hello"),
      p("World")
    )
  ).render
}
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 section Using Plugins.

For more information about this special import syntax, read the Ammonite Documentation for Ivy Dependencies.

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.