Modules
mill.Module serves two main purposes:
-
As
objects, they serve as namespaces that let you group relatedTasks together to keep things neat and organized. -
As
traits, they are re-usable templates that let you replicate groups of relatedTasks and sub-Modules while allowing customizations
Mill’s comes with built-in modules such as mill.scalalib.ScalaModule and
mill.scalalib.CrossSbtModule, but you can also define your own modules to do
things that are not built-in to Mill.
Simple Modules
The path to a Mill module from the root of your build file corresponds to the
path you would use to run tasks within that module from the command line. e.g.
for the following build.mill:
package build
import mill.*
object foo extends Module {
def bar = Task { "hello" }
object qux extends Module {
def baz = Task { "world" }
}
}
-
You can run the two tasks via
mill foo.barormill foo.qux.baz. -
You can use
mill show foo.barormill show foo.baz.quxto make Mill echo out the string value being returned by each Task. -
The two tasks will store their output metadata and files at
./out/foo/bar.{json,dest}and./out/foo/baz/qux.{json,dest}respectively.
> ./mill foo.bar
> ./mill foo.qux.baz
> ./mill show foo.bar
"hello"
> ./mill show foo.qux.baz
"world"
> cat ./out/foo/bar.json # task output path follows module hierarchy
..."value": "hello"...
> cat ./out/foo/qux/baz.json
..."value": "world"...
Module Traits
Modules also provide a way to define and re-use common collections of tasks,
via traits. traits are basically Java or Python classes with some additional
flexibility, and Module traits support everything normal
classes do: inheritence via extends, abstract defs, overrides, super,
adding additional defs, etc.
trait FooModule extends Module {
def bar: T[String] // required override
def qux = Task { bar() + " world" }
}
object foo1 extends FooModule {
def bar = "hello"
def qux = super.qux().toUpperCase // refer to overridden value via super
}
object foo2 extends FooModule {
def bar = "hi"
def baz = Task { qux() + " I am Cow" } // add a new `def`
}
This generates the following module tree and task graph, with the dotted boxes and arrows representing the module tree, and the solid boxes and arrows representing the task graph
Note that the override keyword is optional in mill, as is T{…} wrapper.
> ./mill show foo1.bar
"hello"
> ./mill show foo1.qux
"HELLO WORLD"
> ./mill show foo2.bar
"hi"
> ./mill show foo2.qux
"hi world"
> ./mill show foo2.baz
"hi world I am Cow"
The built-in mill.scalalib package uses this to define ScalaModule,
SbtModule and TestScalaModule, etc. which contain a set of "standard"
operations such as compile, jar or assembly that you may expect from a
typical Scala module.
When defining your own module abstractions, you should be using traits
and not classes due to implementation limitations
moduleDir
Each Module has a moduleDir field that corresponds to the path that
module expects its input files to be on disk.
trait MyModule extends Module {
def sources = Task.Source("sources")
def task = Task { "hello " + os.list(sources().path).map(os.read(_)).mkString(" ") }
}
object outer extends MyModule {
object inner extends MyModule
}
-
The
outermodule has amoduleDirofouter/, and thus aouter.sourcesreferencingouter/sources/ -
The
innermodule has amoduleDirofouter/inner/, and thus aouter.inner.sourcesreferencingouter/inner/sources/
> ./mill show outer.task
"hello contents of file inside outer/sources/"
> ./mill show outer.inner.task
"hello contents of file inside outer/inner/sources/"
You should use moduleDir to set the source folders of your
modules to match the build structure. In almost every case, a module’s source files
live at some relative path within the module’s folder, and using moduleDir
ensures that the relative path to the module’s source files remains the same
regardless of where your module lives in the build hierarchy.
E.g. for mill.scalalib.ScalaModule, the Scala source code is assumed by
default to be in moduleDir / "src" while resources are automatically
assumed to be in moduleDir / "resources".
You can also override moduleDir:
object outer2 extends MyModule {
def moduleDir = super.moduleDir / "nested"
object inner extends MyModule
}
> ./mill show outer2.task
"hello contents of file inside outer2/nested/sources/"
> ./mill show outer2.inner.task
"hello contents of file inside outer2/nested/inner/sources/"
Any overrides propagate down to the module’s children: in the above example,
outer2 would have its moduleDir be outer2/nested/ while
outer.inner would have its moduleDir be outer2/nested/inner/.
Note that moduleDir is meant to be used for a module’s input source
files: source code, config files, library binaries, etc. Output is always in
the out/ folder and cannot be changed, e.g. even with the overridden
moduleDir the output paths are still the default ./out/outer2 and
./out/outer2/inner folders:
> cat ./out/outer2/task.json
..."value": "hello contents of file inside outer2/nested/sources/"...
> cat ./out/outer2/inner/task.json
..."value": "hello contents of file inside outer2/nested/inner/sources/"...
os.pwd of the Mill process is set to an empty sandbox/ folder by default.
When defining a module’s source files, you should always use moduleDir to ensure the
paths defined are relative to the module’s root folder, so the module logic can continue
to work even if moved into a different subfolder. In the rare case where you need the
Mill project root path, and you truly know what you are doing, you can call
WorkspaceRoot.workspaceRoot.
|
Module Overrides
Tasks within Module Traits can be overridden via the override keyword,
with the overridden task callable via super.
You can also override a task with a different type of task, e.g. below
we override sourceRoots which is a Task.Sources with a cached Task{}
that depends on the original via super:
trait Foo extends Module {
def sourceRoots = Task.Sources("src")
def sourceContents = Task {
sourceRoots()
.flatMap(pref => os.walk(pref.path))
.filter(_.ext == "txt")
.sorted
.map(os.read(_))
}
}
trait Bar extends Foo {
def additionalSources = Task.Sources("src2")
def sourceRoots = Task { super.sourceRoots() ++ additionalSources() }
}
object bar extends Bar
> ./mill show bar.sourceContents # includes both source folders
[
"File Data From src/",
"File Data From src2/"
]
Root Modules
You can use object to use a package extends mill.ModuleModule
as the root module of the file:
package build
import mill.*, javalib.*
object `package` extends JavaModule {
def mvnDeps = Seq(
mvn"net.sourceforge.argparse4j:argparse4j:0.9.0",
mvn"org.thymeleaf:thymeleaf:3.1.1.RELEASE"
)
object test extends JavaTests, TestModule.Junit4 {
def mvnDeps = super.mvnDeps() ++ Seq(
mvn"com.google.guava:guava:33.3.0-jre"
)
}
}
Since our object , its files live in a
top-level package extends mill.Modulesrc/ folder, and you can call its tasks via un-prefixed bar
build.mill
src/
foo/Foo.java
resources/
...
test/
src/
foo/FooTest.java
out/
compile.json
compile.dest/
...
test/
compile.json
compile.dest/
...
> ./mill compile # compile sources into classfiles
...
compiling 1 Java source to...
> ./mill run # run the main method, if any
error: argument -t/--text is required
...
> ./mill run --text hello
<h1>hello</h1>
> ./mill test
...
Test foo.FooTest.testEscaping finished, ...
Test foo.FooTest.testSimple finished, ...
Test run foo.FooTest finished: 0 failed, 0 ignored, 2 total, ...
RootModule is useful when you want not only to define top-level tasks yourself,
but to have the top-level tasks inherited from some pre-defined trait (in this
case JavaModule).
RootModules can only have the name package and be defined at the top-level of
a build.mill or package.mill file. If a RootModule is defined, all other
tasks or modules in that file must be defined within it, as it is the root of the
module hierarchy.
Use Case: DIY Java Modules
This section puts together what we’ve learned about Tasks and Modules
so far into a worked example: implementing our own minimal version of
mill.scalalib.JavaModule from first principles.
package build
import mill.*
trait DiyJavaModule extends Module {
def moduleDeps: Seq[DiyJavaModule] = Nil
def mainClass: T[Option[String]] = None
def upstream: T[Seq[PathRef]] = Task { Task.traverse(moduleDeps)(_.classPath)().flatten }
def sources = Task.Source("src")
def compile = Task {
val allSources = os.walk(sources().path)
val cpFlag = Seq("-cp", upstream().map(_.path).mkString(":"))
os.call(
cmd = ("javac", cpFlag, allSources, "-d", Task.dest)
)
PathRef(Task.dest)
}
def classPath = Task { Seq(compile()) ++ upstream() }
def assembly = Task {
for (cp <- classPath()) os.copy(cp.path, Task.dest, mergeFolders = true)
val mainFlags = mainClass().toSeq.flatMap(Seq("-e", _))
os.call(cmd = ("jar", "-c", mainFlags, "-f", Task.dest / "assembly.jar", "."))
PathRef(Task.dest / "assembly.jar")
}
}
This defines the following build graph for DiyJavaModule. Note that some of the
edges (dashed) are not connected; that is because DiyJavaModule is abstract, and
needs to be inherited by a concrete object before it can be used.
Some notable things to call out:
-
def moduleDepsis not a Task. This is necessary because tasks cannot change the shape of the task graph during evaluation, whereasmoduleDepsdefines module dependencies that determine the shape of the graph. -
Using
Task.traverseto recursively gather the upstream classpath. This is necessary to convert theSeq[T[V]]into aT[Seq[V]]that we can work with inside our tasks -
We use the
moduleDirtogether withmill.api.BuildCtx.workspaceRootto infer a default name for the jar of each module. Users can override it if they want, but having a default is very convenient -
def cpFlagis not a task or task, it’s just a normal helper method.
Below, the inherit DiyJavaModule in three objects: foo, bar, and qux:
object foo extends DiyJavaModule {
def moduleDeps = Seq(bar)
def mainClass = Some("foo.Foo")
object bar extends DiyJavaModule
}
object qux extends DiyJavaModule {
def moduleDeps = Seq(foo)
def mainClass = Some("qux.Qux")
}
This results in the following build graph, with the build graph for DiyJavaModule
duplicated three times - once per module - with the tasks wired up between the modules
according to our overrides for moduleDeps
This simple set of DiyJavaModule can be used as follows:
> ./mill showNamed __.sources
{
"foo.sources": ".../foo/src",
"foo.bar.sources": ".../foo/bar/src",
"qux.sources": ".../qux/src"
}
> ./mill show qux.assembly
".../out/qux/assembly.dest/assembly.jar"
> java -jar out/qux/assembly.dest/assembly.jar
Foo.value: 31337
Bar.value: 271828
Qux.value: 9000
> ./mill show foo.assembly
".../out/foo/assembly.dest/assembly.jar"
> java -jar out/foo/assembly.dest/assembly.jar
Foo.value: 31337
Bar.value: 271828
Like any other Tasks, the compilation and packaging of the Java code
is incremental: if you change a file in foo/src/ and run qux.assembly,
foo.compile and qux.compile will be re-computed, but bar.compile will
not as it does not transitively depend on foo.sources. We did not need to
build support for this caching and invalidation ourselves, as it is
automatically done by Mill based on the structure of the build graph.
Note that this is a minimal example is meant for educational purposes: the
JavaModule and ScalaModule that Mill provides is more
complicated to provide additional flexibility and performance. Nevertheless,
this example should give you a good idea of how Mill modules can be
developed, so you can define your own custom modules when the need arises.
Default Tasks
Mill modules can extend TaskModule and specify a defaultTask, which
allows them to be run directly without needing the task name provided explicitly:
package build
import mill.*, javalib.*
import mill.api.DefaultTaskModule
object foo extends DefaultTaskModule {
override def defaultTask() = "bar"
def bar() = Task.Command { println("Hello Bar") }
def qux() = Task.Command { println("Hello Qux") }
}
In this example, the foo module has a defaultTask of bar
That means that we can run foo and it will run foo.bar automatically.
running foo.bar explicitly is still allowed, as is running other tasks
e.g. foo.qux
> ./mill foo # same as running foo.bar
Hello Bar
> ./mill foo.bar
Hello Bar
> ./mill foo.qux
Hello Qux
Default tasks are a convenience that is often used when a module has one "obvious"
task to run, e.g. Mill TestModule`s have `test as the default command name.
Backticked Names
package build
import mill.*
import mill.scalalib.*
object `hyphenated-module` extends Module {
def `hyphenated-task` = Task {
println("hyphenated task in a hyphenated module.")
}
}
object unhyphenatedModule extends Module {
def unhyphenated_task = Task {
println("unhyphenated task in an unhyphenated module.")
}
}
Mill modules and tasks may be composed of the following character types:
-
Alphanumeric (A-Z, a-z, and 0-9)
-
Underscore (
_) -
Hyphen (
-)
Due to Scala naming restrictions, module and task names with hyphens must be
surrounded by back-ticks (`).
Using hyphenated names at the command line is unaffected by these restrictions.
> ./mill hyphenated-module.hyphenated-task
hyphenated task in a hyphenated module.
> ./mill unhyphenatedModule.unhyphenated_task
unhyphenated task in an unhyphenated module.
External Modules
Libraries for use in Mill can define ExternalModules: Modules which are
shared between all builds which use that library:
package foo
import mill.*
object Bar extends mill.api.ExternalModule {
def baz = Task { 1 }
def qux() = Task.Command { println(baz() + 1) }
lazy val millDiscover = Discover[this.type]
}
In the above example, Bar is an ExternalModule living within the foo
Java package, containing the baz task and qux command. Those can be run
from the command line via:
> mill foo.Bar/baz
> mill foo.Bar/qux
ExternalModules are useful for someone providing a library for use with Mill
that is shared by the entire build: for example,
mill.scalalib.JvmWorkerApi/jvmWorker provides a shared Scala compilation
service & cache that is shared between all ScalaModules, and
mill.scalalib.GenIdea/idea lets you generate IntelliJ projects without
needing to define your own Task.Command in your build.mill file
Aliasing External Modules
Mill allows you to alias external modules via def. You can use this to add
shorthand aliases for external modules that have long names, such as
mill.javalib.palantirformat.PalantirFormatModule below:
package build
import mill.*, javalib.*
object foo extends JavaModule
def myAutoformat = mill.javalib.palantirformat.PalantirFormatModule
> cat foo/src/foo/Foo.java # starts off unformatted
package foo;public class Foo{ public static void main(String[] args) {System.out.println("Hello World!");}}
> ./mill myAutoformat # easier to type than `./mill mill.javalib.palantirformat/`
> cat foo/src/foo/Foo.java # code is now formatted
package foo;
public class Foo {
public static void main(String[] args) {
System.out.println("Hello World!");
}
}
Abstract Modules References
When you define an abstract module, often you are referencing an existing module
somewhere in your build. A naive def upstreamModule: FooModule
would create a module alias, which is often not what you want since the referenced
module already has a place in your build hierarchy. In such scenarios, you can use
a ModuleRef(…) to wrap the abstract module, such that the abstract def does
not participate in task query resolution:
package build
import mill.*, javalib.*
import mill.api.ModuleRef
object foo extends JavaModule
object bar extends JavaModule
trait MyTestModule extends JavaModule, TestModule.Junit4 {
def upstreamModule: ModuleRef[JavaModule]
def moduleDeps = Seq(upstreamModule())
}
object footest extends MyTestModule {
def upstreamModule = ModuleRef(foo)
}
object bartest extends MyTestModule {
def upstreamModule = ModuleRef(bar)
}
> ./mill __.testForked
Test foo.FooTests.simple finished, ...
Test bar.BarTests.simple finished, ...
...
> ./mill resolve foo.upstreamModule._ # This fails since it's a `ModuleRef`, not just a `Module`
error: resolve Cannot resolve foo.upstreamModule...