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.plist that pulls values from version.env, including MARKETING_VERSION, BUILD_NUMBER, and optional flags like MENU_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:

  1. Builds a universal release binary using package_app.sh
  2. Signs the .app bundle with the specified APP_IDENTITY and entitlements
  3. Creates a distributable zip (MyApp-1.0.0.zip)
  4. Submits to Apple's Notary Service using xcrun notarytool
  5. Staples the notarization ticket onto the .app bundle
  6. Validates with spctl and stapler

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_NUMBER in version.env to create a unique bundle version.
  • "Invalid Code Signing Entitlements": Edit the generated .entitlements file to remove unsupported keys before signing.
  • "The executable does not have the hardened runtime enabled": Ensure APP_IDENTITY is exported and SIGNING_MODE is not set to adhoc; hardened runtime requires a valid Developer ID certificate.
  • Notarization hangs or fails: Verify credentials using xcrun notarytool history and 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 executable and configure Package.swift with .executableTarget and macOS platform requirements.
  • Version your builds centrally using version.env to ensure consistency across Info.plist and archive names.
  • Build universal binaries by compiling for both arm64 and x86_64, then merge with lipo via the package_app.sh script.
  • Sign locally with ad-hoc signing (SIGNING_MODE=adhoc), create persistent dev identities with setup_dev_signing.sh, or use Developer ID certificates for release.
  • Notarize automatically using sign-and-notarize.sh with 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →