Nucleus
Packaging & distribution

Building for macOS

Package a Nucleus app for macOS as a DMG or a Mac App Store PKG, then sign and notarize the build.

In this tutorial, you'll configure the macOS { } block to package a Nucleus app two ways: a Dmg for direct distribution and a Pkg for the Mac App Store. You'll sign and notarize the build, control the DMG layout, enable Liquid Glass on SDK 26, and build a universal binary.

Before you start

macOS packaging runs on a macOS host and relies on Apple's command-line tooling:

  • A macOS build host with Xcode Command Line Tools. vtool, notarytool, productbuild, and (for layered icons) actool come from that toolchain.
  • An Apple Developer account with the signing certificates you need: Developer ID Application for a DMG, and the 3rd Party Mac Developer certificates plus a provisioning profile for a PKG.
  • A Nucleus project with the Gradle plugin applied. See the Quickstart.

Configure the macOS block

In build.gradle.kts, declare the target formats and configure macOS { } inside nativeDistributions:

build.gradle.kts
nucleus {
    application {
        nativeDistributions {
            targetFormats(TargetFormat.Dmg, TargetFormat.Pkg, TargetFormat.Zip)

            macOS {
                bundleID = "com.example.myapp"
                dockName = "MyApp"
                appCategory = "public.app-category.utilities"
                minimumSystemVersion = "12.0"

                iconFile.set(project.file("icons/app.icns"))
                layeredIconDir.set(project.file("icons/MyApp.icon")) // macOS 26+
            }
        }
    }
}

bundleID is the app's identifier across Apple's ecosystem and uses reverse-DNS notation. layeredIconDir points at a .icon bundle and takes effect only on macOS build hosts with actool from Xcode Command Line Tools 26 or later; without that tooling the build logs a warning and falls back to iconFile.

Sign and notarize the build

Add signing { } and notarization { } to the macOS block:

build.gradle.kts
macOS {
    signing {
        sign.set(true)
        identity.set("Developer ID Application: My Company (TEAMID)")
    }
    notarization {
        appleID.set("dev@example.com")
        password.set(System.getenv("MAC_NOTARIZATION_PASSWORD"))
        teamID.set("TEAMID")
    }
}

notarization { } supports three mutually exclusive authentication modes: Apple ID plus an app-specific password and teamID; a notarytool keychainProfile (with an optional keychainPath); or an App Store Connect API key (apiKey, apiKeyId, apiIssuer). Configure exactly one — combining modes is rejected at validation.

Signing and notarization values also read from Gradle properties, so you can keep secrets out of the build script. See Code signing.

Package a DMG

./gradlew packageDmg

Dmg uses the standard createDistributable flow: a non-sandboxed bundle signed with the Developer ID Application certificate, notarized through xcrun notarytool, and stapled in place. When a notarization { } block is present, packageDmg notarizes automatically.

Control the DMG window with the dmg { } sub-block — window size, icon size, background image, and the positions of the app bundle and the drag-to-/Applications link:

build.gradle.kts
macOS {
    dmg {
        title = "\${productName} \${version}"
        iconSize = 128
        window { x = 400; y = 100; width = 540; height = 380 }
        background.set(project.file("packaging/dmg-bg.png"))

        content(x = 130, y = 220, type = DmgContentType.File, name = "MyApp.app")
        content(x = 410, y = 220, type = DmgContentType.Link, path = "/Applications")
    }
}

Package for the Mac App Store

./gradlew packagePkg

Pkg is always built as a Mac App Store target — there is no appStore flag to set. Nucleus builds a separate sandboxed .app, signs it with the 3rd Party Mac Developer Application certificate, embeds the provisioning profile, and produces the final .pkg through productbuild with the 3rd Party Mac Developer Installer certificate.

Point the block at your entitlements and provisioning profiles:

build.gradle.kts
macOS {
    entitlementsFile.set(project.file("packaging/sandbox-entitlements.plist"))
    runtimeEntitlementsFile.set(project.file("packaging/sandbox-runtime-entitlements.plist"))
    provisioningProfile.set(project.file("packaging/MyApp.provisionprofile"))
    runtimeProvisioningProfile.set(project.file("packaging/MyApp_Runtime.provisionprofile"))
}

Under the hood, PKG triggers the sandboxed pipeline: native libraries are extracted into Contents/Frameworks/, JVM arguments redirect JNI loading there, each .dylib is signed individually, and App Sandbox entitlements are applied. See Sandboxing.

Enable Liquid Glass on SDK 26

macOS 26 enables Liquid Glass only when the launcher's LC_BUILD_VERSION reports SDK 26.0. The plugin patches that Mach-O header with vtool before signing, so any JDK works — you don't need one compiled with Xcode 26. The nucleusPatchMacJvm task caches a patched JVM under build/nucleus/patched-jvm/, so the run task keeps full debugger support.

Patching is on by default. Change the target SDK or disable it with macOsSdkVersion:

build.gradle.kts
macOS {
    macOsSdkVersion = "26.0" // default; set to null to disable patching
}

GraalVM native-image builds take the SDK version from the system linker rather than from vtool. Select Xcode 26 on the build host instead.

Build a universal binary

A universal binary is built in two passes: compile arm64 and x86_64 separately, merge them with lipo, then re-sign inside-out (dylibs, then executables, then the runtime, then the app bundle). The build-macos-universal composite action runs this in CI. See CI/CD.

Customize Info.plist and launch agents

Append raw XML to Info.plist with infoPlist { }, and declare login-time launch agents with launchAgents { }:

build.gradle.kts
macOS {
    infoPlist {
        extraKeysRawXml = """
            <key>NSMicrophoneUsageDescription</key>
            <string>Required for voice notes.</string>
        """.trimIndent()
    }
    launchAgents {
        agent("com.example.myapp.indexer") {
            bundleProgram("Contents/MacOS/indexer")
            arguments("--background")
            startInterval(3600)
        }
    }
}

Launch agents are embedded at Contents/Library/LaunchAgents/ and registered at runtime with SMAppService. bundleProgram, arguments, startInterval, runAtLoad, keepAlive, processType, and calendar are functions on the agent block, not assignable properties.

Reference

The macOS { } block exposes: iconFile, layeredIconDir, bundleID, dockName, setDockNameSameAsPackageName, appCategory, minimumSystemVersion, installationPath, packageName, packageVersion, packageBuildVersion, dmgPackageVersion, pkgPackageVersion, entitlementsFile, runtimeEntitlementsFile, provisioningProfile, runtimeProvisioningProfile, and macOsSdkVersion, plus the sub-blocks signing { }, notarization { }, dmg { }, launchAgents { }, and infoPlist { }. See the full per-property list in the Gradle DSL reference.

installationPath has no default. It applies only to PKG and to the target of the DMG drag-to-/Applications link.

What's next

  • Code signing — signing and notarization credentials in detail.
  • Sandboxing — the Mac App Store pipeline behind PKG builds.
  • CI/CD — universal binaries and signing on GitHub Actions.
  • Gradle DSL reference — every macOS { } property.