#!/usr/bin/env bash
|
#
|
# build-local-macos.sh - Build a locally signed macOS app and DMG for PrivateVoice Dictation.
|
#
|
# This script intentionally does not notarize. It creates or reuses a stable
|
# local Code Signing identity so macOS TCC permissions, such as Accessibility,
|
# do not change on every local rebuild the way they do with ad-hoc signing.
|
#
|
# Usage:
|
# cd VoiceSnapGo
|
# ./scripts/build-local-macos.sh [arm64|x86_64] [--install]
|
#
|
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}}"
|
|
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"
|
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="$(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)"
|
|
TARGET_ARCH=""
|
INSTALL_AFTER_BUILD=0
|
|
step() {
|
echo ""
|
echo "==> $1"
|
}
|
|
fail() {
|
echo "ERROR: $1" >&2
|
exit 1
|
}
|
|
for arg in "$@"; do
|
case "$arg" in
|
arm64|x86_64|amd64)
|
TARGET_ARCH="$arg"
|
;;
|
--install)
|
INSTALL_AFTER_BUILD=1
|
;;
|
*)
|
fail "Unknown argument: $arg"
|
;;
|
esac
|
done
|
|
TARGET_ARCH="${TARGET_ARCH:-$(uname -m)}"
|
|
case "$TARGET_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: $TARGET_ARCH (use arm64 or x86_64)"
|
;;
|
esac
|
|
BUILD_DIR="$PROJECT_DIR/build/local/$DMG_ARCH"
|
APP_BUNDLE="$BUILD_DIR/$APP_NAME.app"
|
DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-build$BUILD_ID-$DMG_ARCH-local.dmg"
|
COMPAT_DMG_PATH="$BUILD_DIR/$APP_NAME-$VERSION-$DMG_ARCH-local.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=""
|
|
ensure_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
|
LOCAL_IDENTITY="$DEFAULT_LOCAL_IDENTITY"
|
fi
|
fi
|
|
if security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$LOCAL_IDENTITY"; then
|
echo "Using existing local signing identity: $LOCAL_IDENTITY"
|
return
|
fi
|
|
step "Creating local signing identity: $LOCAL_IDENTITY"
|
local tmpdir
|
tmpdir="$(mktemp -d)"
|
trap 'rm -rf "${tmpdir:-}"' RETURN
|
|
local conf="$tmpdir/codesign.cnf"
|
local key="$tmpdir/codesign.key"
|
local csr="$tmpdir/codesign.csr"
|
local cert="$tmpdir/codesign.cer"
|
local p12="$tmpdir/codesign.p12"
|
local pass
|
pass="$(uuidgen)"
|
|
cat > "$conf" <<EOF
|
[req]
|
distinguished_name = dn
|
prompt = no
|
|
[dn]
|
CN = $LOCAL_IDENTITY
|
O = PrivateVoice Local
|
|
[v3_codesign]
|
basicConstraints = critical, CA:true
|
keyUsage = critical, digitalSignature, keyCertSign
|
extendedKeyUsage = codeSigning
|
subjectKeyIdentifier = hash
|
authorityKeyIdentifier = keyid,issuer
|
EOF
|
|
openssl req -new -newkey rsa:2048 -nodes \
|
-keyout "$key" \
|
-out "$csr" \
|
-config "$conf" >/dev/null 2>&1
|
|
openssl x509 -req -days 3650 \
|
-in "$csr" \
|
-signkey "$key" \
|
-out "$cert" \
|
-extfile "$conf" \
|
-extensions v3_codesign >/dev/null 2>&1
|
|
openssl pkcs12 -export \
|
-legacy \
|
-inkey "$key" \
|
-in "$cert" \
|
-out "$p12" \
|
-name "$LOCAL_IDENTITY" \
|
-passout "pass:$pass" >/dev/null 2>&1
|
|
security import "$p12" \
|
-k "$KEYCHAIN" \
|
-P "$pass" \
|
-A \
|
-T /usr/bin/codesign \
|
-T /usr/bin/security >/dev/null
|
|
security add-trusted-cert \
|
-d \
|
-r trustRoot \
|
-p codeSign \
|
-k "$KEYCHAIN" \
|
"$cert" >/dev/null 2>&1 || true
|
|
if ! security find-identity -v -p codesigning "$KEYCHAIN" | grep -Fq "$LOCAL_IDENTITY"; then
|
fail "Local signing identity was created but is not available to codesign"
|
fi
|
trap - RETURN
|
rm -rf "$tmpdir"
|
}
|
|
sign_item() {
|
local item="$1"
|
codesign --force \
|
--options runtime \
|
--timestamp=none \
|
--entitlements "$ENTITLEMENTS" \
|
--sign "$LOCAL_IDENTITY" \
|
"$item"
|
}
|
|
step "Preflight checks"
|
[[ -f "$INFO_PLIST" ]] || fail "Info.plist not found at $INFO_PLIST"
|
[[ -f "$ENTITLEMENTS" ]] || fail "entitlements.plist not found at $ENTITLEMENTS"
|
[[ -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")"
|
ensure_local_identity
|
|
step "Cleaning old local 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 ($DMG_ARCH)"
|
cd "$PROJECT_DIR"
|
CGO_ENABLED=1 GOOS=darwin GOARCH="$GOARCH" \
|
go build -buildvcs=false -gcflags=all="-l" -ldflags="-s -w" \
|
-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"
|
cp "$BUILD_DIR/$APP_NAME" "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
|
cp "$PROJECT_DIR/build/darwin/icon.icns" "$APP_BUNDLE/Contents/Resources/icon.icns"
|
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
|
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 with stable local identity"
|
sign_item "$APP_BUNDLE/Contents/Frameworks/$ONNXRUNTIME_DYLIB_NAME"
|
sign_item "$APP_BUNDLE/Contents/Frameworks/libsherpa-onnx-c-api.dylib"
|
sign_item "$APP_BUNDLE/Contents/MacOS/$APP_NAME"
|
sign_item "$APP_BUNDLE"
|
|
step "Verifying app signature"
|
codesign -vvv --deep --strict "$APP_BUNDLE"
|
codesign -dv --verbose=2 "$APP_BUNDLE" 2>&1 | sed -n '1,24p'
|
|
step "Creating DMG"
|
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" \
|
-srcfolder "$DMG_TEMP" \
|
-ov -format UDZO \
|
"$DMG_PATH"
|
rm -rf "$DMG_TEMP"
|
|
step "Signing DMG"
|
codesign --force --timestamp=none --sign "$LOCAL_IDENTITY" "$DMG_PATH"
|
hdiutil verify "$DMG_PATH"
|
cp "$DMG_PATH" "$COMPAT_DMG_PATH"
|
|
if [[ "$INSTALL_AFTER_BUILD" == "1" ]]; then
|
step "Installing to /Applications"
|
osascript -e "tell application \"$APP_NAME\" to quit" 2>/dev/null || true
|
sleep 1
|
pkill -x "$APP_NAME" 2>/dev/null || true
|
rm -rf "/Applications/$APP_NAME.app"
|
ditto "$APP_BUNDLE" "/Applications/$APP_NAME.app"
|
codesign -vvv --deep --strict "/Applications/$APP_NAME.app"
|
fi
|
|
step "Local build complete"
|
echo " App: $APP_BUNDLE"
|
echo " DMG: $DMG_PATH"
|
echo " Compatibility DMG: $COMPAT_DMG_PATH"
|
echo " Signing identity: $LOCAL_IDENTITY"
|