Extending Mill
There are different ways of extending Mill, depending on how much customization and flexibility you need. This page will go through your options from the easiest/least-flexible to the hardest/most-flexible.
import $file and import $ivy
import mill._, scalalib._
import $ivy.`com.lihaoyi::scalatags:0.8.2`, scalatags.Text.all._
import $file.scalaversion, scalaversion.myScalaVersion
object foo extends RootModule with ScalaModule {
def scalaVersion = myScalaVersion
def ivyDeps = Agg(ivy"com.lihaoyi::os-lib:0.9.1")
def htmlSnippet = T{ div(h1("hello"), p("world")).toString }
def resources = T.sources{
os.write(T.dest / "snippet.txt", htmlSnippet())
super.resources() ++ Seq(PathRef(T.dest))
}
}
def myScalaVersion = "2.13.8"
This example illustrates usage of import $file
and import $ivy
. These
allow you to pull in code from outside your build.sc
file:
-
import $file
lets you import other*.sc
files on disk. This lets you split up yourbuild.sc
logic if the file is growing too large. In this tiny example case, we movemyScalaVersion
to anotherversions.sc
file and import it for use. -
import $ivy
lets you import ivy dependencies into yourbuild.sc
, so you can use arbitrary third-party libraries at build-time. This makes lets you perform computations at build-time rather than run-time, speeding up your application start up. In this case, we move the Scalatags rendering logic to build time, so the application code gets a pre-rendere string it can directly print without further work.
> ./mill compile
compiling 1 Scala source...
...
> ./mill run
generated snippet.txt resource: <div><h1>hello</h1><p>world</p></div>
> ./mill show assembly
".../out/assembly.dest/out.jar"
> ./out/assembly.dest/out.jar # mac/linux
generated snippet.txt resource: <div><h1>hello</h1><p>world</p></div>
The Mill Meta-Build
import $meta._
import mill._, scalalib._
import scalatags.Text.all._
object foo extends RootModule with ScalaModule {
def scalaVersion = millbuild.ScalaVersion.myScalaVersion
def ivyDeps = Agg(ivy"com.lihaoyi::os-lib:0.9.1")
def htmlSnippet = T{ h1("hello").toString }
def resources = T.sources{
os.write(T.dest / "snippet.txt", htmlSnippet())
super.resources() ++ Seq(PathRef(T.dest))
}
def forkArgs = Seq(
s"-Dmill.scalatags.version=${millbuild.DepVersions.scalatagsVersion}"
)
}
This example illustrates usage of the mill-build/
folder. Mill’s build.sc
file and it’s import $file
and $ivy
are a shorthand syntax for defining
a Mill ScalaModule
, with sources and ivyDeps
and so on, which is
compiled and executed to perform your build. This module lives in
mill-build/
, and can be enabled via the import $meta._
statement above.
import mill._, scalalib._
object millbuild extends MillBuildRootModule{
val scalatagsVersion = "0.8.2"
def ivyDeps = Agg(ivy"com.lihaoyi::scalatags:$scalatagsVersion")
def generatedSources = T {
os.write(
T.dest / "DepVersions.scala",
s"""package millbuild
object DepVersions{
def scalatagsVersion = "$scalatagsVersion"
}
""".stripMargin
)
super.generatedSources() ++ Seq(PathRef(T.dest))
}
}
package millbuild
object ScalaVersion{
def myScalaVersion = "2.13.10"
}
In this example:
-
Our
myScalaVersion
value comes frommill-build/src/Versions.scala
, while the Scalatags library we use inbuild.sc
comes from thedef ivyDeps
inmill-build/build.sc
. -
We also use
generatedSources
inmill-build/build.sc
to create aDepVersions
object that thebuild.sc
can use to pass thescalatagsVersion
to the application without having to copy-paste the version and keep the two copies in sync
You can customize the mill-build/
module with more flexibility than is
provided by import $ivy
or import $file
, overriding any tasks that are
present on a typical ScalaModule
: scalacOptions
, generatedSources
, etc.
This is useful for large projects where the build itself is a non-trivial
module which requires its own non-trivial customization.
> ./mill compile
compiling 1 Scala source...
...
> ./mill run
Foo.value: <h1>hello</h1>
scalatagsVersion: 0.8.2
> ./mill show assembly
".../out/assembly.dest/out.jar"
> ./out/assembly.dest/out.jar # mac/linux
Foo.value: <h1>hello</h1>
scalatagsVersion: 0.8.2
Using ScalaModule.run as a task
import mill._, scalalib._
object foo extends ScalaModule {
def scalaVersion = "2.13.8"
def moduleDeps = Seq(bar)
def ivyDeps = Agg(ivy"com.lihaoyi::mainargs:0.4.0")
def barWorkingDir = T{ T.dest }
def sources = T{
bar.run(T.task(Args(barWorkingDir(), super.sources().map(_.path))))()
Seq(PathRef(barWorkingDir()))
}
}
object bar extends ScalaModule{
def scalaVersion = "2.13.8"
def ivyDeps = Agg(ivy"com.lihaoyi::os-lib:0.9.1")
}
This example demonstrates using Mill ScalaModule`s as build tasks: rather
than defining the task logic in the `build.sc
, we instead put the build
logic within the bar
module as bar/src/Bar.scala
. In this example, we use
Bar.scala
as a source-code pre-processor on the foo
module source code:
we override foo.sources
, passing the super.sources()
to bar.run
along
with a barWorkingDir
, and returning a PathRef(barWorkingDir())
as the
new foo.sources
.
> ./mill foo.run
...
Foo.value: HELLO
This example does a trivial string-replace of "hello" with "HELLO", but is
enough to demonstrate how you can use Mill ScalaModule`s to implement your
own arbitrarily complex transformations. This is useful for build logic that
may not fit nicely inside a `build.sc
file, whether due to the sheer lines
of code or due to dependencies that may conflict with the Mill classpath
present in build.sc
Evaluator Commands (experimental)
Evaluator Command are experimental and suspected to change. See issue #502 for details.
You can define a command that takes in the current Evaluator
as an argument,
which you can use to inspect the entire build, or run arbitrary tasks.
For example, here is the mill.scalalib.GenIdea/idea
command which uses this
to traverse the module-tree and generate an Intellij project config for your
build.
def idea(ev: Evaluator) = T.command {
mill.scalalib.GenIdea(
implicitly,
ev.rootModule,
ev.discover
)
}