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)actoolcome 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:
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:
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 packageDmgDmg 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:
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 packagePkgPkg 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:
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:
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 { }:
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.