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.