May 22, 2024

Intro to Mill Build: Pleasant Complex Builds

I’ve experienced a few build tools over time: Apache Ant, Apache Maven, Gradle, SBT, MSBuild, make and probably some more I’ve forgotten about. Recently I’ve experimented with the Mill build tool and it is one of the best ones I’ve worked so far.

TLDR: In the Java eco system, I recommend to use Apache Maven when your app/library fits with its defaults. If Maven starts to be painful, consider Mill build and this blog series is for you ;).

Problems a Build Tool Should Help With

A build tool is a program which assembles things, so you think it should trivial. Indeed it often starts as simple script that as it grows, it needs some disciplining to help structuring things. This where build tools come in: They help you structure how things are assembled.

A built tool should give answers to questions like:

  1. Is there 'built-in' support for common build patterns?

  2. What part of the build depends on what other part?

  3. Where do the input files (source and previous build results) for a build step come from?

  4. In what order do things need to run?

  5. Can the build be parallelized?

  6. Where are intermediate results stored on disk? Where are these found? And where is the final outputs?

  7. How & What are things cached?

  8. How do I write a build task?

  9. How is data passed between tasks?

  10. How do I change an existing task?

  11. Is there IDE support?

  12. Can I inspect things? Navigate in definitions?

  13. Is it declarative?

For example, lets compare three build tools. Note that the answers are from my memory / impression, not facts.

✅ means I generally had know how it works.

⚠️ means I works, but can be a hassle sometimes.

❌ means this is missing or very hard to do/understand.


1.Common Build Pattern

✅ Maven Phases

✅ SBT Standard Tasks

2.What Depends on What

✅ Manual/Explicit

⚠️ Fixed Phases

⚠️ Tasks

3.Input Files/Build Steps

✅ Explicit

✅ Defaults/Config

✅ Defaults/Config


✅ Explicit

⚠️ Fixed Phases

️⚠️ Tasks-Preconfigured + Rewiring


️⚠️ Some built in?

6. Storing on Disk

⚠️ Explicit, fragile

❌ Unclear locations, fragile

❌ Unclear locations, fragile


⚠️ Partial + API

8.Language & API for tasks

❌ XML + Java extensions

❌❌❌ Write a plugin/Plugins

⚠️ Scala, SBT API. Scoping-Hell

9. Data-Passing

❌ ?

❌ ?

⚠️ Plain Scala Types?

10. Modify Existing Task

️⚠️ Add more to a Phase

️⚠️ Yes, but confusing

11. IDE Support

❌ Yes: But no real understanding

️✅ Excellent

⚠️ Yes, OK

12. Inspect Things/Navigate etc

⚠️ Navigating of task defs in IDE works. Then zero.

⚠️ Navigation in POM works. Then zero

⚠️️ Navigation works. Some commands. Can resolve the confusion.

13. Declarative?

⚠️ XML document, but imperative tasks

✅ Yes, mostly. Except opaque plugins


Or as summary for each build system:

Apache Ant

Ant is kind of "Make in XML" with a lot of Java related helper tasks. Works, but I generally do not recommend it. It just doesn’t give you much help.

Figure 1. Helpful Ant


First, Maven gave us the Maven repository that is, as package repository goes, quite good. So, I’m endlessly grateful for that. Maven has a declarative nature, which makes tooling like IDE understand Maven builds exceptionally well.

Maven shines if you do a box standard build, like a Java library, a Java web app, some box standard Java app. In these cases its default convention and declarative nature kicks ass. However, where Maven turns to hell is when your build needs very custom, non-standard steps. Like your build also needs to download some thing from a special API, then compile something with a unique tool, compile the same source against 2 different class paths and package that up in some unique way. At that point you end up in Maven plugin hell or call into random scripts. It just gets very ugly and fragile.

Overall, I recommend Maven if you build something fairly standard. But when you start to battle Maven, then it is probably time for a more flexible built tool, like Mill.

