#!/bin/bash # # OpenScreen macOS Build Script # Produces: release//OpenScreen-Mac--.dmg # # Usage: chmod +x scripts/build_macos.sh && ./scripts/build_macos.sh # set -euo pipefail # ── Load .env ───────────────────────────────────────────────────────── PROJECT_ROOT="$(cd "$(dirname "$0")/.." && pwd)" ENV_FILE="${PROJECT_ROOT}/.env" if [ -f "$ENV_FILE" ]; then set -a source "$ENV_FILE" set +a else echo "ERROR: .env file not found at ${ENV_FILE}" echo "Create one with APP_NAME, SIGN_IDENTITY, NOTARY_PROFILE, etc." exit 1 fi # ── Config ──────────────────────────────────────────────────────────── VERSION=$(node -p "require('${PROJECT_ROOT}/package.json').version") RELEASE_DIR="${PROJECT_ROOT}/release/${VERSION}" ENTITLEMENTS="${PROJECT_ROOT}/macos.entitlements" ARCHS=("arm64" "x64") # ── Colors ──────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # No Color BOLD='\033[1m' print_step() { echo -e "\n${CYAN}${BOLD}▸ $1${NC}"; } print_ok() { echo -e "${GREEN}✓ $1${NC}"; } print_warn() { echo -e "${YELLOW}⚠ $1${NC}"; } print_err() { echo -e "${RED}✗ $1${NC}"; } # ── Preflight ───────────────────────────────────────────────────────── echo -e "\n${BOLD}╔══════════════════════════════════════════╗${NC}" echo -e "${BOLD}║ ${APP_NAME} macOS Build Script v${VERSION} ║${NC}" echo -e "${BOLD}╚══════════════════════════════════════════╝${NC}" print_step "Checking prerequisites..." if [[ "$(uname)" != "Darwin" ]]; then print_err "This script must be run on macOS." exit 1 fi print_ok "Running on macOS ($(uname -m))" if ! command -v node &> /dev/null; then print_err "Node.js not found. Please install Node.js first." exit 1 fi print_ok "Node.js found: $(node -v)" if ! command -v npm &> /dev/null; then print_err "npm not found." exit 1 fi print_ok "npm found: $(npm -v)" # Check signing identity if ! security find-identity -v -p codesigning | grep -q "$SIGN_IDENTITY"; then print_err "Signing identity not found: ${SIGN_IDENTITY}" print_err "Run 'security find-identity -v -p codesigning' to see available identities." exit 1 fi print_ok "Signing identity found: ${SIGN_IDENTITY}" # Check notary profile if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" &> /dev/null; then print_err "Notary profile '${NOTARY_PROFILE}' not found in keychain." print_err "Run: xcrun notarytool store-credentials \"${NOTARY_PROFILE}\" --apple-id \"${APPLE_ID}\" --team-id \"${TEAM_ID}\"" exit 1 fi print_ok "Notary profile found: ${NOTARY_PROFILE}" # Check entitlements if [ ! -f "$ENTITLEMENTS" ]; then print_err "Entitlements file not found: ${ENTITLEMENTS}" exit 1 fi print_ok "Entitlements file found" # ── Clean ───────────────────────────────────────────────────────────── cd "$PROJECT_ROOT" print_step "Cleaning previous build artifacts..." rm -rf dist dist-electron "${RELEASE_DIR}" print_ok "Clean complete" # ── Install Dependencies ───────────────────────────────────────────── print_step "Installing dependencies..." npm ci print_ok "Dependencies installed" # ── Build Vite + Electron ──────────────────────────────────────────── print_step "Building Vite + Electron... (this may take a minute)" npx tsc && npx vite build print_ok "Vite + Electron build complete" # ── Package, Sign, Notarize per Architecture ───────────────────────── for ARCH in "${ARCHS[@]}"; do echo "" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" echo -e "${BOLD} Building for: ${ARCH}${NC}" echo -e "${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" # ── Package with electron-builder ───────────────────────────── print_step "[${ARCH}] Packaging with electron-builder..." # Build .app only (--dir), electron-builder handles codesigning # with hardenedRuntime + entitlements from electron-builder.json5 CSC_NAME="$CSC_NAME" npx electron-builder --mac --${ARCH} --dir # Find the .app bundle APP_BUNDLE=$(find "${RELEASE_DIR}" -maxdepth 2 -name "*.app" -type d | grep -i "${ARCH}\|mac" | head -n1) if [ -z "$APP_BUNDLE" ]; then # Fallback: find any .app in the output APP_BUNDLE=$(find "${RELEASE_DIR}" -maxdepth 2 -name "*.app" -type d | head -n1) fi if [ -z "$APP_BUNDLE" ]; then print_err "[${ARCH}] Could not find .app bundle in ${RELEASE_DIR}" exit 1 fi print_ok "[${ARCH}] App bundle: $(basename "$APP_BUNDLE")" # ── Verify codesign on .app ─────────────────────────────────── print_step "[${ARCH}] Verifying .app code signature..." codesign --verify --deep --strict "$APP_BUNDLE" 2>&1 || print_warn "[${ARCH}] Deep verify had warnings (may be expected pre-notarization)" print_ok "[${ARCH}] .app signature verified" # ── Create DMG ──────────────────────────────────────────────── DMG_NAME="${APP_NAME}-Mac-${ARCH}-${VERSION}.dmg" DMG_OUTPUT="${RELEASE_DIR}/${DMG_NAME}" DMG_STAGING="${RELEASE_DIR}/dmg-staging-${ARCH}" print_step "[${ARCH}] Creating DMG..." rm -f "$DMG_OUTPUT" rm -rf "$DMG_STAGING" # Stage: app + Applications shortcut for drag-to-install mkdir -p "$DMG_STAGING" cp -R "$APP_BUNDLE" "$DMG_STAGING/" ln -s /Applications "$DMG_STAGING/Applications" hdiutil create \ -srcfolder "$DMG_STAGING" \ -volname "${APP_NAME}" \ -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" \ -format UDBZ \ "$DMG_OUTPUT" print_ok "[${ARCH}] DMG created: ${DMG_NAME}" rm -rf "$DMG_STAGING" # ── Sign DMG ────────────────────────────────────────────────── print_step "[${ARCH}] Signing DMG..." codesign --force --sign "$SIGN_IDENTITY" --timestamp "$DMG_OUTPUT" print_ok "[${ARCH}] DMG signed" # ── Notarize DMG ────────────────────────────────────────────── print_step "[${ARCH}] Notarizing DMG with Apple... (this may take several minutes)" xcrun notarytool submit "$DMG_OUTPUT" \ --keychain-profile "$NOTARY_PROFILE" \ --wait print_ok "[${ARCH}] DMG notarized" # ── Staple ──────────────────────────────────────────────────── print_step "[${ARCH}] Stapling notarization ticket..." xcrun stapler staple "$DMG_OUTPUT" print_ok "[${ARCH}] Ticket stapled" # ── Validate ────────────────────────────────────────────────── print_step "[${ARCH}] Validating stapled DMG..." xcrun stapler validate "$DMG_OUTPUT" print_ok "[${ARCH}] Validation passed" done # ── Clean up unpacked dirs (keep only DMGs) ─────────────────────────── print_step "Cleaning up intermediate directories..." find "${RELEASE_DIR}" -maxdepth 1 -type d ! -name "$(basename "$RELEASE_DIR")" -exec rm -rf {} + 2>/dev/null || true print_ok "Cleanup complete" # ── Done ────────────────────────────────────────────────────────────── echo "" echo -e "${GREEN}${BOLD}════════════════════════════════════════════${NC}" echo -e "${GREEN}${BOLD} Build & Notarization Complete!${NC}" echo -e "${GREEN}${BOLD}════════════════════════════════════════════${NC}" echo "" for ARCH in "${ARCHS[@]}"; do DMG_NAME="${APP_NAME}-Mac-${ARCH}-${VERSION}.dmg" DMG_PATH="${RELEASE_DIR}/${DMG_NAME}" if [ -f "$DMG_PATH" ]; then DMG_SIZE=$(du -h "$DMG_PATH" | cut -f1) echo -e " 📦 ${BOLD}${ARCH}:${NC} ${DMG_PATH}" echo -e " 📏 ${BOLD}Size:${NC} ${DMG_SIZE}" echo "" fi done echo -e " ${GREEN}All DMGs are fully signed, notarized, and stapled!${NC}" echo -e " ${GREEN}Ready for distribution outside the Mac App Store.${NC}" echo ""