Building for Linux
Configure a Nucleus app to build DEB, RPM, AppImage, and pacman packages and publish to the Snap and Flatpak stores.
In this tutorial, you'll configure a Nucleus app to produce every Linux package
format — DEB, RPM, AppImage, and pacman for direct distribution, plus Snap and
Flatpak for the stores — from a single linux { } block, then build and inspect
the output.
Before you start
- The Nucleus Gradle plugin is applied to your project. See Project setup.
- You are on a Linux build host. DEB, RPM, AppImage, pacman, Snap, and Flatpak are Linux-only formats and do not cross-build from macOS or Windows.
- Format-specific tooling is installed as noted in each step (for example
libfuse2for AppImage,snapcraftfor Snap,flatpak-builderfor Flatpak).
Configure the target formats
In build.gradle.kts, list the Linux formats you want in targetFormats(...) and
describe the package in the linux { } block:
nucleus {
application {
nativeDistributions {
targetFormats(
TargetFormat.Deb,
TargetFormat.Rpm,
TargetFormat.AppImage,
TargetFormat.Pacman,
TargetFormat.Snap,
TargetFormat.Flatpak,
)
homepage = "https://myapp.example.com"
linux {
iconFile.set(project.file("icons/app.png"))
packageName = "myapp"
shortcut = true
appCategory = "Utility"
menuGroup = "Development"
debMaintainer = "Your Name <dev@example.com>"
debDepends = listOf("libfuse2", "libgtk-3-0", "libasound2")
rpmLicenseType = "MIT"
rpmRequires = listOf("gtk3", "libX11", "alsa-lib")
pacmanDepends = listOf("gtk3", "libx11", "alsa-lib")
}
}
}
}homepage is required for DEB and RPM builds. The packaging task fails without it.
Build DEB and RPM packages
Each format has its own Gradle task named after the TargetFormat constant:
./gradlew packageDeb
./gradlew packageRpmThe plugin writes a .desktop entry into each package with the correct
Categories=, MimeType=, StartupWMClass=, and Exec= lines. If your app's
window class doesn't match what the window manager reports, set startupWMClass
explicitly:
linux {
startupWMClass = "com-example-MyApp"
}When startupWMClass is unset, Nucleus derives it from mainClass by replacing
dots with hyphens.
Build an AppImage
An AppImage is a single portable executable that runs on most distributions without installation:
./gradlew packageAppImageThe build host needs libfuse2:
sudo apt-get install -y libfuse2Configure the desktop metadata in the appImage { } sub-block:
linux {
appImage {
category = AppImageCategory.Utility
genericName = "My Application"
synopsis = "A short description"
desktopEntries = mapOf("Keywords" to "editor;text;")
}
}Output size and build time depend on nativeDistributions.compressionLevel:
CompressionLevel | Output | Build time |
|---|---|---|
Store | Largest | Fastest |
Normal (default) | Balanced | Moderate |
Maximum | Small | Slow |
Ultra | Smallest | Slowest |
Ultra additionally rebuilds the .deb at maximum xz effort (xz -9e), roughly 25% smaller
than Maximum. RPM and pacman already use xz -9 and are unaffected.
Prefer Normal or Store for AppImage: Maximum / Ultra map to electron-builder
maximum and make cold starts very slow (squashfs/FUSE decompression). From 2.3.1 you can
override only the AppImage while leaving DEB/DMG on a higher global level:
nativeDistributions {
compressionLevel = CompressionLevel.Ultra
linux {
appImage {
// Snappy FUSE cold start; DEB still uses Ultra above.
compressionLevel = CompressionLevel.Normal
}
}
}Package for Arch Linux
TargetFormat.Pacman produces a pacman archive for Arch Linux and derivatives:
./gradlew packagePacmanSet Arch-specific dependencies and, if needed, an independent version string:
linux {
pacmanDepends = listOf("gtk3", "libx11", "alsa-lib")
pacmanPackageVersion = "2.0.0"
}Add install and remove scripts
Deb, Rpm, and Pacman packages run maintainer scripts as root. Point linux { } at your
own files; Nucleus concatenates the after-scripts after its own template (desktop
integration, AppArmor, optional polkit silent-update helper) and passes the before-scripts
raw to fpm. GraalVM native-image packages honor the same properties.
linux {
afterInstall.set(project.file("packaging/linux/after-install.sh"))
afterRemove.set(project.file("packaging/linux/after-remove.sh"))
beforeInstall.set(project.file("packaging/linux/before-install.sh"))
beforeRemove.set(project.file("packaging/linux/before-remove.sh"))
// Pacman only: the install/remove scripts do not run on an upgrade
afterUpgrade.set(project.file("packaging/linux/after-upgrade.sh"))
beforeUpgrade.set(project.file("packaging/linux/before-upgrade.sh"))
}| Property | When it runs | How it is wired |
|---|---|---|
afterInstall | After the payload is unpacked | Concatenated after Nucleus's template, then electron-builder |
afterRemove | After the payload is deleted | Concatenated after Nucleus's template, then electron-builder |
beforeInstall | Before the payload is unpacked | Passed raw to fpm (--before-install) |
beforeRemove | Before the payload is deleted | Passed raw to fpm (--before-remove) |
afterUpgrade | After a Pacman upgrade swaps the payload | Passed raw to fpm (--after-upgrade) |
beforeUpgrade | Before a Pacman upgrade swaps the payload | Passed raw to fpm (--before-upgrade) |
Typical use: install a systemd unit that ships under appResourcesRootDir, then enable it
from afterInstall. Stop the unit in beforeInstall so binaries under /opt can be
replaced, and disable it in afterRemove.
The payload is installed under /opt/<packageName> (linux.packageName, falling back to
packageName), never under the display appName, so the path contains no spaces and is
safe in unquoted ExecStart= lines.
electron-builder substitutes ${sanitizedProductName} and ${executable} in
afterInstall / afterRemove only when the token is single-quoted:
systemctl enable '${executable}-daemon.service'
systemctl start '${executable}-daemon.service'A double-quoted "${executable}" is left as literal text, so
systemctl enable "${executable}.service" silently no-ops instead of failing. beforeInstall,
beforeRemove, afterUpgrade, and beforeUpgrade get no macro substitution at all —
hardcode unit and path names there.
When Pacman replaces an installed package with a newer version, its .INSTALL calls only
pre_upgrade / post_upgrade. None of afterInstall, afterRemove, beforeInstall, or
beforeRemove run, and there is no fallback to them. Stop the unit in beforeUpgrade and
re-enable or restart it in afterUpgrade, or an upgrade leaves the service dead. Deb and
Rpm ignore both upgrade properties: their install scriptlets already fire on upgrades.
AppImage, Snap, and Flatpak do not run these scripts. AppImage is not installed; Snap and Flatpak have their own hook surfaces.
Publish to the Snap Store
Snap targets the Snap Store. The default plugs cover
desktop integration, X11 and Wayland, networking, audio playback, and OpenGL.
Override confinement when your app needs filesystem access beyond home:
linux {
snap {
confinement = SnapConfinement.Strict
grade = SnapGrade.Stable
base = "core22"
plugs = listOf(
SnapPlug.Desktop,
SnapPlug.Home,
SnapPlug.Wayland,
SnapPlug.Network,
SnapPlug.AudioPlayback,
)
compression = SnapCompression.Xz
}
}./gradlew packageSnapThe build host needs snapd and snapcraft --classic.
Publish to Flathub with Flatpak
Flatpak is sandboxed. Declare the permissions your app needs through finishArgs.
The defaults are --share=ipc, --socket=x11, --socket=wayland,
--socket=pulseaudio, and --device=dri; setting finishArgs replaces them, so
include everything you need:
linux {
flatpak {
runtime = "org.freedesktop.Platform"
runtimeVersion = "23.08"
sdk = "org.freedesktop.Sdk"
finishArgs = listOf(
"--share=ipc",
"--socket=wayland",
"--socket=pulseaudio",
"--device=dri",
"--filesystem=home",
"--share=network",
)
}
}./gradlew packageFlatpakFlatpak is a store format and runs through the
sandboxed pipeline: native libraries are
pre-extracted and JVM arguments are redirected. If flatpak-builder or the
runtime isn't installed, the task is skipped rather than failed.
Register deep links and file associations
Top-level protocol(...) and fileAssociation(...) entries are merged into the
generated .desktop file as MimeType= values:
nativeDistributions {
protocol("MyApp URL", "myapp")
fileAssociation(
mimeType = "application/x-myapp",
extension = "myapp",
description = "MyApp Document",
)
}This produces:
MimeType=x-scheme-handler/myapp;application/x-myapp;No manual desktopEntries override is needed.
Configuration reference
linux { } exposes iconFile, packageVersion, shortcut, startupWMClass,
packageName, appRelease, appCategory, menuGroup, debMaintainer,
debPackageVersion, debDepends, rpmLicenseType, rpmPackageVersion,
rpmRequires, pacmanPackageVersion, pacmanDepends, afterInstall,
afterRemove, beforeInstall, beforeRemove, afterUpgrade, and beforeUpgrade. Sub-blocks are
appImage { }, snap { }, flatpak { }, and signing { }.
appRelease sets the package release number — the digit after the - in DEB and
RPM versions. Bump it when you re-release without changing packageVersion. See
the full Gradle DSL reference.
Snap and Flatpak layer their own desktop files into the user environment and
ignore the traditional .desktop install paths used by DEB and RPM.
What's next
- Code signing — sign DEB and RPM packages with GPG.
- Sandboxing — how the Flatpak pipeline repackages your app.
- Publishing — release built artifacts to GitHub, S3, or a generic server.
- Gradle DSL reference — every
linux { }property.