Distributing Mac apps outside of the App Store requires more than just building and zipping your .app.
Since macOS Catalina, Apple requires all apps to be notarized by Apple to run without warnings on users' machines. Notarization ensures the app is from a known developer and free of malicious code.
Without notarization, users will see a warning saying the app “can’t be opened because Apple cannot check it for malicious software.”
With proper notarization and signing, your app behaves like a first-class citizen on macOS, even outside the App Store.
Here’s a streamlined process to do it correctly:
Step 1: Archive Your App in Xcode
Open your project in Xcode and create an archive:
In the menu, go to Product > Archive.
After the archive builds, the Organizer window will open. Click "Distribute App" on the build you want to export, then click "Direct Distribution".
When it's done, click "Export", and save the app somewhere; you'll receive a signed .app.
Step 2: Create the notarized DMG
To simplify post-export steps, I wrote a script that takes your exported .app, packages it into a .dmg, submits it for notarization, and staples the result — all automatically.
Before doing anything, install the necessary tools:
brew install create-dmg
Now we want to export the credentials that you already have into Xcode to the keychain, so that other tools can use it:
xcrun notarytool store-credentials "AC_PASSWORD" --apple-id $EMAIL --team-id $TEAM_ID
Perfect - you can now use my create-my-dmg.sh script to create a DMG.
#!/bin/bash | |
# brew install create-dmg | |
# xcrun notarytool store-credentials "AC_PASSWORD" --apple-id $EMAIL --team-id $TEAM_ID | |
set -exu | |
APP_BUNDLE="$1" | |
if [[ ! -d "$APP_BUNDLE" ]]; then | |
echo "Usage: $0 <app-bundle-path>" | |
exit 1 | |
fi | |
# Extract version and name from the app bundle | |
INFO_PLIST="$APP_BUNDLE/Contents/Info.plist" | |
VERSION=$(/usr/libexec/PlistBuddy -c "Print CFBundleShortVersionString" "$INFO_PLIST") | |
if [[ -z "$VERSION" ]]; then | |
echo "Error: Could not determine version from Info.plist" | |
exit 1 | |
fi | |
APPNAME=$(/usr/libexec/PlistBuddy -c "Print CFBundleName" "$INFO_PLIST") | |
if [[ -z "$APPNAME" ]]; then | |
echo "Error: Could not determine name from Info.plist" | |
exit 1 | |
fi | |
IDENTITY=$(security find-identity -v -p codesigning | grep "Developer ID Application" | head -n 1 | awk '{print $2}') | |
if [[ -z "$IDENTITY" ]]; then | |
echo "Error: Could not determine signing identity. In order to sign the app, you need to request a Developer ID Application certificate from Apple." | |
exit 1 | |
fi | |
DMG_NAME="$APPNAME-$VERSION.dmg" | |
DMG_PATH="./$DMG_NAME" | |
VOLUME_NAME="$APPNAME $VERSION" | |
echo "Creating DMG for $APPNAME $VERSION -> $DMG_PATH" | |
# Create DMG (using create-dmg) | |
create-dmg \ | |
--volname "$VOLUME_NAME" \ | |
--volicon "$APP_BUNDLE/Contents/Resources/AppIcon.icns" \ | |
--window-pos 200 120 \ | |
--window-size 800 400 \ | |
--icon-size 100 \ | |
--icon "${APPNAME}.app" 200 190 \ | |
--hide-extension "${APPNAME}.app" \ | |
--app-drop-link 600 185 \ | |
--codesign "$IDENTITY" \ | |
--notarize AC_PASSWORD \ | |
"$DMG_PATH" \ | |
"$APP_BUNDLE" |
You can run it using:
create-my-dmg.sh YourMacApp.app
This will create a DMG like YourMacApp-1.2.0.dmg
, correctly notarized and stapled.