Build Tools and Dependency Management: SBT, Mill, and Scala CLI

Build tools are essential for managing Scala projects, handling dependencies, compilation, testing, and deployment. Scala has a rich ecosystem of build tools, each with its own strengths. In this lesson, we'll explore the three main build tools: SBT (the traditional standard), Mill (the modern alternative), and Scala CLI (for scripting and simple projects).

Understanding Build Tools in Scala

Build tools automate common development tasks:

  • Dependency Management: Downloading and managing external libraries
  • Compilation: Compiling Scala and Java source code
  • Testing: Running unit and integration tests
  • Packaging: Creating JARs, Docker images, or native binaries
  • Publishing: Deploying artifacts to repositories
  • Documentation: Generating API documentation

SBT: The Simple Build Tool

SBT is the most widely used build tool in the Scala ecosystem, known for its incremental compilation and powerful DSL.

Basic SBT Project Structure

my-scala-project/
├── build.sbt                 # Main build definition
├── project/
│   ├── build.properties      # SBT version
│   ├── plugins.sbt          # SBT plugins
│   └── Dependencies.scala   # Dependency definitions (optional)
├── src/
│   ├── main/
│   │   ├── scala/           # Main Scala sources
│   │   ├── java/            # Main Java sources (optional)
│   │   └── resources/       # Main resources
│   └── test/
│       ├── scala/           # Test Scala sources
│       ├── java/            # Test Java sources (optional)
│       └── resources/       # Test resources
└── target/                  # Compiled artifacts (generated)

Basic build.sbt Configuration

// build.sbt
ThisBuild / scalaVersion := "3.3.1"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.example"

lazy val root = (project in file("."))
  .settings(
    name := "my-scala-project",

    libraryDependencies ++= Seq(
      // Core dependencies
      "org.typelevel" %% "cats-core" % "2.10.0",
      "org.typelevel" %% "cats-effect" % "3.5.2",
      "io.circe" %% "circe-core" % "0.14.6",
      "io.circe" %% "circe-generic" % "0.14.6",
      "io.circe" %% "circe-parser" % "0.14.6",

      // Test dependencies
      "org.scalatest" %% "scalatest" % "3.2.17" % Test,
      "org.scalatestplus" %% "scalacheck-1-17" % "3.2.17.0" % Test,
      "org.typelevel" %% "cats-effect-testing-scalatest" % "1.5.0" % Test
    ),

    // Compiler options
    scalacOptions ++= Seq(
      "-deprecation",
      "-feature",
      "-unchecked",
      "-Xfatal-warnings",
      "-Xlint"
    ),

    // Test configuration
    Test / parallelExecution := false,
    Test / testOptions += Tests.Argument(TestFrameworks.ScalaTest, "-oD")
  )

project/build.properties

sbt.version=1.9.7

Advanced SBT Configuration

Multi-Module Projects

// build.sbt
ThisBuild / scalaVersion := "3.3.1"
ThisBuild / version := "0.1.0-SNAPSHOT"
ThisBuild / organization := "com.example"

lazy val commonSettings = Seq(
  libraryDependencies ++= Seq(
    "org.scalatest" %% "scalatest" % "3.2.17" % Test
  ),
  scalacOptions ++= Seq(
    "-deprecation",
    "-feature",
    "-unchecked"
  )
)

lazy val core = (project in file("core"))
  .settings(
    commonSettings,
    name := "my-project-core",
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core" % "2.10.0",
      "org.typelevel" %% "cats-effect" % "3.5.2"
    )
  )

lazy val api = (project in file("api"))
  .settings(
    commonSettings,
    name := "my-project-api",
    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-http" % "10.5.3",
      "com.typesafe.akka" %% "akka-stream" % "2.8.5",
      "de.heikoseeberger" %% "akka-http-circe" % "1.39.2"
    )
  )
  .dependsOn(core)

lazy val cli = (project in file("cli"))
  .settings(
    commonSettings,
    name := "my-project-cli",
    libraryDependencies ++= Seq(
      "com.github.scopt" %% "scopt" % "4.1.0"
    )
  )
  .dependsOn(core)

lazy val root = (project in file("."))
  .settings(
    name := "my-project"
  )
  .aggregate(core, api, cli)

