Just some experience notes about Mill.
Some downsides:
import $ivy
magic gives me some pause. It appears hardcoded to Maven Central. In the Minecraft ecosystem we share lots of artifacts in places other than Maven Central.includeBuild("../../")
) to dep on the real project from my test projects. This always struck me as silly, like… is there really not a “pretty” way to write integration tests for your program? how else do you know whether the API is any good?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
.
./.mill-version
, kind of like “the one thing anyone ever cares about in gradle-wrapper.properties
”. If you don’t set a .mill-version
it will use the latest version. The current version of mill atm is 0.12.8
.Next, because I haven’t written any Scala before, I have to tell IDEA where to find a Scala standard library.
build.mill
file, picked Override File Type and chose scala. This made it syntax-highlight the file as Scala but didn’t provide semantic analysis.build.mill
prompted a banner at the top of the screen that said “no scala SDK found” or something. I clicked it and just downloaded whatever scala version it picked which was 2.13.15
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:
.idea
folder from the project lol.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
.
Ok so I have this build.mill
lifted straight from the example.
package build
import mill._, javalib._
object foo extends JavaModule {
def ivyDeps = Agg(
"net.sourceforge.argparse4j:argparse4j:0.9.0",
ivy"org.thymeleaf:thymeleaf:3.1.1.RELEASE"
ivy)
object test extends JavaTests with TestModule.Junit4 {
def ivyDeps = super.ivyDeps() ++ Agg(
"com.google.guava:guava:33.3.0-jre"
ivy)
}
}
(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.
JavaModule
comes from javalib
which is introduced hereJavaModule
of course, which is introduced a bit below here. That definitely feels a little less “gradle magic” than this code sample.JavaModule
if I wanted. But skimming this wouldn’t be a bad first step.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(
/ "src/main/java"
millSourcePath )
override def resources = Task.Sources {
/ "src/main/resources"
millSourcePath }
}
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.
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.
I think it’s instructive to look at the verbose, non JavaModule
build example.
The task types are:
Task
, a piece of Scala code which is executed if its input tasks change and is considered “outdated” if the output is differentTask.Input
, a piece of Scala code which is executed every time and considered “outdated” if the output is differentTask.Source
, a file/folder-tree which is considered outdated when it changesTask.Sources
, a list thereofTask.Command
, which can take parameters
Task.Worker
, a task which sticks around in the Mill daemonThe 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
:
Task.dest
. This is a unique folder created fresh for your task and you can write whatever you want in there.
Task.dest / "myfile.txt"
persistent = true
. But then it’s your responsibility to maintain cache coherency.myTask()
. Doing this will also create a dependency edge on that task
The page is pretty clear
object
modules are namespaces and organizational toolstrait
modules are subclass-bait, used to factor code(See the above glazing of the subprojects system.)
Scala also has class
es but they are not recommended “due to implementation limitations”. Ok. Im not too familiar with Scala but trait
s 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
./mill shutdown
is like ./gradlew --stop
./out
is like gradle’s ./build
and should be gitignoredGoddammit. 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.