320 lines
7.4 KiB
Bash
320 lines
7.4 KiB
Bash
#!/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] [--env-file PATH]
|
||
#
|
||
# Options:
|
||
# --sign Sign app (requires Apple developer certificate)
|
||
# --notarize Notarize app (requires Apple ID)
|
||
# --env-file PATH Use custom .env file (default: project root .env)
|
||
# Build fails if .env doesn't exist
|
||
|
||
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"
|
||
|
||
# Default .env file
|
||
ENV_FILE="$PROJECT_ROOT/.env"
|
||
|
||
# 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
|
||
;;
|
||
--env-file)
|
||
ENV_FILE="$2"
|
||
shift 2
|
||
;;
|
||
*)
|
||
echo "Unknown option: $1"
|
||
exit 1
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Validate env file
|
||
if [ ! -f "$ENV_FILE" ]; then
|
||
echo "❌ Configuration file not found: $ENV_FILE"
|
||
echo "Please provide a valid .env file or use --env-file parameter"
|
||
exit 1
|
||
fi
|
||
|
||
echo "📋 Using configuration: $ENV_FILE"
|
||
|
||
# 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 ""
|
||
|
||
# Export env file for spec file to pick up
|
||
export WEBDROP_ENV_FILE="$ENV_FILE"
|
||
|
||
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
|