The Mill build tool

Just some experience notes about Mill.

Why experiment with this

see what makes mill unique.

Some downsides:

questions I had going in

setup bumpy roads

I use Intellij. So first I had to dig up the Scala plugin, which I uninstalled a long time ago since I didn’t think I’d be writing Scala ever :)

Next I got the wrapper script and put it in ./mill.bat.

Next, because I haven’t written any Scala before, I have to tell IDEA where to find a Scala standard library.

At this point I pasted the example into build.mill.

Also, in my case I was opening an existing project directory and Intellij already thought it was a Gradle project. This needed some whacking:

The initial bsp project import took a little bit; probably downloading some mill bits. Just give it some time to think. Turning off “export SBT project to Bloop before import” seemed to improve things (but i might be imagining it)

There is always some friction between build tools and whatever “the IDE project model” is. In this case it appears the root of the project becomes an IDE-side module called mill-synthetic-root.

playing around

Ok so I have this build.mill lifted straight from the example.

package build
import mill._, javalib._

object foo extends JavaModule {
  def ivyDeps = Agg(
    ivy"net.sourceforge.argparse4j:argparse4j:0.9.0",
    ivy"org.thymeleaf:thymeleaf:3.1.1.RELEASE"
  )

  object test extends JavaTests with TestModule.Junit4 {
    def ivyDeps = super.ivyDeps() ++ Agg(
      ivy"com.google.guava:guava:33.3.0-jre"
    )
  }
}

(IntelliJ prompted me to add override to those def ivyDeps.)

Ok, so I am learning a build tool and a new langauge at the same time.

The default directory for sources is foo/src; first foo (name of the module) then src and .java files go directly in there. This is unlike Maven where you src/main/java. But if you want the Maven format you can extend MavenModule instead of JavaModule. But here is the complete definition of MavenModule.

trait MavenModule extends JavaModule { outer =>

  override def sources = Task.Sources(
    millSourcePath / "src/main/java"
  )
  override def resources = Task.Sources {
    millSourcePath / "src/main/resources"
  }
}

So it’s not too difficult to do yourself :)

Because it’s just scala you can even press Ctrl-O to list all overridable methods! There are a lot of them in javalib but this is already 1000x better discoverability than Gradle.

subprojects

The subprojects story feels 100000x better.

In Gradle, you list subprojects in settings.gradle. In Mill you create several objects in build.mill (you can have several object ___ extends JavaModule).

In Gradle, you put subproject-specific configuration under <subproject name>/build.gradle. In Mill you define the subproject-specific configuration right alongside the definition of the subproject, so they all live in the same file.

In Gradle, you configure all subprojects with a subprojects or allprojects block and then get yelled at because that’s not the right way to do it and actually you’re supposed to create a build convention plugin in buildSrc or whatever the fuck. In Mill you configure all subprojects by creating your own module type (ex. trait MyModule extends JavaModule) and subclass that from your subprojects. (this is called a trait module.)

Like damn this model is so much better. subprojects blocks stop being magic-at-a-distance and start being something you can see is happening, because you extend a different module than JavaModule, and if you ctrl-click the module you can go to it. You can leverage existing object-oriented intuition and do inheritance, abstract methods, and so on.

This also means we don’t need anything like gradle.properties substitution, because you can write a trait (doesn’t have to be a module), with it from your modules, and grab the properties from there.

tasks

I think it’s instructive to look at the verbose, non JavaModule build example.

The task types are:

The most important ones are Source/Sources, used for your project sources, Task, used for compilation and processing tasks, and Input, used for external data you want to slot into the build (like a version number or the git status or whatever)

When writing a general Task:

modules

The page is pretty clear

(See the above glazing of the subprojects system.)

Scala also has classes but they are not recommended “due to implementation limitations”. Ok. Im not too familiar with Scala but traits look like what i need anyway.

Scala has multiple inheritance across traits so you can bring in as many traits as you want. See JavaModule.

Check this out too https://mill-build.org/mill/fundamentals/modules.html#_use_case_diy_java_modules

random cheatsheety things

Goddammit. The gradle “version catalog” thing everyone was hyped about turns out to just be a pear-shaped bill of materials. Mill’s BoM system can load external BoMs off Maven servers, or you can just write one.