release-macos-spm-packaging
macOS SwiftPM App Packaging
Section titled “macOS SwiftPM App Packaging”Overview
Section titled “Overview”Bootstrap a complete SwiftPM macOS app, then build, package, and run it without Xcode. This skill covers the full workflow from project scaffolding to release distribution.
Project Scaffolding
Section titled “Project Scaffolding”Basic Structure
Section titled “Basic Structure”MyApp/├── Package.swift├── Sources/│ └── MyApp/│ ├── MyApp.swift # @main App entry│ └── ContentView.swift├── Resources/│ ├── Assets.xcassets/│ └── Info.plist├── Scripts/│ ├── package_app.sh│ ├── compile_and_run.sh│ └── sign-and-notarize.sh└── version.envPackage.swift
Section titled “Package.swift”import PackageDescription
let package = Package( name: "MyApp", platforms: [.macOS(.v14)], products: [ .executable(name: "MyApp", targets: ["MyApp"]) ], targets: [ .executableTarget( name: "MyApp", resources: [ .process("Resources") ] ) ])version.env
Section titled “version.env”APP_NAME="MyApp"BUNDLE_ID="com.example.myapp"VERSION="1.0.0"BUILD_NUMBER="1"MIN_MACOS="14.0"# Set to 1 for menu bar appsMENU_BAR_APP=0Build and Run
Section titled “Build and Run”Build with SwiftPM
Section titled “Build with SwiftPM”# Debug buildswift build
# Release buildswift build -c release
# Run testsswift testPackage as .app Bundle
Section titled “Package as .app Bundle”Create Scripts/package_app.sh:
#!/bin/bashset -e
source version.env
BUILD_DIR=".build/release"APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"CONTENTS="$APP_BUNDLE/Contents"MACOS="$CONTENTS/MacOS"RESOURCES="$CONTENTS/Resources"
# Build releaseswift build -c release
# Create bundle structurerm -rf "$APP_BUNDLE"mkdir -p "$MACOS" "$RESOURCES"
# Copy binarycp "$BUILD_DIR/$APP_NAME" "$MACOS/"
# Copy resourcescp -r Resources/* "$RESOURCES/" 2>/dev/null || true
# Generate Info.plistcat > "$CONTENTS/Info.plist" << EOF<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"><plist version="1.0"><dict> <key>CFBundleExecutable</key> <string>$APP_NAME</string> <key>CFBundleIdentifier</key> <string>$BUNDLE_ID</string> <key>CFBundleName</key> <string>$APP_NAME</string> <key>CFBundleVersion</key> <string>$BUILD_NUMBER</string> <key>CFBundleShortVersionString</key> <string>$VERSION</string> <key>LSMinimumSystemVersion</key> <string>$MIN_MACOS</string> <key>CFBundlePackageType</key> <string>APPL</string>$([ "$MENU_BAR_APP" = "1" ] && echo " <key>LSUIElement</key> <true/>")</dict></plist>EOF
echo "Created $APP_BUNDLE"Development Run Script
Section titled “Development Run Script”Create Scripts/compile_and_run.sh:
#!/bin/bashset -e
source version.env
# Kill existing instancepkill -x "$APP_NAME" 2>/dev/null || true
# Package./Scripts/package_app.sh
# Launchopen ".build/release/$APP_NAME.app"Code Signing
Section titled “Code Signing”Development Signing
Section titled “Development Signing”# Sign for local developmentcodesign --force --sign - ".build/release/MyApp.app"
# Or with a specific identitycodesign --force --sign "Developer ID Application: Your Name" ".build/release/MyApp.app"Create Stable Dev Identity
Section titled “Create Stable Dev Identity”# Generate self-signed certificate for consistent dev signingsecurity create-keychain -p "" dev-signing.keychainsecurity default-keychain -s dev-signing.keychain# Follow prompts in Keychain Access to create certificateNotarization and Release
Section titled “Notarization and Release”One-time setup. Notarization needs the full Xcode.app, not the lightweight
Command Line Tools package — CLT omits notarytool/stapler. Store credentials
once so scripts never carry a plaintext password:
xcrun notarytool store-credentials "AC_PASSWORD" \ --apple-id "your@email.com" \ --team-id "TEAM_ID"# Prompts interactively for an app-specific password (appleid.apple.com,# Sign-In and Security -> App-Specific Passwords). Stored in the login# keychain under the given profile name; `--keychain-profile "AC_PASSWORD"`# below reads it back. Regenerate if the Apple ID password ever changes --# app-specific passwords go stale silently, with no warning at submit time.Create Scripts/sign-and-notarize.sh:
#!/bin/bashset -e
source version.env
APP_PATH=".build/release/$APP_NAME.app"ZIP_PATH=".build/release/$APP_NAME-$VERSION.zip"
# Sign with Developer IDcodesign --force --options runtime --sign "Developer ID Application: Your Name" "$APP_PATH"
# Create zip for notarizationditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
# Submit for notarization (--keychain-profile reads the credentials stored# above by `store-credentials` -- notarytool does not take a literal# --password value or the altool-style "@keychain:" reference syntax)xcrun notarytool submit "$ZIP_PATH" \ --keychain-profile "AC_PASSWORD" \ --wait
# Staple the ticketxcrun stapler staple "$APP_PATH"
# Re-zip with stapled ticketrm "$ZIP_PATH"ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "Release ready: $ZIP_PATH"Verify the release
Section titled “Verify the release”Run all three before shipping — each catches a different failure mode (wrong signing identity, Gatekeeper rejection, missing/unstapled ticket):
codesign -dv --verbose=4 "$APP_PATH" # confirms who signed it and with what identityspctl -a -vvv -t exec "$APP_PATH" # confirms Gatekeeper will actually accept itxcrun stapler validate "$APP_PATH" # confirms the notarization ticket is attachedSparkle Updates (Optional)
Section titled “Sparkle Updates (Optional)”Generate Appcast Entry
Section titled “Generate Appcast Entry”#!/bin/bashsource version.env
ZIP_PATH=".build/release/$APP_NAME-$VERSION.zip"SIZE=$(stat -f%z "$ZIP_PATH")SIGNATURE=$(./bin/sign_update "$ZIP_PATH")DATE=$(date -R)
cat << EOF<item> <title>Version $VERSION</title> <pubDate>$DATE</pubDate> <sparkle:version>$BUILD_NUMBER</sparkle:version> <sparkle:shortVersionString>$VERSION</sparkle:shortVersionString> <enclosure url="https://example.com/releases/$APP_NAME-$VERSION.zip" length="$SIZE" type="application/octet-stream" sparkle:edSignature="$SIGNATURE" /></item>EOFGitHub Release
Section titled “GitHub Release”# Create taggit tag -a "v$VERSION" -m "Release $VERSION"git push origin "v$VERSION"
# Create GitHub releasegh release create "v$VERSION" \ ".build/release/$APP_NAME-$VERSION.zip" \ --title "v$VERSION" \ --notes "Release notes here"Checklist
Section titled “Checklist”Scaffolding
Section titled “Scaffolding”- Package.swift with correct targets and resources
- version.env with app metadata
- Info.plist template or generation script
- Basic app entry point (@main App)
-
swift buildsucceeds -
swift testpasses - Resources copied correctly
Packaging
Section titled “Packaging”- .app bundle structure correct
- Info.plist generated with correct values
- App launches from Finder
Release
Section titled “Release”- Code signed with Developer ID
- Notarized and stapled
- Verified with
codesign -dv,spctl -a -vvv -t exec, andstapler validate - Zip created for distribution
- (Optional) Sparkle appcast updated