---
name: macos-packaging-distribution
description: >-
  DMG vs pkg tradeoffs, hdiutil/pkgbuild/productbuild usage, direct-download distribution, and Sparkle 2 appcast with EdDSA signing for auto-updates
topics:
  - macos-native
  - distribution
  - packaging
  - sparkle
  - auto-update
volatility: evolving
last-reviewed: 2026-06-18
version-pin: null
sources:
  - url: https://developer.apple.com/documentation/xcode/distributing-your-app-to-registered-devices
  - url: https://sparkle-project.org/documentation/
  - url: https://sparkle-project.org/documentation/publishing/
---

Direct-download macOS distribution requires packaging the signed and notarized app into a container (DMG or pkg) that users can download and install. Auto-update via Sparkle 2 eliminates the manual re-download loop for updates. **A Mac App Store build must not bundle Sparkle or any third-party updater** — the App Store manages its own update mechanism.

## Summary

Use a **DMG** for simple drag-to-Applications distribution; use a **pkg** when you need a scripted install (e.g., command-line tools, system extensions, custom install locations). Create DMGs with `hdiutil create`; create pkg installers with `pkgbuild` and optionally `productbuild` for distribution packages. Add Sparkle 2 for auto-updates: generate an EdDSA key pair with `generate_keys`, embed the public key as `SUPublicEDKey` in `Info.plist`, and publish an `appcast.xml` generated by `generate_appcast`. Sign update archives with the EdDSA private key (stored in the macOS Keychain on your build machine).

## Deep Guidance

### DMG vs PKG

| | DMG | PKG |
|---|---|---|
| Install UX | User drags `.app` to Applications | System Installer.app runs install script |
| Custom install location | No (user chooses) | Yes |
| Command-line tool install | Not practical | Yes |
| System extension install | No | Yes |
| Post-install scripts | No | Yes (`postinstall`, `preinstall`) |
| Uninstall support | User deletes `.app` | Requires separate uninstaller or script |
| Signed with | Developer ID Application (the `.app`) + optional DMG codesign | Developer ID Installer |

For most indie macOS apps shipping a single `.app`, a DMG is simpler and the expected user experience.

### Creating a DMG

The standard flow: prepare a staging folder containing the signed `.app` and an alias to `/Applications`, then wrap it with `hdiutil`.

```bash
# 1. Prepare staging layout
mkdir -p dist/staging
cp -R MyApp.app dist/staging/
ln -s /Applications dist/staging/Applications

# 2. Create compressed read-only DMG
hdiutil create \
  -volname "MyApp 2.0" \
  -srcfolder dist/staging \
  -ov \
  -format UDZO \
  dist/MyApp-2.0.dmg
# -volname: volume label shown in Finder when mounted
# -srcfolder: folder whose contents populate the DMG root
# -ov: overwrite existing output file
# -format UDZO: zlib-compressed read-only image (standard for distribution)

# 3. Sign the DMG (no --options runtime — that's for Mach-O only)
codesign --force \
  --sign "Developer ID Application: Your Name (TEAMID)" \
  --timestamp \
  dist/MyApp-2.0.dmg

# 4. Submit for notarization, then staple (see macos-notarization entry)
```

For a polished DMG with a custom background and icon layout, use `create-dmg` (open-source CLI wrapper) instead of building the layout manually with `hdiutil` calls.

### Creating a PKG Installer

`pkgbuild` creates a component package from a built `.app`. `productbuild` wraps one or more component packages into a distributable product archive with optional welcome/license screens.

```bash
# Simple component package (signs inline with Developer ID Installer):
# Note: pkgbuild does not support --timestamp; secure timestamping is applied
# by productsign --timestamp (re-sign) or productbuild --timestamp (product archive).
pkgbuild \
  --component MyApp.app \
  --install-location /Applications \
  --sign "Developer ID Installer: Your Name (TEAMID)" \
  MyApp-component.pkg

# Full product archive (requires a distribution.xml):
productbuild \
  --distribution distribution.xml \
  --resources ./resources \
  --sign "Developer ID Installer: Your Name (TEAMID)" \
  --timestamp \
  MyInstaller.pkg

# Alternatively, sign an unsigned pkg afterward:
productsign \
  --sign "Developer ID Installer: Your Name (TEAMID)" \
  --timestamp \
  MyApp-unsigned.pkg \
  MyApp-signed.pkg
```

Verify the pkg signature:
```bash
pkgutil --check-signature MyInstaller.pkg
```

### Auto-Update with Sparkle 2

Sparkle 2 is the standard open-source auto-updater for non-App-Store macOS apps. It uses **EdDSA (ed25519)** signatures — the older DSA algorithm from Sparkle 1 is deprecated.

**One-time setup:**

1. Add Sparkle 2 as an SPM or Carthage/CocoaPods dependency (GitHub: `sparkle-project/Sparkle`).
2. Generate the EdDSA key pair (run once; private key is saved to the macOS Keychain automatically):
   ```bash
   ./Sparkle.framework/bin/generate_keys
   # Prints the base64 public key — copy it
   ```
3. Add the public key to `Info.plist`:
   ```xml
   <key>SUPublicEDKey</key>
   <string>BASE64_PUBLIC_KEY_HERE</string>
   ```
4. Add the appcast URL to `Info.plist`:
   ```xml
   <key>SUFeedURL</key>
   <string>https://example.com/appcast.xml</string>
   ```
5. Wire up the Sparkle updater in your app (via `SPUStandardUpdaterController` in SwiftUI or AppKit).

**Per-release workflow:**

Place the signed + notarized + stapled `.dmg` (or `.zip`) in an updates folder, then run `generate_appcast`:

```bash
# generate_appcast scans the folder and signs each archive using the Keychain private key
./Sparkle.framework/bin/generate_appcast /path/to/updates/

# Outputs appcast.xml in that folder — publish to your CDN/server
```

For a single archive (manual signing):
```bash
# sign_update prints the XML attributes to paste into an existing appcast entry:
./Sparkle.framework/bin/sign_update MyApp-2.0.dmg
```

**Appcast structure (`appcast.xml`):**
```xml
<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"
  xmlns:sparkle="http://www.andymatuschak.org/xml-namespaces/sparkle">
  <channel>
    <title>MyApp Changelog</title>
    <item>
      <title>Version 2.0</title>
      <pubDate>Thu, 18 Jun 2026 00:00:00 +0000</pubDate>
      <sparkle:version>200</sparkle:version>          <!-- CFBundleVersion -->
      <sparkle:shortVersionString>2.0</sparkle:shortVersionString>
      <sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
      <enclosure
        url="https://example.com/MyApp-2.0.dmg"
        length="12345678"
        type="application/octet-stream"
        sparkle:edSignature="BASE64_ED_SIGNATURE" />
    </item>
  </channel>
</rss>
```

`generate_appcast` writes `sparkle:edSignature` automatically. Do not use `sparkle:dsaSignature` (the DSA/Sparkle 1 attribute) in new appcasts.

### MAS Builds Must Not Include Sparkle

An app submitted to the Mac App Store must not bundle Sparkle or any other third-party update mechanism. The App Store manages updates centrally. Keep your MAS and direct-download builds as separate Xcode targets or schemes, and gate Sparkle behind a build flag:

```swift
// In your App target's conditional compilation:
#if DIRECT_DOWNLOAD
import Sparkle
// Set up SPUStandardUpdaterController
#endif
```

Or simply exclude Sparkle from the MAS target's linked frameworks entirely.
