Integrate ASR/Omni fixes, API Route, Android IME and confirmation-based vocabulary learning. Complete settings transitions and macOS single-item credential storage. Synchronize all application versions to 2.0.0-Beta.4+build.20260930. Integrated PRs: #1116, #1120, #1121, #1123, #1124, #1067 and #1128. Linux #1117 remains independent and is excluded from automatic release. Validated: full cross-platform CI, 18 UI motion cases, 112 frontend test files, 1245 Core tests (1 ignored) and 572 macOS native tests. Co-authored-by: HKLHaoBin <we3q@qq.com>
236 lines
9.5 KiB
Bash
Executable File
236 lines
9.5 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# One-shot build of the macOS release .app / .dmg.
|
||
#
|
||
# macOS NSXxxUsageDescription entries live in src-tauri/Info.plist and are merged by Tauri
|
||
# before generating the .app and .dmg, so an uploaded DMG never carries a stale Info.plist.
|
||
#
|
||
# Usage: run from the app/ directory
|
||
# ./scripts/build-mac.sh # build + sign + install to /Applications
|
||
# INSTALL=0 ./scripts/build-mac.sh # build only, no install
|
||
|
||
set -euo pipefail
|
||
|
||
cd "$(dirname "$0")/.."
|
||
|
||
APP="src-tauri/target/release/bundle/macos/OpenLess.app"
|
||
INFO="$APP/Contents/Info.plist"
|
||
DMG_DIR="src-tauri/target/release/bundle/dmg"
|
||
INSTALL="${INSTALL:-1}"
|
||
|
||
if [ -z "${APPLE_CERTIFICATE:-}" ] && [ -z "${APPLE_SIGNING_IDENTITY:-}" ]; then
|
||
export APPLE_SIGNING_IDENTITY="-"
|
||
echo "▶ 未检测到 Apple 签名证书,使用 ad-hoc 签名(下载分发仍会触发 Gatekeeper)"
|
||
else
|
||
echo "▶ 检测到 Apple 签名环境,交给 Tauri 做 Developer ID 签名 / 公证"
|
||
fi
|
||
|
||
echo "▶ 检查 Apple Silicon MLX 构建依赖"
|
||
npm run check:macos-metal-toolchain
|
||
|
||
source scripts/macos-build-env.sh
|
||
echo "▶ Cargo release codegen units: ${CARGO_PROFILE_RELEASE_CODEGEN_UNITS} (macOS only)"
|
||
echo "▶ Cargo release strip: ${CARGO_PROFILE_RELEASE_STRIP} (macOS only)"
|
||
echo "▶ Rust proc-macro host wrapper: ${RUSTC_WRAPPER}"
|
||
|
||
# Cargo creates different directories for build-script executables and OUT_DIR. Clean only
|
||
# among the metallib outputs, keeping all build-script caches to avoid recompiling every time.
|
||
KEEP_QWEN_DIR=""
|
||
for d in src-tauri/target/release/build/qwen3-asr-rs-*; do
|
||
[ -s "$d/out/lib/mlx.metallib" ] || continue
|
||
if [ -z "$KEEP_QWEN_DIR" ] || [ "$d/out/lib/mlx.metallib" -nt "$KEEP_QWEN_DIR/out/lib/mlx.metallib" ]; then
|
||
KEEP_QWEN_DIR="$d"
|
||
fi
|
||
done
|
||
if [ -n "$KEEP_QWEN_DIR" ]; then
|
||
for d in src-tauri/target/release/build/qwen3-asr-rs-*; do
|
||
[ -s "$d/out/lib/mlx.metallib" ] || continue
|
||
[ "$d" = "$KEEP_QWEN_DIR" ] || rm -rf "$d"
|
||
done
|
||
fi
|
||
|
||
echo "▶ tauri build"
|
||
TAURI_BUILD_ARGS=(build --ci)
|
||
case "$(uname -m)" in
|
||
arm64)
|
||
MAC_BUNDLE_ARCH="aarch64"
|
||
TAURI_BUILD_ARGS+=(--config src-tauri/tauri.macos-mlx.conf.json)
|
||
;;
|
||
x86_64)
|
||
MAC_BUNDLE_ARCH="x64"
|
||
;;
|
||
*)
|
||
echo "✗ 不支持的 macOS 构建架构:$(uname -m)"
|
||
exit 1
|
||
;;
|
||
esac
|
||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ -n "${TAURI_SIGNING_PRIVATE_KEY_PATH:-}" ]; then
|
||
TAURI_BUILD_ARGS+=(--config '{"bundle":{"createUpdaterArtifacts":true}}')
|
||
fi
|
||
APP_VERSION="$(node -p "require('./package.json').version")"
|
||
DMG_PATH="$DMG_DIR/OpenLess_${APP_VERSION}_${MAC_BUNDLE_ARCH}.dmg"
|
||
|
||
# Remove deliverables, keep Cargo caches. Hot builds reuse binaries with old timestamps, so
|
||
# bundle freshness can't be judged from compiled-file mtimes; a failed Tauri run must not leave
|
||
# the previous round's installer in place either.
|
||
rm -rf "$APP"
|
||
rm -f "$DMG_PATH" "${APP}.tar.gz" "${APP}.tar.gz.sig"
|
||
TAURI_BUILD_ARGS+=(-- --locked --timings)
|
||
if [ "$MAC_BUNDLE_ARCH" = "aarch64" ]; then
|
||
# Tauri skips Finder AppleScript in CI. Write deterministic Finder metadata
|
||
# into its temporary image before Tauri compresses and signs the final DMG.
|
||
DMG_LAYOUT_ENV_DIR="$(mktemp -d "${TMPDIR:-/tmp}/openless-dmg-python.XXXXXX")"
|
||
cleanup_dmg_environment() { rm -rf "$DMG_LAYOUT_ENV_DIR"; }
|
||
trap cleanup_dmg_environment EXIT
|
||
python3 -m venv "$DMG_LAYOUT_ENV_DIR"
|
||
DMG_LAYOUT_PYTHON="$DMG_LAYOUT_ENV_DIR/bin/python3"
|
||
"$DMG_LAYOUT_PYTHON" -m pip install --quiet --disable-pip-version-check \
|
||
--only-binary=:all: --no-deps --require-hashes -r scripts/macos-dmg-requirements.txt
|
||
"$DMG_LAYOUT_PYTHON" scripts/macos-dmg-layout.test.py
|
||
CI=true TAURI_BUNDLER_DMG_IGNORE_CI=false \
|
||
OPENLESS_DMG_LAYOUT_ROOT="$PWD" OPENLESS_DMG_LAYOUT_PYTHON="$DMG_LAYOUT_PYTHON" \
|
||
OPENLESS_DMG_LAYOUT_STAMP="$DMG_LAYOUT_ENV_DIR/layout-applied" \
|
||
PATH="$PWD/scripts/macos-dmg-bin:$PATH" npm run tauri -- "${TAURI_BUILD_ARGS[@]}"
|
||
if [ ! -s "$DMG_LAYOUT_ENV_DIR/layout-applied" ]; then
|
||
echo "✗ Tauri 未调用 DMG 布局步骤,中止交付"
|
||
exit 1
|
||
fi
|
||
"$DMG_LAYOUT_PYTHON" scripts/macos-dmg-layout.py verify "$DMG_PATH"
|
||
cleanup_dmg_environment
|
||
trap - EXIT
|
||
else
|
||
npm run tauri -- "${TAURI_BUILD_ARGS[@]}"
|
||
fi
|
||
|
||
if [ ! -f "$APP/Contents/MacOS/openless" ]; then
|
||
echo "✗ $APP 缺失或不是本次构建的产物(打包未完成),中止"
|
||
exit 1
|
||
fi
|
||
# The DMG is always generated by Tauri (with the signing/notarization chain); a hand-rolled
|
||
# hdiutil DMG bypasses those steps and the bundle contract tests explicitly forbid it. Missing
|
||
# DMG = fail, no fallback.
|
||
if [ ! -f "$DMG_PATH" ]; then
|
||
echo "✗ 未找到本次构建的 DMG:$DMG_PATH(tauri build 未完成打包)"
|
||
exit 1
|
||
fi
|
||
|
||
echo "▶ 校验 Info.plist / 签名"
|
||
/usr/libexec/PlistBuddy -c "Print :NSMicrophoneUsageDescription" "$INFO" > /dev/null
|
||
bash scripts/check-macos-speech-usage-description.sh "$INFO"
|
||
codesign -d --entitlements :- "$APP" 2> /dev/null | grep -q "com.apple.security.device.audio-input"
|
||
codesign --verify --deep --strict --verbose=2 "$APP" 2>&1 | tail -2
|
||
|
||
if [ "$MAC_BUNDLE_ARCH" = "aarch64" ]; then
|
||
echo "▶ 校验 MLX metallib 已进入 app / DMG / updater"
|
||
APP_METALLIB="$APP/Contents/Resources/mlx.metallib"
|
||
if [ ! -s "$APP_METALLIB" ]; then
|
||
echo "✗ Apple Silicon app 缺少 Contents/Resources/mlx.metallib"
|
||
exit 1
|
||
fi
|
||
# Non-Mach-O files inside Contents/MacOS are treated as nested code by codesign.
|
||
if [ -e "$APP/Contents/MacOS/mlx.metallib" ]; then
|
||
echo "✗ mlx.metallib 不能放在 Contents/MacOS(ad-hoc codesign 会失败)"
|
||
exit 1
|
||
fi
|
||
APP_METALLIB_SHA="$(shasum -a 256 "$APP_METALLIB" | awk '{print $1}')"
|
||
|
||
if [ ! -f "$DMG_PATH" ]; then
|
||
echo "✗ 未找到 Tauri 生成的 DMG:$DMG_PATH"
|
||
exit 1
|
||
fi
|
||
DMG_MOUNT="$(mktemp -d "${TMPDIR:-/tmp}/openless-dmg-verify.XXXXXX")"
|
||
cleanup_dmg_mount() {
|
||
hdiutil detach "$DMG_MOUNT" > /dev/null 2>&1 || true
|
||
rmdir "$DMG_MOUNT" > /dev/null 2>&1 || true
|
||
}
|
||
trap cleanup_dmg_mount EXIT
|
||
hdiutil attach "$DMG_PATH" -readonly -nobrowse -mountpoint "$DMG_MOUNT" > /dev/null
|
||
DMG_METALLIB="$DMG_MOUNT/OpenLess.app/Contents/Resources/mlx.metallib"
|
||
if [ ! -s "$DMG_METALLIB" ]; then
|
||
echo "✗ DMG 中缺少 OpenLess.app/Contents/Resources/mlx.metallib"
|
||
exit 1
|
||
fi
|
||
if [ -e "$DMG_MOUNT/OpenLess.app/Contents/MacOS/mlx.metallib" ]; then
|
||
echo "✗ DMG 中的 mlx.metallib 不能放在 Contents/MacOS"
|
||
exit 1
|
||
fi
|
||
DMG_METALLIB_SHA="$(shasum -a 256 "$DMG_METALLIB" | awk '{print $1}')"
|
||
if [ "$DMG_METALLIB_SHA" != "$APP_METALLIB_SHA" ]; then
|
||
echo "✗ app 与 DMG 中的 mlx.metallib SHA-256 不一致"
|
||
exit 1
|
||
fi
|
||
cleanup_dmg_mount
|
||
trap - EXIT
|
||
|
||
if [ -n "${TAURI_SIGNING_PRIVATE_KEY:-}" ] || [ -n "${TAURI_SIGNING_PRIVATE_KEY_PATH:-}" ]; then
|
||
UPDATER_ARCHIVE="src-tauri/target/release/bundle/macos/OpenLess.app.tar.gz"
|
||
if [ ! -f "$UPDATER_ARCHIVE" ]; then
|
||
echo "✗ 未找到 Tauri updater archive:$UPDATER_ARCHIVE"
|
||
exit 1
|
||
fi
|
||
UPDATER_METALLIB_SHA="$(tar -xOf "$UPDATER_ARCHIVE" \
|
||
OpenLess.app/Contents/Resources/mlx.metallib | shasum -a 256 | awk '{print $1}')"
|
||
if [ "$UPDATER_METALLIB_SHA" != "$APP_METALLIB_SHA" ]; then
|
||
echo "✗ app 与 updater 中的 mlx.metallib SHA-256 不一致"
|
||
exit 1
|
||
fi
|
||
fi
|
||
echo "✓ MLX metallib sha256=$APP_METALLIB_SHA"
|
||
elif [ -e "$APP/Contents/MacOS/mlx.metallib" ] || [ -e "$APP/Contents/Resources/mlx.metallib" ]; then
|
||
echo "✗ Intel app 不应包含 Apple Silicon MLX metallib"
|
||
exit 1
|
||
fi
|
||
|
||
HAS_DEVELOPER_ID=0
|
||
if [ -n "${APPLE_CERTIFICATE:-}" ] \
|
||
|| { [ -n "${APPLE_SIGNING_IDENTITY:-}" ] && [ "${APPLE_SIGNING_IDENTITY}" != "-" ]; }; then
|
||
HAS_DEVELOPER_ID=1
|
||
fi
|
||
HAS_NOTARIZATION_CREDENTIALS=0
|
||
if { [ -n "${APPLE_ID:-}" ] \
|
||
&& [ -n "${APPLE_PASSWORD:-}" ] \
|
||
&& [ -n "${APPLE_TEAM_ID:-}" ]; } \
|
||
|| { [ -n "${APPLE_API_KEY:-}" ] && [ -n "${APPLE_API_ISSUER:-}" ]; }; then
|
||
HAS_NOTARIZATION_CREDENTIALS=1
|
||
fi
|
||
if [ "$HAS_DEVELOPER_ID" = "1" ] && [ "$HAS_NOTARIZATION_CREDENTIALS" = "1" ]; then
|
||
echo "▶ 校验 Gatekeeper 与公证票据"
|
||
spctl --assess --type execute --verbose=2 "$APP"
|
||
xcrun stapler validate "$APP"
|
||
xcrun stapler validate "$DMG_PATH"
|
||
fi
|
||
|
||
echo "▶ 清理发布产物扩展属性"
|
||
# This only guarantees the CI/local build artifacts themselves are clean; browser downloads may
|
||
# still re-add quarantine. The real fix sparing users manual xattr is Developer ID signing +
|
||
# Apple notarization.
|
||
xattr -cr "$APP" 2> /dev/null || true
|
||
find "$DMG_DIR" -maxdepth 1 -name '*.dmg' -exec xattr -c {} \; 2> /dev/null || true
|
||
|
||
echo "▶ 校验 quarantine 属性"
|
||
if xattr -pr com.apple.quarantine "$APP" > /dev/null 2>&1; then
|
||
echo "✗ $APP 仍包含 com.apple.quarantine"
|
||
exit 1
|
||
fi
|
||
while IFS= read -r dmg; do
|
||
if xattr -p com.apple.quarantine "$dmg" > /dev/null 2>&1; then
|
||
echo "✗ $dmg 仍包含 com.apple.quarantine"
|
||
exit 1
|
||
fi
|
||
done < <(find "$DMG_DIR" -maxdepth 1 -name '*.dmg' -print)
|
||
|
||
if [ "$INSTALL" = "1" ]; then
|
||
echo "▶ 装到 /Applications"
|
||
pkill -f "OpenLess.app/Contents/MacOS/openless" 2> /dev/null || true
|
||
sleep 1
|
||
# Reset TCC before every reinstall: the ad-hoc signing hash changes on each build, so old
|
||
# authorizations break immediately; without a reset, "checked in System Settings" silently
|
||
# stops working.
|
||
tccutil reset Accessibility com.openless.app 2> /dev/null || true
|
||
tccutil reset Microphone com.openless.app 2> /dev/null || true
|
||
rm -rf /Applications/OpenLess.app
|
||
cp -R "$APP" /Applications/
|
||
xattr -dr com.apple.quarantine /Applications/OpenLess.app 2> /dev/null || true
|
||
echo "✓ 装好了:/Applications/OpenLess.app"
|
||
echo " 打开方式:open /Applications/OpenLess.app"
|
||
fi
|