Java Build Examples

On this page, we will explore the Mill build tool via a series of simple Java example projects. Each project demonstrates one particular feature of the Mill build tool, and is also an executable codebase you can download and run. By the end of this page, you will be familiar with how to configure Mill to work with realistic Java codebases: cross-building, testing, and publishing them.

Many of the APIs covered here are listed in the API documentation:

Common Configuration Overrides

This example shows some of the common tasks you may want to override on a ScalaModule: specifying the mainClass, adding additional sources/resources, generating resources, and setting compilation/run options.

build.mill (download, browse)
package build
import mill._, javalib._

object `package` extends RootModule with JavaModule {
  // You can have arbitrary numbers of third-party dependencies
  def ivyDeps = Agg(
    ivy"org.apache.commons:commons-text:1.12.0"
  )

  // Choose a main class to use for `.run` if there are multiple present
  def mainClass: T[Option[String]] = Some("foo.Foo2")

  // Add (or replace) source folders for the module to use
  def sources = Task.Sources{
    super.sources() ++ Seq(PathRef(millSourcePath / "custom-src"))
  }

  // Add (or replace) resource folders for the module to use
  def resources = Task.Sources{
    super.resources() ++ Seq(PathRef(millSourcePath / "custom-resources"))
  }

  // Generate sources at build time
  def generatedSources: T[Seq[PathRef]] = Task {
    for(name <- Seq("A", "B", "C")) os.write(
      Task.dest / s"Foo$name.java",
      s"""
package foo;
public class Foo$name {
  public static String value = "hello $name";
}
      """.stripMargin
    )

    Seq(PathRef(Task.dest))
  }

  // Pass additional JVM flags when `.run` is called or in the executable
  // generated by `.assembly`
  def forkArgs: T[Seq[String]] = Seq("-Dmy.custom.property=my-prop-value")

  // Pass additional environmental variables when `.run` is called. Note that
  // this does not apply to running externally via `.assembly
  def forkEnv: T[Map[String, String]] = Map("MY_CUSTOM_ENV" -> "my-env-value")
}

If you want to better understand how the various upstream tasks feed into a task of interest, such as run, you can visualize their relationships via

> mill show visualizePlan run
VisualizePlanJava.svg

(right-click open in new tab to see full sized)

Note the use of millSourcePath, Task.dest, and PathRef when preforming various filesystem operations:

  1. millSourcePath refers to the base path of the module. For the root module, this is the root of the repo, and for inner modules it would be the module path e.g. for module foo.bar.qux the millSourcePath would be foo/bar/qux. This can also be overriden if necessary

  2. Task.dest refers to the destination folder for a task in the out/ folder. This is unique to each task, and can act as both a scratch space for temporary computations as well as a place to put "output" files, without worrying about filesystem conflicts with other tasks

  3. PathRef is a way to return the contents of a file or folder, rather than just its path as a string. This ensures that downstream tasks properly invalidate when the contents changes even when the path stays the same

> mill run
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
FooA.value: hello A
FooB.value: hello B
FooC.value: hello C
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
MY_CUSTOM_ENV: my-env-value

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

> ./out/assembly.dest/out.jar # mac/linux
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
FooA.value: hello A
FooB.value: hello B
FooC.value: hello C
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value

Custom Tasks

This example shows how to define target that depend on other tasks:

  1. For generatedSources, we override an the task and make it depend directly on ivyDeps to generate its source files. In this example, to include the list of dependencies as tuples within a static object

  2. For lineCount, we define a brand new task that depends on sources, and then override forkArgs to use it. That lets us access the line count at runtime using sys.props and print it when the program runs

build.mill (download, browse)
package build
import mill._, javalib._

object `package` extends RootModule with JavaModule {
  def ivyDeps = Agg(ivy"net.sourceforge.argparse4j:argparse4j:0.9.0")

  def generatedSources: T[Seq[PathRef]] = Task {
    val prettyIvyDeps = for(ivyDep <- ivyDeps()) yield {
      val org = ivyDep.dep.module.organization.value
      val name = ivyDep.dep.module.name.value
      val version = ivyDep.dep.version
      s""" "$org:$name:$version" """
    }
    val ivyDepsString = prettyIvyDeps.mkString(" + \"\\n\" + \n")
    os.write(
      Task.dest / s"MyDeps.java",
      s"""
package foo;
public class MyDeps {
  public static String value =
    $ivyDepsString;
}
      """.stripMargin
    )

    Seq(PathRef(Task.dest))
  }

