Building projects with sbt

If you have reached this section you probably have a system that is now able to compile and run Scala Native programs.

Minimal sbt project

The easiest way to make a fresh project is to use our official gitter8 template. In an empty working directory, execute:

sbt new scala-native/scala-native.g8

Note: On Windows, new project should not be created in mounted directories, like external storage devices. In the case of WSL2 (Windows Subsystem Linux), Windows file system drives like C or D are perceived as mounted. So creating new projects in these locations will not work. In the WSL2 environment, it is recommended to create projects in the user files path, e.g /home/<USER>/sn-projects.

This will:

  • start sbt.

  • prompt for a project name

  • use the .g8 template to generate a basic project with that name.

  • create a project sub-directory with the project name.

  • copy the contents at these template links to the corresponding location in this new project sub-directory.

To use the new project:

  • Change the current working directory to the new project directory.

    • For example, on linux with a project named AnswerToProjectNamePrompt, type cd AnswerToProjectNamePrompt.

  • Type sbt run.

This will get everything compiled and should have the expected output!

Please refer to the faq if you encounter any problems.

The generated project is a starting point. After the first run, you should review the software versions in the generated files and, possibly, update or customize them. Scaladex is a useful resource for software versions.

Sbt settings and tasks

Use nativeConfig in sbt to provide settings. This is often done in a project’s build.sbt.

Warning: If you change settings you should clean your project to remove generated code to be safe.

Scala Native starts execution with a NativeConfig object, called nativeConfig, filled with default values:

show ThisBuild / nativeConfig

Each withX() method creates a new NativeConfig with the indicated X value set. All other settings are taken from the Config object being accessed.

To show nativeConfig values active in current scope at any point in time:

show nativeConfig

To set a new value and replace any previous setting:

import scala.scalanative.build._

nativeConfig ~= {
  _.withLTO(LTO.thin)
    .withMode(Mode.releaseFast)
    .withGC(GC.commix)
}

To append a value to the right of any previous setting:

import scala.scalanative.build._

// Enable verbose reporting during compilation
nativeConfig ~= { c =>
  c.withCompileOptions(_ :+ "-v")
}

// Use an alternate linker
nativeConfig ~= { c =>
  c.withLinkingOptions(c.linkingOptions ++ Seq("-fuse-ld=mold"))
}

/* The keen observer will note that "-fuse-ld=mold" could also have been
 *  set using "withCompileOptions".
 */
Since Name Type Description
0.1 compile Analysis Compile Scala code to NIR
0.1 run Unit Compile, link and run the generated binary
0.1 package File Similar to standard package with addition of NIR
0.1 publish Unit Similar to standard publish with addition of NIR (1)
0.1 nativeLink File Link NIR and generate native binary
0.4.0 nativeConfig NativeConfig Configuration of the Scala Native plugin
0.5.0 nativeLinkReleaseFast File Alias for nativeLink using fast release build mode (2)
0.5.0 nativeLinkReleaseFull File Alias for nativeLink using full release build mode (2)

For the details of available NativeConfig options see API

  1. See Publishing and Cross compilation for details.

  2. See Compilation modes for details.

Compilation modes

Scala Native supports three distinct linking modes:

  1. debug. (default)

    Default mode. Optimized for shortest compilation time. Runs fewer optimizations and is much more suited for iterative development workflow. Similar to clang’s -O0.

  2. release. (deprecated since 0.4.0)

    Aliases to release-full.

  3. release-fast. (introduced in 0.4.0)

    Optimize for runtime performance while still trying to keep quick compilation time and small emitted code size. Similar to clang’s -O2 with addition of link-time optimization over the whole application code.

  4. release-size. (introduced in 0.5.0)

    Optimize for reduced output size while still trying to keep quick compilation time and relatively fast runtime performance. Similar to clang’s -Oz with addition of link-time optimization over the whole application code.

  5. release-full. (introduced in 0.4.0)

    Optimized for best runtime performance, even if hurts compilation time and code size. This modes includes a number of more aggresive optimizations such type-driven method duplication and more aggresive inliner. Similar to clang’s -O3 with addition of link-time optimization over the whole application code.

Garbage collectors

  1. immix. (default since 0.3.8, introduced in 0.3)

    Immix is a mostly-precise mark-region tracing garbage collector. More information about the collector is available as part of the original 0.3.0 announcement.

  2. commix. (introduced in 0.4)

    Commix is parallel mark and concurrent sweep garbage collector based on Immix

  3. boehm. (default through 0.3.7)

    Conservative generational garbage collector. More information is available at the Github project “ivmai/bdgc” page.

  4. none. (experimental, introduced in 0.2)

    Garbage collector that allocates things without ever freeing them. Useful for short-running command-line applications or applications where garbage collections pauses are not acceptable.

Cross compilation using target triple

The target triple can be set to allow cross compilation (introduced in 0.4.0). Use the following approach in sbt to set the target triple:

nativeConfig ~= { _.withTargetTriple("x86_64-apple-macosx10.14.0") }

you may create a few dedicated projects with different target triples. If you have multiple project definitions for different macOS architectures, eg:

lazy val sandbox64 = project.in(file("sandbox"))
    .settings(nativeConfig ~= { _.withTargetTriple("arm64-apple-darwin20.6.0") })

lazy val sandboxM1 = project.in(file("sandbox"))
    .settings(nativeConfig ~= { _.withTargetTriple("x86_64-apple-darwin20.6.0") })

These project definitions allow to produce different binaries - one dedicated for the x86_64 platform and another one for arm64. You may easily combine them to one so called fat binary or universal binary via lipo:

lipo -create sandbox64/target/scala-2.12/sandbox64-out sandboxM1/target/scala-2.12/sandboxM1-out -output sandbox-out

which produces sandbox-out that can be used at any platform.

You may use `FatELF to build fat binaries for Linux.

Build target

Setting build target allows you to specify to what type of object your project should be linked to. As an example, to link it as dynamic library use the following command:

nativeConfig ~= { _.withBuildTarget(BuildTarget.libraryDynamic) }
  1. application (default)

    Results in creating ready to use executable program.

  2. libraryDynamic

    Results in dynamic library being built based on entry point methods annotated with @exported, for details see interop.

  3. libraryStatic

    Results in building static library using the same semantincs as in the libraryDynamic. Exported methods should handle exceptions, as they might not be able to be catched in the program using static library. Building static library requires LLVM 14 or newer.

Publishing

Scala Native supports sbt’s standard workflow for the package distribution:

  1. Compile your code.

  2. Generate a jar with all of the class files and NIR files.

  3. Publish the jar to sonatype, bintray or any other 3rd party hosting service.

Once the jar has been published, it can be resolved through sbt’s standard package resolution system.

Cross compilation

sbt-crossproject is an sbt plugin that lets you cross-compile your projects against all three major platforms in Scala: JVM, JavaScript via Scala.js, and native via Scala Native. It is based on the original cross-project idea from Scala.js and supports the same syntax for existing JVM/JavaScript cross-projects. Please refer to the project’s README for details.

Continue to lang.