Packaging a Scala CLI app in 2022

July 17, 2022

In past work projects, when I’ve been on a team needing a way to package a Scala application, I’ve relied on sbt-assembly. sbt-assembly is great for what it’s for — it bundles up dependencies and application code in a single fat jar file, and you can run it anywhere with a Java runtime (subject to all the normal caveats about Java versions).

That’s great, but more recently, support for bundling Scala apps in other ways has flourished. scala-native targets generic POSIX environments, Scala.js targets the browser and hit 1.0 in 2020, and GraalVM provides an alternate Java runtime environment “designed to accelerate the execution of applications written in […] JVM languages.”.

Each of those is developed enough to have its own sbt plugin. Partly because I wanted the tool, and partly because I wanted to play around with new application bundling options, I used GraalVM and Scala.js to bundle up a CLI app for adding rainbow brackets to streaming input.

Rainbow brackets

Rainbow brackets are great. No one has ever looked at an editor with a rainbow bracket plugin enabled and said “you know what, too much syntactical info in the bracket colors for me.” However! Not everyone has access to rainbow brackets everywhere they could. You while Looking at Source Code probably feels an ineffable unease when looking at monochrome source code. You while Looking at Logs and Waxing Inexplicably French is probably like “ehhhh it’s an inscrutable mass, que sera sera.”

what is this garbage

But there’s another way! It turns out matching parentheses problems are not very difficult puzzles. If you desire in your heart of hearts to have special colors for brackets, parentheses, and braces that happen to complete each other… you don’t need to suffer in vain, or something.

The code

Algorithmically, the CLI is really simple. For some streaming input, if you see a character that opens a pair like [, (, or {, rewrite it to a special colorized version of that same character and push to a stack. If you see a character that closes a pair like ], ), or }, rewrite it to a special colorized version of that same character and pop off a stack.

We have a few fancy options here. I was inspired by Michael Pilquist’s hexdump4s and got excited about functional abstractions and doing the right thing™️. I wanted to model the state of the parentheses counter using either a Ref or a StateT[Stream[F, *], Color, *]. Fancy values! It turns out I didn’t need fancy values. I enthusiastically stole the “file path or stdin” handling from hexdump4s, then tracked the state of the color wheel using a very unsafe, privately immutable Ring trait. It tracks a collection of values with size n against an index modulo an internal counter.

With the Ring in hand, it was pretty straightforward to handle the rest. decline is wonderful for config / command line argument parsing, fs2 is the GOAT for constant memory IO, and cats-parse is simple and nice for defining shapes of custom string formats on the fly. The decline + cats-parse combo was great for defining the shapes of my color arguments. Since CLIs have to deal with horrible string formats all the time, it was great to be able to concissely represent the rules for good strings and hook that into argument parsing. Note that once we’re in application logic, everything is purely in domain types — the function responsible for transforming input is colorize, and it knows nothing of effects or streaming or arguments, just “if you can give me a palette and a character, I can color that character.”

The end result looks like this:

Colored log output, with matching parentheses, brackets, and braces colored

So that’s the pinnacle of side project completion — it runs on my machine for toy inputs.

Bundling for linux

But there are other machines [citation needed], and it’s possible that someone who doesn’t want to learn how to use sbt might want rainbow brackets for their log output. No one has asked me for this, but it’s technically possible.

I used sbt-native-packager as the packaging plugin, but instead of following the GraalVM JVM setup guides, I fired up a nix repl, loaded up nixpkgs, crossed my fingers, and pressed <TAB> a few times after typing graal. There was a package available (graalvm-11-ce) so I added it to flake.nix, , and gave packaging the app up a try.

Cross-project configuration for packaging the app was embarrassingly easy:

lazy val rootJVM = root.jvm
  .settings(
    graalVMNativeImageOptions ++= Seq(
      // prevents image creation and prints more output when bundling
      // is *waves hands* incomplete
      "--no-fallback",
      // creates basically a fat jar instead of a binary that requires
      // path / classpath information to execute
      "--static",
      // provides nicer logging output when exceptions are encountered
      "-H:+ReportExceptionStackTraces"
    ),
    GraalVMNativeImage / name := "rainbow-parens"
  )
  .enablePlugins(GraalVMNativeImagePlugin)

The --static option was really important; it was the difference between being able to run the CLI if I copied it somewhere else on my machine and not being able to run it. However, that’s… not very many options to specify. If I compare it to common sbt-assembly options at least, it’s fewer lines and less cryptic than what I’ve wound up with working on other projects.

Bundling for MacOS

MacOS is very special and required special handling. In principle it should be possible to set up GraalVM packaging on MacOS, but I don’t really know anything about GraalVM or MacOS, so…

A very good dog floating around a space station

But there’s another option! Scala.js cross-project setups are pretty well ironed out, and all of my dependencies are cross-published for Scala.js, so here’s all I had to do to bundle up a nice node.js script:

lazy val root =
  (crossProject(JSPlatform, JVMPlatform).crossType(CrossType.Pure) in file("."))
    .settings(settings: _*)
    .jsSettings(
      scalaJSUseMainModuleInitializer := true,
      scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) },
      Compile / fullOptJS / artifactPath := baseDirectory.value / "rainbow-parens.js"
    )

MacOS users have a little more work to do to make sure they can use rainbow-parens as a CLI, but not much more work.

Release and publication

Because Node.js is basically universal, I didn’t have to split the release workflow. Also, because I have a nix shell available, I don’t have to do much in terms of dependency setup in CI — instead I can set up nix and let it handle making sure I have everything I need. The bundling step invokes sbt commands in a nix shell then points a release action at the artifacts. And we’re done!

This experiment was also a bit inspired by @hillelogram’s mise en place — the application here is really simple, so it was fine to take a little more technical risk with the release tooling. And it worked really well! “Always mise en place” is a good kitchen rule, and it turns out to work well for taking risk out of future technical choices too.


Profile picture

Written by James Santucci who lives in Denver and works at Flock Freight pooling shipments into shared truckloads to save the planet, hopefully. You can follow him on Twitter