Scala 3 Odyssey: Part 3 — Empowering Scala Development with SCALA-CLI

Antonel Ernest Pazargic
7 min readJun 11, 2024

--

Generated by OpenAI DALL-E

A new article is here, fresh off the press, focusing on scala-cli, a powerful tool designed to enhance Scala development.

If you missed my first articles in this mini-series, you can find at

They are a gently introduction to scala programming languages and the main tools helping (easing) the development of scala projects.

In this article I am focused on the scala-cli, a tool that every scala developer must know about and use.

Scala-cli

Introduction

It is well known the pleasure brought by scala (version 3 in particular) to scala development around the world.
But history unveiled to us that that one of the major pain points was the tooling support.
This is where scala-cli comes into play and hugely diminish this problem.
It’s a tool that aims to empower scala development by providing a set of options that can be used to create, build, test and run scala projects.
Another big step towards improving the tooling support is the fact that scala-cli is going to be builtin from scala 3.5 onward.
Moreover, Metals along with VS Code (or neovim) streamline the development process (but this is a topic for another article).

Without further ado let’s dive into the scala-cli world.

Installation

There are quite some ways to install scala-cli.

The most common ones are:

  • using sdkman tool, via

sdk install scalacli

or

sdk install scalacli 1.3.2

if you want to install a specific version.

  • using coursier CLI, by issuing the following command

cs install scala-cli

Needless to say the (macOS) omnipresent homebrew tool can be used to install scala-cli, via

brew install scala-cli

But, for a scala developer this is not at all a common way to install scala-cli.

Regardless the installation method, the success of the installation can be checked by issuing the following command:

scala-cli — version

Noteworthy features

Scala-cli has tons of features, but I am going to mention some of the most important ones:

  • compile code,
  • run code (*.scala files, *.sc scripts or a SBT scala project),
  • run tests,
  • package the project (fatjar, native with GraalVM or scala native, etc),
  • manage dependencies using directives or command line arguments,
  • manage java and scala versions using directives or command line arguments,
  • use ammonite REPL via scala-cli,
  • allow java VM options to be passed to the running project or file,
  • use scalaftm formatting support,
  • generate documentation…

Usage

The simple hello world example.

  • Create a file named hello.sc and add the following code:
println("Hello scala-cli amazing world")
  • Run the code by issuing the following command

scala-cli run hello.scala

*.sc are scala scripts, which means that they run all the code in the file.
This is like the *.py files in python, or *.js files in javascript.

On the other hand we might have *.scala files with main entry-point, and we’d like to run the main method in the file.

For this case, create a file Hello.scala and edit it as per below

@main def hello() =
println("Hello scala-cli amazing world!")

Run this scala file by using the same above command

scala-cli run Hello.scala

The output might look, more of less, as per below

$ scala-cli run Hello.scala
Compiled project (Scala 3.4.2, JVM (17))
Hello scala-cli amazing world!

Notes:

  • As a keen-eyed reader, you might have noticed that the output contains additional information along with the application output — in our case, the Scala version,
    This superfluous information in console might be removed by adding the following argument -q (or — quiet)

scala-cli run -q Hello.scala

  • The command scala-cli run is so widely used that it has a shortcut, like not passing run option at all, like in

scala-cli Hello.scala

Use case to highlight the power of scala-cli

Let’s suppose we have a project with a peculiar behavior for a very specific combination of java, scala and scalatest versions.

Those versions supposedly are:

  • java 11,
  • scala 3.3.3,
  • scalatest-shouldmatchers 3.2.18

The hypothetical case is that should contain matcher doesn’t work in this combination of versions.
So that we put in place the following code in the scalatestcase.sc file:

//> using jvm 11
//> using scala 3.3.3
//> using dep org.scalatest::scalatest-shouldmatchers:3.2.18
import org.scalatest.matchers.should.Matchers.*

List(1, 2, 3) should contain (10)

and run it

scala-cli run scalatestcase.sc

scala-cli takes care of downloading the specified versions of java, scala and scalatest-shouldmatchers, and run the code, as can be seen in the following output.

scalatest matcher failes

We were not on the right path with our assumption, but scala-cli easy this kind of testing when different versions of libraries, scala and java are involved.

Next use case- create a simple CLI and package it as a native image

Given the following code in the greetcli.scala file:

@main
def main(name: String, count: Int) =
for i <- 1 to count do
println(s"Hello, $name!")

and after testing it using

$ scala-cli run greetcli.scala — Tony 3
Compiling project (Scala 3.4.1, JVM (17))
Hello, Tony!
Hello, Tony!
Hello, Tony!

we might want to package it as a native image (with scala-native).

It might be hard to find an easy way of packaging the CLI script as a native image, but in scala-cli is just a matter of running the following command

scala-cli — power package — native greetcli.scala — scala-version 3.4.2 -o greet

After a few seconds necessary to download the scala native dependencies, and compile the code, the native image is created.

The native image (our CLI) can be run by executing the following command:

$ time ./greet Tony 3
Hello, Tony!
Hello, Tony!
Hello, Tony!
./greet Tony 3 0.00s user 0.00s system 66% cpu 0.007 total

Notes:

  • I’ve wrapped the command in the time command to show the execution time of the native image.
  • The size of the obtained native image is 1.5 MB, and it runs instantaneously, which is quite impressive.
    This might be an amazing feature for those who want to create a CLI tools.

Types of artifacts that can be created by scala-cli are:

  • lightweight launcher JARs,
  • standard library JARs,
  • so called “assemblies” or “fat JARs”,
  • docker container,
  • JavaScript files for Scala.js code,
  • GraalVM native image executables,
  • native executables for Scala Native code,
  • OS-specific formats, such as deb or rpm (Linux), pkg (macOS), or MSI (Windows).

Many other scala-cli features are available, such as:

  • a command option to install scala-cli autocomplete in the shell

scala-cli install-completions

  • scala-cli can be used for simple java files, as can be seen below

Hello.java file

public class Hello {
public static void main(String[] args) {
System.out.println("Hello scala-cli amazing world!");
}
}

and by running the command

scala-cli run Hello.java

the output will obviously be:

Hello scala-cli amazing world!

Ammonite integration

There have been many explanations of scala-cli’s capabilities, but I believe that showcasing the amazing Scala companion, Ammonite REPL, completes the picture of scala-cli’s new frontier.
Ammonite facilitates a seamless programming by experimenting, directly in your preferred terminal (console).
This way the usage of an IDE and build tools might be avoided, with all the burden that comes with them, at least for those unfamiliar with them (or probably to power users).
A sneak peak into this ammonite related scala-cli feature is shown by running the following command:

scala-cli repl — ammonite — power

We get jump into the ammonite REPL, with all the its power, such as code completion, syntax highlight, scala API signatures, output, compilation errors and exceptions pretty printing and many more.

Ammonite

References

  • scala-cli
  • scala-cli packaging
  • ammonite
  • scala-cli in scala 3.5+

Closing thoughts

My fingers got tired of typing, but I hope the reader got a glimpse of scala-cli power.
I really hope you enjoyed the article at least as much as I enjoyed writing it.
Scala-cli is a powerful tool that can be used to streamline the scala development process.
And I consider that along with Visual Studio Code and the other tools I’ve touched briefly in first articles (and going to be described in the next ones), it lays down the foundation for the most of the next articles in the series.

Until next time, happy coding, and if you like my articles don’t forget to clap!

Thank you, and stay tuned for more!

--

--