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