How to Build macOS Apps with Swift Package Manager and Code-Signing Workflows
You can build, sign, and notarize macOS applications entirely with Swift Package Manager (SPM) without creating an Xcode project by using shell scripts that handle universal binary creation, .app bundle packaging, and automated code-signing workflows.
This guide demonstrates how to construct a complete macOS app distribution pipeline using only SPM and command-line tools, based on the reference implementation in the Dimillian/Skills repository. Whether you are shipping to the App Store or distributing directly to customers, you can automate the entire process—from scaffolding to notarized .zip archives—without ever opening Xcode.
Scaffold a Swift Package Manager macOS Project
Begin by creating a new executable package. According to references/scaffold.md, initialize the project structure with:
mkdir MyApp && cd MyApp
swift package init --type executable
Edit Package.swift to specify macOS 14 as the minimum platform and declare an executable target with resource support:
// swift-tools-version:6.2
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.macOS(.v14)],
targets: [
.executableTarget(
name: "MyApp",
path: "Sources/MyApp",
resources: [.process("Resources")]
)
]
)
Create a SwiftUI entry point in Sources/MyApp/MyApp.swift:
import SwiftUI
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
Text("Hello, macOS!")
}
}
}
Place assets like icons and data files under Sources/MyApp/Resources/.
Configure Versioning and Environment
Centralize version management using the provided template. Copy assets/templates/version.env to your repository root:
cp assets/templates/version.env ./
Edit the file to define your marketing version and build number:
MARKETING_VERSION=1.0.0
BUILD_NUMBER=1
The packaging scripts automatically source these variables to populate Info.plist entries and archive names.
Build Universal Binaries for Apple Silicon and Intel
To support both arm64 and x86_64 architectures, set the ARCHES environment variable before building. The assets/templates/package_app.sh script detects this variable and invokes the Swift compiler for each architecture, then uses lipo to merge the outputs via the install_binary function.
Run individual architecture builds:
export ARCHES="arm64 x86_64"
swift build -c release --arch arm64
swift build -c release --arch x86_64
When you run the packaging script, it automatically handles the fat binary creation.
Package the .app Bundle
Copy the helper scripts from the repository into your project:
mkdir -p Scripts
cp assets/templates/package_app.sh Scripts/
cp assets/templates/setup_dev_signing.sh Scripts/
chmod +x Scripts/*.sh
Execute the packaging script:
Scripts/package_app.sh release
This script performs several critical operations:
- Creates the bundle directory structure (
MyApp.app/Contents/MacOS/,Resources/, etc.) - Generates a dynamic
Info.plistthat pulls values fromversion.env, includingMARKETING_VERSION,BUILD_NUMBER, and optional flags likeMENU_BAR_APP - Copies the universal binary and Swift Package Manager resource bundles
- Strips extended attributes and prepares entitlements files
The final .app bundle appears in your repository root.
Implement Code-Signing Workflows
The repository supports three distinct signing modes controlled via SIGNING_MODE and APP_IDENTITY.
Ad-hoc Development Signing
For local testing without keychain prompts, use ad-hoc signing. The package_app.sh script passes --sign "-" to codesign when you set:
SIGNING_MODE=adhoc Scripts/package_app.sh release
This produces an ad-hoc signature sufficient for local testing, though Gatekeeper will require manual approval.
Stable Development Identity
To create a persistent development certificate, run the helper script:
Scripts/setup_dev_signing.sh
This creates a self-signed certificate named "MyApp Development" and imports it into your login keychain. Export the identity for subsequent builds:
export APP_IDENTITY='MyApp Development'
When APP_IDENTITY is set, package_app.sh automatically applies --timestamp --options runtime to enable hardened runtime.
Release and App Store Distribution
For production builds, obtain a Developer ID Application certificate from Apple. Configure the environment:
export APP_IDENTITY="Developer ID Application: Your Name (TEAMID)"
The hardened runtime is automatically enforced when using a valid Developer ID.
Automate Notarization and Stapling
The assets/templates/sign-and-notarize.sh script automates the complete release pipeline:
- Builds a universal release binary using
package_app.sh - Signs the
.appbundle with the specifiedAPP_IDENTITYand entitlements - Creates a distributable zip (
MyApp-1.0.0.zip) - Submits to Apple's Notary Service using
xcrun notarytool - Staples the notarization ticket onto the
.appbundle - Validates with
spctlandstapler
Configure your App Store Connect API credentials:
export APP_STORE_CONNECT_API_KEY_P8="-----BEGIN PRIVATE KEY-----..."
export APP_STORE_CONNECT_KEY_ID="ABC123XYZ"
export APP_STORE_CONNECT_ISSUER_ID="12345678-90ab-cdef-1234-567890abcdef"
export APP_IDENTITY="Developer ID Application: Your Name (TEAMID)"
Run the full workflow:
Scripts/sign-and-notarize.sh
Upon completion, you will see Done: MyApp-1.0.0.zip, containing a notarized application ready for distribution.
Troubleshooting Common Signing Failures
When working with package_app.sh and sign-and-notarize.sh, you may encounter specific errors:
- "The software asset has already been uploaded": Increment
BUILD_NUMBERinversion.envto create a unique bundle version. - "Invalid Code Signing Entitlements": Edit the generated
.entitlementsfile to remove unsupported keys before signing. - "The executable does not have the hardened runtime enabled": Ensure
APP_IDENTITYis exported andSIGNING_MODEis not set toadhoc; hardened runtime requires a valid Developer ID certificate. - Notarization hangs or fails: Verify credentials using
xcrun notarytool historyand check for expired App Store Connect API keys. - Stapler validation fails: Wait approximately 60 seconds after notarization completes for the ticket to propagate before validating.
The SKILL.md file contains additional validation checkpoints and detailed troubleshooting guidance.
Summary
- Scaffold macOS apps with
swift package init --type executableand configurePackage.swiftwith.executableTargetand macOS platform requirements. - Version your builds centrally using
version.envto ensure consistency acrossInfo.plistand archive names. - Build universal binaries by compiling for both
arm64andx86_64, then merge withlipovia thepackage_app.shscript. - Sign locally with ad-hoc signing (
SIGNING_MODE=adhoc), create persistent dev identities withsetup_dev_signing.sh, or use Developer ID certificates for release. - Notarize automatically using
sign-and-notarize.shwith App Store Connect API credentials, producing distribution-ready zip files.
Frequently Asked Questions
Do I need Xcode to build macOS apps with Swift Package Manager?
No. You can build complete macOS applications using only the Swift command-line tools. The references/scaffold.md file in the Dimillian/Skills repository provides step-by-step instructions for creating executable targets, configuring resources, and managing dependencies without an .xcodeproj file. You only need Xcode installed for the underlying SDKs and command-line toolchains.
How do I create a universal binary that runs on both Intel and Apple Silicon Macs?
Set the ARCHES environment variable to "arm64 x86_64" and run swift build for each architecture. The assets/templates/package_app.sh script automatically detects this variable, builds each slice, and uses lipo via the install_binary function to create a fat binary inside the .app bundle. This ensures your application runs natively on both Apple Silicon and Intel-based Macs.
What is the difference between ad-hoc signing and Developer ID signing?
Ad-hoc signing (--sign "-") creates a cryptographic seal without identity information, requiring users to right-click and open the app. Developer ID signing uses a certificate issued by Apple to identify your team, enables the hardened runtime (--options runtime), and satisfies Gatekeeper requirements for distribution outside the App Store. Use ad-hoc for local testing and Developer ID for customer releases.
How do I fix "hardened runtime" errors during notarization?
The hardened runtime is automatically enabled when you sign with a valid Developer ID and include the --options runtime flag. Ensure you have exported APP_IDENTITY with your Developer ID Application certificate and that SIGNING_MODE is not set to adhoc. The sign-and-notarize.sh script sets these flags automatically when a proper identity is detected.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →