#!/bin/bash # Build macOS DMG package using PyInstaller # # This script builds the WebDrop Bridge application for macOS and creates # a distributable DMG image. # # Requirements: # - PyInstaller 6.0+ # - Python 3.10+ # - Xcode Command Line Tools (for code signing) # - create-dmg (optional, for custom DMG: brew install create-dmg) # # Usage: # bash build_macos.sh [--sign] [--notarize] set -e # Exit on error # Configuration PROJECT_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" BUILD_DIR="$PROJECT_ROOT/build" DIST_DIR="$BUILD_DIR/dist/macos" TEMP_BUILD="$BUILD_DIR/temp/macos" SPECS_DIR="$BUILD_DIR/specs" SPEC_FILE="$BUILD_DIR/webdrop_bridge.spec" APP_NAME="WebDropBridge" DMG_VOLUME_NAME="WebDrop Bridge" VERSION="1.0.0" # Parse arguments SIGN_APP=0 NOTARIZE_APP=0 while [[ $# -gt 0 ]]; do case $1 in --sign) SIGN_APP=1 shift ;; --notarize) NOTARIZE_APP=1 shift ;; *) echo "Unknown option: $1" exit 1 ;; esac done # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' # No Color # Logging functions log_info() { echo -e "${BLUE}ℹ️${NC} $1" } log_success() { echo -e "${GREEN}✅${NC} $1" } log_warning() { echo -e "${YELLOW}⚠️${NC} $1" } log_error() { echo -e "${RED}❌${NC} $1" } # Main build function main() { echo "==========================================" echo "🚀 WebDrop Bridge macOS Build" echo "==========================================" echo "" # Check prerequisites check_prerequisites # Clean previous builds clean_builds # Build with PyInstaller build_executable # Create DMG create_dmg # Optional: Sign and notarize if [ $SIGN_APP -eq 1 ]; then sign_app fi if [ $NOTARIZE_APP -eq 1 ]; then notarize_app fi echo "" echo "==========================================" log_success "Build completed successfully" echo "==========================================" echo "" log_info "Output: $DIST_DIR/" ls -lh "$DIST_DIR" } check_prerequisites() { log_info "Checking prerequisites..." # Check Python if ! command -v python3 &> /dev/null; then log_error "Python 3 not found" exit 1 fi log_success "Python 3 found: $(python3 --version)" # Check PyInstaller if ! python3 -m pip show pyinstaller &> /dev/null; then log_error "PyInstaller not installed. Run: pip install pyinstaller" exit 1 fi log_success "PyInstaller installed" # Check spec file if [ ! -f "$SPEC_FILE" ]; then log_error "Spec file not found: $SPEC_FILE" exit 1 fi log_success "Spec file found" echo "" } clean_builds() { log_info "Cleaning previous builds..." for dir in "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR"; do if [ -d "$dir" ]; then rm -rf "$dir" log_success "Removed $dir" fi done mkdir -p "$DIST_DIR" "$TEMP_BUILD" "$SPECS_DIR" echo "" } build_executable() { log_info "Building macOS executable with PyInstaller..." echo "" python3 -m PyInstaller \ --distpath="$DIST_DIR" \ --buildpath="$TEMP_BUILD" \ --specpath="$SPECS_DIR" \ "$SPEC_FILE" if [ ! -d "$DIST_DIR/$APP_NAME.app" ]; then log_error "Application bundle not created" exit 1 fi log_success "Application bundle built successfully" log_info "Output: $DIST_DIR/$APP_NAME.app" echo "" } create_dmg() { log_info "Creating DMG package..." echo "" DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg" # Remove existing DMG if [ -f "$DMG_FILE" ]; then rm -f "$DMG_FILE" fi # Check if create-dmg is available if command -v create-dmg &> /dev/null; then log_info "Using create-dmg for professional DMG..." create-dmg \ --volname "$DMG_VOLUME_NAME" \ --icon-size 128 \ --window-size 512 400 \ --app-drop-link 380 200 \ "$DMG_FILE" \ "$DIST_DIR/$APP_NAME.app" else log_warning "create-dmg not found, using hdiutil (less stylish)" log_info "For professional DMG: brew install create-dmg" # Create temporary DMG directory structure DMG_TEMP="$TEMP_BUILD/dmg_contents" mkdir -p "$DMG_TEMP" # Copy app bundle cp -r "$DIST_DIR/$APP_NAME.app" "$DMG_TEMP/" # Create symlink to Applications folder ln -s /Applications "$DMG_TEMP/Applications" # Create DMG hdiutil create \ -volname "$DMG_VOLUME_NAME" \ -srcfolder "$DMG_TEMP" \ -ov \ -format UDZO \ "$DMG_FILE" # Clean up rm -rf "$DMG_TEMP" fi if [ ! -f "$DMG_FILE" ]; then log_error "DMG file not created" exit 1 fi # Get file size SIZE=$(du -h "$DMG_FILE" | cut -f1) log_success "DMG created successfully" log_info "Output: $DMG_FILE (Size: $SIZE)" echo "" } sign_app() { log_info "Signing application..." echo "" # Get signing identity from environment or use ad-hoc SIGNING_ID="${APPLE_SIGNING_ID:--}" codesign \ --deep \ --force \ --verify \ --verbose \ --options=runtime \ --sign "$SIGNING_ID" \ "$DIST_DIR/$APP_NAME.app" if [ $? -ne 0 ]; then log_error "Code signing failed" exit 1 fi log_success "Application signed successfully" echo "" } notarize_app() { log_info "Notarizing application..." echo "" # Requires: # - APPLE_ID environment variable # - APPLE_PASSWORD environment variable (app-specific password) # - APPLE_TEAM_ID environment variable if [ -z "$APPLE_ID" ] || [ -z "$APPLE_PASSWORD" ]; then log_error "APPLE_ID and APPLE_PASSWORD environment variables required for notarization" return 1 fi DMG_FILE="$DIST_DIR/${APP_NAME}-${VERSION}.dmg" # Upload for notarization log_info "Uploading to Apple Notarization Service..." xcrun notarytool submit "$DMG_FILE" \ --apple-id "$APPLE_ID" \ --password "$APPLE_PASSWORD" \ --team-id "${APPLE_TEAM_ID}" \ --wait if [ $? -ne 0 ]; then log_error "Notarization failed" return 1 fi # Staple the notarization xcrun stapler staple "$DMG_FILE" log_success "Application notarized successfully" echo "" } # Run main function main