Strong Maven Standards
Figure 2. Standardization via Maven


A build tool specialized for Scala. Again, works well for box standard builds. However, as soon as you need more you’ll end up in hell. A special kind of hell.

My general advice: Try Maven first. If you need more Scala support, like cross builds or ScalaJS support, maybe consider SBT. I personally start with Mill right away these days =)

Standard Model of particles ahem SBT
Figure 3. Standard Model of particles ahem SBT.

Finally, Mill!

Ok, why do I like Mill: I think it boils down these reasons:

  • Tow core concepts: Tasks and modules. Everything is built out of these two building blocks.

  • The core concepts are then mapped nearly perfectly to Scala concepts: A task is a method, a module is a object. This means things you already know just work. You benefit from all your existing knowledge.

  • These concepts and naming are very consistent, like they also end up on disk.

Mill Grinds down (build) tasks
Figure 4. Mill Grinds down (build) tasks

Overall, Mill ends up filling out questions above very well:


1.Common Build Pattern

✅ Mill Standard Modules

2.What Depends on What

✅ Scala Call Hierarchy & inspect command

3.Input Files/Build Steps

✅ Defaults Standard Modules / Explicit


✅ Task Dependencies


✅ Built in

6. Storing on Disk

✅ Clear per task storage location


✅ By Default

8.Language & API for tasks

✅ Scala + familiar libraries

9. Data-Passing

✅ Plain Scala Types?

10. Modify Existing Task

️⚠️️ Scala Inheritance override

11. IDE Support

️⚠️️ OK via BSP

12. Inspect Things/Navigate etc

✅ Scala / Inspection Commands

13. Declarative?


Let’s start

This is a blog series, so I’ll go into aspects of Mill later. However, lets get minuscule build going for a Hello World java app:

  1. Download mill to your project directory:

curl -L > mill && chmod +x mill

Note, if you need native Windows support, you need a more advanced startup script, see here.

  1. Then create a Hello World file for Java in the src directory:

class HelloWorld{
    public static void main(String[] args){
        System.out.println("I'm built with Mill")
  1. Next, write the build definition in a file:
import mill._
import scalalib._

object demo extends RootModule with JavaModule{
  1. OK, it is time to do our first mill command: ./mill run

roman@roman-box ~/d/p/mill-demo> ./mill run
[] [49/53] compile
[info] compiling 1 Scala source to /home/roman/dev/private-dev/mill-demo/out/mill-build/compile.dest/classes ...
[info] done compiling
[24/37] compile
[info] compiling 1 Java source to /home/roman/dev/private-dev/mill-demo/out/compile.dest/classes ...
[info] done compiling
[37/37] run
I'm built with Mill

This builds the app and runs it immediately. The first time you use Mill, it will take some time, as it downloads mill itself etc to get the build going. The second time your run ./mill run it will be way faster.

Questions starting

Ok, we know that we can build and run the app with ./mill run. But we have our first question. We somehow use a common build pattern. But how do I know what it can do?

I’ll go into how Mill does common build patterns in follow up posts. Our build used the JavaModule standard build configuration here. Lets see what it can do with the ./mill resolve command.

Search for tasks:
./mill resolve _
(long list)
(long list cont...)

That means there is a jar task, so we can run that:

Create the Jar:
./mill jar
[29/29] jar

Ok, Mill built a jar, but where is it? This is where the ./mill show [task] command comes in. It will run the task and show the result of it. For example the built jar files path:

Create the Jar and show where it is:
./mill show jar
[1/1] show > [22/29] zincReportCachedProblems

So, the jar is in /home/roman/dev/private-dev/mill-demo/out/jar.dest/out.jar

Next time

That is it for this post. In this blog post I claimed that Mill is great. But I did not elaborate why it is great. In the next blog posts, I’ll show how Mill answers most of the the questions I’ve listed in this post.

In this series:
Tags: Scala Java Development