Files
TRIPandHKLHaoBin 6f9af89583 release: OpenLess 2.0.0-Beta.4+build.20260930 (#1126)
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>
2026-09-30 20:32:53 +08:00

236 lines
9.5 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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