| New file |
| | |
| | | #!/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" |
| | | |
| | | 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" |
| | | [[ -f "$x86_dylib" ]] || fail "Matching x86_64 dylib not found: $x86_dylib" |
| | | lipo -create "$arm_dylib" "$x86_dylib" -output "$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" |