| New file |
| | |
| | | #!/usr/bin/env bash |
| | | # |
| | | # build-local-macos.sh - Build a locally signed macOS app and DMG for VoiceSnap. |
| | | # |
| | | # 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="VoiceSnap" |
| | | LOCAL_IDENTITY="${VOICESNAP_LOCAL_SIGN_IDENTITY:-VoiceSnap Local Code Signing}" |
| | | 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)" |
| | | |
| | | 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@v1.12.24/lib/$SHERPA_ARCH" |
| | | |
| | | ensure_local_identity() { |
| | | 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 = VoiceSnap 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" |
| | | 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" |
| | | cp "$SHERPA_LIB_DIR/libsherpa-onnx-c-api.dylib" "$APP_BUNDLE/Contents/Frameworks/" |
| | | cp "$SHERPA_LIB_DIR/libonnxruntime.1.23.2.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/libonnxruntime.1.23.2.dylib \ |
| | | "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib" |
| | | |
| | | step "Code signing with stable local identity" |
| | | sign_item "$APP_BUNDLE/Contents/Frameworks/libonnxruntime.1.23.2.dylib" |
| | | 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" |