Setting up Scala 3: Coursier, Scala CLI and sbt

Install the toolchain with Coursier, run scripts with Scala CLI, and lay out an sbt project you can build and test from the terminal.

Coursier and the toolchain

Coursier is the installer that the Scala ecosystem itself recommends. It fetches the JVM, Scala CLI, sbt and scalafmt, and keeps them on a predictable path. Install it once and the rest of the toolchain becomes one command each.

# install Coursier, then the tools
curl -fL https://github.com/coursier/coursier/releases/latest/download/cs-x86_64-pc-linux.gz | gzip -d > cs
chmod +x cs && ./cs setup          # adds Coursier to PATH and installs the basics

cs install scala-cli sbt scalafmt
cs java --jvm temurin:21           # install and print the JAVA_HOME of a JDK
eval "$(cs java --env --jvm 21)"   # put that JDK on PATH for this shell

scala-cli version
sbt --version
scala --version
ToolWhat it isUse it for
scala-cliCompile, run, test and package a set of filesScripts, katas, small tools, prototyping
sbtThe long-standing build toolMulti-module application builds and publishing
millA faster alternative build toolLarge builds where sbt startup annoys you
scalafmtThe formatterFormatting on save and in CI
MetalsThe language serverVS Code, Neovim, any LSP editor
Scala 3 / Scala 2.13Two language linesNew code targets Scala 3

Scala CLI declares its dependencies inside the source file with directives, which removes the need for a build file while you are exploring an idea.

//> using scala 3.5.0
//> using dep com.lihaoyi::os-lib:0.10.7
//> using test.dep org.scalameta::munit:1.0.0

import java.nio.file.Files

@main def demo(): Unit =
  val files = os.list(os.pwd).take(5)
  println(files.mkString(", "))
scala-cli run demo.scala
scala-cli test .
scala-cli package . -o app --assembly     # a runnable fat jar
scala-cli repl

An sbt project layout

project/
  build.properties      # sbt version
  plugins.sbt           # plugin declarations
src/
  main/scala/           # production sources in packages
  main/resources/       # files loaded from the classpath
  test/scala/           # tests
build.sbt               # the build definition
// project/build.properties
// sbt.version=1.10.2

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

lazy val root = (project in file("."))
  .settings(
    name := "shop",
    libraryDependencies ++= Seq(
      "org.typelevel" %% "cats-core"   % "2.12.0",
      "com.lihaoyi"   %% "os-lib"      % "0.10.7",
      "org.scalameta" %% "munit"       % "1.0.0" % Test
    ),
    scalacOptions ++= Seq(
      "-deprecation",
      "-feature",
      "-unchecked",
      "-Wunused:all",
      "-Werror"
    ),
    Compile / run / fork := true
  )
  • %% appends the Scala binary version to the artifact name, so cats-core_3 and cats-core_2.13 are different artifacts. Use % for a pure Java library that has no Scala variant.
  • Every sbt command is a task or a setting, and a setting is only evaluated once per session. reload after editing build.sbt; ~compile recompiles on change.
  • -Werror with -Wunused:all is the Scala 3 equivalent of turning warnings into a build gate. Add it from the first commit.
  • Use ThisBuild / for settings that should apply to every subproject, and per-project .settings(...) for the rest.
sbt compile
sbt "testOnly com.example.PricingSuite"
sbt run
sbt console                    # a REPL with the project on the classpath
sbt scalafmtCheck              # formatting gate for CI

Editor support

Metals is the language server behind VS Code and most other editors. It imports the build through Bloop, which produces a compilation database that is also useful for continuous compilation.

# VS Code: install the Metals extension and run "Metals: Import build"
# Neovim: install nvim-metals and point it at your sbt or Scala CLI build
# IntelliJ IDEA: use the Scala plugin; it has its own compiler

# a .scalafmt.conf keeps everyone's diffs small
cat > .scalafmt.conf <<'EOF'
version = "3.8.1"
runner.dialect = scala3
maxColumn = 100
EOF
scalafmt --test          # fails when a file is unformatted
💡
Commit the build files, the sbt and Scala versions, and the formatter configuration. A Scala project whose versions live only in somebody's shell history is a project that only builds on one machine.

FAQ

Scala 2 or Scala 3?
New projects should target Scala 3. The ecosystem has largely migrated, the syntax is cleaner, and the compiler is based on the newer TASTy format. Scala 2.13 remains relevant for libraries that must be cross-published and for older Spark versions.
Why is the first compile so slow?
sbt resolves dependencies, starts the compiler server and compiles the whole source set. Later compiles are incremental. Keep the sbt shell open rather than starting a new one for every command.

Syntax and immutability sbt, dependencies and project structure

Last refreshed 2026-09-18.