Skip to Content
Course content

87: Mill as an sbt Alternative

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why should I even bother with Mill if sbt is the industry standard?

Look, sbt is incredibly powerful, but if you've spent any time with it, you know it can feel like a black box. You write a build.sbt file that looks like a weird hybrid of Scala and a custom DSL, and then you spend half your morning fighting with the shell or waiting for a reload that seems to take forever. I've been there.

Mill takes a different approach. Instead of a DSL, Mill builds are just Scala code. When you run a Mill command, it's essentially executing a Scala program that describes your build. This makes the build logic much more predictable and easier to debug. Plus, Mill is generally faster because it has a much more aggressive and reliable caching system. It doesn't try to "guess" what changed; it tracks the inputs to every single task. If the inputs haven't changed, it just gives you the cached result. Period.

What does a Mill build file actually look like compared to sbt?

In sbt, you have a flat build.sbt file. In Mill, you have a build.sc file. The biggest shift is that Mill uses objects to represent modules. Let's say we're building a small WeatherClient library that fetches data from an API. Here is how I'd structure that in Mill:

import mill._, scala.scale._

object weather extends ScalaModule {
  def scalaVersion = "3.3.1"

  def ivyDeps = Agg(
    ivy"com.lihaoyi::upickleScala:3.1.0",
    ivy"com.lihaoyi::nutype:0.12.0"
  )
}

object weatherTest extends ScalaModule {
  def scalaVersion = "3.3.1"
  
  // This is where Mill shines: explicit dependencies between modules
  def moduleDeps = Seq(weather)
  
  def ivyDeps = Agg(
    ivy"org.scalatest::scalatest:3.2.17"
  )
}

Notice how weatherTest explicitly depends on weather? It's clean, it's type-safe, and you don't have to wonder where the dependency is coming from. You just define a module, tell Mill what it needs, and you're done.

How do I actually run things in Mill?

You'll find the CLI much more intuitive than the sbt shell. There is no "shell" you enter and stay in; you just run commands from your terminal. Mill uses a dot-notation that maps directly to the objects in your build.sc.

To compile your library, you'd run:

./mill mw compile

(Assuming you aliased weather to mw or just used the full name ./mill weather.compile). To run your tests, it's just as simple:

./mill weatherTest.test

I personally love that I can just use my standard zsh or bash aliases and piping without feeling like the build tool is trying to hijack my entire terminal experience. It feels like a tool, not an environment.




📋 Practical Task

Migrating the "FinancialCalculator" Project to Mill

You have a small project called FinancialCalculator that currently uses sbt. It consists of a main library and a test suite. Your goal is to migrate this to Mill to improve build times.

Current sbt configuration:

name := "FinancialCalculator"
version := "0.1"
scalaVersion := "3.3.1"
libraryDependencies += "org.typelevel" %% "cats-core" % "2.10.0"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test

Your Task:

  1. Create a build.sc file.
  2. Define a calc module that includes the cats-core dependency.
  3. Define a calcTest module that depends on the calc module and includes the scalatest dependency.
  4. Ensure both modules use Scala version 3.3.1.

Once you've written the file, verify that the calcTest module correctly references the calc module using moduleDeps.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.