sbt-snx is an sbt 2.x plugin for Scala Native projects, Scala 3 only. It drives the Scala Native toolchain directly and models the native target - the OS/arch you build for and the toolchain's resolved C library or ABI - as build settings and tasks.
Status: pre-release and under active development. The API is not yet stable.
Most Scala Native projects do not need this plugin. Use the official, battle-tested sbt-scala-native plugin unless you need per-OS/arch classified resolution and publishing, or automatic propagation of native link requirements (system libraries, frameworks, etc) across the dependency graph, or direct per-platform control of the native build configuration.
// project/plugins.sbt
addSbtPlugin("africa.shuwari" % "sbt-snx" % "<version>")// build.sbt
enablePlugins(SNXPlugin)Enabling the plugin adds the Scala Native compiler plugin and runtime libraries. Settings and tasks are namespaced under
an SNX object; the platform types (TargetPlatform, OS, Arch, NativeRuntime, ABI) are auto-imported.
TargetPlatform pairs an OS (Linux/Darwin/Windows) with an Arch (X86_64/Aarch64) - the setting-time
coordinate you choose, defaulting to the build host. NativeRuntime is the task-time key you match on: it refines the
target with the toolchain ABI - the C library on Linux (Glibc/Musl), the runtime ABI on Windows (Msvc/MinGw)
- resolved from the native target triple, so each case exposes only the values valid for its operating system:
Linux(arch, abi),Darwin(arch), andWindows(arch, abi). You set aTargetPlatform; you match on aNativeRuntime. An unsupported operating system, architecture, or toolchain ABI fails the build withUnsupportedTargetException.
A project produces one SNX.deliverable: NIR (the default - a platform-independent jar that a downstream Scala
Native build resolves and links), a native Library (a .so/.dylib/.dll, or a .a/.lib when linked
statically), or an Executable. SNX.linkage selects Static or Dynamic linking per platform (default Dynamic);
a static Executable is supported only where the toolchain allows it (musl or MSVC) and fails fast elsewhere.
SNX.link links the binary for the enclosing configuration. run runs it, forwarding arguments and run / envVars;
test runs the project's test frameworks as a native binary (it requires Test / fork := false). run, runMain,
and test override sbt's defaults.
SNX.config is the resolved Scala Native configuration: the discovered toolchain, then the scalar settings
(SNX.mode, SNX.gc, SNX.lto, SNX.optimize, SNX.sanitizer, SNX.multithreading), then the matched per-platform
SNX.modifiers, applied last so a modifier has the final say. A Modifier[Native] is a partial function from the
resolved NativeRuntime to a Native transform, carried with Modifier.platform. The Native surface offers the raw
option channels (linkOptions, compileOptions, cOptions, cppOptions), the structured library/include/
define, the scalars, and embedResources/finalFields/debugSymbols/linktimeProperty, with update for anything
else. Modifier.wholeArchive(archive) force-links a whole static archive in each platform's linker syntax.
SNX.includeDirs and SNX.libDirs add -I/-L directories - host-discovered paths are dropped when cross-targeting,
so a cross build is not contaminated by the host toolchain's directories - and SNX.clang/SNX.clangPP override the
discovered compilers. .c, .cpp, and .S sources under src/main/resources/scala-native/ are compiled into the
binary at link time.
SNX.dependencies carries classified or requirement-bearing native dependencies; plain JVM/NIR dependencies may stay in
libraryDependencies. % NativeClassifier resolves a dependency under the build's OS/arch classifier, and options
attaches the per-platform link Usage requirements a dependency needs but does not declare itself - useful for an
under-declaring dependency that ships no descriptor of its own:
SNX.dependencies += "org.acme" %% "blas" % "0.9" % NativeClassifier options {
case Linux(_, _) => Usage.libraries("m")
}These requirements fold into this project's own link and propagate into its published descriptor, so they reach downstream consumers too.
A native library that bundles C may require its consumers to link extra system libraries, frameworks, or whole
archives - things Scala Native does not propagate on its own (the library name of a @link-annotated binding already
does). SNX.usage declares these per platform, as toolchain-neutral tokens. They render into the library's own link
and travel in a descriptor inside the library's jar; a consumer resolving the library folds the descriptor for its own
runtime and renders each token into its link. So the requirements arrive, correct, however deep the library sits in the
dependency graph - a consuming application never restates them. Usage composes per channel with ++: libraries
(-l), frameworks (macOS -framework), wholeArchive, defines (a -D a consumer's own C must match), linkFlags
(a raw escape), and multithreaded (require the consumer to link with multithreading).
A plain NIR library publishes a single, platform-independent jar that carries its descriptor. A per-platform NIR
library - one whose compiled NIR genuinely differs per target - sets SNX.classified := true and is built once per
target (each pinning SNX.target); each build publishes its content and descriptor under the build's OS/arch
classifier (name_native0.5_3-version-os-arch.jar), leaving a manifest-only placeholder on the unclassified main
coordinate. A consumer resolves such a dependency with % NativeClassifier. Either way, propagation is automatic.
| Key | Type | Default |
|---|---|---|
SNX.target |
TargetPlatform |
the build host |
SNX.runtime |
NativeRuntime (task) |
resolved from target + toolchain |
SNX.deliverable |
Deliverable |
NIR |
SNX.linkage |
PartialFunction[NativeRuntime, Linkage] |
Dynamic |
SNX.mode .gc .lto .optimize .sanitizer .multithreading |
scalars | Scala Native's defaults |
SNX.clang .clangPP |
Option[File] |
discovered clang / clang++ |
SNX.includeDirs .libDirs |
Seq[File] |
empty (host paths cross-stripped) |
SNX.modifiers |
Seq[Modifier[Native]] |
empty |
SNX.dependencies |
Seq[NativeDependency] |
empty |
SNX.usage |
PartialFunction[NativeRuntime, Usage] |
empty (exported link requirements) |
SNX.config |
Native (task) |
resolved configuration |
SNX.link |
File (task) |
links the binary |
SNX.classified |
Boolean |
false (publish per OS/arch) |
SNX.host is the build host's TargetPlatform. run, runMain, and test are overridden for Scala Native.
A multi-module Scala Native project with all three deliverables: a per-platform NIR library, a native library, and an application that consumes both.
// build.sbt
scalaVersion := "3.8.4"
organization := "com.example"
version := "0.1.0"
// A per-platform NIR library: its bundled C needs a system library, which it declares once via `SNX.usage`. Published
// per OS/arch as a classified jar that carries the descriptor.
val core = project
.enablePlugins(SNXPlugin)
.settings(
SNX.classified := true,
SNX.usage := {
case Linux(_, _) => Usage.libraries("z")
case Darwin(_) => Usage.frameworks("Security")
case Windows(_, _) => Usage.libraries("zlib")
}
)
// A native library (.so/.dylib/.dll), linked statically where the toolchain supports it.
val engine = project
.enablePlugins(SNXPlugin)
.dependsOn(core)
.settings(
SNX.deliverable := Library,
SNX.linkage := { case p if p.supportsStaticLinking => Static; case _ => Dynamic }
)
// The application, with a per-platform tweak. It consumes both modules and never restates core's link requirements:
// core's descriptor propagates them into this link automatically.
val app = project
.enablePlugins(SNXPlugin)
.dependsOn(core, engine)
.settings(
SNX.deliverable := Executable,
SNX.modifiers += Modifier.platform { case Linux(_, Glibc) => _.lto(LTO.thin) }
)When app links, it folds the descriptors on its classpath, so core's per-platform requirements - -lz on Linux,
-framework Security on macOS, -lzlib on Windows - are applied to the link automatically. Neither app nor engine
declares them.
sbt 2.x's built-in project matrix cross-builds one source set across platforms. Its nativePlatform row is bound to the
official Scala Native plugin, so sbt-snx adds snxPlatform, enabling sbt-snx on a Scala Native row instead:
val core = projectMatrix
.jvmPlatform(scalaVersions = Seq("3.8.4"))
.snxPlatform(scalaVersions = Seq("3.8.4"), settings = Seq(SNX.deliverable := Executable))This expands into a JVM subproject (core) and a Scala Native subproject (coreNative), each compiling its shared
scala sources plus its own platform source directory (scalajvm / scalanative). snxPlatform mirrors
nativePlatform's overloads, also taking axisValues and either settings or a configure: Project => Project for
per-row configuration, so a build can migrate from the official plugin by replacing nativePlatform with snxPlatform.
Apache License 2.0. Copyright Shuwari Africa Ltd.