  def lineCount: T[Int] = Task {
    sources()
      .flatMap(pathRef => os.walk(pathRef.path))
      .filter(_.ext == "java")
      .map(os.read.lines(_).size)
      .sum
  }

  def forkArgs: T[Seq[String]] = Seq(s"-Dmy.line.count=${lineCount()}")

  def printLineCount() = Task.Command { println(lineCount()) }
}

Mill lets you define new cached Targets using the Task {…​} syntax, depending on existing Targets e.g. foo.sources via the foo.sources() syntax to extract their current value, as shown in lineCount above. The return-type of a Target has to be JSON-serializable (using uPickle) and the Target is cached when first run until its inputs change (in this case, if someone edits the foo.sources files which live in foo/src. Cached Targets cannot take parameters.

Note that depending on a task requires use of parentheses after the task name, e.g. ivyDeps(), sources() and lineCount(). This converts the task of type T[V] into a value of type V you can make use in your task implementation.

This example can be run as follows:

> mill run --text hello
text: hello
MyDeps.value: net.sourceforge.argparse4j:argparse4j:0.9.0
my.line.count: 24

> mill show lineCount
24

> mill printLineCount
24

Custom targets and commands can contain arbitrary code. Whether you want to download files using requests.get, shell-out to Webpack to compile some Javascript, generate sources to feed into a compiler, or create some custom jar/zip assembly with the files you want , all of these can simply be custom targets with your code running in the Task {…​} block.

You can create arbitrarily long chains of dependent targets, and Mill will handle the re-evaluation and caching of the targets' output for you. Mill also provides you a Task.dest folder for you to use as scratch space or to store files you want to return: all files a task creates should live within Task.dest, and any files you want to modify should be copied into Task.dest before being modified. That ensures that the files belonging to a particular target all live in one place, avoiding file-name conflicts and letting Mill automatically invalidate the files when the target’s inputs change.

Overriding Tasks

build.mill (download, browse)
package build
import mill._, javalib._

object foo extends JavaModule {

  def sources = T{
    os.write(
      Task.dest / "Foo.java",
      """package foo;

public class Foo {
    public static void main(String[] args) {
        System.out.println("Hello World");
    }
}
      """.stripMargin
    )
    Seq(PathRef(Task.dest))
  }

  def compile = Task {
    println("Compiling...")
    super.compile()
  }

  def run(args: Task[Args] = Task.Anon(Args())) = Task.Command {
    println("Running..." + args().value.mkString(" "))
    super.run(args)()
  }
}

You can re-define targets and commands to override them, and use super if you want to refer to the originally defined task. The above example shows how to override compile and run to add additional logging messages, and we override sources which was Task.Sources for the src/ folder with a plain T{…​} target that generates the necessary source files on-the-fly.

Note that this example replaces your src/ folder with the generated sources. If you want to add generated sources, you can either override generatedSources, or you can override sources and use super to include the original source folder:

object foo2 extends JavaModule {
  def generatedSources = T{
    os.write(Task.dest / "Foo.java", """...""")
    Seq(PathRef(Task.dest))
  }
}

object foo3 extends JavaModule {
  def sources = T{
    os.write(Task.dest / "Foo.java", """...""")
    super.sources() ++ Seq(PathRef(Task.dest))
  }
}

In Mill builds the override keyword is optional.

> mill foo.run
Compiling...
Running...
Hello World

Nesting Modules

build.mill (download, browse)
package build
import mill._, javalib._

trait MyModule extends JavaModule {
  def ivyDeps = Agg(
    ivy"net.sourceforge.argparse4j:argparse4j:0.9.0",
    ivy"org.apache.commons:commons-text:1.12.0"
  )
}

object foo extends MyModule {
  def moduleDeps = Seq(bar, qux)

  object bar extends MyModule
  object qux extends MyModule {
    def moduleDeps = Seq(bar)
  }
}

object baz extends MyModule {
  def moduleDeps = Seq(foo.bar, foo.qux, foo)
}

Modules can be nested arbitrarily deeply within each other. The outer module can be the same kind of module as the ones within, or it can be a plain Module if we just need a wrapper to put the modules in without any tasks defined on the wrapper.

The outer module can also depend on the inner module (as shown above), and vice versa, but not both at the same.

Running tasks on the nested modules requires the full module path foo.bar.run

> mill resolve __.run
foo.bar.run
foo.qux.run
baz.run

> mill foo.run --bar-text hello --qux-text world --foo-text today
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>
Foo.value: <p>today</p>

> mill baz.run --bar-text hello --qux-text world --foo-text today --baz-text yay
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>
Foo.value: <p>today</p>
Baz.value: <p>yay</p>

> mill foo.qux.run --bar-text hello --qux-text world
Bar.value: <h1>hello</h1>
Qux.value: <p>world</p>

Maven-Compatible Modules

build.mill (download, browse)
package build
import mill._, javalib._

object foo extends MavenModule {
  object test extends MavenTests with TestModule.Junit4
}

MavenModule is a variant of JavaModule that uses the more verbose folder layout of Maven, SBT, and other tools:

  • foo/src/main/java

  • foo/src/test/java

Rather than Mill’s

  • foo/src

  • foo/test/src

This is especially useful if you are migrating from Maven to Mill (or vice versa), during which a particular module may be built using both Maven and Mill at the same time

> mill foo.compile
compiling 1 Java source...

> mill foo.test.compile
compiling 1 Java source...

> mill foo.test.test
...foo.FooTests.hello ...

> mill foo.test
...foo.FooTests.hello ...

Realistic Java Example Project

build.mill (download, browse)
package build
import mill._, javalib._, publish._

trait MyModule extends JavaModule with PublishModule {
  def publishVersion = "0.0.1"

  def pomSettings = PomSettings(
    description = "Hello",
    organization = "com.lihaoyi",
    url = "https://github.com/lihaoyi/example",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github("lihaoyi", "example"),
    developers = Seq(Developer("lihaoyi", "Li Haoyi", "https://github.com/lihaoyi"))
  )

  def ivyDeps = Agg(ivy"org.apache.commons:commons-text:1.12.0")

  object test extends JavaTests with TestModule.Junit4
}

object foo extends MyModule {
  def moduleDeps = Seq(bar, qux)

  def generatedSources = Task {
    os.write(
      Task.dest / "Version.java",
      s"""
package foo;
public class Version {
    public static String value() {
        return "${publishVersion()}";
    }
}
      """.stripMargin
    )
    Seq(PathRef(Task.dest))
  }
}

object bar extends MyModule {
  def moduleDeps = Seq(qux)
}

object qux extends MyModule

A semi-realistic build setup, combining all the individual Mill concepts:

  • Three `JavaModule`s that depend on each other

  • With unit testing and publishing set up

  • With generated sources to include the publishVersion as a string in the code, so it can be printed at runtime

Note that for multi-module builds like this, using queries to run tasks on multiple targets at once can be very convenient:

__.test
__.publishLocal

Also note how you can use traits to bundle together common combinations of modules: MyModule not only defines a JavaModule with some common configuration, but it also defines a object test module within it with its own configuration. This is a very useful technique for managing the often repetitive module structure in a typical project

> mill resolve __.run
bar.run
bar.test.run
foo.run
foo.test.run
qux.run

> mill foo.run
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: 31337

> mill bar.test
...bar.BarTests.test ...

> mill qux.run
Qux.value: 31337

> mill __.compile

> mill __.test
...bar.BarTests.test ...
...foo.FooTests.test ...

> mill __.publishLocal
Publishing Artifact(com.lihaoyi,foo,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,bar,0.0.1) to ivy repo...
Publishing Artifact(com.lihaoyi,qux,0.0.1) to ivy repo...
...

> mill show foo.assembly # mac/linux
".../out/foo/assembly.dest/out.jar"

> ./out/foo/assembly.dest/out.jar # mac/linux
foo version 0.0.1
Foo.value: <h1>hello</h1>
Bar.value: <p>world</p>
Qux.value: 31337

Example Builds for Real Projects

Mill comes bundled with example builds for real-world open-source projects, demonstrating how Mill can be used to build code outside of tiny example codebases:

JimFS

build.mill (download, browse)
package build
import mill._, javalib._, publish._

def sharedCompileIvyDeps = T{
  Agg(
    ivy"com.google.auto.service:auto-service:1.0.1",
    ivy"com.google.code.findbugs:jsr305:3.0.2",
    ivy"org.checkerframework:checker-compat-qual:2.5.5",
    ivy"com.ibm.icu:icu4j:73.1",
  )
}


object jimfs extends PublishModule with MavenModule {
  def publishVersion = "1.3.3.7"

  def pomSettings = PomSettings(
    description = artifactName(),
    organization = "com.google",
    url = "https://github.com/google/jimfs",
    licenses = Seq(License.MIT),
    versionControl = VersionControl.github(owner = "google", repo = "jimfs"),
    developers = Nil
  )

  def ivyDeps = sharedCompileIvyDeps() ++ Agg(
    ivy"com.google.guava:guava:31.1-android",
  )

  def javacOptions = Seq("-processor", "com.google.auto.service.processor.AutoServiceProcessor")

  object test extends MavenTests {
    def ivyDeps = sharedCompileIvyDeps() ++ Agg(
      ivy"junit:junit:4.13.2",
      ivy"com.google.guava:guava-testlib:31.1-android",
      ivy"com.google.truth:truth:1.1.3",
      ivy"com.github.sbt:junit-interface:0.13.2",
      ivy"com.ibm.icu:icu4j:73.1",
    )

    def testFramework = "com.novocode.junit.JUnitFramework"
  }
}

JimFS is a small Java library implementing an in-memory filesystem. It is commonly used in test suites to validate filesystem operations without needing to write to disk.

It has a relatively simple codebase structure, a single module and test suite. It has a number of compile-time-only dependencies shared between the library and test suite. One wrinkle is that it uses annotation processors as part of its build, which Mill supports by providing the relevant ivyDeps of the annotation processor and providing javacOptions to invoke it.

> ./mill jimfs.test
Test run com.google.common.jimfs.FileTest started
Test run com.google.common.jimfs.FileTest finished: 0 failed, 0 ignored, 7 total...
...

Apache Commons IO

build.mill (download, browse)
package build
import mill._, javalib._, publish._
import $ivy.`com.lihaoyi::mill-contrib-jmh:$MILL_VERSION`
import contrib.jmh.JmhModule

object `package` extends RootModule with PublishModule with MavenModule {
  def publishVersion = "2.17.0-SNAPSHOT"

  def pomSettings = PomSettings(
    description = artifactName(),
    organization = "org.apache.commons",
    url = "https://github.com/apache/commons-io",
    licenses = Seq(License.`Apache-2.0`),
    versionControl = VersionControl.github(owner = "apache", repo = "commons-io"),
    developers = Nil
  )

  object test extends MavenTests with TestModule.Junit5 with  JmhModule{
    def testSandboxWorkingDir = false
    def jmhCoreVersion = "1.37"
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"org.junit.jupiter:junit-jupiter:5.10.3",
      ivy"org.junit-pioneer:junit-pioneer:1.9.1",
      ivy"net.bytebuddy:byte-buddy:1.14.18",
      ivy"net.bytebuddy:byte-buddy-agent:1.14.18",
      ivy"org.mockito:mockito-inline:4.11.0",
      ivy"com.google.jimfs:jimfs:1.3.0",
      ivy"org.apache.commons:commons-lang3:3.14.0",
      ivy"commons-codec:commons-codec:1.17.1",
      ivy"org.openjdk.jmh:jmh-core:1.37",
      ivy"org.openjdk.jmh:jmh-generator-annprocess:1.37",
    )
  }
}

