#!/usr/bin/env bash # # Code sign a macOS .app bundle for distribution # Usage: codesign_mac_app.sh [entitlements_path] # # Environment variables: # SIGNING_IDENTITY - The signing identity (default: "Developer ID Application") # KEYCHAIN_PASSWORD - Password to unlock the build keychain (optional) set -euxo pipefail APP_PATH="$1" ENTITLEMENTS_PATH="${2:-}" SIGNING_IDENTITY="${SIGNING_IDENTITY:-Developer ID Application}" if [ ! -d "$APP_PATH" ]; then echo "ERROR: App not found at $APP_PATH" exit 1 fi # Unlock keychain if password provided if [ -n "${KEYCHAIN_PASSWORD:-}" ]; then security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain || true fi echo "=== Signing nested components first ===" # Sign all dylibs find "$APP_PATH" -name "*.dylib" -print0 | while IFS= read -r -d '' item; do echo "Signing dylib: $item" codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" done # Sign all bundles (plugins) find "$APP_PATH" -name "*.bundle" -print0 | while IFS= read -r -d '' item; do echo "Signing bundle: $item" codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" done # Sign XPC services (but skip ones inside Sparkle.framework - they're already signed) find "$APP_PATH" -name "*.xpc" -print0 | while IFS= read -r -d '' item; do if [[ "$item" == *"Sparkle.framework"* ]]; then echo "Skipping Sparkle XPC service (pre-signed): $item" continue fi echo "Signing XPC service: $item" codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" done # Sign nested apps (but skip ones inside Sparkle.framework - they're already signed) find "$APP_PATH" -path "*/Frameworks/*.app" -print0 | while IFS= read -r -d '' item; do if [[ "$item" == *"Sparkle.framework"* ]]; then echo "Skipping Sparkle nested app (pre-signed): $item" continue fi echo "Signing nested app: $item" codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" done # Sign standalone executables inside frameworks (but skip Sparkle.framework internals) find "$APP_PATH" -path "*/Frameworks/*/Versions/*/Autoupdate" -type f -print0 | while IFS= read -r -d '' item; do if [[ "$item" == *"Sparkle.framework"* ]]; then echo "Skipping Sparkle executable (pre-signed): $item" continue fi echo "Signing executable: $item" codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" done # Sign all frameworks (after their contents are signed) # Use --deep for Sparkle.framework to handle its XPC services find "$APP_PATH" -name "*.framework" -print0 | while IFS= read -r -d '' item; do if [[ "$item" == *"Sparkle.framework" ]]; then echo "Signing Sparkle framework with --deep: $item" codesign --deep --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" else echo "Signing framework: $item" codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$item" fi done echo "=== Signing main app bundle ===" if [ -n "$ENTITLEMENTS_PATH" ] && [ -f "$ENTITLEMENTS_PATH" ]; then echo "Using entitlements: $ENTITLEMENTS_PATH" codesign --force --verify --verbose --timestamp --options runtime \ --entitlements "$ENTITLEMENTS_PATH" \ --sign "$SIGNING_IDENTITY" "$APP_PATH" else codesign --force --verify --verbose --timestamp --options runtime \ --sign "$SIGNING_IDENTITY" "$APP_PATH" fi echo "=== Verifying signature ===" codesign --verify --verbose=4 "$APP_PATH" echo "=== Checking Gatekeeper assessment ===" spctl --assess --type exec -v "$APP_PATH" || echo "Note: Gatekeeper may reject until notarized" echo "Code signing complete: $APP_PATH"