#!/usr/bin/env bash # # build-local-macos-universal.sh - Build a locally signed universal macOS DMG. # # The script reuses the existing per-architecture local build script, then # combines the app binary and bundled dylibs with lipo before re-signing. # set -euo pipefail APP_NAME="PrivateVoice Dictation" DEFAULT_LOCAL_IDENTITY="PrivateVoice Local Code Signing" LEGACY_LOCAL_IDENTITY="VoiceSnap Local Code Signing" LOCAL_IDENTITY="${PRIVATEVOICE_LOCAL_SIGN_IDENTITY:-${VOICESNAP_LOCAL_SIGN_IDENTITY:-}}" KEYCHAIN="${PRIVATEVOICE_LOCAL_KEYCHAIN:-${VOICESNAP_LOCAL_KEYCHAIN:-$HOME/Library/Keychains/login.keychain-db}}" LOCAL_BUILD_FLAVOR="${PRIVATEVOICE_LOCAL_BUILD_FLAVOR:-local}" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" INFO_PLIST="$PROJECT_DIR/build/darwin/Info.plist" ENTITLEMENTS="$PROJECT_DIR/build/darwin/entitlements.plist" VERSION="$(defaults read "$INFO_PLIST" CFBundleShortVersionString 2>/dev/null || echo "2.1.1")" BUILD_ID="$(defaults read "$INFO_PLIST" CFBundleVersion 2>/dev/null || date +%Y%m%d.%H%M)" ARM_BUILD_DIR="$PROJECT_DIR/build/local/arm64" X86_BUILD_DIR="$PROJECT_DIR/build/local/x86_64" BUILD_DIR="$PROJECT_DIR/build/local/universal" ARM_APP="$ARM_BUILD_DIR/$APP_NAME.app" X86_APP="$X86_BUILD_DIR/$APP_NAME.app" APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-universal-$LOCAL_BUILD_FLAVOR.dmg" COMPAT_DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-universal-$LOCAL_BUILD_FLAVOR.dmg" step() { echo "" echo "==> $1" } fail() { echo "ERROR: $1" >&2 exit 1 } resolve_local_identity() { if [[ -z "$LOCAL_IDENTITY" ]]; then if security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$DEFAULT_LOCAL_IDENTITY"; then LOCAL_IDENTITY="$DEFAULT_LOCAL_IDENTITY" elif security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$LEGACY_LOCAL_IDENTITY"; then LOCAL_IDENTITY="$LEGACY_LOCAL_IDENTITY" else fail "Local signing identity not found; run build-local-macos.sh first" fi fi if ! security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$LOCAL_IDENTITY"; then fail "Local signing identity not available to codesign: $LOCAL_IDENTITY" fi } sign_item() { local item="$1" codesign --force \ --options runtime \ --timestamp=none \ --entitlements "$ENTITLEMENTS" \ --sign "$LOCAL_IDENTITY" \ "$item" } step "Building arm64 app" "$SCRIPT_DIR/build-local-macos.sh" arm64 step "Building x86_64 app" "$SCRIPT_DIR/build-local-macos.sh" x86_64 step "Preflight checks" [[ -d "$ARM_APP" ]] || fail "arm64 app not found at $ARM_APP" [[ -d "$X86_APP" ]] || fail "x86_64 app not found at $X86_APP" [[ -f "$ENTITLEMENTS" ]] || fail "entitlements.plist not found at $ENTITLEMENTS" resolve_local_identity step "Assembling universal app" rm -rf "$BUILD_DIR" mkdir -p "$BUILD_DIR" ditto "$ARM_APP" "$APP_BUNDLE" lipo -create \ "$ARM_APP/Contents/MacOS/$APP_NAME" \ "$X86_APP/Contents/MacOS/$APP_NAME" \ -output "$APP_BUNDLE/Contents/MacOS/$APP_NAME.universal" mv "$APP_BUNDLE/Contents/MacOS/$APP_NAME.universal" "$APP_BUNDLE/Contents/MacOS/$APP_NAME" for arm_dylib in "$ARM_APP"/Contents/Frameworks/*.dylib; do dylib_name="$(basename "$arm_dylib")" x86_dylib="$X86_APP/Contents/Frameworks/$dylib_name" out_dylib="$APP_BUNDLE/Contents/Frameworks/$dylib_name" out_tmp="$out_dylib.universal" [[ -f "$x86_dylib" ]] || fail "Matching x86_64 dylib not found: $x86_dylib" lipo -create "$arm_dylib" "$x86_dylib" -output "$out_tmp" mv "$out_tmp" "$out_dylib" done step "Code signing universal app" for dylib in "$APP_BUNDLE"/Contents/Frameworks/*.dylib; do sign_item "$dylib" done sign_item "$APP_BUNDLE/Contents/MacOS/$APP_NAME" sign_item "$APP_BUNDLE" step "Verifying universal app" codesign -vvv --deep --strict "$APP_BUNDLE" lipo -archs "$APP_BUNDLE/Contents/MacOS/$APP_NAME" step "Creating universal DMG" DMG_TEMP="$BUILD_DIR/dmg-staging" rm -rf "$DMG_TEMP" "$DMG_PATH" "$COMPAT_DMG_PATH" mkdir -p "$DMG_TEMP" cp -R "$APP_BUNDLE" "$DMG_TEMP/" ln -s /Applications "$DMG_TEMP/Applications" hdiutil create -volname "$APP_NAME" \ -srcfolder "$DMG_TEMP" \ -ov -format UDZO \ "$DMG_PATH" rm -rf "$DMG_TEMP" step "Signing universal DMG" codesign --force --timestamp=none --sign "$LOCAL_IDENTITY" "$DMG_PATH" hdiutil verify "$DMG_PATH" cp "$DMG_PATH" "$COMPAT_DMG_PATH" step "Universal local build complete" echo " App: $APP_BUNDLE" echo " DMG: $DMG_PATH" echo " Compatibility DMG: $COMPAT_DMG_PATH" echo " Signing identity: $LOCAL_IDENTITY"