The Apache Commons IO library contains utility classes, stream implementations, file filters, file comparators, endian transformation classes, and much more.

The core library commonsio is dependency-free, but the test suite commonsio.test as a number of libraries included. It also ships with JMH benchmarks, which Mill supports via the built in JMH plugin

> ./mill compile
compiling 254 Java sources...
...

> ./mill test.compile
compiling 261 Java sources...
...

> ./mill test.testOnly org.apache.commons.io.FileUtilsTest
Test org.apache.commons.io.FileUtilsTest#testCopyFile1() started
Test org.apache.commons.io.FileUtilsTest#testCopyFile1() finished, took ...
...

> ./mill test.testOnly org.apache.commons.io.FileSystemTest
Test org.apache.commons.io.FileSystemTest#testIsLegalName() started
Test org.apache.commons.io.FileSystemTest#testIsLegalName() finished, took ...
...

> ./mill test.runJmh '.*PathUtilsContentEqualsBenchmark' -bm SingleShotTime
Benchmark                                                                Mode  Cnt ...
PathUtilsContentEqualsBenchmark.testCurrent_fileContentEquals              ss    5 ...
PathUtilsContentEqualsBenchmark.testCurrent_fileContentEquals_Blackhole    ss    5 ...
PathUtilsContentEqualsBenchmark.testProposal_contentEquals                 ss    5 ...
PathUtilsContentEqualsBenchmark.testProposal_contentEquals_Blackhole       ss    5 ...

Real World Mill Builds

C3P0

C3P0 is a JDBC connection pooling library written in Java, built using the Mill build tool.