#!/usr/bin/env bash # # build-appstore-macos.sh - Build a locally signed App Sandbox POC bundle. # # This is not the final Mac App Store upload pipeline. It intentionally uses a # local signing identity so we can verify sandbox behavior before wiring Apple # Distribution certificates and provisioning profiles. # # Usage: # cd VoiceSnapGo # ./scripts/build-appstore-macos.sh [arm64|x86_64] # # For local QA app refresh without creating a DMG: # PRIVATEVOICE_APPSTORE_LOCAL_APP_ONLY=1 ./scripts/build-appstore-macos.sh arm64 # # Optional candidate version override: # PRIVATEVOICE_APP_VERSION=2.2.3 PRIVATEVOICE_APP_BUILD=YYYYMMDD.HHMM # set -euo pipefail APP_NAME="PrivateVoice Dictation" LOCAL_IDENTITY="${PRIVATEVOICE_APPSTORE_LOCAL_SIGN_IDENTITY:-${PRIVATEVOICE_LOCAL_SIGN_IDENTITY:-}}" FALLBACK_LOCAL_IDENTITY="PrivateVoice Local Code Signing" KEYCHAIN="${PRIVATEVOICE_LOCAL_KEYCHAIN:-$HOME/Library/Keychains/login.keychain-db}" APPSTORE_MIN_SYSTEM_VERSION="${PRIVATEVOICE_APPSTORE_MIN_SYSTEM_VERSION:-13.4}" APP_ONLY="${PRIVATEVOICE_APPSTORE_LOCAL_APP_ONLY:-0}" 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-appstore.plist" PRIVACY_MANIFEST="$PROJECT_DIR/build/darwin/PrivacyInfo.xcprivacy" GOMODCACHE="$(go env GOMODCACHE)" SHERPA_MACOS_MODULE_VERSION="$(cd "$PROJECT_DIR" && go list -m -f '{{.Version}}' github.com/k2-fsa/sherpa-onnx-go-macos)" VERSION="${PRIVATEVOICE_APP_VERSION:-$(defaults read "$INFO_PLIST" CFBundleShortVersionString 2>/dev/null || echo "2.1.12")}" BUILD_ID="${PRIVATEVOICE_APP_BUILD:-$(defaults read "$INFO_PLIST" CFBundleVersion 2>/dev/null || date +%Y%m%d.%H%M)}" TARGET_ARCH="${1:-$(uname -m)}" step() { echo "" echo "==> $1" } fail() { echo "ERROR: $1" >&2 exit 1 } resolve_signing_identity() { if [[ -n "$LOCAL_IDENTITY" ]]; then return fi LOCAL_IDENTITY="$( security find-identity -v -p codesigning "$KEYCHAIN" | sed -n 's/.*"\(Apple Development:[^"]*\)".*/\1/p' | head -n 1 )" if [[ -z "$LOCAL_IDENTITY" ]]; then LOCAL_IDENTITY="$FALLBACK_LOCAL_IDENTITY" fi } case "$TARGET_ARCH" in arm64) GOARCH="arm64" SHERPA_ARCH="aarch64-apple-darwin" OUT_ARCH="arm64" ;; x86_64|amd64) GOARCH="amd64" SHERPA_ARCH="x86_64-apple-darwin" OUT_ARCH="x86_64" ;; *) fail "Unknown architecture: $TARGET_ARCH (use arm64 or x86_64)" ;; esac BUILD_DIR="$PROJECT_DIR/build/appstore-local/$OUT_ARCH" APP_BUNDLE="$BUILD_DIR/$APP_NAME.app" DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-$OUT_ARCH-appstore-sandbox-poc.dmg" SHERPA_LIB_DIR="$GOMODCACHE/github.com/k2-fsa/sherpa-onnx-go-macos@$SHERPA_MACOS_MODULE_VERSION/lib/$SHERPA_ARCH" ONNXRUNTIME_DYLIB="" ONNXRUNTIME_DYLIB_NAME="" sign_plain() { local item="$1" codesign --force \ --options runtime \ --timestamp=none \ --sign "$LOCAL_IDENTITY" \ "$item" } sign_sandboxed() { local item="$1" codesign --force \ --options runtime \ --timestamp=none \ --entitlements "$ENTITLEMENTS" \ --sign "$LOCAL_IDENTITY" \ "$item" } fix_binary_rpaths() { local binary="$1" # The sherpa-onnx macOS module injects its cache lib directory as an LC_RPATH # ahead of bundle Frameworks. In a sandboxed signed app, dyld may then load the # module-cache dylib with a different signing identity and crash on launch. install_name_tool -delete_rpath "$SHERPA_LIB_DIR" "$binary" 2>/dev/null || true install_name_tool -add_rpath @executable_path/../Frameworks "$binary" 2>/dev/null || true } step "Preflight checks" [[ -f "$INFO_PLIST" ]] || fail "Info.plist not found at $INFO_PLIST" [[ -f "$ENTITLEMENTS" ]] || fail "App Store entitlements not found at $ENTITLEMENTS" [[ -f "$PRIVACY_MANIFEST" ]] || fail "PrivacyInfo.xcprivacy not found at $PRIVACY_MANIFEST" [[ -f "$PROJECT_DIR/build/darwin/icon.icns" ]] || fail "icon.icns not found" [[ -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")" resolve_signing_identity security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$LOCAL_IDENTITY" || fail "Local signing identity not found: $LOCAL_IDENTITY" step "Cleaning old App Sandbox POC build" rm -rf "$APP_BUNDLE" "$DMG_PATH" mkdir -p "$BUILD_DIR" step "Building frontend" cd "$PROJECT_DIR/frontend" if [[ ! -d node_modules ]]; then npm ci --prefer-offline fi npm run build step "Building Go binary ($OUT_ARCH, appstore tag)" cd "$PROJECT_DIR" CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \ MACOSX_DEPLOYMENT_TARGET="$APPSTORE_MIN_SYSTEM_VERSION" \ CGO_CFLAGS="${CGO_CFLAGS:-} -mmacosx-version-min=$APPSTORE_MIN_SYSTEM_VERSION" \ CGO_LDFLAGS="${CGO_LDFLAGS:-} -mmacosx-version-min=$APPSTORE_MIN_SYSTEM_VERSION" \ go build -tags appstore -buildvcs=false -gcflags=all="-l" \ -ldflags="-s -w -X main.appVersion=$VERSION -X main.appBuild=$BUILD_ID" \ -o "$BUILD_DIR/$APP_NAME" step "Assembling $APP_NAME.app" 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" /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" \ "$APP_BUNDLE/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $BUILD_ID" \ "$APP_BUNDLE/Contents/Info.plist" /usr/libexec/PlistBuddy -c "Set :LSMinimumSystemVersion $APPSTORE_MIN_SYSTEM_VERSION" \ "$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" cp "$PRIVACY_MANIFEST" "$APP_BUNDLE/Contents/Resources/PrivacyInfo.xcprivacy" find "$PROJECT_DIR/build/darwin" -maxdepth 1 -type d -name "*.lproj" \ -exec cp -R {} "$APP_BUNDLE/Contents/Resources/" \; cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib" "$APP_BUNDLE/Contents/Frameworks/" cp "$ONNXRUNTIME_DYLIB" "$APP_BUNDLE/Contents/Frameworks/" step "Fixing rpaths" install_name_tool -add_rpath @executable_path/../Frameworks \ "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 2>/dev/null || true fix_binary_rpaths "$APP_BUNDLE/Contents/MacOS/$APP_NAME" 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" step "Code signing App Sandbox POC" sign_plain "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME" sign_plain "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib" sign_sandboxed "$APP_BUNDLE/Contents/MacOS/$APP_NAME" sign_sandboxed "$APP_BUNDLE" step "Verifying signature and entitlements" codesign -vvv --deep --strict "$APP_BUNDLE" codesign -d --entitlements :- "$APP_BUNDLE" step "Creating POC DMG" if [[ "$APP_ONLY" == "1" ]]; then echo " Skipped because PRIVATEVOICE_APPSTORE_LOCAL_APP_ONLY=1" else DMG_TEMP="$BUILD_DIR/dmg-staging" rm -rf "$DMG_TEMP" mkdir -p "$DMG_TEMP" cp -R "$APP_BUNDLE" "$DMG_TEMP/" ln -s /Applications "$DMG_TEMP/Applications" hdiutil create -volname "$APP_NAME Sandbox POC" \ -srcfolder "$DMG_TEMP" \ -ov -format UDZO \ "$DMG_PATH" rm -rf "$DMG_TEMP" fi step "App Sandbox POC build complete" echo " App: $APP_BUNDLE" if [[ "$APP_ONLY" == "1" ]]; then echo " DMG: skipped" else echo " DMG: $DMG_PATH" fi echo " Signing identity: $LOCAL_IDENTITY"