Docs menu
Customizing the build
How to shape an sbt build on Primcloud: pre and post tasks, subproject builds, sbt options, native dependencies, and Debian packages.
On this page
Most Scala apps deploy with no customization at all. Primcloud runs sbt compile stage and that's the whole story. When you need more, it's sbt's own extension points doing the work: task overrides from system.properties or the environment, JVM options through SBT_OPTS, and, for OS-level libraries, Debian packages declared in project.toml.
Build hooks#
Custom pre- and post-build work goes through sbt tasks. Define them in build.sbt and hook them in one of two ways.
Prepend tasks with sbt.pre-tasks or SBT_PRE_TASKS. These run between clean (if enabled) and the default compile stage:
sbt.pre-tasks=my-pre-task
Multiple tasks are space-separated (sbt.pre-tasks=task1 task2), which makes the build sbt task1 task2 compile stage. The environment variable SBT_PRE_TASKS does the same.
Or wire the task into compile or stage inside sbt. Standard sbt dependency wiring works, with no Primcloud configuration needed:
lazy val stampBuildInfo = taskKey[Unit]("Write BUILD_SHA to the staging output")
stampBuildInfo := {
val sha = sys.env.getOrElse("BUILD_SHA", "unknown")
IO.write(baseDirectory.value / "target" / "universal" / "stage" / "BUILD_SHA", sha)
}
stage := stage.dependsOn(stampBuildInfo).value
Environment variables you set in the Secrets panel reach sbt tasks through sys.env.
Task and project overrides#
Primcloud exposes a small set of build knobs, readable from system.properties or environment variables. When both set the same key, system.properties wins.
system.properties |
Environment | Effect |
|---|---|---|
sbt.project |
SBT_PROJECT |
Build one subproject; the default tasks become {project}/compile and {project}/stage. |
sbt.pre-tasks |
SBT_PRE_TASKS |
Space-separated tasks prepended to the default. |
sbt.tasks |
SBT_TASKS |
Space-separated tasks that replace compile stage entirely. |
sbt.clean |
SBT_CLEAN |
true prepends a clean task. |
sbt.available-at-launch |
SBT_AVAILABLE_AT_LAUNCH |
true keeps sbt and its caches in the runtime image (rarely useful). |
Build a subproject. For a multi-project build where only one project deploys:
sbt.project=web-server
The build becomes sbt web-server/compile web-server/stage.
Replace the default tasks. When your build doesn't fit the compile stage shape:
sbt.tasks=assembly
The build becomes sbt assembly, and you're now on the hook for producing whatever your Procfile invokes. Primcloud no longer runs stage.
Clean before every build. Discouraged (it turns off sbt's incremental compilation and lengthens every deploy), but supported:
sbt.clean=true
Passing options to sbt's JVM#
SBT_OPTS is passed straight to the JVM that runs sbt. Prefix each JVM flag with -J:
Set in the Secrets panel:
SBT_OPTS=-J-Xss4m -J-Xmx2g
If your repo has an .sbtopts file at the root, its contents are appended to SBT_OPTS at build time. Use .sbtopts for options that should ride with the code, and the environment variable for values you'd rather not commit.
Native dependencies#
Two shapes of "native" a Scala app can lean on.
JNI bindings shipped inside JARs. Libraries like sqlite-jdbc, netty-tcnative, and jnr-posix bundle platform-specific .so files inside their JARs and load them at runtime. sbt resolves these like any other dependency; the JARs land on the classpath and the natives load with no extra setup.
OS-level shared libraries that a JNI library links against, or that your app shells out to: libpq, libmariadb, libtensorflow, and the like. These come from the operating system, not from sbt. Install them as Debian packages, below.
Debian packages#
When your app needs OS-level libraries or CLI tools that aren't in Primcloud's base image, declare them in a project.toml at the repo root, and Primcloud installs them during the build.
Declare packages to install:
[_]
schema-version = "0.2"
[com.heroku.buildpacks.deb-packages]
install = [
"libpq5",
"libmariadb-dev",
]
Each entry is a Debian package name from the Ubuntu 24.04 archive. Installed packages have their libraries added to LD_LIBRARY_PATH for both build and runtime, so a JNI System.loadLibrary call resolves at runtime without extra configuration.
Fuller options (rarely needed):
- Skip transitive dependencies:
{ name = "some-package", skip_dependencies = true } - Force reinstall:
{ name = "some-package", force = true } - Install from a URL:
download = ["https://example.com/package-1.2.3.deb"] - Add a custom Debian repository: a
[[com.heroku.buildpacks.deb-packages.sources]]table withuri,suites,components,arch, andsigned_by(the source key as an ASCII-armored PGP block).
Note
Migrating from Heroku Classic. An Aptfile at the repo root is not read. The build detects it, prints a warning, and installs nothing. Move your package list into project.toml under the schema above.