Scaffold, build, and package SwiftPM-based macOS apps without Xcode project. Use when you need a from-scratch macOS app layout, SwiftPM targets/resources, custom .app bundle assembly, or signing/notarization steps outside Xcode.
Scanned 9/2/2026
Install to Claude Code
npx -y skills add patrickserrano/lacquer --skill release-macos-spm-packaging --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Release Macos Spm Packaging?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/patrickserrano-release-macos-spm-packaging)More formats (shields.io, HTML) on the badges page.
---
name: release-macos-spm-packaging
description: Scaffold, build, and package SwiftPM-based macOS apps without Xcode project. Use when you need a from-scratch macOS app layout, SwiftPM targets/resources, custom .app bundle assembly, or signing/notarization steps outside Xcode.
---
# macOS SwiftPM App Packaging
## 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
### 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.env
```
### Package.swift
```swift
// swift-tools-version: 5.9
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
```bash
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 apps
MENU_BAR_APP=0
```
## Build and Run
### Build with SwiftPM
```bash
# Debug build
swift build
# Release build
swift build -c release
# Run tests
swift test
```
### Package as .app Bundle
Create `Scripts/package_app.sh`:
```bash
#!/bin/bash
set -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 release
swift build -c release
# Create bundle structure
rm -rf "$APP_BUNDLE"
mkdir -p "$MACOS" "$RESOURCES"
# Copy binary
cp "$BUILD_DIR/$APP_NAME" "$MACOS/"
# Copy resources
cp -r Resources/* "$RESOURCES/" 2>/dev/null || true
# Generate Info.plist
cat > "$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
Create `Scripts/compile_and_run.sh`:
```bash
#!/bin/bash
set -e
source version.env
# Kill existing instance
pkill -x "$APP_NAME" 2>/dev/null || true
# Package
./Scripts/package_app.sh
# Launch
open ".build/release/$APP_NAME.app"
```
## Code Signing
### Development Signing
```bash
# Sign for local development
codesign --force --sign - ".build/release/MyApp.app"
# Or with a specific identity
codesign --force --sign "Developer ID Application: Your Name" ".build/release/MyApp.app"
```
### Create Stable Dev Identity
```bash
# Generate self-signed certificate for consistent dev signing
security create-keychain -p "" dev-signing.keychain
security default-keychain -s dev-signing.keychain
# Follow prompts in Keychain Access to create certificate
```
## 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:
```bash
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`:
```bash
#!/bin/bash
set -e
source version.env
APP_PATH=".build/release/$APP_NAME.app"
ZIP_PATH=".build/release/$APP_NAME-$VERSION.zip"
# Sign with Developer ID
codesign --force --options runtime --sign "Developer ID Application: Your Name" "$APP_PATH"
# Create zip for notarization
ditto -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 ticket
xcrun stapler staple "$APP_PATH"
# Re-zip with stapled ticket
rm "$ZIP_PATH"
ditto -c -k --keepParent "$APP_PATH" "$ZIP_PATH"
echo "Release ready: $ZIP_PATH"
```
### Verify the release
Run all three before shipping — each catches a different failure mode (wrong
signing identity, Gatekeeper rejection, missing/unstapled ticket):
```bash
codesign -dv --verbose=4 "$APP_PATH" # confirms who signed it and with what identity
spctl -a -vvv -t exec "$APP_PATH" # confirms Gatekeeper will actually accept it
xcrun stapler validate "$APP_PATH" # confirms the notarization ticket is attached
```
## Sparkle Updates (Optional)
### Generate Appcast Entry
```bash
#!/bin/bash
source 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>
EOF
```
## GitHub Release
```bash
# Create tag
git tag -a "v$VERSION" -m "Release $VERSION"
git push origin "v$VERSION"
# Create GitHub release
gh release create "v$VERSION" \
".build/release/$APP_NAME-$VERSION.zip" \
--title "v$VERSION" \
--notes "Release notes here"
```
## Checklist
### 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)
### Build
- [ ] `swift build` succeeds
- [ ] `swift test` passes
- [ ] Resources copied correctly
### Packaging
- [ ] .app bundle structure correct
- [ ] Info.plist generated with correct values
- [ ] App launches from Finder
### Release
- [ ] Code signed with Developer ID
- [ ] Notarized and stapled
- [ ] Verified with `codesign -dv`, `spctl -a -vvv -t exec`, and `stapler validate`
- [ ] Zip created for distribution
- [ ] (Optional) Sparkle appcast updated
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!