#!/bin/bash set -euo pipefail # DBSyn direct distribution: Developer ID signed, notarized app and DMG. # macOS/Xcode required. Install as scripts/release-dbsyn-direct.sh. APP_NAME=DBSyn SCHEME=DBSyn PROJECT=DBSyn.xcodeproj TEAM_ID="${TEAM_ID:-39R6U3437D}" SIGNING_IDENTITY="${SIGNING_IDENTITY:-B99D353C88996866C49E4389B375645F76870A3F}" NOTARY_PROFILE="${NOTARY_PROFILE:-orion-notary}" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" if [[ -d "$SCRIPT_DIR/$PROJECT" ]]; then ROOT_DIR="$SCRIPT_DIR" elif [[ -d "$SCRIPT_DIR/../$PROJECT" ]]; then ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" else echo "ERROR: Cannot locate $PROJECT" >&2; exit 1; fi BUILD_DIR="$ROOT_DIR/build/direct-distribution" ARCHIVE_PATH="$BUILD_DIR/$APP_NAME.xcarchive" EXPORT_DIR="$BUILD_DIR/export" EXPORT_OPTIONS="$BUILD_DIR/ExportOptions.plist" APP_PATH="$EXPORT_DIR/$APP_NAME.app" APP_ZIP="$BUILD_DIR/$APP_NAME-notarization.zip" DMG_STAGE="$BUILD_DIR/dmg-stage" RW_DMG="$BUILD_DIR/$APP_NAME-rw.dmg" TMP_DMG="$BUILD_DIR/$APP_NAME.dmg" RELEASE_DIR="$ROOT_DIR/releases" DMG_PATH="$RELEASE_DIR/$APP_NAME.dmg" MOUNT_POINT="$BUILD_DIR/dmg-mount" for tool in xcodebuild xcrun codesign spctl ditto hdiutil shasum security swift sips /usr/libexec/PlistBuddy; do command -v "$tool" >/dev/null || { echo "ERROR: Missing $tool" >&2; exit 1; } done if ! security find-identity -v -p codesigning | grep -F "$SIGNING_IDENTITY" >/dev/null; then echo "ERROR: Developer ID identity not found: $SIGNING_IDENTITY" >&2; exit 1 fi xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null || { echo "ERROR: Notarytool profile '$NOTARY_PROFILE' unavailable" >&2; exit 1; } cd "$ROOT_DIR" rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" "$EXPORT_DIR" "$DMG_STAGE" "$RELEASE_DIR" "$MOUNT_POINT" cat > "$EXPORT_OPTIONS" < methoddeveloper-id signingStyleautomatic teamID$TEAM_ID PLIST echo '==> Archiving DBSyn with hardened runtime...' xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration Release \ -destination 'generic/platform=macOS' -archivePath "$ARCHIVE_PATH" \ DEVELOPMENT_TEAM="$TEAM_ID" CODE_SIGN_STYLE=Automatic \ ENABLE_HARDENED_RUNTIME=YES \ clean archive echo '==> Exporting Developer ID app...' xcodebuild -exportArchive -archivePath "$ARCHIVE_PATH" \ -exportPath "$EXPORT_DIR" -exportOptionsPlist "$EXPORT_OPTIONS" [[ -d "$APP_PATH" ]] || { echo "ERROR: Missing exported app" >&2; exit 1; } # Xcode's generated Info.plist can omit CFBundleIconFile even when the # build setting specifies it. Set it on the exported bundle BEFORE the # final Developer ID signature, so the signed app includes the icon key. ICON_PATH="$APP_PATH/Contents/Resources/DBSyn.icns" [[ -s "$ICON_PATH" ]] || { echo "ERROR: Missing bundled icon $ICON_PATH" >&2; exit 1; } PLIST_PATH="$APP_PATH/Contents/Info.plist" ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" 2>/dev/null || true)" if [[ "$ICON_KEY" != DBSyn && "$ICON_KEY" != DBSyn.icns ]]; then echo "==> Setting CFBundleIconFile in exported app (before signing)..." if /usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH" >/dev/null 2>&1; then /usr/libexec/PlistBuddy -c 'Set :CFBundleIconFile DBSyn.icns' "$PLIST_PATH" else /usr/libexec/PlistBuddy -c 'Add :CFBundleIconFile string DBSyn.icns' "$PLIST_PATH" fi fi ICON_KEY="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIconFile' "$PLIST_PATH")" [[ "$ICON_KEY" == DBSyn || "$ICON_KEY" == DBSyn.icns ]] || { echo "ERROR: CFBundleIconFile still incorrect: '$ICON_KEY'" >&2; exit 1; } echo "==> Application icon verified: $ICON_KEY" sips -g pixelWidth -g pixelHeight "$ICON_PATH" # Export may omit hardened runtime; sign exported app explicitly. # Sign embedded code first if your project adds frameworks/helpers in future. echo '==> Developer ID signing with hardened runtime...' codesign --force --deep --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$APP_PATH" codesign --verify --deep --strict --verbose=2 "$APP_PATH" SIGNATURE_INFO="$(codesign -dv --verbose=4 "$APP_PATH" 2>&1 || true)" printf '%s\n' "$SIGNATURE_INFO" | grep -E 'Identifier=|Authority=|TeamIdentifier=|flags=' || true [[ "$SIGNATURE_INFO" == *"TeamIdentifier=$TEAM_ID"* ]] || { echo 'ERROR: Wrong signing team' >&2; exit 1; } [[ "$SIGNATURE_INFO" == *'(runtime)'* ]] || { echo 'ERROR: Hardened runtime missing' >&2; exit 1; } echo '==> Notarizing and stapling app...' ditto -c -k --keepParent "$APP_PATH" "$APP_ZIP" xcrun notarytool submit "$APP_ZIP" --keychain-profile "$NOTARY_PROFILE" --wait xcrun stapler staple "$APP_PATH" xcrun stapler validate "$APP_PATH" spctl --assess --type execute --verbose=4 "$APP_PATH" echo '==> Preparing writable DMG...' ditto "$APP_PATH" "$DMG_STAGE/$APP_NAME.app" ln -s /Applications "$DMG_STAGE/Applications" # Create writable image first: the custom volume icon must be set ON the # mounted filesystem, not on the staging directory. hdiutil create -size 100m -fs HFS+ -volname "$APP_NAME" -format UDRW -ov "$RW_DMG" MOUNTED=0 cleanup() { if [[ "$MOUNTED" == 1 ]]; then hdiutil detach "$MOUNT_POINT" -quiet || true; fi } trap cleanup EXIT hdiutil attach "$RW_DMG" -nobrowse -noverify -mountpoint "$MOUNT_POINT" -quiet MOUNTED=1 ditto "$DMG_STAGE/$APP_NAME.app" "$MOUNT_POINT/$APP_NAME.app" ln -s /Applications "$MOUNT_POINT/Applications" cp "$ICON_PATH" "$MOUNT_POINT/.VolumeIcon.icns" xcrun SetFile -a C "$MOUNT_POINT" hdiutil detach "$MOUNT_POINT" -quiet MOUNTED=0 hdiutil convert "$RW_DMG" -format UDZO -o "$TMP_DMG" -ov # A DMG *file* icon is different from its mounted volume icon. Use AppKit to # assign a Finder custom icon to the DMG file before signing/notarization. # This creates Finder metadata (custom icon resource/finder flag). ICON_SWIFT="$BUILD_DIR/set-dmg-file-icon.swift" cat > "$ICON_SWIFT" <<'SWIFT' import AppKit import Foundation let arguments = CommandLine.arguments precondition(arguments.count == 3, "Usage: icon.swift ") guard let icon = NSImage(contentsOfFile: arguments[1]) else { fputs("ERROR: Cannot load ICNS image\n", stderr); exit(1) } guard NSWorkspace.shared.setIcon(icon, forFile: arguments[2], options: []) else { fputs("ERROR: NSWorkspace could not set DMG Finder icon\n", stderr); exit(1) } print("Custom Finder icon assigned to DMG file") SWIFT echo '==> Assigning Finder icon to DMG file...' swift "$ICON_SWIFT" "$ICON_PATH" "$TMP_DMG" echo '==> Signing and notarizing DMG...' codesign --force --timestamp --sign "$SIGNING_IDENTITY" "$TMP_DMG" codesign --verify --verbose=2 "$TMP_DMG" xcrun notarytool submit "$TMP_DMG" --keychain-profile "$NOTARY_PROFILE" --wait xcrun stapler staple "$TMP_DMG" xcrun stapler validate "$TMP_DMG" spctl --assess --type open --context context:primary-signature --verbose=4 "$TMP_DMG" # Publish using ditto so Finder metadata/resource forks survive the copy. ditto "$TMP_DMG" "$DMG_PATH" echo '==> Release ready:' ls -lh "$DMG_PATH" shasum -a 256 "$DMG_PATH" printf '\n========== DBSyn RELEASE COMPLETE ==========\n%s\n' "$DMG_PATH"