Scala Single-File Scripts
This page documents the use cases and features of Mill’s single-file Scala modules, or "scripts". These are Scala modules with a single source file, with build configuration included in a header comment block at the top of the file.
Scala scripts can be standalone or part of a larger Mill project.
They can be run via ./mill Foo.scala from the command line, or have other tasks on
them executed via ./mill Foo.scala:compile or ./mill Foo.scala:assembly.
Apart from being limited to a single source file, single-file script modules otherwise
support all the same tasks that normal ScalaModules do.
Mill Scala scripts typically make use of their Bundled Libraries to provide an easy-to-get-started experience writing Scala scripts.
Script Use Cases
Mill Single-file Scala programs can make it more convenient to script simple command-line workflows interacting with files, subprocesses, and HTTP endpoints. This section walks through a few examples where one-off single-file Scala programs can be useful in day-to-day development.
While initially single-file Scala programs may be a bit more verbose than the equivalent
Bash script containing cp or curl commands, as the script grows in complexity the value
of IDE support, typechecking, and JVM libraries makes writing them in Scala an attractive
proposition. This is especially true if you already have developers fluent in Scala which
may not be as familiar with the intricacies of writing robust and maintainable Bash code.
JSON API Client
Below is a simple script using the bundled libraries to write a program that crawls wikipedia’s JSON API and saves 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
}
@main
def main(startArticle: String, depth: Int) = {
var seen = Set(startArticle)
var current = Set(startArticle)
for (i <- Range(0, depth)) {
current = current.flatMap(fetchLinks(_)).filter(!seen.contains(_))
seen = seen ++ current
}
pprint.log(seen)
os.write(os.pwd / "fetched.json", upickle.stream(seen, indent = 4))
}
> ./mill JsonApiClient.scala --start-article singapore --depth 2
...
"Calling code",
"+65",
"British Empire",
"1st Parliament of Singapore",
...
)
> cat fetched.json
[
"Calling code",
"+65",
"British Empire",
"1st Parliament of Singapore",
...
]
HTML Web Scraper
Below is another web crawler, but instead of interacting with Wikipedia via a JSON API
it scrapes the website’s HTML pages using JSoup. JSoup is not bundled with Mill, but
can be included easily via mvnDeps header declaration:
//| mvnDeps: [org.jsoup:jsoup:1.7.2]
import org.jsoup._
import scala.collection.JavaConverters._
def fetchLinks(title: String): Seq[String] = {
Jsoup.connect(s"https://en.wikipedia.org/wiki/$title")
.header("User-Agent", "Mozilla/5.0 (compatible; JsoupBot/1.0; +https://example.com/bot)")
.get().select("main p a").asScala.toSeq.map(_.attr("href"))
.collect { case s"/wiki/$rest" => rest }
}
@main
def main(startArticle: String, depth: Int) = {
var seen = Set(startArticle)
var current = Set(startArticle)
for (i <- Range(0, depth)) {
current = current.flatMap(fetchLinks(_)).filter(!seen.contains(_))
seen = seen ++ current
}
pprint.log(seen)
}
> ./mill HtmlScraper.scala --start-article singapore --depth 1
...
"Hokkien",
"Conscription_in_Singapore",
"Malaysia_Agreement",
"Government_of_Singapore",
...
Web Server
This example shows off running a simple webserver in a single-file script. While
a production-quality website is a lot more complex and would need a proper build.mill
file, sometimes you just need a simple webserver running for local testing or experimentation.
In these scenarios, writing a single-file script in Scala to run the server is a great
way to start, and it can always grow into a more full-featured project later if necessary.
In this example, the webserver script uses the Cask framework to run the web server
//| mvnDeps: [com.lihaoyi::cask:0.9.1]
object WebServer extends cask.MainRoutes {
@cask.post("/reverse-string")
def doThing(request: cask.Request) = {
request.text().reverse
}
initialize()
}
> ./mill WebServer.scala:runBackground
> curl -d 'helloworld' localhost:8080/reverse-string
dlrowolleh
Database Queries
This example shows populating and querying a simple database using a Scala script.
While you often want to query databases using SQL directly, sometimes it is useful to be
be able to query them programmatically from Scala, possibly making use of the Scala
libraries and application code you use in the rest of your codebase. It uses a local
Sqlite database populated by the contents of a local sqlite-customers.sql file.
In this case, we make use of the popular ScalaSql library to perform the queries:
//| mvnDeps:
//| - com.lihaoyi::scalasql:0.2.3
//| - com.lihaoyi::scalasql-namedtuples:0.2.3
//| - org.xerial:sqlite-jdbc:3.43.0.0
import scalasql.simple._, SqliteDialect._
import java.time.LocalDate
case class Buyer(id: Int, name: String, dateOfBirth: LocalDate)
object Buyer extends SimpleTable[Buyer]
case class ShippingInfo(id: Int, buyerId: Int, shippingDate: LocalDate)
object ShippingInfo extends SimpleTable[ShippingInfo]
def main(args: Array[String]): Unit = {
// Initialize database
val dataSource = new org.sqlite.SQLiteDataSource()
dataSource.setUrl(s"jdbc:sqlite:./file.db")
val sqliteClient = new scalasql.DbClient.DataSource(dataSource, config = new scalasql.Config {})
sqliteClient.transaction { db =>
db.updateRaw(os.read(os.pwd / "sqlite-customers.sql")) // Populate database from SQL file
val names = db.run( // Find names of buyers whose shipping date is today or later
Buyer.select.join(ShippingInfo)(_.id === _.buyerId)
.filter { case (b, s) => s.shippingDate >= LocalDate.parse(args(0)) }
.map { case (b, s) => b.name })
for (name <- names) println(name)
}
}
> ./mill Database.scala 2011-01-01
James Bond
John Doe
Static Site Generator
This example is a small static site generator written as a single-file Scala script.
It lists markdown files in a post/ folder, converts them to HTML using the
Commonmark-Java library, wraps them in
a HTML template and writes them to HTML files in an out/ folder along with an index.html
page that links to them:
//| mvnDeps:
//| - com.lihaoyi::scalatags:0.13.1
//| - com.atlassian.commonmark:commonmark:0.13.1
import scalatags.Text.all._
@main def main() = {
val postInfo = os
.list(os.pwd / "post")
.map { p =>
val s"$prefix - $suffix.md" = p.last
(prefix, suffix, p)
}
.sortBy(_._1.toInt)
os.remove.all(os.pwd / "site-out")
os.makeDir.all(os.pwd / "site-out/post")
for ((_, suffix, path) <- postInfo) {
val parser = org.commonmark.parser.Parser.builder().build()
val document = parser.parse(os.read(path))
val renderer = org.commonmark.renderer.html.HtmlRenderer.builder().build()
val output = renderer.render(document)
os.write(
os.pwd / "site-out/post" / (suffix.replace(" ", "-").toLowerCase + ".html"),
doctype("html")(
html(
body(
h1(a("Blog"), " / ", suffix),
raw(output)
)
)
)
)
}
}
> ./mill -i StaticSite.scala
> cat site-out/post/my-first-post.html
<!DOCTYPE html><html><body><h1><a>Blog</a> / My First Post</h1><p>Sometimes you want numbered lists:</p>
<ol>
<li>One</li>
<li>Two</li>
<li>Three</li>
</ol>
</body></html>
Packaging Assemblies and Native Binaries
Mill Single-File Scripts can be packaged into executable assemblies or Graal
native images for convenient distribution or deployment. Note that to create
Graal native image binaries, you need to define your jvmId to be a graalvm
version
//| scalaVersion: 3.7.3
//| jvmId: "graalvm-community:17"
//| nativeImageOptions: ["--no-fallback"]
@main def main() = {
println("Hello World")
}
> ./mill Bar.scala:assembly
> ./out/Bar.scala/assembly.dest/out.jar # mac/linux
Hello World
> ./mill Bar.scala:nativeImage
> ./out/Bar.scala/nativeImage.dest/native-executable
Hello World
Opening a Script REPL
To open a Scala REPL with your script loaded and available, you can use the following command. This can be useful to interactively exercise and experiment with the code defined in your script.
./mill -i Foo.scala:console
Custom Script Module Classes
By default, Mill single-file Scala modules inherit their behavior from the builtin
mill.script.ScalaModule.
However, you can also customize them 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 qux
def getLineCount() = {
scala.io.Source
.fromResource("line-count.txt")
.mkString
}
@main def main() = {
println(s"Line Count: ${getLineCount()}")
}
package millbuild
import mill.*, scalalib.*, script.*
class LineCountScalaModule(scriptConfig: ScriptModule.Config)
extends mill.script.ScalaModule(scriptConfig) {
/** 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 Qux.scala
...
Line Count: 15
> ./mill show Qux.scala:lineCount
15
Your custom LineCountScalaModule must be a class take a
mill.script.ScriptModule.Config as a parameter that is passed to the
mill.script.ScalaModule. 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.
Note that single-file scripts can only inherit from a single concrete module class,
unlike normal config-based or
programmatic modules that can inherit
from one-or-more abstract module `trait`s. This difference arises from the way scripts
are instantiated dynamically when resolved, rather than being compiled ahead-of-time like
other modules are.
Relative and Absolute Script moduleDeps
Mill single-file scripts can import each other via either relative or absolute imports.
For example, given a bar/Bar.scala file such as below:
//| scalaVersion: 3.7.1
package bar
def generateHtml(text: String) = "<h1>" + text + "</h1>"
It can be imported via a ./Bar.scala import relative to its own enclosing folder
(in this case bar/), as shown below where it is imported from the bar/BarTests.scala
test suite in the same folder:
//| extends: [mill.script.ScalaModule.Utest]
//| moduleDeps: [./Bar.scala]
//| mvnDeps:
//| - com.lihaoyi::utest:0.9.1
package bar
import utest.*
object BarTests extends TestSuite {
def tests = Tests {
assert(generateHtml("hello") == "<h1>hello</h1>")
}
}
Or it can be imported via bar/Bar.scala absolute import, as shown below where it
is imported from the foo/Foo.scala
//| moduleDeps: [bar/Bar.scala]
//| scalaVersion: 3.7.1
package foo
def main(args: Array[String]): Unit = println(bar.generateHtml(args(0)))
This examples can be exercised as follows:
> ./mill bar/Bar.scala:compile
> ./mill bar/BarTests.scala
> ./mill foo/Foo.scala --text hello
Project moduleDeps
Single-file scripts can depend on programmatic modules. This can be useful to integrate your scripts into a large project, where your scripts may make of classes and methods defined in your library or application code to perform domain-specific actions that may not be available in the standard library or third-party libraries.
package build
import mill.*, scalalib.*
object bar extends ScalaModule {
def scalaVersion = "3.7.1"
def mvnDeps = Seq(mvn"com.lihaoyi::scalatags:0.13.1")
}
//| moduleDeps: [bar]
def main(args: Array[String]) = {
println(bar.Bar.generateHtml(args(0)))
}
> ./mill Foo.scala hello
<h1>hello</h1>
Bundled Libraries
Mill Scala scripts come with a number of bundled libraries for these use cases. 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.
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.
Furthermore, @main in Mill scala scripts is aliased to @mainargs.main.
And as we saw earlier, you can include any other libraries you wish using //| mvnDeps.
If you do not want these default bundled libraries, you can make your script extend
mill.script.ScalaModule.Raw:
//| extends: [mill.script.ScalaModule.Raw]
def main(args: Array[String]): Unit = {
println(os.read(os.pwd / "file.txt"))
}
> ./mill Foo.scala
error: ...Not found: os
If your script extends mill.script.ScalaModule.Raw, you no longer have access to
these libraries by default. You can instead use classes from the Java standard library:
//| extends: [mill.script.ScalaModule.Raw]
def main(args: Array[String]): Unit = {
println(java.nio.file.Files.readString(java.nio.file.Path.of("file.txt")))
}
> ./mill Bar.scala
hello
Or you can include the libraries you need explicitly via mvnDeps:
//| extends: [mill.script.ScalaModule.Raw]
//| mvnDeps: [com.lihaoyi::os-lib:0.11.4]
def main(args: Array[String]): Unit = {
println(os.read(os.pwd / "file.txt"))
}
> ./mill Qux.scala
hello