Files
agent-desktop/scripts/pack-tauri/build_macos_pyinstaller.sh

199 lines
5.8 KiB
Bash

#!/usr/bin/env bash
# Build QwenPaw 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 "QwenPaw 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.svg
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 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/QwenPaw Desktop.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}/QwenPaw-Tauri-${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 ""
UPDATER_NAME="${DIST_ROOT}/QwenPaw-Tauri-${VERSION}-macOS.app.tar.gz"
case "$(uname -m)" in
arm64 | aarch64) UPDATER_TARGET="darwin-aarch64" ;;
*) UPDATER_TARGET="darwin-x86_64" ;;
esac
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 "Updater: ${UPDATER_NAME}"
echo ""
echo "Test: open \"${STAGED_APP_PATH}\""
echo ""