The Mill Meta-Build
The meta-build manages the compilation of the build.mill
.
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.
Autoformatting the build.mill
As an example of running a task on the meta-build, you can format the build.mill
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.mill
(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 tasks that refer to the source files as argument -
sources
this selects thesources
tasks of the meta-build, which at least contains thebuild.mill
.
Finding 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
Sharing Libraries between build.mill
and Application Code
package build
import $meta._
import mill._, scalalib._
import scalatags.Text.all._
object `package` extends RootModule with ScalaModule {
def scalaVersion = "2.13.4"
def ivyDeps = Agg(
ivy"com.lihaoyi::scalatags:${millbuild.DepVersions.scalatagsVersion}",
ivy"com.lihaoyi::os-lib:0.10.7"
)
def htmlSnippet = Task { h1("hello").toString }
def resources = Task.Sources{
os.write(Task.dest / "snippet.txt", htmlSnippet())
super.resources() ++ Seq(PathRef(Task.dest))
}
}
This example illustrates usage of the mill-build/
folder. Mill’s build.mill
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 "meta-build" module lives in
mill-build/
, and can be enabled via the import $meta._
statement above.
package build
import mill._, scalalib._
object `package` extends MillBuildRootModule{
val scalatagsVersion = "0.12.0"
def ivyDeps = Agg(ivy"com.lihaoyi::scalatags:$scalatagsVersion")
def generatedSources = Task {
os.write(
Task.dest / "DepVersions.scala",
s"""
package millbuild
object DepVersions{
def scalatagsVersion = "$scalatagsVersion"
}
""".stripMargin
)
super.generatedSources() ++ Seq(PathRef(Task.dest))
}
}
package foo
import scalatags.Text.all._
object Foo {
def main(args: Array[String]): Unit = {
println("Build-time HTML snippet: " + os.read(os.resource / "snippet.txt"))
println("Run-time HTML snippet: " + p("world"))
}
}
Typically, if you wanted to use a library such as Scalatags in both your build.mill
as well as your application code, you would need to duplicate the version of Scalatags
used in both your import $ivy
statement as well as your def ivyDeps
statement.
The meta-build lets us avoid this duplication: we use generatedSources
in
mill-build/build.mill
to create a DepVersions
object that the build.mill
can use to pass the
scalatagsVersion
to the application without having to copy-paste the
version and keep the two copies in sync.
When we run the application, we can see both the Build-time HTML snippet
and the
Run-time HTML snippet
being printed out, both using the same version of Scalatags
but performing the rendering of the HTML in two different places:
> mill compile
...
> mill run
Build-time HTML snippet: <h1>hello</h1>
Run-time HTML snippet: <p>world</p>
> mill show assembly
".../out/assembly.dest/out.jar"
> ./out/assembly.dest/out.jar # mac/linux
Build-time HTML snippet: <h1>hello</h1>
Run-time HTML snippet: <p>world</p>
This example is trivial, but in larger builds there may be much more scenarios
where you may want to keep the libraries used in your build.mill
and the libraries
used in your application code consistent. With the mill-build/build.mill
configuration
enabled by import $meta._
, this becomes possible.
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.
You can also run tasks on the meta-build by using the --meta-level
cli option.
> mill --meta-level 1 show sources
[
.../build.mill",
.../mill-build/src"
]
> mill --meta-level 2 show sources
.../mill-build/build.mill"
Sharing Source Code between build.mill
and Application Code
package build
import $meta._
import mill._, scalalib._
object `package` extends RootModule with ScalaModule {
def scalaVersion = millbuild.ScalaVersion.myScalaVersion
def sources = Task.Sources{
super.sources() ++ Seq(PathRef(millSourcePath / "mill-build/src"))
}
}
package build
import mill._
object `package` extends MillBuildRootModule
package millbuild
object ScalaVersion{
def myScalaVersion = "2.13.10"
}
package foo
object Foo {
def main(args: Array[String]): Unit = {
println("scalaVersion: " + millbuild.ScalaVersion.myScalaVersion)
}
}
This example shows another use of the Mill meta-build: because the meta-build
is just a normal Scala module (with some special handling for the .sc
file extension),
your build.mill
file can take normal Scala source files that are placed in mill-build/src/
This allows us to share those sources with your application code by appropriately
configuring your def sources
in build.mill
above.
> mill run
scalaVersion: 2.13.10
Here we only share a trivial def myScalaVersion = "2.13.10"
definition between
the build.mill
and application code. But this ability to share arbitrary code between
your application and your build opens up a lot of possibilities:
-
Run-time initialization logic can be placed in
mill-build/src/
, shared with thebuild.mill
and used to perform build-time preprocessing, reducing the work needing to be done at application start and reducing startup latencies. -
Build-time
build.mill
logic can be placed inmill-build/src/
and shared with your run-time application code, allowing greater flexibility in e.g. exercising the shared logic in response to user input that’s not available at build time.
In general, the Mill meta-build with its mill-build
folder is meant to blur the line
between build-time logic and run-time logic. Often you want to do the same thing in both
places: at build-time where possible to save time at runtime, and at runtime where
necessary to make use of user input. With the Mill meta-build, you can write logic
comprising the same source code and using the same libraries in both environments,
giving you flexibility in where your logic ends up running