Nucleus
Packaging & distribution

Sandboxing

How Nucleus builds sandboxed distributions for the App Store, Flatpak, and MSIX store formats.

Three store formats require a sandboxed build: Pkg (macOS App Sandbox), Flatpak (Linux), and AppX (Windows MSIX). Sandboxed apps cannot extract native libraries to a temporary directory at runtime or load arbitrary code, so Nucleus runs a second build pipeline for these formats. This page describes what that pipeline does and how to configure the parts that need per-project values.

Request a store format

Add a store format to targetFormats. The sandboxed pipeline is enabled automatically when at least one store format is present.

build.gradle.kts
nucleus {
    application {
        nativeDistributions {
            targetFormats(
                // Non-sandboxed pipeline
                TargetFormat.Dmg,
                TargetFormat.Nsis,
                TargetFormat.Deb,
                // Sandboxed pipeline
                TargetFormat.Pkg,
                TargetFormat.AppX,
                TargetFormat.Flatpak,
            )
        }
    }
}

Run the standard packaging task. Both pipelines run in the same invocation:

./gradlew packageDistributionForCurrentOS

How it works

FormatOSSandbox model
PkgmacOSApp Sandbox
AppXWindowsMSIX — full trust, but with packaging constraints
FlatpakLinuxFlatpak sandbox

When at least one store format is configured, Nucleus registers additional Gradle tasks that:

  1. Scan dependency JARs for .dylib, .jnilib, .so, and .dll files and extract them — into Contents/Frameworks/ on macOS, and into resources/ on Windows and Linux.
  2. Rewrite the JARs without those native entries to avoid duplicating them.
  3. Merge your appResources with the extracted libraries.
  4. Inject JVM arguments so java.library.path, jna.library.path, and jna.boot.library.path point at the signed extraction directory. JNA also receives nounpack=true and nosys=true.
  5. Sign each native binary in Contents/Frameworks/ individually with codesign (macOS).
  6. Rewire Skiko and icudtl.dat to the same directory.

The non-sandboxed pipeline keeps running in parallel, so Dmg, Nsis, and Deb are built unchanged.

AOT cache and sandboxing

When generating an AOT cache for a sandboxed build, Nucleus strips jspawnhelper's code signature during the training phase — macOS terminates sandboxed forked processes otherwise — and re-applies it with the runtime entitlements before packaging.

Windows AppX

AppX runs at full trust and has the same system access as a regular Win32 app. It joins the sandboxed pipeline only because MSIX packaging forbids extracting DLLs to a temporary directory at runtime.

Configure macOS entitlements

Nucleus ships default entitlements for both sandboxed and non-sandboxed builds.

Non-sandboxed builds (Dmg, Zip):

<key>com.apple.security.cs.allow-jit</key>                        <true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/>
<key>com.apple.security.cs.disable-library-validation</key>       <true/>

Sandboxed builds (Pkg):

<key>com.apple.security.app-sandbox</key>                         <true/>
<key>com.apple.security.cs.allow-jit</key>                        <true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key> <true/>
<key>com.apple.security.cs.disable-library-validation</key>       <true/>
<key>com.apple.security.network.client</key>                      <true/>
<key>com.apple.security.files.user-selected.read-write</key>      <true/>

The runtime entitlements applied to the bundled JVM binaries are stricter: no network and no file access. Only your app code declares capabilities. Override either set per build:

build.gradle.kts
macOS {
    entitlementsFile.set(project.file("packaging/entitlements.plist"))
    runtimeEntitlementsFile.set(project.file("packaging/runtime-entitlements.plist"))
}

Embed provisioning profiles

Mac App Store submission requires provisioning profiles embedded at Contents/embedded.provisionprofile:

build.gradle.kts
macOS {
    provisioningProfile.set(project.file("packaging/MyApp.provisionprofile"))
    runtimeProvisioningProfile.set(project.file("packaging/MyApp_Runtime.provisionprofile"))
}

Configure the Flatpak sandbox

Flatpak permissions are declared through finishArgs:

build.gradle.kts
linux {
    flatpak {
        finishArgs = listOf(
            "--share=ipc",
            "--socket=wayland",
            "--socket=pulseaudio",
            "--device=dri",
            "--filesystem=home",
            "--share=network",
        )
    }
}

See Linux targets for the remaining Flatpak options.

Tasks

These tasks are registered only when a store format is requested.

TaskPurpose
extractNativeLibsForSandboxingExtract .dylib, .so, and .dll files from dependency JARs.
stripNativeLibsFromJarsRewrite the JARs without their native entries.
prepareSandboxedAppResourcesMerge appResources with the extracted libraries.
createSandboxedDistributableBuild the app image with the sandbox JVM arguments.
generateSandboxedAotCacheTrain the AOT cache inside the sandbox.

Notes

appStore is deprecated and ignored. Pkg is always treated as an App Store build: Nucleus applies the 3rd Party Mac Developer certificates and sandbox entitlements automatically.

  • A single ./gradlew packageReleaseDistributionForCurrentOS builds both pipelines in CI. Sandboxed artifacts are written to <format>-sandboxed/ subdirectories.
  • The sandboxed pipeline builds on the inside-out signing order used for universal macOS binaries. See Code signing.

What's next

  • Code signing — sign and notarize macOS, Windows, and Linux builds.
  • macOS targets — entitlements, provisioning profiles, and DMG layout.
  • Linux targets — Flatpak, Snap, and AppImage configuration.