# How to Build macOS Apps with Swift Package Manager and Code-Signing Workflows

> Build sign and notarize macOS apps using SPM and shell scripts eliminating Xcode projects. Streamline your development with automated universal binary creation and code signing workflows.

- Repository: [Thomas Ricouard/Skills](https://github.com/Dimillian/Skills)
- Tags: how-to-guide
- Published: 2026-04-01

---

**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](https://github.com/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`](https://github.com/Dimillian/Skills/blob/main/references/scaffold.md), initialize the project structure with:

```bash
mkdir MyApp && cd MyApp
swift package init --type executable

```

Edit [`Package.swift`](https://github.com/Dimillian/Skills/blob/main/Package.swift) to specify macOS 14 as the minimum platform and declare an **executable target** with resource support:

```swift
// 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`](https://github.com/Dimillian/Skills/blob/main/Sources/MyApp/MyApp.swift):

```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:

```bash
cp assets/templates/version.env ./

```

Edit the file to define your marketing version and build number:

```text
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`](https://github.com/Dimillian/Skills/blob/main/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:

```bash
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:

```bash
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:

```bash
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`](https://github.com/Dimillian/Skills/blob/main/package_app.sh) script passes `--sign "-"` to `codesign` when you set:

```bash
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:

```bash
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:

```bash
export APP_IDENTITY='MyApp Development'

```

When `APP_IDENTITY` is set, [`package_app.sh`](https://github.com/Dimillian/Skills/blob/main/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:

```bash
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`](https://github.com/Dimillian/Skills/blob/main/assets/templates/sign-and-notarize.sh) script automates the complete release pipeline:

1. Builds a universal release binary using [`package_app.sh`](https://github.com/Dimillian/Skills/blob/main/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:

```bash
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:

```bash
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`](https://github.com/Dimillian/Skills/blob/main/package_app.sh) and [`sign-and-notarize.sh`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/package_app.sh) script.
- **Sign** locally with ad-hoc signing (`SIGNING_MODE=adhoc`), create persistent dev identities with [`setup_dev_signing.sh`](https://github.com/Dimillian/Skills/blob/main/setup_dev_signing.sh), or use Developer ID certificates for release.
- **Notarize** automatically using [`sign-and-notarize.sh`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/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`](https://github.com/Dimillian/Skills/blob/main/sign-and-notarize.sh) script sets these flags automatically when a proper identity is detected.