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:
Is there 'built-in' support for common build patterns?
What part of the build depends on what other part?
Where do the input files (source and previous build results) for a build step come from?
In what order do things need to run?
Can the build be parallelized?
Where are intermediate results stored on disk? Where are these found? And where is the final outputs?
How & What are things cached?
How do I write a build task?
How is data passed between tasks?
How do I change an existing task?
Is there IDE support?
Can I inspect things? Navigate in definitions?
Is it declarative?
Is stable & backwards compatible?
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.
Ant | Maven | SBT | |
---|---|---|---|
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 |
4.Order | ✅ Explicit | ⚠️ Fixed Phases | ️⚠️ Tasks-Preconfigured + Rewiring |
5.Parallelization | ❌ | ❌ | ️⚠️ Some built in? |
6. Storing on Disk | ⚠️ Explicit, fragile | ❌ Unclear locations, fragile | ❌ Unclear locations, fragile |
7.Caching | ❌ | ❌ | ⚠️ 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 | ❌❌❌ |
14. Stable? | ✅ Yes | ✅ Yes | ⚠️️ Yes-ish, but it had a very fragile past |
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.
Maven
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.
SBT
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 =)
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.
Overall, Mill ends up filling out questions above very well:
Mill | |
---|---|
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 |
4.Order | ✅ Task Dependencies |
5.Parallelization | ✅ Built in |
6. Storing on Disk | ✅ Clear per task storage location |
7.Caching | ✅ 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? | ❌❌❌ |
13. Stable? | ❌ Not yet |
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:
Download mill to your project directory:
curl -L https://github.com/com-lihaoyi/mill/releases/download/0.11.7/0.11.7 > mill && chmod +x mill
Note, if you need native Windows support, you need a more advanced startup script, see here.
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")
}
}
Next, write the build definition in a
build.sc
file:
import mill._
import scalalib._
object demo extends RootModule with JavaModule{
}
OK, it is time to do our first mill command:
./mill run
roman@roman-box ~/d/p/mill-demo> ./mill run [build.sc] [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.
./mill resolve _
(long list)
jar
javacOptions
javadocOptions
(long list cont...)
That means there is a jar
task, so we can run that:
./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:
./mill show jar
[1/1] show > [22/29] zincReportCachedProblems
"ref:v0:99b775ff:/home/roman/dev/private-dev/mill-demo/out/jar.dest/out.jar"
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.