From 441f7731af05f2d5c092bca0d537a318bac7b458 Mon Sep 17 00:00:00 2001
From: Ariver <shanghai3168@gmail.com>
Date: Mon, 18 May 2026 02:21:35 +0800
Subject: [PATCH] build: use stable local macOS signing
---
TODO.md | 9 +
VoiceSnapGo/app.go | 2
VoiceSnapGo/scripts/build-local-macos.sh | 251 ++++++++++++++++++++++++++++++++++++++++++++++++++
VoiceSnapGo/build/darwin/Info.plist | 2
4 files changed, 262 insertions(+), 2 deletions(-)
diff --git a/TODO.md b/TODO.md
index 2a89645..f4c281b 100644
--- a/TODO.md
+++ b/TODO.md
@@ -17,6 +17,15 @@
## Done
+- [2026-05-18] 将本地 macOS 打包从 ad-hoc 签名改为固定本地 Code Signing 身份,减少辅助功能权限失效。
+ - 新增 `VoiceSnapGo/scripts/build-local-macos.sh`,用于本地构建、稳定签名、生成 DMG,可选安装到 `/Applications`。
+ - 自动创建/复用钥匙串身份 `VoiceSnap Local Code Signing`,签名 `.app`、内部 dylib 和 `.dmg`。
+ - 当前安装包签名已验证: `Authority=VoiceSnap Local Code Signing`。
+ - build: `20260518.0211`
+ - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap.app`
+ - 输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-build20260518.0211-arm64-local.dmg`
+ - 兼容输出: `VoiceSnapGo/build/local/arm64/VoiceSnap-2.1.1-arm64-local.dmg` 已替换为同一份新包。
+ - 已安装到 `/Applications/VoiceSnap.app`;本次签名身份变化后需要在系统设置里重新添加一次辅助功能权限。
- [2026-05-18] 修复停止录音时 `ma_device_stop` 与音频回调互等导致 App 假死的问题。
- 根因: `Recorder.StopAndGetSamples()` 持有录音互斥锁调用 `device.Stop()`;底层 `ma_device_stop` 等待音频回调结束,而音频回调同时等待同一把锁,形成互等。
- 修复: 停止录音时先在锁内摘除设备并标记非录音,释放锁后再调用 `Stop/Uninit`,避免底层停止设备时阻塞音频回调。
diff --git a/VoiceSnapGo/app.go b/VoiceSnapGo/app.go
index c296b47..efd4a6c 100755
--- a/VoiceSnapGo/app.go
+++ b/VoiceSnapGo/app.go
@@ -27,7 +27,7 @@
const (
appVersion = "2.1.1"
- appBuild = "20260518.0125"
+ appBuild = "20260518.0211"
appDisplayVersion = appVersion + " (build " + appBuild + ")"
appName = "VoiceSnap"
diff --git a/VoiceSnapGo/build/darwin/Info.plist b/VoiceSnapGo/build/darwin/Info.plist
index 82b1146..a448a21 100755
--- a/VoiceSnapGo/build/darwin/Info.plist
+++ b/VoiceSnapGo/build/darwin/Info.plist
@@ -17,7 +17,7 @@
<key>CFBundleShortVersionString</key>
<string>2.1.1</string>
<key>CFBundleVersion</key>
- <string>20260518.0125</string>
+ <string>20260518.0211</string>
<key>LSMinimumSystemVersion</key>
<string>11.0</string>
<key>NSMicrophoneUsageDescription</key>
diff --git a/VoiceSnapGo/scripts/build-local-macos.sh b/VoiceSnapGo/scripts/build-local-macos.sh
new file mode 100755
index 0000000..6e79d69
--- /dev/null
+++ b/VoiceSnapGo/scripts/build-local-macos.sh
@@ -0,0 +1,251 @@
+#!/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"
--
Gitblit v1.9.3