Mill: The Modern Build Tool

Mill is a newer build tool that aims to be simpler and faster than SBT, with better IDE integration and clearer semantics.

Basic Mill Project Structure

my-mill-project/
├── build.sc              # Build definition
├── src/                  # Main sources
│   └── Main.scala
├── test/
│   └── src/
│       └── MainTest.scala
└── out/                  # Build output (generated)

Basic build.sc Configuration

// build.sc
import mill._, scalalib._

object myproject extends ScalaModule {
  def scalaVersion = "3.3.1"

  def ivyDeps = Agg(
    ivy"org.typelevel::cats-core:2.10.0",
    ivy"org.typelevel::cats-effect:3.5.2",
    ivy"io.circe::circe-core:0.14.6",
    ivy"io.circe::circe-generic:0.14.6",
    ivy"io.circe::circe-parser:0.14.6"
  )

  def scalacOptions = Seq(
    "-deprecation",
    "-feature",
    "-unchecked",
    "-Xfatal-warnings"
  )

  object test extends ScalaTests {
    def ivyDeps = Agg(
      ivy"org.scalatest::scalatest:3.2.17",
      ivy"org.scalatestplus::scalacheck-1-17:3.2.17.0"
    )

    def testFramework = "org.scalatest.tools.Framework"
  }
}

Scala CLI: For Scripts and Simple Projects

Scala CLI is a lightweight tool for running Scala scripts and managing simple projects without complex build configurations.

Installing Scala CLI

# Install via coursier
cs install scala-cli

# Or download directly
curl -fL https://github.com/Virtuslab/scala-cli/releases/latest/download/scala-cli-x86_64-pc-linux.gz | gzip -d > scala-cli
chmod +x scala-cli

Basic Scala CLI Usage

// hello.scala
//> using scala "3.3.1"
//> using dep "org.typelevel::cats-core:2.10.0"

import cats.implicits._

@main def hello(name: String = "World"): Unit =
  println(s"Hello, $name!".some.fold("No name")(_ + " 🎉"))
# Run script
scala-cli run hello.scala -- "Scala CLI"

# Compile to JAR
scala-cli package hello.scala -o hello.jar

# Run compiled JAR
java -jar hello.jar "From JAR"

Dependency Management Best Practices

Version Management

// build.sbt - Using variables for version management
val CatsVersion = "2.10.0"
val CatsEffectVersion = "3.5.2"
val CirceVersion = "0.14.6"
val Http4sVersion = "0.23.23"
val ScalaTestVersion = "3.2.17"

libraryDependencies ++= Seq(
  "org.typelevel" %% "cats-core" % CatsVersion,
  "org.typelevel" %% "cats-effect" % CatsEffectVersion,
  "io.circe" %% "circe-core" % CirceVersion,
  "io.circe" %% "circe-generic" % CirceVersion,
  "io.circe" %% "circe-parser" % CirceVersion,
  "org.http4s" %% "http4s-ember-client" % Http4sVersion,
  "org.http4s" %% "http4s-ember-server" % Http4sVersion,
  "org.scalatest" %% "scalatest" % ScalaTestVersion % Test
)

Dependency Scopes

libraryDependencies ++= Seq(
  // Compile scope (default)
  "org.typelevel" %% "cats-core" % "2.10.0",

  // Test scope
  "org.scalatest" %% "scalatest" % "3.2.17" % Test,

  // Provided scope (available at compile time, not packaged)
  "javax.servlet" % "javax.servlet-api" % "4.0.1" % Provided,

  // Runtime scope
  "ch.qos.logback" % "logback-classic" % "1.4.11" % Runtime,

  // Optional scope
  "com.typesafe" % "config" % "1.4.3" % Optional
)

Build Optimization

Parallel Compilation

// build.sbt
Global / concurrentRestrictions += Tags.limit(Tags.Compile, 4)
ThisBuild / parallelExecution := true
Test / parallelExecution := false  // Tests might conflict

Incremental Compilation

// build.sbt
ThisBuild / incOptions := incOptions.value.withLogRecompileOnMacro(false)

Continuous Integration Configuration

GitHub Actions with SBT


# .github/workflows/ci.yml
name: CI