#!/usr/bin/env bash
|
#
|
# build-release-macos.sh — Build a signed & notarized macOS DMG for PrivateVoice Dictation
|
#
|
# Prerequisites (one-time):
|
# 1. Install Developer ID certificate in Keychain
|
# 2. Store notarization credentials:
|
# xcrun notarytool store-credentials "privatevoice" \
|
# --apple-id "your@apple.id" --team-id "3HY4G7M2AL" \
|
# --password "app-specific-password"
|
#
|
# Usage:
|
# cd VoiceSnapGo && ./scripts/build-release-macos.sh [arm64|x86_64|all]
|
# Default: build for current machine architecture
|
#
|
set -euo pipefail
|
|
# ── Configuration ─────────────────────────────────────────────────────────────
|
IDENTITY="Developer ID Application: Ningbo Zhuozhi Innovation Network Technology Co., Ltd. (3HY4G7M2AL)"
|
KEYCHAIN_PROFILE="privatevoice"
|
|
APP_NAME="PrivateVoice Dictation"
|
BUNDLE_ID="com.privatevoice.input"
|
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
VERSION=$(defaults read "$PROJECT_DIR/build/darwin/Info.plist" CFBundleShortVersionString 2>/dev/null || echo "2.1.0")
|
|
ENTITLEMENTS="$PROJECT_DIR/build/darwin/entitlements.plist"
|
INFO_PLIST="$PROJECT_DIR/build/darwin/Info.plist"
|
GOMODCACHE=$(go env GOMODCACHE)
|
SHERPA_MACOS_MODULE_VERSION="$(cd "$PROJECT_DIR" && go list -m -f '{{.Version}}' github.com/k2-fsa/sherpa-onnx-go-macos)"
|
ONNXRUNTIME_DYLIB=""
|
ONNXRUNTIME_DYLIB_NAME=""
|
|
# ── Architecture mapping ────────────────────────────────────────────────────
|
resolve_arch() {
|
local arch="$1"
|
case "$arch" in
|
arm64)
|
GOARCH="arm64"
|
SHERPA_ARCH="aarch64-apple-darwin"
|
DMG_ARCH="arm64"
|
;;
|
x86_64|amd64)
|
GOARCH="amd64"
|
SHERPA_ARCH="x86_64-apple-darwin"
|
DMG_ARCH="x86_64"
|
;;
|
*)
|
fail "Unknown architecture: $arch (use arm64 or x86_64)"
|
;;
|
esac
|
SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH"
|
DMG_NAME="${APP_NAME}-v${VERSION}-${DMG_ARCH}.dmg"
|
BUILD_DIR="$PROJECT_DIR/build/release/${DMG_ARCH}"
|
APP_BUNDLE="$BUILD_DIR/${APP_NAME}.app"
|
}
|
|
# ── Parse argument ──────────────────────────────────────────────────────────
|
TARGET_ARCH="${1:-$(uname -m)}"
|
|
if [[ "$TARGET_ARCH" == "all" ]]; then
|
echo "==> Building for ALL architectures"
|
"$0" arm64
|
"$0" x86_64
|
echo ""
|
echo "==> All builds complete!"
|
ls -lh "$PROJECT_DIR/build/release/"*/*.dmg
|
exit 0
|
fi
|
|
resolve_arch "$TARGET_ARCH"
|
echo "==> Target: $DMG_ARCH (GOARCH=$GOARCH)"
|
|
# ── Helpers ───────────────────────────────────────────────────────────────────
|
step() { echo ""; echo "==> $1"; }
|
fail() { echo "ERROR: $1" >&2; exit 1; }
|
|
# ── Preflight checks ─────────────────────────────────────────────────────────
|
step "Preflight checks"
|
[[ -d "$SHERPA_LIB_DIR" ]] || fail "sherpa-onnx dylibs not found at $SHERPA_LIB_DIR"
|
ONNXRUNTIME_DYLIB="$(find "$SHERPA_LIB_DIR" -maxdepth 1 -type f -name 'libonnxruntime.*.dylib' | sort | tail -n 1)"
|
[[ -f "$ONNXRUNTIME_DYLIB" ]] || fail "onnxruntime dylib not found in $SHERPA_LIB_DIR"
|
ONNXRUNTIME_DYLIB_NAME="$(basename "$ONNXRUNTIME_DYLIB")"
|
[[ -f "$ENTITLEMENTS" ]] || fail "entitlements.plist not found at $ENTITLEMENTS"
|
[[ -f "$INFO_PLIST" ]] || fail "Info.plist not found at $INFO_PLIST"
|
security find-identity -v -p codesigning | grep -q "$IDENTITY" || fail "Signing identity not found in keychain"
|
|
# ── Step 1: Clean ─────────────────────────────────────────────────────────────
|
step "Cleaning old build artifacts"
|
rm -rf "$BUILD_DIR"
|
mkdir -p "$BUILD_DIR"
|
|
# ── Step 2: Build frontend ────────────────────────────────────────────────────
|
step "Building frontend"
|
cd "$PROJECT_DIR/frontend"
|
npm install --prefer-offline
|
npm run build
|
cd "$PROJECT_DIR"
|
|
# ── Step 3: Build Go binary ──────────────────────────────────────────────────
|
step "Building Go binary ($DMG_ARCH, GOARCH=$GOARCH)"
|
CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \
|
go build -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \
|
-o "$BUILD_DIR/${APP_NAME}"
|
|
# ── Step 4: Assemble .app bundle ─────────────────────────────────────────────
|
step "Assembling ${APP_NAME}.app bundle"
|
mkdir -p "$APP_BUNDLE/Contents/MacOS"
|
mkdir -p "$APP_BUNDLE/Contents/Frameworks"
|
mkdir -p "$APP_BUNDLE/Contents/Resources"
|
|
cp "$INFO_PLIST" "$APP_BUNDLE/Contents/Info.plist"
|
cp "$BUILD_DIR/${APP_NAME}" "$APP_BUNDLE/Contents/MacOS/${APP_NAME}"
|
cp "$PROJECT_DIR/build/darwin/icon.icns" \
|
"$APP_BUNDLE/Contents/Resources/icon.icns"
|
|
# Copy dylibs
|
cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib" "$APP_BUNDLE/Contents/Frameworks/"
|
cp "$ONNXRUNTIME_DYLIB" "$APP_BUNDLE/Contents/Frameworks/"
|
|
# ── Step 5: Fix rpaths ───────────────────────────────────────────────────────
|
step "Fixing rpaths with install_name_tool"
|
|
# Add @executable_path/../Frameworks rpath to binary
|
install_name_tool -add_rpath @executable_path/../Frameworks \
|
"$APP_BUNDLE/Contents/MacOS/${APP_NAME}" 2>/dev/null || true
|
|
# Fix dylib install names
|
install_name_tool -id @rpath/libsherpa-onnx-c-api.dylib \
|
"$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
|
|
install_name_tool -id "@rpath/$ONNXRUNTIME_DYLIB_NAME" \
|
"$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
|
|
# Fix libsherpa's dependency on libonnxruntime
|
install_name_tool -change \
|
@rpath/libonnxruntime.1.23.2.dylib \
|
"@rpath/$ONNXRUNTIME_DYLIB_NAME" \
|
"$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib" 2>/dev/null || true
|
install_name_tool -change \
|
@rpath/libonnxruntime.1.24.4.dylib \
|
"@rpath/$ONNXRUNTIME_DYLIB_NAME" \
|
"$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib" 2>/dev/null || true
|
|
# ── Step 6: Code sign ────────────────────────────────────────────────────────
|
step "Code signing"
|
|
# Sign dylibs first (inside-out signing)
|
codesign --force --options runtime \
|
--entitlements "$ENTITLEMENTS" \
|
--sign "$IDENTITY" \
|
"$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
|
|
codesign --force --options runtime \
|
--entitlements "$ENTITLEMENTS" \
|
--sign "$IDENTITY" \
|
"$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
|
|
# Sign the main binary
|
codesign --force --options runtime \
|
--entitlements "$ENTITLEMENTS" \
|
--sign "$IDENTITY" \
|
"$APP_BUNDLE/Contents/MacOS/${APP_NAME}"
|
|
# Sign the whole .app bundle
|
codesign --force --options runtime \
|
--entitlements "$ENTITLEMENTS" \
|
--sign "$IDENTITY" \
|
"$APP_BUNDLE"
|
|
# Verify signature
|
step "Verifying code signature"
|
codesign -vvv --deep --strict "$APP_BUNDLE"
|
|
# ── Step 7: Create DMG ───────────────────────────────────────────────────────
|
step "Creating DMG"
|
|
DMG_PATH="$BUILD_DIR/$DMG_NAME"
|
DMG_TEMP="$BUILD_DIR/dmg-staging"
|
|
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 8: Sign DMG ─────────────────────────────────────────────────────────
|
step "Signing DMG"
|
codesign --force --sign "$IDENTITY" "$DMG_PATH"
|
|
# ── Step 9: Notarize ─────────────────────────────────────────────────────────
|
step "Submitting for notarization (this may take a few minutes)..."
|
xcrun notarytool submit "$DMG_PATH" \
|
--keychain-profile "$KEYCHAIN_PROFILE" \
|
--wait
|
|
# ── Step 10: Staple ──────────────────────────────────────────────────────────
|
step "Stapling notarization ticket"
|
xcrun stapler staple "$DMG_PATH"
|
|
# ── Done ──────────────────────────────────────────────────────────────────────
|
step "Build complete! ($DMG_ARCH)"
|
echo ""
|
echo " Arch: $DMG_ARCH"
|
echo " DMG: $DMG_PATH"
|
echo " Size: $(du -h "$DMG_PATH" | cut -f1)"
|
echo ""
|
echo "Verification commands:"
|
echo " codesign -vvv --deep --strict \"$APP_BUNDLE\""
|
echo " spctl -a -vvv \"$APP_BUNDLE\""
|
echo " xcrun stapler validate \"$DMG_PATH\""
|