Building for macOS
DMG, PKG (Mac App Store), universal binaries, notarization, Liquid Glass.
macOS asks more of an installer than any other desktop OS: code signing, notarization, App Sandbox for the Store, layered icons, hardened runtime, and now Liquid Glass at SDK 26. Nucleus drives all of it from the macOS { … } block.
TL;DR
- Two formats:
Dmg(direct distribution) andPkg(Mac App Store, sandboxed). - Liquid Glass is on by default — the launcher Mach-O
LC_BUILD_VERSIONis patched to SDK 26 viavtool. - Universal binaries via the CI
build-macos-universalaction —lipomerges arm64 and x64 then re-signs inside-out. entitlementsFile,provisioningProfile, andinfoPlisthooks are first-class DSL.
Install
Comes with the Gradle plugin.
Quickstart
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+
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")
}
}
}
}
}./gradlew packageDmg runs notarization automatically when a notarization { } block is configured.
How it works
DMG and PKG, two pipelines
Dmg uses the standard createDistributable flow — non-sandboxed, signed with Developer ID Application, notarized via xcrun notarytool, stapled in place. Pkg is always treated as a Mac App Store target: a separate sandboxed .app is built, signed with 3rd Party Mac Developer Application, provisioning profile embedded, and the final .pkg produced via productbuild + 3rd Party Mac Developer Installer. No appStore flag — Nucleus infers it.
Liquid Glass without Xcode 26-built JDK
macOS 26 enables Liquid Glass only when the binary's LC_BUILD_VERSION reports SDK 26.0. The Nucleus plugin patches that header on the launcher Mach-O via vtool before signing, so any JDK works — you don't need a JDK compiled with Xcode 26. The patching task nucleusPatchMacJvm caches a patched JVM under build/nucleus/patched-jvm/ so the run task keeps full debugger support.
macOS {
macOsSdkVersion = "26.0" // default; set null to disable patching
}GraalVM native-image builds get the SDK version from the system linker — set Xcode 26 on the CI runner instead.
DMG appearance
The dmg { … } block exposes window size, icon positioning, background image, and the drag-to-/Applications arrow:
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")
}
}Mac App Store sandbox
PKG triggers the sandboxed pipeline: native libs extracted into Contents/Frameworks/, JVM args redirect JNI loading there, every .dylib is signed individually, App Sandbox entitlements applied.
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"))
}Universal binaries
Two-pass: build arm64 and x86_64 separately, merge with lipo, then re-sign inside-out (dylibs → executables → runtime → app bundle). The build-macos-universal composite action does this in CI — see CI/CD.
infoPlist and launch agents
Append raw XML to Info.plist and declare login-time launch agents:
macOS {
infoPlist {
extraKeysRawXml = """
<key>NSMicrophoneUsageDescription</key>
<string>Required for voice notes.</string>
""".trimIndent()
}
launchAgents {
agent("com.example.myapp.indexer") {
bundleProgram = "Contents/MacOS/indexer"
arguments = listOf("--background")
startInterval = 3600
}
}
}Reference
The macOS { } block exposes: iconFile, layeredIconDir, bundleID, dockName, setDockNameSameAsPackageName, appCategory, minimumSystemVersion, installationPath (defaults to /Applications), packageName, packageVersion, packageBuildVersion, dmgPackageVersion, pkgPackageVersion, entitlementsFile, runtimeEntitlementsFile, provisioningProfile, runtimeProvisioningProfile, macOsSdkVersion, plus sub-blocks signing { }, notarization { }, dmg { }, launchAgents { }, infoPlist { }. Full per-property reference in the Gradle DSL reference.
Notes
- Layered icons require Xcode Command Line Tools 26+ (
actool) and only take effect on macOS build hosts. Missing tooling logs a warning and falls back to the traditional.icns. - Notarization supports three auth modes (Apple ID + app-specific password,
notarytoolkeychain profile, or App Store Connect API key). Pick one — configuring multiple is rejected at validation. See code signing. installationPathonly applies to PKG and the DMG drag-to-Applications arrow target.