Skip to content

Sandboxing

Nucleus automatically manages a sandboxed build pipeline for store formats. When your target formats include PKG, AppX, or Flatpak, the plugin splits the build into two parallel pipelines: one for direct-distribution formats (DMG, NSIS, DEB...) and one for sandboxed store formats.

Store Formats

Format OS Sandbox Type
PKG macOS App Sandbox
AppX Windows UWP container
Flatpak Linux Flatpak sandbox

The plugin determines which formats require sandboxing via TargetFormat.isStoreFormat:

targetFormats(
    // Direct distribution — non-sandboxed pipeline
    TargetFormat.Dmg,
    TargetFormat.Nsis,
    TargetFormat.Deb,

    // Store distribution — sandboxed pipeline
    TargetFormat.Pkg,
    TargetFormat.AppX,
    TargetFormat.Flatpak,
)

Both pipelines run in the same ./gradlew packageDistributionForCurrentOS invocation. No extra configuration is needed.

What the Sandboxed Pipeline Does

When at least one store format is configured, Nucleus registers additional Gradle tasks that handle the constraints imposed by OS-level sandboxing:

1. Extract Native Libraries from JARs

Sandboxed apps (especially macOS App Sandbox) cannot load unsigned native code extracted to temp directories at runtime. The plugin scans all dependency JARs for .dylib, .jnilib, .so, and .dll files and extracts them to the app's resources/ directory.

Task: extractNativeLibsForSandboxing

2. Strip Native Libraries from JARs

After extraction, the plugin rewrites JARs without the native library entries to avoid duplication in the final package.

Task: stripNativeLibsFromJars

3. Prepare Sandboxed App Resources

A separate Sync task merges the user's appResources with the extracted native libraries into a single resources directory for the sandboxed app-image.

Task: prepareSandboxedAppResources

4. Inject Sandboxing JVM Arguments

The sandboxed app-image is configured with JVM arguments that redirect native library loading to the pre-extracted resources directory:

-Djava.library.path=$APPDIR/resources
-Djna.nounpack=true
-Djna.nosys=true
-Djna.boot.library.path=$APPDIR/resources
-Djna.library.path=$APPDIR/resources

This ensures JNA/JNI libraries are loaded from signed, pre-extracted locations instead of being dynamically extracted to temp at runtime.

5. Sign Native Libraries in Resources (macOS)

On macOS, all .dylib files in the sandboxed app's resources/ directory are individually code-signed so they pass Gatekeeper checks.

6. Handle Skiko and icudtl.dat

The Skiko library path is adjusted to point to resources/ instead of the app root. The companion icudtl.dat file is copied alongside the Skiko native library.

macOS App Sandbox

Entitlements

The plugin ships default entitlements for both sandboxed and non-sandboxed builds:

Non-sandboxed (DMG, ZIP — direct distribution):

<dict>
    <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/>
</dict>

Sandboxed app (PKG — App Store):

<dict>
    <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/>
</dict>

Sandboxed runtime (JVM runtime binaries):

<dict>
    <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/>
</dict>

The runtime entitlements are more restrictive (no network, no file access) since only the app code should declare capabilities.

Custom Entitlements

Override the defaults for additional capabilities (camera, microphone, etc.):

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

Provisioning Profiles (Mac App Store)

Mac App Store builds require provisioning profiles:

macOS {
    appStore = true
    provisioningProfile.set(project.file("packaging/MyApp.provisionprofile"))
    runtimeProvisioningProfile.set(project.file("packaging/MyApp_Runtime.provisionprofile"))
}

AOT Cache and Sandboxing

When generating an AOT cache for a sandboxed build, the plugin temporarily strips the code signature from jspawnhelper during the training phase. macOS kills sandboxed forked processes, so the signature must be removed for AOT training and re-applied afterward with the runtime entitlements.

This is handled automatically — no configuration needed.

Windows AppX Sandbox

AppX packages run inside a UWP container. Capabilities are declared via the AppX manifest settings:

windows {
    appx {
        publisher = "CN=D541E802-6D30-446A-864E-2E8ABD2DAA5E"
        identityName = "MyCompany.MyApp"
        publisherDisplayName = "My Company"
        applicationId = "MyApp"
    }
}

See Windows Targets for all AppX settings.

Flatpak Sandbox

Flatpak apps are sandboxed by default. Use finishArgs to grant permissions:

linux {
    flatpak {
        finishArgs = listOf(
            "--share=ipc",
            "--socket=x11",
            "--socket=wayland",
            "--socket=pulseaudio",
            "--device=dri",
            "--filesystem=home",         // access home directory
            "--share=network",           // network access
        )
    }
}

See Linux Targets for all Flatpak settings.

CI Integration

The sandboxed pipeline runs transparently in CI. A single ./gradlew packageReleaseDistributionForCurrentOS builds both sandboxed and non-sandboxed formats:

- name: Setup Nucleus
  uses: ./.github/actions/setup-nucleus
  with:
    jbr-version: '25b176.4'
    packaging-tools: 'true'
    flatpak: 'true'     # Flatpak sandbox support
    snap: 'true'
    setup-gradle: 'true'
    setup-node: 'true'

- name: Build packages
  run: ./gradlew packageReleaseDistributionForCurrentOS --stacktrace --no-daemon

The setup-nucleus action installs all dependencies needed for sandboxed builds:

  • Linux: Flatpak SDK/runtime, Snapcraft
  • macOS: JBR 25 (for entitlements signing via codesign)
  • Windows: No extra setup needed (AppX uses the built-in Windows SDK)

Sandboxed outputs go to <format>-sandboxed/ subdirectories and are uploaded alongside non-sandboxed artifacts. The post-build jobs (universal macOS binary, MSIX bundle, publish) handle both transparently.

Gradle Tasks

Task Description
extractNativeLibsForSandboxing Extract .dylib/.so/.dll from dependency JARs
stripNativeLibsFromJars Rewrite JARs without native libraries
prepareSandboxedAppResources Merge app resources + extracted native libs
createSandboxedDistributable Build app-image with sandbox JVM args
generateSandboxedAotCache AOT cache for sandboxed distributable
package<Pkg\|AppX\|Flatpak> Final packaging using sandboxed distributable

These tasks are only registered when at least one store format is configured.