July 22, 2024

Build Questions answered by Mill builds

I’ve started this series with a list of questions you will encounter in non trivial builds. I claimed that Mill had great at answering most of these questions, but left the actual answers open. Well, after showing Mill for a bit, it is time to answer them.

Ticking Off Boxes
Figure 1. Ticking Off Boxes

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

✅ Mill does this by providing standard modules, which are regular Scala traits (Java,think interfaces with default methods). There are standard modules for Java, Scala, ScalaJS and different testing frameworks. Because modules are regular Scala traits, it is easy to create a module for your build patterns.

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

✅ Task depends on other targets which they call. You can use your IDE to find callee in the actual build definition. The mill inspect {your-task} will tell you inputs of a particular task. mill visualizePlan {your-task} will give you a whole dependency graph.

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

✅ A task returns its output via regular return. Follow up tasks consume the results of task, like any other method call would do. For files, return them as a PathRef from tasks.

4. In what order do things need to run?

✅ Determined by the tasks. If a task depends on another task, the things it depends on run first.

5. Can the build be parallelized?

✅ Yes, by specifying a -j {number-of-concurrent-jobs} when invoking the mill command.

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

✅ Each target task gets a directory assign, which is ./out/{modules}/{task-name}.dest. In the task code you use the T.dest to store things. Final outputs are in the task directory of the task invoked via CLI.

7. How & What are things cached?

✅ If a tasks inputs are the same as to a previous run, the the task is skipped and its previous result is returned. The returned values of a task are cached, including the tasks output directory.

8. How do I write a build task?

✅ You use Scala, with the full power of the Java API plus a selection of very useful extra libraries included libraries.

9. How is data passed between tasks?

✅ Task return the results, which can be arbitrary values and file references. The values returned can be used by the caller like a regular method call.

10. How do I change an existing task?

 You override the task definition of the Module you are using. The regular Scala inheritance and overriding rules apply.
✅ Up side: No new rules, familiar for Scala devs, familiar enough for Java devs.
️⚠️️ Down side: It might be sometimes a bit more limiting than you want.
✅ Up side: It is limited to this understood mechanism, no magic from a distance modifications to build allowed.

11. Is there IDE support?

️⚠️️ For importing the Mill build structure mill.idea.GenIdea/idea and ./mill mill.bsp.BSP/install, which works ok, but is not the fully integrated IDE support you enjoy for something like Maven. ⚠️️ The build definitions are in Scala, so you have language support for that. But again, you wont get any deep insights from the IDE. And the IDE might struggle to understand advanced import statement Mill supports.

12. Can I inspect things? Navigate in definitions?

✅ You get great mill commands like resolve, inspect, show, visualizePlan, etc. And for the build files themself you get the support from you files, as they are written in Scala.

13. Is it declarative?

❌ No. No. Mill must compile your build definition first to introspect it. The task/module definitions then have somewhat a declarative feel. The actual tasks are imperative stops of operations.

Summary

Mill does a great job answering questions you might have for a build. It’s minimalism in concepts, it’s clear design and its re-use of concepts developers are already familiar gives a great package, where you are rarely puzzled by the build.

Tags: Scala Mill Build Java Development