Table of Contents

Extending Mill

Cross BuildsMill Internals

There are many 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 Ammonite-Ops library that comes bundled with Mill, or even plain java.nio/java.lang.Process. Each target gets its own T.ctx().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 { deployFoo(foo.assembly()) }

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 {
    def ivyDeps = Agg(ivy"org.scalatest::scalatest:3.0.4")
    def testFrameworks = Seq("")

import $file

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

def fooValue() = 31337 
import $
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:

import $ivy.`com.lihaoyi::scalatags:0.6.2`

def generatedHtml = T {
  import scalatags.Text.all._

This creates the generatedHtml target which can then be used however you would like: written to a file, further processed, etc.

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.

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

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: all, 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.

About the Author: Haoyi is a software engineer, an early contributor to Scala.js, and the author of many open-source Scala tools such as Mill, the Ammonite REPL and FastParse.

If you've enjoy using Mill, or enjoyed using Haoyi's other open source libraries, please chip in (or get your Company to chip in!) via Patreon so he can continue his open-source work

Cross BuildsMill Internals