Config-Based Scala Modules
This page documents the usage of simple config-based Scala modules defined by
build.mill.yaml
and package.mill.yaml
files. These are less flexible but easier to get started with than the
full build.mill
or package.mill
build files, which makes them ideal for small
projects which do not need additional flexibility.
Common Configuration Overrides for Simple Modules
This example shows some of the common tasks you may want to override on a
ScalaModule
: specifying the mainClass
, adding additional
sources/resources, and setting compilation/run
options.
extends: [mill.scalalib.ScalaModule]
scalaVersion: 2.13.16
mvnDeps:
- com.lihaoyi::scalatags:0.13.1
- com.lihaoyi::os-lib:0.11.4
mainClass: "foo.Foo2"
sources: ["./src", "./custom-src"]
resources: ["./resources", "./custom-resources"]
forkArgs: ["-Dmy.custom.property=my-prop-value"]
forkEnv: { "MY_CUSTOM_ENV": "my-env-value" }
scalacOptions: ["-deprecation", "-Xfatal-warnings"]
> ./mill run
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
MY_CUSTOM_ENV: my-env-value
> ./mill show assembly
".../out/assembly.dest/out.jar"
> ./out/assembly.dest/out.jar # mac/linux
Foo2.value: <h1>hello2</h1>
Foo.value: <h1>hello</h1>
MyResource: My Resource Contents
MyOtherResource: My Other Resource Contents
my.custom.property: my-prop-value
> sed -i.bak 's/Foo2 {/Foo2 { println(this + "hello")/g' custom-src/Foo2.scala
> ./mill compile # demonstrate -deprecation/-Xfatal-warnings flags
error: object Foo2 { println(this + "hello")
error: ^
error: ...Implicit injection of + is deprecated. Convert to String to call +...
Note that the .build.yaml
config files only let you set simple configuration keys,
and do not let you extend your build with code e.g. to generate sources. For more flexibility,
see Programmatic Module Configuration.
Packaging and Publishing Simple Modules
Mill Single-File Projects have the full flexibility of the Mill build tool, just configured
using the YAML build header syntax rather than a separate
build.mill
file. That means you can configure the JVM version,
compile flags or runtime flags, create
Executable Assemblies
or Graal Native Images,
Publishing to Maven Central
etc.
The example below shows a few of those options configured in the YAML build header of a single-file project, and then made of use from the command line:
extends: [mill.scalalib.ScalaModule, mill.scalalib.PublishModule, mill.scalalib.NativeImageModule]
scalaVersion: 3.7.3
jvmId: "graalvm-community:24"
nativeImageOptions: ["--no-fallback"]
publishVersion: "0.0.1"
artifactName: "example"
pomSettings:
description: "Example"
organization: "com.lihaoyi"
url: "https://github.com/com.lihaoyi/example"
licenses: ["MIT"]
versionControl: "https://github.com/com.lihaoyi/example"
developers: [{"name": "Li Haoyi", "email": "example@example.com"}]
> ./mill nativeImage
> out/nativeImage.dest/native-executable
Hello Graal Native: 24...
> ./mill publishLocal
Publishing Artifact(com.lihaoyi,example...,0.0.1) to ivy repo ...
Apart from publishing locally, you can also publish this single-file project to Sonatype Maven Central via:
> ./mill mill.javalib.SonatypeCentralPublishModule/
Most configuration def
s in Mill can be used to configure single-file projects, and
most tasks and commands can also be used as well. This gives you a lot of flexibility
in working with your single-file project until it becomes complex enough to need
a dedicated build.mill
file.
Custom Script Module Classes
By default, single-file Mill script modules inherit their behavior from the standard
mill.javalib.JavaModule
, mill.scalalib.ScalaModule
, or mill.scalalib.KotlinModule
.
However, you can also customize it to inherit from a custom Module
class that you define
as part of your meta-build in mill-build/src/
. For example, if we want to add a resource
file generated by processing the source file of the script, this can be done in a custom
LineCountScalaModule
as shown below:
extends: [millbuild.LineCountScalaModule]
scalaVersion: 3.7.3
package millbuild
import mill.*, scalalib.*
trait LineCountScalaModule extends mill.scalalib.ScalaModule {
/** Total number of lines in module source files */
def lineCount = Task {
allSourceFiles().map(f => os.read.lines(f.path).size).sum
}
/** Generate resources using lineCount of sources */
override def resources = Task {
os.write(Task.dest / "line-count.txt", "" + lineCount())
super.resources() ++ Seq(PathRef(Task.dest))
}
}
> ./mill run
...
Line Count: 17
> ./mill show lineCount
17
Your custom LineCountScalaModule
must be a class
take two parameters
val millScriptFile: os.Path
, and override val moduleDeps: Seq[JavaModule]
that will be
populated by Mill, and inherit from mill.simple.Scala.Base
or some other subclass of
mill.simple.SimpleModule
. This can then be used via //| extends: LineCountScalaModule
in the header of your script file.
Custom script module classes allows you to customize the semantics of your Java, Scala, or Kotlin
single-file script modules. If you have a large number of scripts with a similar configuration,
or you need customizations that cannot be done in the YAML build header, placing these
customizations in a custom script module class can let you centrally define the behavior
and standardize it across all scripts that inherit it via extends.
Depending on Programmatic Modules from Simple Modules
package build
import mill.*, scalalib.*
object bar extends ScalaModule {
def scalaVersion = "3.7.1"
def mvnDeps = Seq(mvn"com.lihaoyi::scalatags:0.13.1")
}
extends: [mill.scalalib.ScalaModule]
moduleDeps: [build.bar]
scalaVersion: 3.7.3
mvnDeps: [com.lihaoyi::mainargs:0.7.6]
package bar
import scalatags.Text.all.*
object Bar {
def generateHtml(text: String) = {
val value = h1(text)
value.toString
}
}
> ./mill foo.run --text hello
<h1>hello</h1>
Single-File Scripts
Mill also allows you to run single-file Scala programs (often referred to as scripts) easily from the command-line, even those that contain third-party dependencies or other such configuration. These can be useful as a replacement for Bash scripts, letting you write small scripts or programs in Scala with full access to third-party libraries and other build-tool features.
For example, given the Scala program below, it can be run directly using Mill:
//| mvnDeps:
//| - "com.lihaoyi::scalatags:0.13.1"
//| - "com.lihaoyi::mainargs:0.7.6"
import scalatags.Text.all.*
import mainargs.{main, ParserForMethods}
object Foo {
def generateHtml(text: String) = {
h1(text).toString
}
@main
def main(text: String) = {
println(generateHtml(text))
}
def main(args: Array[String]): Unit = ParserForMethods(this).runOrExit(args)
}
> ./mill Foo.scala --text hello
compiling 1 Scala source to...
<h1>hello</h1>
The ./mill Foo.{language-ext}
syntax is shorthand for ./mill Foo.{language-ext}:run
.
You can also call other tasks on your script modules, such as Foo.{langauge-ext}:assembly
below:
> ./mill Foo.scala:run --text hello
<h1>hello</h1>
> ./mill show Foo.scala:assembly # show the output of the assembly task
".../out/Foo.scala/assembly.dest/out.jar"
> java -jar ./out/Foo.scala/assembly.dest/out.jar --text hello
<h1>hello</h1>
> ./out/Foo.scala/assembly.dest/out.jar --text hello # mac/linux
<h1>hello</h1>
Script files can have test suites, usually written in a separate test script. The test script
specifies what script it tests via moduleDeps
, and can have its own mvnDeps
in addition
to those of the upstream script. The test script can then exercise functions from the upstream
script as shown below:
//| moduleDeps: [Foo.scala]
//| mvnDeps:
//| - "com.google.guava:guava:33.3.0-jre"
import com.google.common.html.HtmlEscapers.htmlEscaper
object FooTests {
def main(args: Array[String]): Unit = {
val result = Foo.generateHtml("hello")
assert(result == "<h1>hello</h1>")
println(result)
val result2 = Foo.generateHtml("<hello>")
val expected2 = "<h1>" + htmlEscaper().escape("<hello>") + "</h1>"
assert(result2 == expected2)
println(result2)
}
}
> ./mill FooTests.scala
<h1>hello</h1>
<h1><hello></h1>
Again, you can pass the name of the task explicitly via :
, e.g. :run
below
> ./mill FooTests.scala:run # specifying the test task explicitly
<h1>hello</h1>
<h1><hello></h1>
Scripts Builtin Libraries
Mill Scala scripts come with a number of bundled libraries for convenience. These are largely the same as the libraries available in Mill build files:
These libraries can make it more convenient to script simple command-line workflows interacting with files, subprocesses, and HTTP endpoints. For example, below is a simple script using these libraries to crawl wikipedia and save the crawl results to a file:
def fetchLinks(title: String): Seq[String] = {
val resp = requests.get.stream(
"https://en.wikipedia.org/w/api.php",
params = Seq(
"action" -> "query",
"titles" -> title,
"prop" -> "links",
"format" -> "json"
)
)
for {
page <- ujson.read(resp)("query")("pages").obj.values.toSeq
links <- page.obj.get("links").toSeq
link <- links.arr
} yield link("title").str
}
@mainargs.main
def main(startArticle: String, depth: Int) = {
var seen = Set(startArticle)
var current = Set(startArticle)
for (i <- Range(0, depth)) {
val nextTitleLists = current.flatMap(fetchLinks(_))
current = nextTitleLists.filter(!seen.contains(_))
seen = seen ++ current
}
os.write(os.pwd / "fetched.json", upickle.stream(seen, indent = 4))
}
def main(args: Array[String]): Unit = mainargs.ParserForMethods(this).runOrExit(args)
> ./mill Foo.scala --start-article singapore --depth 2
> cat fetched.json
[
"Calling code",
"+65",
"British Empire",
"1st Parliament of Singapore",
...
]
These bundled libraries are only available in single-file scripts for convenience; in
config-based modules or programmatic modules, you would need to add them to mvnDeps
explicitly.
You can also disable use of these libraries in scripts via //| includDefaultScriptMvnDeps: false
.