#!/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"
|