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.12.0`, 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
The meta-build manages the compilation of the build.sc
.
If you don’t configure it explicitly, a built-in synthetic meta-build is used.
To customize it, you need to explicitly enable it with import $meta._
.
Once enabled, the meta-build lives in the mill-build/
directory.
It needs to contain a top-level module of type MillBuildRootModule
.
Meta-builds are recursive, which means, it can itself have a nested meta-builds, and so on.
To run a task on a meta-build, you specifying the --meta-level
option to select the meta-build level.
Example: Format the build.sc
As an example of running a task on the meta-build, you can format the build.sc
with Scalafmt.
Everything is already provided by Mill.
You only need a .scalafmt.conf
config file which at least needs configure the Scalafmt version.
build.sc
(and potentially included files)$ mill --meta-level 1 mill.scalalib.scalafmt.ScalafmtModule/reformatAll sources
-
--meta-level 1
selects the first meta-build. Without any customization, this is the only built-in meta-build. -
mill.scalalib.scalafmt.ScalafmtModule/reformatAll
is a generic task to format scala source files with Scalafmt. It requires the targets that refer to the source files as argument -
sources
this selects thesources
targets of the meta-build, which at least contains thebuild.sc
.
Example: Find plugin updates
Mill plugins are defined as ivyDeps
in the meta-build.
Hence, you can easily search for updates with the external mill.scalalib.Dependency
module.
$ mill --meta-level 1 mill.scalalib.Dependency/showUpdates Found 1 dependency update for de.tototec:de.tobiasroeser.mill.vcs.version_mill0.11_2.13 : 0.3.1-> 0.4.0
Example: Customizing the 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.12.0"
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.12.0
> mill show assembly
".../out/assembly.dest/out.jar"
> ./out/assembly.dest/out.jar # mac/linux
Foo.value: <h1>hello</h1>
scalatagsVersion: 0.12.0
You can also run tasks on the meta-build by using the --meta-level
cli option.
> mill --meta-level 1 show sources
[
.../build.sc",
.../mill-build/src"
]
> mill --meta-level 2 show sources
.../mill-build/build.sc"
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
Importing Contrib Modules
import mill._, scalalib._
import $ivy.`com.lihaoyi::mill-contrib-buildinfo:`
import mill.contrib.buildinfo.BuildInfo
object foo extends ScalaModule with BuildInfo {
def scalaVersion = "2.13.10"
def buildInfoPackageName = "foo"
def buildInfoMembers = Seq(
BuildInfo.Value("scalaVersion", scalaVersion()),
)
}
This example illustrates usage of Mill contrib
plugins. These are Mill
plugins contributed by Mill user that are maintained within the Mill
repo, published under mill-contrib-*
.
> mill foo.run
...
foo.BuildInfo.scalaVersion: 2.13.10
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
)
}