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
import mill._, javalib._
object foo 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 = T.sources{
super.sources() ++ Seq(PathRef(millSourcePath / "custom-src"))
}
// Add (or replace) resource folders for the module to use
def resources = T.sources{
super.resources() ++ Seq(PathRef(millSourcePath / "custom-resources"))
}
// Generate sources at build time
def generatedSources: T[Seq[PathRef]] = T {
for(name <- Seq("A", "B", "C")) os.write(
T.dest / s"Foo$name.java",
s"""
package foo;
public class Foo$name {
public static String value = "hello $name";
}
""".stripMargin
)
Seq(PathRef(T.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")
}
Note the use of millSourcePath
, T.dest
, and PathRef
when preforming
various filesystem operations:
-
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 modulefoo.bar.qux
themillSourcePath
would befoo/bar/qux
. This can also be overriden if necessary -
T.dest
refers to the destination folder for a task in theout/
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 -
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
import mill._, javalib._
object foo extends RootModule with JavaModule {
def ivyDeps = Agg(ivy"net.sourceforge.argparse4j:argparse4j:0.9.0")
def generatedSources: T[Seq[PathRef]] = T {
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(
T.dest / s"MyDeps.java",
s"""
package foo;
public class MyDeps {
public static String value =
$ivyDepsString;
}
""".stripMargin
)
Seq(PathRef(T.dest))
}
def lineCount: T[Int] = T {
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() = T.command { println(lineCount()) }
}
Mill lets you define new cached Targets using the T {…}
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 T {…}
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 T.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 T.dest
, and any files you want to modify should be copied into
T.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
import mill._, javalib._
object foo extends JavaModule {
def sources = T{
os.write(
T.dest / "Foo.java",
"""package foo;
public class Foo {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
""".stripMargin
)
Seq(PathRef(T.dest))
}
def compile = T {
println("Compiling...")
super.compile()
}
def run(args: Task[Args] = T.task(Args())) = T.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 T.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(T.dest / "Foo.java", """...""")
Seq(PathRef(T.dest))
}
}
object foo3 extends JavaModule {
def sources = T{
os.write(T.dest / "Foo.java", """...""")
super.sources() ++ Seq(PathRef(T.dest))
}
}
In Mill builds the override
keyword is optional.
> mill foo.run
Compiling...
Running...
Hello World
Nesting Modules
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>
Java Module With Test Suite
import mill._, javalib._
object foo extends JavaModule {
object test extends JavaTests {
def testFramework = "com.novocode.junit.JUnitFramework"
def ivyDeps = Agg(
ivy"com.novocode:junit-interface:0.11",
ivy"org.mockito:mockito-core:4.6.1"
)
}
}
This build defines a single module with a test suite, configured to use
"JUnit" as the testing framework, along with Mockito. Test suites are themselves
JavaModule
s, nested within the enclosing module,
and have all the normal tasks like
foo.test.compile
available to run, but with an additional .test
task
that runs the tests. You can also run the test suite directly, in which case
it will run the .test
task as the default task for that module.
> mill foo.compile
compiling 1 ... source...
> mill foo.test.compile
compiling 1 ... source...
> mill foo.test.test
...foo.FooTests.hello ...
...foo.FooTests.world ...
> mill foo.test
...foo.FooTests.hello ...
...foo.FooTests.world ...
For convenience, you can also use one of the predefined test frameworks:
-
TestModule.Junit4
-
TestModule.Junit5
-
TestModule.TestNg
-
TestModule.Munit
-
TestModule.ScalaTest
-
TestModule.Specs2
-
TestModule.Utest
-
TestModule.ZioTest
object bar extends JavaModule {
object test extends JavaTests with TestModule.Junit4{
def ivyDeps = super.ivyDeps() ++ Agg(
ivy"org.mockito:mockito-core:4.6.1"
)
}
}
> mill bar.test
...bar.BarTests.hello ...
...bar.BarTests.world ...
By default, tests are run in a subprocess, and forkArg
and forkEnv
can be
overridden to pass JVM flags & environment variables. You can also use
mill foo.test.testLocal
To run tests in-process in an isolated classloader.
If you want to pass any arguments to the test framework, simply put them after
foo.test
in the command line. e.g. uTest
lets you pass in a selector to decide which test to run, which in Mill would be:
> mill bar.test bar.BarTests.hello
...bar.BarTests.hello ...
You can also define multiple test suites if you want, e.g.:
object qux extends JavaModule {
object test extends JavaTests with TestModule.Junit5
object integration extends JavaTests with TestModule.Junit5
}
This example also demonstrates using Junit 5 instead of Junit 4,
with
Each of which will expect their sources to be in their respective foo/test
and
foo/integration
folder.
> mill qux.test
...qux.QuxTests...hello...
...qux.QuxTests...world...
> mill qux.integration
...qux.QuxIntegrationTests...helloworld...
> mill qux.{test,integration}
...qux.QuxTests...hello...
...qux.QuxTests...world...
...qux.QuxIntegrationTests...helloworld...
Publish Module
import mill._, javalib._, publish._
object foo 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")
)
)
}
This is an example JavaModule
with added publishing capabilities via
PublishModule
. This requires that you define an additional
publishVersion
and pomSettings
with the relevant metadata, and provides
the .publishLocal
and publishSigned
tasks for publishing locally to the
machine or to the central maven repository
> mill foo.publishLocal
Publishing Artifact(com.lihaoyi,foo,0.0.1) to ivy repo...
The artifactName
defaults to the name of your module (in this case foo
)
but can be overridden. The organization
is defined in pomSettings
.
You may also check and update the values of sonatypeUri
and sonatypeSnapshotUri
,
which may not be correct if you have a newer Sonatype account (when created after Feb. 2021).
Staging Releases
Once you’ve mixed in PublishModule
, you can publish your libraries to maven
central via:
mill mill.scalalib.PublishModule/publishAll \
foo.publishArtifacts \
lihaoyi:$SONATYPE_PASSWORD \
--gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b
This uploads them to oss.sonatype.org
where you can log-in and stage/release
them manually. You can also pass in the --release true
flag to perform the
staging/release automatically:
Recent versions of Sonatype credentials can be passed via environment variables ( |
mill mill.scalalib.PublishModule/publishAll \
foo.publishArtifacts \
lihaoyi:$SONATYPE_PASSWORD \
--gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b \
--release true
If you want to publish/release multiple modules, you can use the or
_
wildcard syntax:
mill mill.scalalib.PublishModule/publishAll \
__.publishArtifacts \
lihaoyi:$SONATYPE_PASSWORD \
--gpgArgs --passphrase=$GPG_PASSWORD,--batch,--yes,-a,-b \
--release true
To publish to repository other than oss.sonaytype.org
such as internal hosted
nexus at example.company.com
, you can pass in the --sonatypeUri
and
--sonatypeSnapshotUri
parameters to uploads to different site:
mill mill.scalalib.PublishModule/publishAll \
foo.publishArtifacts \
lihaoyi:$SONATYPE_PASSWORD \
--sonatypeUri http://example.company.com/release \
--sonatypeSnaphostUri http://example.company.com/snapshot
Since Feb. 2021 any new Sonatype accounts have been created on
The symptom of using the "wrong" URL for publishling is typically a 403 error code, in response to the publish request. Typically
|
Non-Staging Releases (classic Maven uploads)
If the site does not support staging releases as oss.sonatype.org
and s01.oss.sonatype.org
do (for
example, a self-hosted OSS nexus site), you can pass in the
--stagingRelease false
option to simply upload release artifacts to corresponding
maven path under sonatypeUri
instead of staging path.
mill mill.scalalib.PublishModule/publishAll \
foo.publishArtifacts \
lihaoyi:$SONATYPE_PASSWORD \
--sonatypeUri http://example.company.com/release \
--stagingRelease false
Maven-Compatible Modules
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
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 = T {
os.write(
T.dest / "Version.java",
s"""
package foo;
public class Version {
public static String value() {
return "${publishVersion()}";
}
}
""".stripMargin
)
Seq(PathRef(T.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 trait
s to bundle together common combinations of
modules: My=Module
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
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.
Project home: https://github.com/google/jimfs
> ./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
import mill._, javalib._, publish._
import $ivy.`com.lihaoyi::mill-contrib-jmh:$MILL_VERSION`
import contrib.jmh.JmhModule
object commonsio 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 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
Project home: https://github.com/apache/commons-io
> ./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.