Files
agent-desktop/scripts/pack-tauri/build_macos_pyinstaller.sh
T
Pine f135a07ebe build: macOS 打包脚本确保生成 DMG 安装包
根因:build_macos_pyinstaller.sh Step 3 显式指定 --bundles app,覆盖了
tauri.conf.json 里的 dmg target,只生成 .app 然后手动打 ZIP,完全没生成 DMG。

修复:
1. --bundles app → --bundles app,dmg(同时构建 .app 和 .dmg)
2. Step 4 增加 DMG 产物收集:从 bundle/dmg/*.dmg 复制到 dist/,
   命名 YunOPC-Hub-{VERSION}-macOS.dmg,输出大小和路径
3. 构建完成摘要增加 DMG 路径
4. package.json 增加快捷脚本:tauri:dev / tauri:build /
   tauri:build:mac / tauri:build:mac:debug
5. tauri.conf.json bundle.macOS.dmg 增加专业配置:
   windowSize [640,440] + applicationFolder=true(DMG 内显示
   /Applications 快捷方式,支持拖拽安装)

macOS DMG 打包依赖系统自带 hdiutil,无需额外安装。
2026-09-07 23:29:53 +08:00

239 lines
7.5 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build YunOPC-Hub with Tauri for macOS (PyInstaller backend)
# Creates a self-contained desktop app with bundled Python backend
#
# Usage:
# ./scripts/pack-tauri/build_macos_pyinstaller.sh
set -e
REPO_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$REPO_ROOT"
VERSION=$(sed -n 's/^__version__[[:space:]]*=[[:space:]]*"\([^"]*\)".*/\1/p' src/pineagents/__version__.py)
echo "========================================="
echo "YunOPC-Hub Tauri Build - macOS (PyInstaller)"
echo "========================================="
echo "Version: ${VERSION}"
echo ""
SIGN_MACOS_BUNDLE="${REPO_ROOT}/scripts/pack-tauri/sign_macos_bundle.sh"
# Step 0: Prerequisites
echo "== Step 0: Checking Prerequisites =="
missing=()
if command -v npm &>/dev/null; then
echo " [OK] npm ($(npm --version))"
else
echo " [MISSING] npm"
echo " Install Node.js: https://nodejs.org/"
missing+=("npm")
fi
if command -v rustc &>/dev/null; then
echo " [OK] rustc ($(rustc --version))"
else
echo " [MISSING] rustc (Rust)"
echo " Install: https://rustup.rs"
missing+=("rustc")
fi
if command -v uv &>/dev/null; then
echo " [OK] uv ($(uv --version))"
else
echo " [MISSING] uv"
echo " Install: https://docs.astral.sh/uv/getting-started/installation/"
missing+=("uv")
fi
if [ ${#missing[@]} -gt 0 ]; then
echo ""
echo "Missing prerequisites: ${missing[*]}"
echo "Install the missing tools and re-run this script."
exit 1
fi
echo ""
if [ ! -f "${SIGN_MACOS_BUNDLE}" ]; then
echo "ERROR: macOS signing helper not found at ${SIGN_MACOS_BUNDLE}"
exit 1
fi
if [ -z "${APPLE_SIGNING_IDENTITY:-}" ] && [ -z "${APPLE_CERTIFICATE:-}" ]; then
# The Tauri app and PyInstaller sidecar are native Mach-O executables.
# Keep their signature state consistent with ad-hoc signatures when no
# Developer ID certificate is configured. This matches the legacy desktop
# package behavior: signed enough for local loading, not notarized.
export APPLE_SIGNING_IDENTITY="-"
echo "Using ad-hoc macOS code signing"
fi
if [ -z "${PYINSTALLER_CODESIGN_IDENTITY:-}" ]; then
# PyInstaller uses the same identity as the final app for bundled Mach-O
# files; "-" means ad-hoc signing on macOS.
export PYINSTALLER_CODESIGN_IDENTITY="${APPLE_SIGNING_IDENTITY:-}"
fi
echo ""
# Step 1: Build console static assets
echo "== Step 1: Building Console Static Assets =="
cd console
yarn install --immutable
echo "Generating Tauri icons..."
npm exec -- tauri icon ../scripts/pack/assets/icon.png
echo "Syncing Tauri version..."
node ../scripts/pack-tauri/sync_tauri_version.mjs
echo "Building console frontend..."
npm run build:prod
cd ..
echo "Console static assets built"
echo ""
# Step 2: Build PyInstaller backend
echo "== Step 2: Building PyInstaller Backend =="
bash scripts/pack-tauri/build_pyinstaller.sh
echo "PyInstaller backend built"
echo ""
echo "== Step 2b: Signing PyInstaller Backend =="
bash "${SIGN_MACOS_BUNDLE}" \
"${REPO_ROOT}/console/src-tauri/binaries/qwenpaw-backend" \
"${APPLE_SIGNING_IDENTITY}"
echo "PyInstaller backend signed"
echo ""
# Step 2c: Load Tauri v2 signing private key for updater artifacts
# 使用 Tauri 原生格式私钥(tauri signer generate 生成,单行 base64,无密码)
if [ -z "${TAURI_SIGNING_PRIVATE_KEY}" ]; then
SIGN_KEY_FILE="${REPO_ROOT}/mykey/tauri-signer.key"
if [ -f "${SIGN_KEY_FILE}" ]; then
export TAURI_SIGNING_PRIVATE_KEY="$(cat "${SIGN_KEY_FILE}")"
export TAURI_SIGNING_PRIVATE_KEY_PASSWORD="${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-}"
echo "== Step 2c: Loaded Tauri signing key from ${SIGN_KEY_FILE} =="
else
echo "WARNING: TAURI_SIGNING_PRIVATE_KEY not set and ${SIGN_KEY_FILE} not found"
echo "WARNING: Tauri updater artifacts (.sig) will NOT be generated"
fi
else
echo "== Step 2c: Using Tauri signing key from environment =="
fi
echo ""
# Step 3: Build Tauri app
echo "== Step 3: Building Tauri App =="
BUNDLE_DIR="${REPO_ROOT}/console/src-tauri/target/release/bundle"
rm -rf "${BUNDLE_DIR}/dmg" "${BUNDLE_DIR}/macos"
cd console
echo "Building for macOS..."
npm exec -- tauri build \
--config src-tauri/tauri.version.conf.json \
--bundles app,dmg
cd ..
echo "Tauri app built"
echo ""
APP_PATH="${BUNDLE_DIR}/macos/YunOPC-Hub.app"
if [ ! -d "${APP_PATH}" ]; then
echo "ERROR: No Tauri macOS app found at ${APP_PATH}"
exit 1
fi
HELPER_PATH="${APP_PATH}/Contents/MacOS/qwenpaw-computer-use-helper"
if [ ! -x "${HELPER_PATH}" ]; then
echo "ERROR: Computer Use helper was not bundled at ${HELPER_PATH}"
exit 1
fi
echo "== Step 3b: Signing Final macOS App =="
bash "${SIGN_MACOS_BUNDLE}" \
"${APP_PATH}" \
"${APPLE_SIGNING_IDENTITY}"
echo "Final macOS app signed and verified"
echo ""
# Step 4: Collect distribution artifacts
echo "== Step 4: Collecting Distribution Artifacts =="
DIST="${DIST:-dist}"
if [[ "${DIST}" = /* ]]; then
DIST_ROOT="${DIST}"
else
DIST_ROOT="${REPO_ROOT}/${DIST}"
fi
DIST_DIR="${DIST_ROOT}/tauri-macos"
rm -rf "${DIST_DIR}"
mkdir -p "${DIST_DIR}"
# Match the legacy macOS package shape: one zip containing one .app bundle.
cp -R "${APP_PATH}" "${DIST_DIR}/"
STAGED_APP_PATH="${DIST_DIR}/$(basename "${APP_PATH}")"
echo ".app copied to ${STAGED_APP_PATH}"
# Create ZIP archive
ZIP_NAME="${DIST_ROOT}/YunOPC-Hub-${VERSION}-macOS.zip"
if [ -f "${ZIP_NAME}" ]; then
rm -f "${ZIP_NAME}"
fi
if command -v ditto &>/dev/null; then
ditto -c -k --sequesterRsrc --keepParent "${STAGED_APP_PATH}" "${ZIP_NAME}"
else
cd "${DIST_DIR}"
zip -r "${ZIP_NAME}" "$(basename "${STAGED_APP_PATH}")"
cd "${REPO_ROOT}"
fi
if [ -f "${ZIP_NAME}" ]; then
SIZE=$(du -sh "${ZIP_NAME}" | cut -f1)
echo "Created ${ZIP_NAME} (${SIZE})"
else
echo "ERROR: Failed to create ZIP archive"
exit 1
fi
echo ""
# Collect DMG disk image (Tauri dmg bundle target)
DMG_GLOB="${BUNDLE_DIR}/dmg"/*.dmg
DMG_FILES=(${DMG_GLOB})
if [ ${#DMG_FILES[@]} -gt 0 ] && [ -f "${DMG_FILES[0]}" ]; then
DMG_SRC="${DMG_FILES[0]}"
DMG_NAME="YunOPC-Hub-${VERSION}-macOS.dmg"
DMG_DEST="${DIST_ROOT}/${DMG_NAME}"
cp -f "${DMG_SRC}" "${DMG_DEST}"
DMG_SIZE=$(du -sh "${DMG_DEST}" | cut -f1)
echo "Created ${DMG_DEST} (${DMG_SIZE})"
else
echo "WARNING: No DMG file found at ${BUNDLE_DIR}/dmg/*.dmg"
echo " Tauri dmg bundle may have been skipped. Check that hdiutil is available."
fi
echo ""
UPDATER_NAME="${DIST_ROOT}/YunOPC-Hub-${VERSION}-macOS.app.tar.gz"
case "$(uname -m)" in
arm64 | aarch64) UPDATER_TARGET="darwin-aarch64" ;;
*) UPDATER_TARGET="darwin-x86_64" ;;
esac
python3 "${REPO_ROOT}/scripts/pack-tauri/generate_update_manifest.py" stage \
--bundle-dir "${BUNDLE_DIR}/macos" \
--pattern '*.app.tar.gz' \
--target "${UPDATER_TARGET}" \
--output "${UPDATER_NAME}" \
--pubkey-config "${REPO_ROOT}/console/src-tauri/tauri.version.conf.json" || \
"${REPO_ROOT}/.venv/bin/python" "${REPO_ROOT}/scripts/pack-tauri/generate_update_manifest.py" stage \
--bundle-dir "${BUNDLE_DIR}/macos" \
--pattern '*.app.tar.gz' \
--target "${UPDATER_TARGET}" \
--output "${UPDATER_NAME}" \
--pubkey-config "${REPO_ROOT}/console/src-tauri/tauri.version.conf.json"
echo ""
echo "========================================="
echo "Build Complete!"
echo "========================================="
echo "App: ${APP_PATH}"
echo "Distribution: ${DIST_DIR}"
echo "Archive: ${ZIP_NAME}"
echo "DMG: ${DMG_DEST:-not generated}"
echo "Updater: ${UPDATER_NAME}"
echo ""
echo "Test: open \"${STAGED_APP_PATH}\""
echo ""